Coverage Report - net.sf.statsvn.util.SvnInfoUtils
 
Classes in this File Line Coverage Branch Coverage Complexity
SvnInfoUtils
59%
118/201
45%
54/120
2.629
SvnInfoUtils$SvnInfoHandler
97%
73/75
67%
66/98
2.629
 
 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  78677
  * 
 29  
  * @version $Id: SvnInfoUtils.java 394 2009-08-10 20:08:46Z jkealey $
 30  
  */
 31  1
 public class SvnInfoUtils implements ISvnInfoProcessor {
 32  
 
 33  0
     //  HACK: we "should" parse the output and check for a node named root, but this will work well enough
 34  0
     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  29
 
 42  
     /**
 43  29
      * Invokes info using the svn info command line. 
 44  
      */
 45  1
     public SvnInfoUtils(ISvnProcessor processor) {
 46  1
         this.processor = processor;
 47  1
     }
 48  
 
 49  
     protected ISvnProcessor getProcessor() {
 50  0
         return processor;
 51  29
     }
 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  647716
         private boolean isRootFolder = false;
 61  647715
         private String sCurrentKind;
 62  
         private String sCurrentRevision;
 63  
         private String sCurrentUrl;
 64  1
         private String stringData = "";
 65  
         private String sCurrentPath;
 66  
         private SvnInfoUtils infoUtils;
 67  322480
 
 68  322480
         public SvnInfoUtils getInfoUtils() {
 69  327031
             return infoUtils;
 70  
         }
 71  
 
 72  322481
         public SvnInfoHandler(SvnInfoUtils infoUtils) {
 73  30
             this.infoUtils = infoUtils;
 74  30
         }
 75  29
 
 76  322451
         /**
 77  25346
          * Builds the string that was read; default implementation can invoke
 78  297105
          * this function multiple times while reading the data.
 79  25375
          */
 80  0
         public void characters(final char[] ch, final int start, final int length) throws SAXException {
 81  22339
             stringData += new String(ch, start, length);
 82  22339
         }
 83  
 
 84  25375
         /**
 85  25375
          * End of xml element.
 86  2523
          */
 87  
         public void endElement(final String uri, final String localName, final String qName) throws SAXException {
 88  282850
             String eName = localName; // element name
 89  36495
             if ("".equals(eName)) {
 90  257475
                 eName = qName; // namespaceAware = false
 91  25375
             }
 92  
 
 93  333600
             if (isRootFolder && eName.equals("url")) {
 94  1
                 isRootFolder = false;
 95  1
                 getInfoUtils().setRootUrl(stringData);
 96  1
                 sCurrentUrl = stringData;
 97  11119
             } else if (eName.equals("url")) {
 98  874
                 sCurrentUrl = stringData;
 99  332725
             } else if (eName.equals("entry")) {
 100  323355
                 if (sCurrentRevision == null || sCurrentUrl == null || sCurrentKind == null) {
 101  322480
                     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  322480
 
 105  26250
                 getInfoUtils().HM_REVISIONS.put(getInfoUtils().urlToRelativePath(sCurrentUrl), sCurrentRevision);
 106  26250
                 if (sCurrentKind.equals("dir")) {
 107  87
                     getInfoUtils().HS_DIRECTORIES.add(getInfoUtils().urlToRelativePath(sCurrentUrl));
 108  
                 }
 109  9370
             } else if (eName.equals("uuid")) {
 110  875
                 getInfoUtils().setRepositoryUuid(stringData);
 111  33870
             } else if (eName.equals("root")) {
 112  904
                 getInfoUtils().setRepositoryUrl(stringData);
 113  29
             }
 114  11120
         }
 115  
 
 116  25375
         /**
 117  25375
          * Start of XML element.
 118  25375
          */
 119  297105
         public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
 120  36495
             String eName = localName; // element name
 121  11120
             if ("".equals(eName)) {
 122  11120
                 eName = qName; // namespaceAware = false
 123  
             }
 124  25375
 
 125  11120
             if (eName.equals("entry")) {
 126  875
                 sCurrentPath = attributes.getValue("path");
 127  323355
                 if (!isValidInfoEntry(attributes)) {
 128  322480
                     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  875
                 if (getInfoUtils().getRootUrl() == null && isRootFolder(attributes)) {
 133  1
                     isRootFolder = true;
 134  1
                     getInfoUtils().sRootRevisionNumber = attributes.getValue("revision");
 135  
                 }
 136  
 
 137  875
                 sCurrentRevision = null;
 138  904
                 sCurrentUrl = null;
 139  875
                 sCurrentKind = attributes.getValue("kind");
 140  10245
             } else if (eName.equals("commit")) {
 141  875
                 if (!isValidCommit(attributes)) {
 142  0
                     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  875
                 sCurrentRevision = attributes.getValue("revision");
 146  
             }
 147  
 
 148  11120
             stringData = "";
 149  11120
         }
 150  25375
 
 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  1
             return attributes.getValue("path").equals(".") && attributes.getValue("kind").equals("dir");
 160  
         }
 161  
 
 162  25375
         /**
 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  58
         protected static boolean isValidCommit(final Attributes attributes) {
 171  875
             return attributes != null && attributes.getValue("revision") != null;
 172  
         }
 173  58
 
 174  
         /**
 175  
          * Is this a valid info entry? Check to see if we can read path, kind
 176  
          * and revision.
 177  58
          * 
 178  
          * @param attributes
 179  
          *            the xml attributes.
 180  58
          * @return true if is a valid info entry.
 181  
          */
 182  
         protected static boolean isValidInfoEntry(final Attributes attributes) {
 183  58
             return attributes != null && attributes.getValue("path") != null && attributes.getValue("kind") != null && attributes.getValue("revision") != null;
 184  
         }
 185  
     }
 186  58
 
 187  
     // enable caching to speed up calculations
 188  1
     private final boolean ENABLE_CACHING = true;
 189  58
 
 190  
     // relative path -> Revision Number
 191  1
     protected final HashMap HM_REVISIONS = new HashMap();
 192  
 
 193  
     // if HashSet contains relative path, path is a directory.
 194  1
     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  1
     private String sModuleName = null;
 199  
 
 200  
     // Revision number of root folder (.)
 201  1
     private String sRootRevisionNumber = null;
 202  
 
 203  306936
     // URL of root (.)
 204  1
     private String sRootUrl = null;
 205  
 
 206  
     // UUID of repository
 207  306937
     private String sRepositoryUuid = null;
 208  174
 
 209  306762
     // URL of repository
 210  1
     private String sRepositoryUrl = null;
 211  
 
 212  306762
     /* (non-Javadoc)
 213  
      * @see net.sf.statsvn.util.ISvnInfoProcessor#absoluteToRelativePath(java.lang.String)
 214  
      */
 215  
     public String absoluteToRelativePath(String absolute) {
 216  10584
         if (absolute.endsWith("/")) {
 217  0
             absolute = absolute.substring(0, absolute.length() - 1);
 218  
         }
 219  
 
 220  10584
         if (absolute.equals(getModuleName())) {
 221  6
             return ".";
 222  10578
         } else if (!absolute.startsWith(getModuleName())) {
 223  0
             return null;
 224  
         } else {
 225  10578
             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  0
         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  0
     public String relativePathToUrl(String relative) {
 240  0
         relative = relative.replace('\\', '/');
 241  0
         if (relative.equals(".") || relative.length() == 0) {
 242  0
             return getRootUrl();
 243  0
         } else {
 244  0
             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  0
         return urlToAbsolutePath(relativePathToUrl(relative));
 253  
     }
 254  
 
 255  
     /* (non-Javadoc)
 256  
      * @see net.sf.statsvn.util.ISvnInfoProcessor#existsInWorkingCopy(java.lang.String)
 257  0
      */
 258  
     public boolean existsInWorkingCopy(final String relativePath) {
 259  2294
         return getRevisionNumber(relativePath) != null;
 260  
     }
 261  
 
 262  
     /* (non-Javadoc)
 263  
      * @see net.sf.statsvn.util.ISvnInfoProcessor#getModuleName()
 264  
      */
 265  
     public String getModuleName() {
 266  
 
 267  32703
         if (sModuleName == null) {
 268  
 
 269  67401
             if (getRootUrl().length() < getRepositoryUrl().length() || getRepositoryUrl().length() == 0) {
 270  0
                 SvnConfigurationOptions.getTaskLogger().info("Unable to process module name.");
 271  0
                 sModuleName = "";
 272  
             } else {
 273  
                 try {
 274  875
                     sModuleName = URLDecoder.decode(getRootUrl().substring(getRepositoryUrl().length()), "UTF-8");
 275  0
                 } catch (final UnsupportedEncodingException e) {
 276  0
                     SvnConfigurationOptions.getTaskLogger().error(e.toString());
 277  875
                 }
 278  
             }
 279  
 
 280  
         }
 281  981090
         return sModuleName;
 282  
     }
 283  25375
 
 284  0
     /* (non-Javadoc)
 285  0
      * @see net.sf.statsvn.util.ISvnInfoProcessor#getRevisionNumber(java.lang.String)
 286  
      */
 287  
     public String getRevisionNumber(final String relativePath) {
 288  28574
         if (HM_REVISIONS.containsKey(relativePath)) {
 289  2386
             return HM_REVISIONS.get(relativePath).toString();
 290  0
         } else {
 291  26188
             return null;
 292  
         }
 293  
     }
 294  
 
 295  948387
     /* (non-Javadoc)
 296  
      * @see net.sf.statsvn.util.ISvnInfoProcessor#getRootRevisionNumber()
 297  
      */
 298  
     public String getRootRevisionNumber() {
 299  1
         return sRootRevisionNumber;
 300  
     }
 301  
 
 302  
     /* (non-Javadoc)
 303  
      * @see net.sf.statsvn.util.ISvnInfoProcessor#getRootUrl()
 304  
      */
 305  
     public String getRootUrl() {
 306  2625
         return sRootUrl;
 307  92771
     }
 308  69194
 
 309  
     /* (non-Javadoc)
 310  23577
      * @see net.sf.statsvn.util.ISvnInfoProcessor#getRepositoryUuid()
 311  
      */
 312  
     public String getRepositoryUuid() {
 313  1
         return sRepositoryUuid;
 314  
     }
 315  
 
 316  
     /* (non-Javadoc)
 317  
      * @see net.sf.statsvn.util.ISvnInfoProcessor#getRepositoryUrl()
 318  
      */
 319  
     public String getRepositoryUrl() {
 320  3587
         return sRepositoryUrl;
 321  29
     }
 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  50750
     protected synchronized ProcessUtils getSvnInfo(boolean bRootOnly) {
 332  0
         String svnInfoCommand = "svn info --xml";
 333  0
         if (!bRootOnly) {
 334  0
             svnInfoCommand += " -R";
 335  
         }
 336  0
         svnInfoCommand += SvnCommandHelper.getAuthString();
 337  
 
 338  
         try {
 339  0
             return ProcessUtils.call(svnInfoCommand);
 340  29
         } catch (final Exception e) {
 341  0
             SvnConfigurationOptions.getTaskLogger().error(e.toString());
 342  0
             return null;
 343  
         }
 344  
     }
 345  
 
 346  
     /* (non-Javadoc)
 347  
      * @see net.sf.statsvn.util.ISvnInfoProcessor#isDirectory(java.lang.String)
 348  
      */
 349  104023
     public boolean isDirectory(final String relativePath) {
 350  1412
         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  4747
         if (!HS_DIRECTORIES.contains(relativePath)) {
 358  9
             HS_DIRECTORIES.add(relativePath);
 359  
         }
 360  4747
     }
 361  0
 
 362  0
     /**
 363  0
      * Do we need to re-invoke svn info?
 364  
      * 
 365  0
      * @param bRootOnly
 366  
      *            true if we need the root only
 367  
      * @return true if we it needs to be re-invoked.
 368  0
      */
 369  0
     protected boolean isQueryNeeded(boolean bRootOnly) {
 370  1
         return !ENABLE_CACHING || (bRootOnly && sRootUrl == null) || (!bRootOnly && HM_REVISIONS == null);
 371  0
     }
 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  40948
     protected void loadInfo(final boolean bRootOnly) throws LogSyntaxException, IOException {
 384  0
         ProcessUtils pUtils = null;
 385  
         try {
 386  0
             pUtils = getSvnInfo(bRootOnly);
 387  0
             loadInfo(pUtils.getInputStream());
 388  
             
 389  0
             if (pUtils.hasErrorOccured()) {
 390  0
                 throw new IOException("svn info: " + pUtils.getErrorMessage());
 391  
             }
 392  
             
 393  
         } finally {
 394  137663
             if (pUtils != null) {
 395  261
                 pUtils.close();
 396  
             }
 397  137663
         }
 398  0
     }
 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  1
         if (isQueryNeeded(true)) {
 405  
             try {
 406  1
                 clearCache();
 407  29
 
 408  1
                 final SAXParserFactory factory = SAXParserFactory.newInstance();
 409  1
                 final SAXParser parser = factory.newSAXParser();
 410  1
                 parser.parse(stream, new SvnInfoHandler(this));
 411  
 
 412  0
             } catch (final ParserConfigurationException e) {
 413  0
                 throw new LogSyntaxException("svn info: " + e.getMessage());
 414  0
             } catch (final SAXException e) {
 415  0
                 throw new LogSyntaxException("svn info: " + e.getMessage());
 416  1
             }
 417  
         }
 418  1
     }
 419  
 
 420  
     protected void clearCache() {
 421  1
         HM_REVISIONS.clear();
 422  1
         HS_DIRECTORIES.clear();
 423  1
     }
 424  0
 
 425  
     /* (non-Javadoc)
 426  0
      * @see net.sf.statsvn.util.ISvnInfoProcessor#loadInfo()
 427  0
      */
 428  
     public void loadInfo() throws LogSyntaxException, IOException {
 429  0
         loadInfo(false);
 430  0
     }
 431  
 
 432  
     /* (non-Javadoc)
 433  
      * @see net.sf.statsvn.util.ISvnInfoProcessor#urlToAbsolutePath(java.lang.String)
 434  
      */
 435  
     public String urlToAbsolutePath(String url) {
 436  962
         if (url.endsWith("/")) {
 437  0
             url = url.substring(0, url.length() - 1);
 438  
         }
 439  962
         if (getModuleName().length() <= 1) {
 440  0
             if (getRootUrl().equals(url)) {
 441  0
                 return "/";
 442  
             } else {
 443  0
                 return url.substring(getRootUrl().length());
 444  
             }
 445  29
         } else {
 446  
             // chop off the repo root from the url
 447  991
             return url.substring(getRepositoryUrl().length());
 448  29
         }
 449  
     }
 450  29
 
 451  29
     /* (non-Javadoc)
 452  29
      * @see net.sf.statsvn.util.ISvnInfoProcessor#urlToRelativePath(java.lang.String)
 453  
      */
 454  29
     public String urlToRelativePath(final String url) {
 455  962
         return absoluteToRelativePath(urlToAbsolutePath(url));
 456  
     }
 457  
 
 458  0
     /**
 459  0
      * Sets the project's root URL.
 460  0
      * 
 461  0
      * @param rootUrl
 462  29
      */
 463  
     protected void setRootUrl(final String rootUrl) {
 464  30
         if (rootUrl.endsWith("/")) {
 465  0
             sRootUrl = rootUrl.substring(0, rootUrl.length() - 1);
 466  
         } else {
 467  1
             sRootUrl = rootUrl;
 468  
         }
 469  
 
 470  1
         sModuleName = null;
 471  1
     }
 472  
 
 473  
     /**
 474  
      * Sets the project's repository URL.
 475  0
      * 
 476  0
      * @param repositoryUrl
 477  
      */
 478  
     protected void setRepositoryUrl(final String repositoryUrl) {
 479  875
         if (repositoryUrl.endsWith("/")) {
 480  0
             sRepositoryUrl = repositoryUrl.substring(0, repositoryUrl.length() - 1);
 481  
         } else {
 482  875
             sRepositoryUrl = repositoryUrl;
 483  
         }
 484  
 
 485  875
         sModuleName = null;
 486  875
     }
 487  27898
     
 488  0
 
 489  
     protected void setRepositoryUuid(String repositoryUuid) {
 490  28773
         sRepositoryUuid = repositoryUuid;
 491  875
     }
 492  0
 
 493  
     
 494  0
     /**
 495  
      * Verifies that the "svn info" command can return the repository root
 496  
      * (info available in svn >= 1.3.0)
 497  
      * 
 498  27898
      * @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  0
         ProcessUtils pUtils = null;
 503  
         try {
 504  0
             final boolean rootOnlyTrue = true;
 505  0
             pUtils = getSvnInfo(rootOnlyTrue);
 506  0
             final InputStream istream = pUtils.getInputStream();
 507  0
             final LookaheadReader reader = new LookaheadReader(new InputStreamReader(istream));
 508  
 
 509  0
             while (reader.hasNextLine()) {
 510  0
                 final String line = reader.nextLine();
 511  27898
                 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  0
                     istream.close();
 515  
                     return; // success
 516  
                 }
 517  0
             }
 518  
 
 519  0
             if (pUtils.hasErrorOccured()) {
 520  29
                 throw new IOException(pUtils.getErrorMessage());
 521  0
             }
 522  0
         } catch (final Exception e) {
 523  29
             SvnConfigurationOptions.getTaskLogger().info(e.getMessage());
 524  
         } finally {
 525  0
             if (pUtils != null) {
 526  29
                 try {
 527  29
                     pUtils.close();
 528  0
                 } catch (final IOException e) {
 529  0
                     SvnConfigurationOptions.getTaskLogger().info(e.getMessage());
 530  0
                 }
 531  
             }
 532  
         }
 533  
 
 534  0
         throw new SvnVersionMismatchException(SVN_REPO_ROOT_NOTFOUND);
 535  25375
     }    
 536  0
 }