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
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;
89 if ("".equals(eName)) {
90 eName = qName;
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;
121 if ("".equals(eName)) {
122 eName = qName;
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
188 private final boolean ENABLE_CACHING = true;
189
190
191 protected final HashMap HM_REVISIONS = new HashMap();
192
193
194 protected final HashSet HS_DIRECTORIES = new HashSet();
195
196
197
198 private String sModuleName = null;
199
200
201 private String sRootRevisionNumber = null;
202
203
204 private String sRootUrl = null;
205
206
207 private String sRepositoryUuid = null;
208
209
210 private String sRepositoryUrl = null;
211
212
213
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
230
231
232 public String absolutePathToUrl(final String absolute) {
233 return getRepositoryUrl() + (absolute.endsWith("/") ? absolute.substring(0, absolute.length() - 1) : absolute);
234 }
235
236
237
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
249
250
251 public String relativeToAbsolutePath(final String relative) {
252 return urlToAbsolutePath(relativePathToUrl(relative));
253 }
254
255
256
257
258 public boolean existsInWorkingCopy(final String relativePath) {
259 return getRevisionNumber(relativePath) != null;
260 }
261
262
263
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
285
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
296
297
298 public String getRootRevisionNumber() {
299 return sRootRevisionNumber;
300 }
301
302
303
304
305 public String getRootUrl() {
306 return sRootUrl;
307 }
308
309
310
311
312 public String getRepositoryUuid() {
313 return sRepositoryUuid;
314 }
315
316
317
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
347
348
349 public boolean isDirectory(final String relativePath) {
350 return HS_DIRECTORIES.contains(relativePath);
351 }
352
353
354
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
401
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
426
427
428 public void loadInfo() throws LogSyntaxException, IOException {
429 loadInfo(false);
430 }
431
432
433
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
447 return url.substring(getRepositoryUrl().length());
448 }
449 }
450
451
452
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
513
514 istream.close();
515 return;
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 }