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;
68 if ("".equals(eName)) {
69 eName = qName;
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;
100 if ("".equals(eName)) {
101 eName = qName;
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
167 private static final boolean ENABLE_CACHING = true;
168
169
170 private static final HashMap HM_REVISIONS = new HashMap();
171
172
173 private static final HashSet HS_DIRECTORIES = new HashSet();
174
175
176
177 private static String sModuleName = null;
178
179
180 private static String sRootRevisionNumber = null;
181
182
183 private static String sRootUrl = null;
184
185
186 private static String sRepositoryUuid = null;
187
188
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
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
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 }