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