View Javadoc

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  public final class SvnInfoUtils {
29  	/**
30  	 * A utility class (only static methods) should be final and have
31  	 * a private constructor.
32  	 */
33  	private SvnInfoUtils() {
34  	}
35  
36  	/**
37  	 * SAX parser for the svn info --xml command.
38  	 * 
39  	 * @author jkealey
40  	 */
41  	protected static class SvnInfoHandler extends DefaultHandler {
42  
43  		private boolean isRootFolder = false;
44  
45  		private String sCurrentKind;
46  
47  		private String sCurrentRevision;
48  
49  		private String sCurrentUrl;
50  
51  		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  			stringData += new String(ch, start, length);
61  		}
62  
63  		/**
64  		 * End of xml element.
65  		 */
66  		public void endElement(final String uri, final String localName, final String qName) throws SAXException {
67  			String eName = localName; // element name
68  			if ("".equals(eName)) {
69  				eName = qName; // namespaceAware = false
70  			}
71  
72  			if (isRootFolder && eName.equals("url")) {
73  				isRootFolder = false;
74  				setRootUrl(stringData);
75  				sCurrentUrl = stringData;
76  			} else if (eName.equals("url")) {
77  				sCurrentUrl = stringData;
78  			} else if (eName.equals("entry")) {
79  				if (sCurrentRevision == null || sCurrentUrl == null || sCurrentKind == null) {
80  					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  				HM_REVISIONS.put(urlToRelativePath(sCurrentUrl), sCurrentRevision);
85  				if (sCurrentKind.equals("dir")) {
86  					HS_DIRECTORIES.add(urlToRelativePath(sCurrentUrl));
87  				}
88  			} else if (eName.equals("uuid")) {
89  				sRepositoryUuid = stringData;
90  			} else if (eName.equals("root")) {
91  				setRepositoryUrl(stringData);
92  			}
93  		}
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  			String eName = localName; // element name
100 			if ("".equals(eName)) {
101 				eName = qName; // namespaceAware = false
102 			}
103 
104 			if (eName.equals("entry")) {
105 				sCurrentPath = attributes.getValue("path");
106 				if (!isValidInfoEntry(attributes)) {
107 					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 				if (sRootUrl == null && isRootFolder(attributes)) {
112 					isRootFolder = true;
113 					sRootRevisionNumber = attributes.getValue("revision");
114 				}
115 
116 				sCurrentRevision = null;
117 				sCurrentUrl = null;
118 				sCurrentKind = attributes.getValue("kind");
119 			} else if (eName.equals("commit")) {
120 				if (!isValidCommit(attributes)) {
121 					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 				sCurrentRevision = attributes.getValue("revision");
125 			}
126 
127 			stringData = "";
128 		}
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 			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 			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 			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 	private static final HashMap HM_REVISIONS = new HashMap();
171 
172 	// if HashSet contains relative path, path is a directory.
173 	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 	private static String sModuleName = null;
178 
179 	// Revision number of root folder (.)
180 	private static String sRootRevisionNumber = null;
181 
182 	// URL of root (.)
183 	private static String sRootUrl = null;
184 
185 	// UUID of repository
186 	private static String sRepositoryUuid = null;
187 
188 	// URL of repository
189 	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 		if (absolute.endsWith("/")) {
204 			absolute = absolute.substring(0, absolute.length() - 1);
205 		}
206 
207 		if (absolute.equals(getModuleName())) {
208 			return ".";
209 		} else if (!absolute.startsWith(getModuleName())) {
210 			return null;
211 		} else {
212 			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 		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 		relative = relative.replace('\\', '/');
240 		if (relative.equals(".") || relative.length() == 0) {
241 			return getRootUrl();
242 		} else {
243 			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 		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 		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 		if (sModuleName == null) {
282 
283 			if (getRootUrl().length() < getRepositoryUrl().length() || getRepositoryUrl().length() == 0) {
284 				SvnConfigurationOptions.getTaskLogger().info("Unable to process module name.");
285 				sModuleName = "";
286 			} else {
287 				try {
288 					sModuleName = URLDecoder.decode(getRootUrl().substring(getRepositoryUrl().length()), "UTF-8");
289 				} catch (final UnsupportedEncodingException e) {
290 					SvnConfigurationOptions.getTaskLogger().error(e.toString());
291 				}
292 			}
293 
294 		}
295 		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 		if (HM_REVISIONS.containsKey(relativePath)) {
308 			return HM_REVISIONS.get(relativePath).toString();
309 		} else {
310 			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 		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 		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 		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 		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 		String svnInfoCommand = "svn info --xml";
362 		if (!bRootOnly) {
363 			svnInfoCommand += " -R";
364 		}
365 		svnInfoCommand += SvnCommandHelper.getAuthString();
366 
367 		try {
368 			return ProcessUtils.call(svnInfoCommand);
369 		} catch (final Exception e) {
370 			SvnConfigurationOptions.getTaskLogger().error(e.toString());
371 			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 		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 		if (!HS_DIRECTORIES.contains(relativePath)) {
395 			HS_DIRECTORIES.add(relativePath);
396 		}
397 	}
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 		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 		ProcessUtils pUtils = null;
422 		try {
423 			pUtils = getSvnInfo(bRootOnly);
424 			loadInfo(pUtils);
425 		} finally {
426 			if (pUtils != null) {
427 				pUtils.close();
428 			}
429 		}
430 	}
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 		if (isQueryNeeded(true)) {
446 			try {
447 				HM_REVISIONS.clear();
448 				HS_DIRECTORIES.clear();
449 
450 				final SAXParserFactory factory = SAXParserFactory.newInstance();
451 				final SAXParser parser = factory.newSAXParser();
452 				parser.parse(pUtils.getInputStream(), new SvnInfoHandler());
453 
454 				if (pUtils.hasErrorOccured()) {
455 					throw new IOException("svn info: " + pUtils.getErrorMessage());
456 				}
457 
458 			} catch (final ParserConfigurationException e) {
459 				throw new LogSyntaxException("svn info: " + e.getMessage());
460 			} catch (final SAXException e) {
461 				throw new LogSyntaxException("svn info: " + e.getMessage());
462 			}
463 		}
464 	}
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 		loadInfo(false);
476 	}
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 		if (url.endsWith("/")) {
488 			url = url.substring(0, url.length() - 1);
489 		}
490 		if (getModuleName().length() <= 1) {
491 			if (getRootUrl().equals(url)) {
492 				return "/";
493 			} else {
494 				return url.substring(getRootUrl().length());
495 			}
496 		} else {
497 			// chop off the repo root from the url
498 			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 		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 		if (rootUrl.endsWith("/")) {
521 			sRootUrl = rootUrl.substring(0, rootUrl.length() - 1);
522 		} else {
523 			sRootUrl = rootUrl;
524 		}
525 
526 		sModuleName = null;
527 	}
528 
529 	/**
530 	 * Sets the project's repository URL.
531 	 * 
532 	 * @param repositoryUrl
533 	 */
534 	protected static void setRepositoryUrl(final String repositoryUrl) {
535 		if (repositoryUrl.endsWith("/")) {
536 			sRepositoryUrl = repositoryUrl.substring(0, repositoryUrl.length() - 1);
537 		} else {
538 			sRepositoryUrl = repositoryUrl;
539 		}
540 
541 		sModuleName = null;
542 	}
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 		if ((originalPattern == null) || (originalPattern.length() == 0) || (originalString == null)) {
553 			return originalString;
554 		}
555 
556 		final StringBuffer newString = new StringBuffer(originalString.length());
557 		int index = 0;
558 		final int originalLength = originalPattern.length();
559 		int previousIndex = 0;
560 
561 		while ((index = originalString.indexOf(originalPattern, index)) != -1) {
562 			newString.append(originalString.substring(previousIndex, index)).append(newPattern);
563 			index += originalLength;
564 			previousIndex = index;
565 		}
566 
567 		if (previousIndex == 0) {
568 			newString.append(originalString);
569 		} else if (previousIndex != originalString.length()) {
570 			newString.append(originalString.substring(previousIndex));
571 		}
572 
573 		return newString.toString();
574 	}
575 
576 }