View Javadoc

1   package net.sf.statsvn.util;
2   
3   import java.io.IOException;
4   import java.io.InputStream;
5   import java.io.InputStreamReader;
6   import java.io.UnsupportedEncodingException;
7   import java.net.URLDecoder;
8   import java.util.HashMap;
9   import java.util.HashSet;
10  
11  import javax.xml.parsers.ParserConfigurationException;
12  import javax.xml.parsers.SAXParser;
13  import javax.xml.parsers.SAXParserFactory;
14  
15  import net.sf.statcvs.input.LogSyntaxException;
16  import net.sf.statcvs.util.LookaheadReader;
17  import net.sf.statsvn.output.SvnConfigurationOptions;
18  
19  import org.xml.sax.Attributes;
20  import org.xml.sax.SAXException;
21  import org.xml.sax.helpers.DefaultHandler;
22  
23  /**
24   * Utilities class that manages calls to svn info. Used to find repository
25   * information, latest revision numbers, and directories.
26   * 
27   * @author Jason Kealey <jkealey@shade.ca>
28   * 
29   * @version $Id: SvnInfoUtils.java 394 2009-08-10 20:08:46Z jkealey $
30   */
31  public class SvnInfoUtils implements ISvnInfoProcessor {
32  
33      //  HACK: we "should" parse the output and check for a node named root, but this will work well enough
34      private static final String SVN_INFO_WITHREPO_LINE_PATTERN = ".*<root>.+</root>.*";
35  
36      protected static final String SVN_REPO_ROOT_NOTFOUND = "Repository root not available - verify that the project was checked out with svn version "
37              + SvnStartupUtils.SVN_MINIMUM_VERSION + " or above.";
38  
39      
40      protected ISvnProcessor processor;
41  
42      /**
43       * Invokes info using the svn info command line. 
44       */
45      public SvnInfoUtils(ISvnProcessor processor) {
46          this.processor = processor;
47      }
48  
49      protected ISvnProcessor getProcessor() {
50          return processor;
51      }
52  
53      /**
54       * SAX parser for the svn info --xml command.
55       * 
56       * @author jkealey
57       */
58      protected static class SvnInfoHandler extends DefaultHandler {
59  
60          private boolean isRootFolder = false;
61          private String sCurrentKind;
62          private String sCurrentRevision;
63          private String sCurrentUrl;
64          private String stringData = "";
65          private String sCurrentPath;
66          private SvnInfoUtils infoUtils;
67  
68          public SvnInfoUtils getInfoUtils() {
69              return infoUtils;
70          }
71  
72          public SvnInfoHandler(SvnInfoUtils infoUtils) {
73              this.infoUtils = infoUtils;
74          }
75  
76          /**
77           * Builds the string that was read; default implementation can invoke
78           * this function multiple times while reading the data.
79           */
80          public void characters(final char[] ch, final int start, final int length) throws SAXException {
81              stringData += new String(ch, start, length);
82          }
83  
84          /**
85           * End of xml element.
86           */
87          public void endElement(final String uri, final String localName, final String qName) throws SAXException {
88              String eName = localName; // element name
89              if ("".equals(eName)) {
90                  eName = qName; // namespaceAware = false
91              }
92  
93              if (isRootFolder && eName.equals("url")) {
94                  isRootFolder = false;
95                  getInfoUtils().setRootUrl(stringData);
96                  sCurrentUrl = stringData;
97              } else if (eName.equals("url")) {
98                  sCurrentUrl = stringData;
99              } else if (eName.equals("entry")) {
100                 if (sCurrentRevision == null || sCurrentUrl == null || sCurrentKind == null) {
101                     throw new SAXException("Invalid svn info xml; unable to find revision or url for path [" + sCurrentPath + "]" + " revision="
102                             + sCurrentRevision + " url:" + sCurrentUrl + " kind:" + sCurrentKind);
103                 }
104 
105                 getInfoUtils().HM_REVISIONS.put(getInfoUtils().urlToRelativePath(sCurrentUrl), sCurrentRevision);
106                 if (sCurrentKind.equals("dir")) {
107                     getInfoUtils().HS_DIRECTORIES.add(getInfoUtils().urlToRelativePath(sCurrentUrl));
108                 }
109             } else if (eName.equals("uuid")) {
110                 getInfoUtils().setRepositoryUuid(stringData);
111             } else if (eName.equals("root")) {
112                 getInfoUtils().setRepositoryUrl(stringData);
113             }
114         }
115 
116         /**
117          * Start of XML element.
118          */
119         public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
120             String eName = localName; // element name
121             if ("".equals(eName)) {
122                 eName = qName; // namespaceAware = false
123             }
124 
125             if (eName.equals("entry")) {
126                 sCurrentPath = attributes.getValue("path");
127                 if (!isValidInfoEntry(attributes)) {
128                     throw new SAXException("Invalid svn info xml for entry element. Please verify that you have checked out this project using "
129                             + "Subversion 1.3 or above, not only that you are currently using this version.");
130                 }
131 
132                 if (getInfoUtils().getRootUrl() == null && isRootFolder(attributes)) {
133                     isRootFolder = true;
134                     getInfoUtils().sRootRevisionNumber = attributes.getValue("revision");
135                 }
136 
137                 sCurrentRevision = null;
138                 sCurrentUrl = null;
139                 sCurrentKind = attributes.getValue("kind");
140             } else if (eName.equals("commit")) {
141                 if (!isValidCommit(attributes)) {
142                     throw new SAXException("Invalid svn info xml for commit element. Please verify that you have checked out this project using "
143                             + "Subversion 1.3 or above, not only that you are currently using this version.");
144                 }
145                 sCurrentRevision = attributes.getValue("revision");
146             }
147 
148             stringData = "";
149         }
150 
151         /**
152          * Is this the root of the workspace?
153          * 
154          * @param attributes
155          *            the xml attributes
156          * @return true if is the root folder.
157          */
158         protected  boolean isRootFolder(final Attributes attributes) {
159             return attributes.getValue("path").equals(".") && attributes.getValue("kind").equals("dir");
160         }
161 
162         /**
163          * Is this a valid commit? Check to see if wec an read the revision
164          * number.
165          * 
166          * @param attributes
167          *            the xml attributes
168          * @return true if is a valid commit.
169          */
170         protected static boolean isValidCommit(final Attributes attributes) {
171             return attributes != null && attributes.getValue("revision") != null;
172         }
173 
174         /**
175          * Is this a valid info entry? Check to see if we can read path, kind
176          * and revision.
177          * 
178          * @param attributes
179          *            the xml attributes.
180          * @return true if is a valid info entry.
181          */
182         protected static boolean isValidInfoEntry(final Attributes attributes) {
183             return attributes != null && attributes.getValue("path") != null && attributes.getValue("kind") != null && attributes.getValue("revision") != null;
184         }
185     }
186 
187     // enable caching to speed up calculations
188     private final boolean ENABLE_CACHING = true;
189 
190     // relative path -> Revision Number
191     protected final HashMap HM_REVISIONS = new HashMap();
192 
193     // if HashSet contains relative path, path is a directory.
194     protected final HashSet HS_DIRECTORIES = new HashSet();
195 
196     // Path of . in repository. Can only be calculated if given an element from
197     // the SVN log.
198     private String sModuleName = null;
199 
200     // Revision number of root folder (.)
201     private String sRootRevisionNumber = null;
202 
203     // URL of root (.)
204     private String sRootUrl = null;
205 
206     // UUID of repository
207     private String sRepositoryUuid = null;
208 
209     // URL of repository
210     private String sRepositoryUrl = null;
211 
212     /* (non-Javadoc)
213      * @see net.sf.statsvn.util.ISvnInfoProcessor#absoluteToRelativePath(java.lang.String)
214      */
215     public String absoluteToRelativePath(String absolute) {
216         if (absolute.endsWith("/")) {
217             absolute = absolute.substring(0, absolute.length() - 1);
218         }
219 
220         if (absolute.equals(getModuleName())) {
221             return ".";
222         } else if (!absolute.startsWith(getModuleName())) {
223             return null;
224         } else {
225             return absolute.substring(getModuleName().length() + 1);
226         }
227     }
228 
229     /* (non-Javadoc)
230      * @see net.sf.statsvn.util.ISvnInfoProcessor#absolutePathToUrl(java.lang.String)
231      */
232     public String absolutePathToUrl(final String absolute) {
233         return getRepositoryUrl() + (absolute.endsWith("/") ? absolute.substring(0, absolute.length() - 1) : absolute);
234     }
235 
236     /* (non-Javadoc)
237      * @see net.sf.statsvn.util.ISvnInfoProcessor#relativePathToUrl(java.lang.String)
238      */
239     public String relativePathToUrl(String relative) {
240         relative = relative.replace('\\', '/');
241         if (relative.equals(".") || relative.length() == 0) {
242             return getRootUrl();
243         } else {
244             return getRootUrl() + "/" + (relative.endsWith("/") ? relative.substring(0, relative.length() - 1) : relative);
245         }
246     }
247 
248     /* (non-Javadoc)
249      * @see net.sf.statsvn.util.ISvnInfoProcessor#relativeToAbsolutePath(java.lang.String)
250      */
251     public String relativeToAbsolutePath(final String relative) {
252         return urlToAbsolutePath(relativePathToUrl(relative));
253     }
254 
255     /* (non-Javadoc)
256      * @see net.sf.statsvn.util.ISvnInfoProcessor#existsInWorkingCopy(java.lang.String)
257      */
258     public boolean existsInWorkingCopy(final String relativePath) {
259         return getRevisionNumber(relativePath) != null;
260     }
261 
262     /* (non-Javadoc)
263      * @see net.sf.statsvn.util.ISvnInfoProcessor#getModuleName()
264      */
265     public String getModuleName() {
266 
267         if (sModuleName == null) {
268 
269             if (getRootUrl().length() < getRepositoryUrl().length() || getRepositoryUrl().length() == 0) {
270                 SvnConfigurationOptions.getTaskLogger().info("Unable to process module name.");
271                 sModuleName = "";
272             } else {
273                 try {
274                     sModuleName = URLDecoder.decode(getRootUrl().substring(getRepositoryUrl().length()), "UTF-8");
275                 } catch (final UnsupportedEncodingException e) {
276                     SvnConfigurationOptions.getTaskLogger().error(e.toString());
277                 }
278             }
279 
280         }
281         return sModuleName;
282     }
283 
284     /* (non-Javadoc)
285      * @see net.sf.statsvn.util.ISvnInfoProcessor#getRevisionNumber(java.lang.String)
286      */
287     public String getRevisionNumber(final String relativePath) {
288         if (HM_REVISIONS.containsKey(relativePath)) {
289             return HM_REVISIONS.get(relativePath).toString();
290         } else {
291             return null;
292         }
293     }
294 
295     /* (non-Javadoc)
296      * @see net.sf.statsvn.util.ISvnInfoProcessor#getRootRevisionNumber()
297      */
298     public String getRootRevisionNumber() {
299         return sRootRevisionNumber;
300     }
301 
302     /* (non-Javadoc)
303      * @see net.sf.statsvn.util.ISvnInfoProcessor#getRootUrl()
304      */
305     public String getRootUrl() {
306         return sRootUrl;
307     }
308 
309     /* (non-Javadoc)
310      * @see net.sf.statsvn.util.ISvnInfoProcessor#getRepositoryUuid()
311      */
312     public String getRepositoryUuid() {
313         return sRepositoryUuid;
314     }
315 
316     /* (non-Javadoc)
317      * @see net.sf.statsvn.util.ISvnInfoProcessor#getRepositoryUrl()
318      */
319     public String getRepositoryUrl() {
320         return sRepositoryUrl;
321     }
322 
323     /**
324      * Invokes svn info.
325      * 
326      * @param bRootOnly
327      *            true if should we check for the root only or false otherwise
328      *            (recurse for all files)
329      * @return the response.
330      */
331     protected synchronized ProcessUtils getSvnInfo(boolean bRootOnly) {
332         String svnInfoCommand = "svn info --xml";
333         if (!bRootOnly) {
334             svnInfoCommand += " -R";
335         }
336         svnInfoCommand += SvnCommandHelper.getAuthString();
337 
338         try {
339             return ProcessUtils.call(svnInfoCommand);
340         } catch (final Exception e) {
341             SvnConfigurationOptions.getTaskLogger().error(e.toString());
342             return null;
343         }
344     }
345 
346     /* (non-Javadoc)
347      * @see net.sf.statsvn.util.ISvnInfoProcessor#isDirectory(java.lang.String)
348      */
349     public boolean isDirectory(final String relativePath) {
350         return HS_DIRECTORIES.contains(relativePath);
351     }
352 
353     /* (non-Javadoc)
354      * @see net.sf.statsvn.util.ISvnInfoProcessor#addDirectory(java.lang.String)
355      */
356     public void addDirectory(final String relativePath) {
357         if (!HS_DIRECTORIES.contains(relativePath)) {
358             HS_DIRECTORIES.add(relativePath);
359         }
360     }
361 
362     /**
363      * Do we need to re-invoke svn info?
364      * 
365      * @param bRootOnly
366      *            true if we need the root only
367      * @return true if we it needs to be re-invoked.
368      */
369     protected boolean isQueryNeeded(boolean bRootOnly) {
370         return !ENABLE_CACHING || (bRootOnly && sRootUrl == null) || (!bRootOnly && HM_REVISIONS == null);
371     }
372 
373     /**
374      * Loads the information from svn info if needed.
375      * 
376      * @param bRootOnly
377      *            load only the root?
378      * @throws LogSyntaxException
379      *             if the format of the svn info is invalid
380      * @throws IOException
381      *             if we can't read from the response stream.
382      */
383     protected void loadInfo(final boolean bRootOnly) throws LogSyntaxException, IOException {
384         ProcessUtils pUtils = null;
385         try {
386             pUtils = getSvnInfo(bRootOnly);
387             loadInfo(pUtils.getInputStream());
388             
389             if (pUtils.hasErrorOccured()) {
390                 throw new IOException("svn info: " + pUtils.getErrorMessage());
391             }
392             
393         } finally {
394             if (pUtils != null) {
395                 pUtils.close();
396             }
397         }
398     }
399 
400     /* (non-Javadoc)
401      * @see net.sf.statsvn.util.ISvnInfoProcessor#loadInfo(net.sf.statsvn.util.ProcessUtils)
402      */
403     public void loadInfo(final InputStream stream) throws LogSyntaxException, IOException {
404         if (isQueryNeeded(true)) {
405             try {
406                 clearCache();
407 
408                 final SAXParserFactory factory = SAXParserFactory.newInstance();
409                 final SAXParser parser = factory.newSAXParser();
410                 parser.parse(stream, new SvnInfoHandler(this));
411 
412             } catch (final ParserConfigurationException e) {
413                 throw new LogSyntaxException("svn info: " + e.getMessage());
414             } catch (final SAXException e) {
415                 throw new LogSyntaxException("svn info: " + e.getMessage());
416             }
417         }
418     }
419 
420     protected void clearCache() {
421         HM_REVISIONS.clear();
422         HS_DIRECTORIES.clear();
423     }
424 
425     /* (non-Javadoc)
426      * @see net.sf.statsvn.util.ISvnInfoProcessor#loadInfo()
427      */
428     public void loadInfo() throws LogSyntaxException, IOException {
429         loadInfo(false);
430     }
431 
432     /* (non-Javadoc)
433      * @see net.sf.statsvn.util.ISvnInfoProcessor#urlToAbsolutePath(java.lang.String)
434      */
435     public String urlToAbsolutePath(String url) {
436         if (url.endsWith("/")) {
437             url = url.substring(0, url.length() - 1);
438         }
439         if (getModuleName().length() <= 1) {
440             if (getRootUrl().equals(url)) {
441                 return "/";
442             } else {
443                 return url.substring(getRootUrl().length());
444             }
445         } else {
446             // chop off the repo root from the url
447             return url.substring(getRepositoryUrl().length());
448         }
449     }
450 
451     /* (non-Javadoc)
452      * @see net.sf.statsvn.util.ISvnInfoProcessor#urlToRelativePath(java.lang.String)
453      */
454     public String urlToRelativePath(final String url) {
455         return absoluteToRelativePath(urlToAbsolutePath(url));
456     }
457 
458     /**
459      * Sets the project's root URL.
460      * 
461      * @param rootUrl
462      */
463     protected void setRootUrl(final String rootUrl) {
464         if (rootUrl.endsWith("/")) {
465             sRootUrl = rootUrl.substring(0, rootUrl.length() - 1);
466         } else {
467             sRootUrl = rootUrl;
468         }
469 
470         sModuleName = null;
471     }
472 
473     /**
474      * Sets the project's repository URL.
475      * 
476      * @param repositoryUrl
477      */
478     protected void setRepositoryUrl(final String repositoryUrl) {
479         if (repositoryUrl.endsWith("/")) {
480             sRepositoryUrl = repositoryUrl.substring(0, repositoryUrl.length() - 1);
481         } else {
482             sRepositoryUrl = repositoryUrl;
483         }
484 
485         sModuleName = null;
486     }
487     
488 
489     protected void setRepositoryUuid(String repositoryUuid) {
490         sRepositoryUuid = repositoryUuid;
491     }
492 
493     
494     /**
495      * Verifies that the "svn info" command can return the repository root
496      * (info available in svn >= 1.3.0)
497      * 
498      * @throws SvnVersionMismatchException
499      *             if <tt>svn info</tt> failed to provide a non-empty repository root
500      */
501     public synchronized void checkRepoRootAvailable() throws SvnVersionMismatchException {
502         ProcessUtils pUtils = null;
503         try {
504             final boolean rootOnlyTrue = true;
505             pUtils = getSvnInfo(rootOnlyTrue);
506             final InputStream istream = pUtils.getInputStream();
507             final LookaheadReader reader = new LookaheadReader(new InputStreamReader(istream));
508 
509             while (reader.hasNextLine()) {
510                 final String line = reader.nextLine();
511                 if (line.matches(SVN_INFO_WITHREPO_LINE_PATTERN)) {
512                     // We have our <root> element in the svn info AND it's not empty --> checkout performed 
513                     // with a compatible version of subversion client.
514                     istream.close();
515                     return; // success
516                 }
517             }
518 
519             if (pUtils.hasErrorOccured()) {
520                 throw new IOException(pUtils.getErrorMessage());
521             }
522         } catch (final Exception e) {
523             SvnConfigurationOptions.getTaskLogger().info(e.getMessage());
524         } finally {
525             if (pUtils != null) {
526                 try {
527                     pUtils.close();
528                 } catch (final IOException e) {
529                     SvnConfigurationOptions.getTaskLogger().info(e.getMessage());
530                 }
531             }
532         }
533 
534         throw new SvnVersionMismatchException(SVN_REPO_ROOT_NOTFOUND);
535     }    
536 }