KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > net > sourceforge > cruisecontrol > sourcecontrols > ConcurrentVersionsSystem


1 /********************************************************************************
2  * CruiseControl, a Continuous Integration Toolkit
3  * Copyright (c) 2001-2003, ThoughtWorks, Inc.
4  * 651 W Washington Ave. Suite 600
5  * Chicago, IL 60661 USA
6  * All rights reserved.
7  *
8  * Redistribution and use in source and binary forms, with or without
9  * modification, are permitted provided that the following conditions
10  * are met:
11  *
12  * + Redistributions of source code must retain the above copyright
13  * notice, this list of conditions and the following disclaimer.
14  *
15  * + Redistributions in binary form must reproduce the above
16  * copyright notice, this list of conditions and the following
17  * disclaimer in the documentation and/or other materials provided
18  * with the distribution.
19  *
20  * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
21  * names of its contributors may be used to endorse or promote
22  * products derived from this software without specific prior
23  * written permission.
24  *
25  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
26  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
27  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
28  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
29  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
30  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
31  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
32  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
33  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
34  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36  ********************************************************************************/

37 package net.sourceforge.cruisecontrol.sourcecontrols;
38
39 import java.io.BufferedReader JavaDoc;
40 import java.io.File JavaDoc;
41 import java.io.IOException JavaDoc;
42 import java.io.InputStream JavaDoc;
43 import java.io.InputStreamReader JavaDoc;
44 import java.io.PrintWriter JavaDoc;
45 import java.text.ParseException JavaDoc;
46 import java.text.SimpleDateFormat JavaDoc;
47 import java.util.ArrayList JavaDoc;
48 import java.util.Date JavaDoc;
49 import java.util.Hashtable JavaDoc;
50 import java.util.List JavaDoc;
51 import java.util.Map JavaDoc;
52 import java.util.StringTokenizer JavaDoc;
53
54 import net.sourceforge.cruisecontrol.CruiseControlException;
55 import net.sourceforge.cruisecontrol.Modification;
56 import net.sourceforge.cruisecontrol.SourceControl;
57 import net.sourceforge.cruisecontrol.util.Commandline;
58 import net.sourceforge.cruisecontrol.util.DateUtil;
59 import net.sourceforge.cruisecontrol.util.OSEnvironment;
60 import net.sourceforge.cruisecontrol.util.StreamConsumer;
61 import net.sourceforge.cruisecontrol.util.StreamPumper;
62 import net.sourceforge.cruisecontrol.util.ValidationHelper;
63
64 import org.apache.log4j.Logger;
65
66 /**
67  * This class implements the SourceControlElement methods for a CVS repository.
68  * The call to CVS is assumed to work without any setup. This implies that if
69  * the authentication type is pserver the call to cvs login should be done
70  * prior to calling this class.
71  * <p/>
72  * There are also differing CVS client/server implementations (e.g. the <i>official</i>
73  * CVS and the CVSNT fork).
74  * <p/>
75  * Note that the log formats of the official CVS have changed starting from version 1.12.9.
76  * This class currently knows of 2 different outputs referred to as the 'old'
77  * and the 'new' output formats.
78  *
79  * @author <a HREF="mailto:pj@thoughtworks.com">Paul Julius</a>
80  * @author Robert Watkins
81  * @author Frederic Lavigne
82  * @author <a HREF="mailto:jcyip@thoughtworks.com">Jason Yip</a>
83  * @author Marc Paquette
84  * @author <a HREF="mailto:johnny.cass@epiuse.com">Johnny Cass</a>
85  * @author <a HREF="mailto:m@loonsoft.com">McClain Looney</a>
86  */

87 public class ConcurrentVersionsSystem implements SourceControl {
88     /**
89      * name of the official cvs as returned as part of the 'cvs version' command output
90      */

91     static final String JavaDoc OFFICIAL_CVS_NAME = "CVS";
92     static final Version DEFAULT_CVS_SERVER_VERSION = new Version(OFFICIAL_CVS_NAME, "1.11");
93     public static final String JavaDoc LOG_DATE_FORMAT = "yyyy/MM/dd HH:mm:ss z";
94
95     /**
96      * Represents the version of a CVS client or server
97      */

98     static class Version {
99         private final String JavaDoc cvsName;
100         private final String JavaDoc cvsVersion;
101
102         public Version(String JavaDoc name, String JavaDoc version) {
103             if (name == null) {
104                 throw new IllegalArgumentException JavaDoc("name can't be null");
105             }
106             if (version == null) {
107                 throw new IllegalArgumentException JavaDoc("version can't be null");
108             }
109             this.cvsName = name;
110             this.cvsVersion = version;
111         }
112
113         public String JavaDoc getCvsName() {
114             return cvsName;
115         }
116
117         public String JavaDoc getCvsVersion() {
118             return cvsVersion;
119         }
120
121         public boolean equals(Object JavaDoc o) {
122             if (this == o) {
123                 return true;
124             } else if (!(o instanceof Version)) {
125                 return false;
126             }
127
128             final Version version = (Version) o;
129
130             if (!cvsName.equals(version.cvsName)) {
131                 return false;
132             } else if (!cvsVersion.equals(version.cvsVersion)) {
133                 return false;
134             }
135
136             return true;
137         }
138
139         public int hashCode() {
140             int result;
141             result = cvsName.hashCode();
142             result = 29 * result + cvsVersion.hashCode();
143             return result;
144         }
145
146         public String JavaDoc toString() {
147             return cvsName + " " + cvsVersion;
148         }
149     }
150
151
152     private Hashtable JavaDoc properties = new Hashtable JavaDoc();
153     private String JavaDoc property;
154     private String JavaDoc propertyOnDelete;
155
156     /**
157      * CVS allows for mapping user names to email addresses.
158      * If CVSROOT/users exists, it's contents will be parsed and stored in this
159      * hashtable.
160      */

161     private Hashtable JavaDoc mailAliases;
162
163     /**
164      * The caller can provide the CVSROOT to use when calling CVS, or
165      * the CVSROOT environment variable will be used.
166      */

167     private String JavaDoc cvsroot;
168
169     /**
170      * The caller must indicate where the local copy of the repository
171      * exists.
172      */

173     private String JavaDoc local;
174
175     /**
176      * The CVS tag we are dealing with.
177      */

178     private String JavaDoc tag;
179
180     /**
181      * The CVS module we are dealing with.
182      */

183     private String JavaDoc module;
184
185     /**
186      * The version of the cvs server
187      */

188     private Version cvsServerVersion;
189
190     /**
191      * enable logging for this class
192      */

193     private static Logger log = Logger.getLogger(ConcurrentVersionsSystem.class);
194
195     /**
196      * This line delimits seperate files in the CVS log information.
197      */

198     private static final String JavaDoc CVS_FILE_DELIM =
199             "=============================================================================";
200
201     /**
202      * This is the keyword that precedes the name of the RCS filename in the CVS
203      * log information.
204      */

205     private static final String JavaDoc CVS_RCSFILE_LINE = "RCS file: ";
206
207     /**
208      * This is the keyword that precedes the name of the working filename in the
209      * CVS log information.
210      */

211     private static final String JavaDoc CVS_WORKINGFILE_LINE = "Working file: ";
212
213     /**
214      * This line delimits the different revisions of a file in the CVS log
215      * information.
216      */

217     private static final String JavaDoc CVS_REVISION_DELIM = "----------------------------";
218
219     /**
220      * This is the keyword that precedes the timestamp of a file revision in the
221      * CVS log information.
222      */

223     private static final String JavaDoc CVS_REVISION_DATE = "date:";
224
225     /**
226      * This is the name of the tip of the main branch, which needs special handling with
227      * the log entry parser
228      */

229     private static final String JavaDoc CVS_HEAD_TAG = "HEAD";
230
231     /**
232      * This is the keyword that tells us when we have reaced the ned of the
233      * header as found in the CVS log information.
234      */

235     private static final String JavaDoc CVS_DESCRIPTION = "description:";
236
237     /**
238      * This is a state keyword which indicates that a revision to a file was not
239      * relevant to the current branch, or the revision consisted of a deletion
240      * of the file (removal from branch..).
241      */

242     private static final String JavaDoc CVS_REVISION_DEAD = "dead";
243
244     /**
245      * System dependent new line seperator.
246      */

247     private static final String JavaDoc NEW_LINE = System.getProperty("line.separator");
248
249     /**
250      * This is the date format returned in the log information from CVS.
251      */

252     private final SimpleDateFormat JavaDoc logDateFormatter = new SimpleDateFormat JavaDoc(LOG_DATE_FORMAT);
253
254     /**
255      * Sets the CVSROOT for all calls to CVS.
256      *
257      * @param cvsroot CVSROOT to use.
258      */

259     public void setCvsRoot(String JavaDoc cvsroot) {
260         this.cvsroot = cvsroot;
261     }
262
263     /**
264      * Sets the local working copy to use when making calls to CVS.
265      *
266      * @param local String indicating the relative or absolute path to the local
267      * working copy of the module of which to find the log history.
268      */

269     public void setLocalWorkingCopy(String JavaDoc local) {
270         this.local = local;
271     }
272
273     /**
274      * Set the cvs tag. Note this should work with names, numbers, and anything
275      * else you can put on log -rTAG
276      *
277      * @param tag the cvs tag
278      */

279     public void setTag(String JavaDoc tag) {
280         this.tag = tag;
281     }
282
283     /**
284      * Set the cvs module name. Note that this is only used when
285      * localworkingcopy is not set.
286      *
287      * @param module the cvs module
288      */

289     public void setModule(String JavaDoc module) {
290         this.module = module;
291     }
292
293     public void setProperty(String JavaDoc property) {
294         this.property = property;
295     }
296
297     public void setPropertyOnDelete(String JavaDoc propertyOnDelete) {
298         this.propertyOnDelete = propertyOnDelete;
299     }
300
301     protected Version getCvsServerVersion() {
302         if (cvsServerVersion == null) {
303
304             Commandline commandLine = getCommandline();
305             commandLine.setExecutable("cvs");
306
307             if (cvsroot != null) {
308                 commandLine.createArgument().setValue("-d");
309                 commandLine.createArgument().setValue(cvsroot);
310             }
311
312             commandLine.createArgument().setLine("version");
313
314             Process JavaDoc p = null;
315             try {
316                 if (local != null) {
317                     commandLine.setWorkingDirectory(local);
318                 }
319
320                 p = commandLine.execute();
321                 logErrorStream(p);
322                 InputStream JavaDoc is = p.getInputStream();
323                 BufferedReader JavaDoc in = new BufferedReader JavaDoc(new InputStreamReader JavaDoc(is));
324
325                 cvsServerVersion = extractCVSServerVersionFromCVSVersionCommandOutput(in);
326
327                 log.debug("cvs server version: " + cvsServerVersion);
328
329                 p.waitFor();
330                 p.getInputStream().close();
331                 p.getOutputStream().close();
332                 p.getErrorStream().close();
333             } catch (IOException JavaDoc e) {
334                 log.error("Failed reading cvs server version", e);
335             } catch (CruiseControlException e) {
336                 log.error("Failed reading cvs server version", e);
337             } catch (InterruptedException JavaDoc e) {
338                 log.error("Failed reading cvs server version", e);
339             }
340
341             if (p == null || p.exitValue() != 0 || cvsServerVersion == null) {
342                 if (p == null) {
343                     log.debug("Process p was null in CVS.getCvsServerVersion()");
344                 } else {
345                     log.debug("Process exit value = " + p.exitValue());
346                 }
347                 cvsServerVersion = DEFAULT_CVS_SERVER_VERSION;
348                 log.warn("problem getting cvs server version; using " + cvsServerVersion);
349             }
350         }
351         return cvsServerVersion;
352     }
353
354     /**
355      * This method retrieves the cvs server version from the specified output.
356      * The line it parses will have the following format:
357      * <pre>
358      * Server: Concurrent Versions System (CVS) 1.11.16 (client/server)
359      * </pre>
360      *
361      * @param in
362      * @return the version of null if the version couldn't be extracted
363      * @throws IOException
364      */

365     private Version extractCVSServerVersionFromCVSVersionCommandOutput(BufferedReader JavaDoc in) throws IOException JavaDoc {
366         String JavaDoc line = in.readLine();
367         if (line == null) {
368             return null;
369         }
370         if (line.startsWith("Client:")) {
371             line = in.readLine();
372             if (line == null) {
373                 return null;
374             }
375             if (!line.startsWith("Server:")) {
376                 log.warn("Warning expected a line starting with \"Server:\" but got " + line);
377                 // we try anyway
378
}
379         }
380         log.debug("server version line: " + line);
381         int nameBegin = line.indexOf(" (");
382         int nameEnd = line.indexOf(") ", nameBegin);
383         final String JavaDoc name;
384         final String JavaDoc version;
385         if (nameBegin == -1 || nameEnd < nameBegin || nameBegin + 2 >= line.length()) {
386             log.warn("cvs server version name couldn't be parsed from " + line);
387             return null;
388         }
389         name = line.substring(nameBegin + 2, nameEnd);
390         int verEnd = line.indexOf(" ", nameEnd + 2);
391         if (verEnd < nameEnd + 2) {
392             log.warn("cvs server version number couldn't be parsed from " + line);
393             return null;
394         }
395         version = line.substring(nameEnd + 2, verEnd);
396
397         return new Version(name, version);
398     }
399
400     public boolean isCvsNewOutputFormat() {
401         Version version = getCvsServerVersion();
402         if (OFFICIAL_CVS_NAME.equals(version.getCvsName())) {
403             String JavaDoc csv = version.getCvsVersion();
404             StringTokenizer JavaDoc st = new StringTokenizer JavaDoc(csv, ".");
405             try {
406                 st.nextToken();
407                 int subversion = Integer.parseInt(st.nextToken());
408                 if (subversion > 11) {
409                     if (subversion == 12) {
410                         if (Integer.parseInt(st.nextToken()) < 9) {
411                             return false;
412                         }
413                     }
414                     return true;
415                 }
416             } catch (Throwable JavaDoc e) {
417                 log.warn("problem identifying cvs server. Assuming output is of 'old' type");
418             }
419         }
420         return false;
421     }
422
423
424     public Map JavaDoc getProperties() {
425         return properties;
426     }
427
428     /**
429      * for mocking *
430      */

431     protected OSEnvironment getOSEnvironment() {
432         return new OSEnvironment();
433     }
434
435     public void validate() throws CruiseControlException {
436         ValidationHelper.assertFalse(local == null && (cvsroot == null || module == null),
437                 "must specify either 'localWorkingCopy' or 'cvsroot' and 'module' on CVS");
438         ValidationHelper.assertFalse(local != null && (cvsroot != null || module != null),
439                 "if 'localWorkingCopy' is specified then cvsroot and module are not allowed on CVS");
440         ValidationHelper.assertFalse(local != null && !new File JavaDoc(local).exists(),
441                 "Local working copy \"" + local + "\" does not exist!");
442     }
443
444     /**
445      * Returns a List of Modifications detailing all the changes between the
446      * last build and the latest revision at the repository
447      *
448      * @param lastBuild last build time
449      * @return maybe empty, never null.
450      */

451     public List JavaDoc getModifications(Date JavaDoc lastBuild, Date JavaDoc now) {
452         mailAliases = getMailAliases();
453
454         List JavaDoc mods = null;
455         try {
456             mods = execHistoryCommand(buildHistoryCommand(lastBuild, now));
457         } catch (Exception JavaDoc e) {
458             log.error("Log command failed to execute succesfully", e);
459         }
460
461         if (mods == null) {
462             return new ArrayList JavaDoc();
463         }
464         return mods;
465     }
466
467     /**
468      * Get CVS's idea of user/address mapping. Only runs once per class
469      * instance. Won't run if the mailAlias was already set.
470      *
471      * @return a Hashtable containing the mapping defined in CVSROOT/users.
472      * If CVSROOT/users doesn't exist, an empty Hashtable is returned.
473      */

474     private Hashtable JavaDoc getMailAliases() {
475         if (mailAliases == null) {
476             mailAliases = new Hashtable JavaDoc();
477             Commandline commandLine = getCommandline();
478             commandLine.setExecutable("cvs");
479
480             if (cvsroot != null) {
481                 commandLine.createArgument().setValue("-d");
482                 commandLine.createArgument().setValue(cvsroot);
483             }
484
485             commandLine.createArgument().setLine("-q co -p CVSROOT/users");
486
487             Process JavaDoc p = null;
488             try {
489                 if (local != null) {
490                     commandLine.setWorkingDirectory(local);
491                 }
492
493                 p = commandLine.execute();
494                 logErrorStream(p);
495                 InputStream JavaDoc is = p.getInputStream();
496                 BufferedReader JavaDoc in = new BufferedReader JavaDoc(new InputStreamReader JavaDoc(is));
497
498                 String JavaDoc line;
499                 while ((line = in.readLine()) != null) {
500                     addAliasToMap(line);
501                 }
502
503                 p.waitFor();
504                 p.getInputStream().close();
505                 p.getOutputStream().close();
506                 p.getErrorStream().close();
507             } catch (Exception JavaDoc e) {
508                 log.error("Failed reading mail aliases", e);
509             }
510
511             if (p == null || p.exitValue() != 0) {
512                 if (p == null) {
513                     log.debug("Process p was null in CVS.getMailAliases()");
514                 } else {
515                     log.debug("Process exit value = " + p.exitValue());
516                 }
517                 log.warn("problem getting CVSROOT/users; using empty email map");
518                 mailAliases = new Hashtable JavaDoc();
519             }
520         }
521
522         return mailAliases;
523     }
524
525     void addAliasToMap(String JavaDoc line) {
526         log.debug("Mapping " + line);
527         int colon = line.indexOf(':');
528
529         if (colon >= 0) {
530             String JavaDoc user = line.substring(0, colon);
531             String JavaDoc address = line.substring(colon + 1);
532             mailAliases.put(user, address);
533
534         }
535     }
536
537     /**
538      * @param lastBuildTime
539      * @param checkTime
540      * @return CommandLine for "cvs -d CVSROOT -q log -N -dlastbuildtime<checktime "
541      */

542     public Commandline buildHistoryCommand(Date JavaDoc lastBuildTime, Date JavaDoc checkTime) throws CruiseControlException {
543         Commandline commandLine = getCommandline();
544         commandLine.setExecutable("cvs");
545
546         if (cvsroot != null) {
547             commandLine.createArgument().setValue("-d");
548             commandLine.createArgument().setValue(cvsroot);
549         }
550         commandLine.createArgument().setValue("-q");
551
552         if (local != null) {
553             commandLine.setWorkingDirectory(local);
554             commandLine.createArgument().setValue("log");
555         } else {
556             commandLine.createArgument().setValue("rlog");
557         }
558
559         boolean useHead = tag == null || tag.equals(CVS_HEAD_TAG) || tag.equals("");
560         if (useHead) {
561             commandLine.createArgument().setValue("-N");
562         }
563         String JavaDoc dateRange = formatCVSDate(lastBuildTime) + "<" + formatCVSDate(checkTime);
564         commandLine.createArgument().setValue("-d" + dateRange);
565
566         if (!useHead) {
567             // add -b and -rTAG to list changes relative to the current branch,
568
// not relative to the default branch, which is HEAD
569

570             // note: -r cannot have a space between itself and the tag spec.
571
commandLine.createArgument().setValue("-r" + tag);
572         } else {
573             // This is used to include the head only if a Tag is not specified.
574
commandLine.createArgument().setValue("-b");
575         }
576
577         if (local == null) {
578             commandLine.createArgument().setValue(module);
579         }
580
581         return commandLine;
582     }
583
584     // factory method for mock...
585
protected Commandline getCommandline() {
586         return new Commandline();
587     }
588
589     static String JavaDoc formatCVSDate(Date JavaDoc date) {
590         return DateUtil.formatCVSDate(date);
591     }
592
593     /**
594      * Parses the input stream, which should be from the cvs log command. This
595      * method will format the data found in the input stream into a List of
596      * Modification instances.
597      *
598      * @param input InputStream to get log data from.
599      * @return List of Modification elements, maybe empty never null.
600      * @throws IOException
601      */

602     protected List JavaDoc parseStream(InputStream JavaDoc input) throws IOException JavaDoc {
603         BufferedReader JavaDoc reader = new BufferedReader JavaDoc(new InputStreamReader JavaDoc(input));
604
605         // Read to the first RCS file name. The first entry in the log
606
// information will begin with this line. A CVS_FILE_DELIMITER is NOT
607
// present. If no RCS file lines are found then there is nothing to do.
608

609         String JavaDoc line = readToNotPast(reader, CVS_RCSFILE_LINE, null);
610         ArrayList JavaDoc mods = new ArrayList JavaDoc();
611
612         while (line != null) {
613             // Parse the single file entry, which may include several
614
// modifications.
615
List JavaDoc returnList = parseEntry(reader, line);
616
617             //Add all the modifications to the local list.
618
mods.addAll(returnList);
619
620             // Read to the next RCS file line. The CVS_FILE_DELIMITER may have
621
// been consumed by the parseEntry method, so we cannot read to it.
622
line = readToNotPast(reader, CVS_RCSFILE_LINE, null);
623         }
624
625         return mods;
626     }
627
628     private void getRidOfLeftoverData(InputStream JavaDoc stream) {
629         StreamPumper outPumper = new StreamPumper(stream, (PrintWriter JavaDoc) null);
630         new Thread JavaDoc(outPumper).start();
631     }
632
633     private List JavaDoc execHistoryCommand(Commandline command) throws Exception JavaDoc {
634         Process JavaDoc p = command.execute();
635
636         logErrorStream(p);
637         InputStream JavaDoc cvsLogStream = p.getInputStream();
638         List JavaDoc mods = parseStream(cvsLogStream);
639
640         getRidOfLeftoverData(cvsLogStream);
641         p.waitFor();
642         p.getInputStream().close();
643         p.getOutputStream().close();
644         p.getErrorStream().close();
645
646         return mods;
647     }
648
649     protected void setMailAliases(Hashtable JavaDoc mailAliases) {
650         this.mailAliases = mailAliases;
651     }
652
653     private static void logErrorStream(Process JavaDoc p) {
654         logErrorStream(p.getErrorStream());
655     }
656     static void logErrorStream(InputStream JavaDoc error) {
657         StreamConsumer warnLogger = new StreamConsumer() {
658             public void consumeLine(String JavaDoc line) {
659                 log.warn(line);
660             }
661         };
662         StreamPumper errorPumper =
663                 new StreamPumper(error, null, warnLogger);
664         new Thread JavaDoc(errorPumper).start();
665     }
666
667     //(PENDING) Extract CVSEntryParser class
668

669     /**
670      * Parses a single file entry from the reader. This entry may contain zero or
671      * more revisions. This method may consume the next CVS_FILE_DELIMITER line
672      * from the reader, but no further.
673      * <p/>
674      * When the log is related to a non branch tag, only the last modification for each file will be listed.
675      *
676      * @param reader Reader to parse data from.
677      * @return modifications found in this entry; maybe empty, never null.
678      * @throws IOException
679      */

680     private List JavaDoc parseEntry(BufferedReader JavaDoc reader, String JavaDoc rcsLine) throws IOException JavaDoc {
681         ArrayList JavaDoc mods = new ArrayList JavaDoc();
682
683         String JavaDoc nextLine = "";
684
685         // Read to the working file name line to get the filename.
686
// If working file name line isn't found we'll extract is from the RCS file line
687
String JavaDoc workingFileName;
688         if (module != null && cvsroot != null) {
689             final String JavaDoc repositoryRoot = cvsroot.substring(cvsroot.lastIndexOf(":") + 1);
690             final int startAt = "RCS file: ".length() + repositoryRoot.length();
691             workingFileName = rcsLine.substring(startAt, rcsLine.length() - 2);
692         } else {
693             String JavaDoc workingFileLine = readToNotPast(reader, CVS_WORKINGFILE_LINE, null);
694             workingFileName = workingFileLine.substring(CVS_WORKINGFILE_LINE.length());
695         }
696
697         String JavaDoc branchRevisionName = parseBranchRevisionName(reader, tag);
698         boolean newCVSVersion = isCvsNewOutputFormat();
699         while (nextLine != null && !nextLine.startsWith(CVS_FILE_DELIM)) {
700             nextLine = readToNotPast(reader, "revision", CVS_FILE_DELIM);
701             if (nextLine == null) {
702                 //No more revisions for this file.
703
break;
704             }
705
706             StringTokenizer JavaDoc tokens = new StringTokenizer JavaDoc(nextLine, " ");
707             tokens.nextToken();
708             String JavaDoc revision = tokens.nextToken();
709             if (tag != null && !tag.equals(CVS_HEAD_TAG)) {
710                 if (!revision.equals(branchRevisionName)) {
711                     // Indeed this is a branch, not just a regular tag
712
String JavaDoc itsBranchRevisionName = revision.substring(0, revision.lastIndexOf('.'));
713                     if (!itsBranchRevisionName.equals(branchRevisionName)) {
714                         break;
715                     }
716                 }
717             }
718
719             // Read to the revision date. It is ASSUMED that each revision
720
// section will include this date information line.
721
nextLine = readToNotPast(reader, CVS_REVISION_DATE, CVS_FILE_DELIM);
722             if (nextLine == null) {
723                 break;
724             }
725
726             tokens = new StringTokenizer JavaDoc(nextLine, " \t\n\r\f;");
727             // First token is the keyword for date, then the next two should be
728
// the date and time stamps.
729
tokens.nextToken();
730             String JavaDoc dateStamp = tokens.nextToken();
731             String JavaDoc timeStamp = tokens.nextToken();
732
733             // skips the +0000 part of new format
734
if (newCVSVersion) {
735                 tokens.nextToken();
736             }
737             // The next token should be the author keyword, then the author name.
738
tokens.nextToken();
739             String JavaDoc authorName = tokens.nextToken();
740
741             // The next token should be the state keyword, then the state name.
742
tokens.nextToken();
743             String JavaDoc stateKeyword = tokens.nextToken();
744
745             // if no lines keyword then file is added
746
boolean isAdded = !tokens.hasMoreTokens();
747
748             // All the text from now to the next revision delimiter or working
749
// file delimiter constitutes the messsage.
750
String JavaDoc message = "";
751             nextLine = reader.readLine();
752             boolean multiLine = false;
753
754             while (nextLine != null
755                     && !nextLine.startsWith(CVS_FILE_DELIM)
756                     && !nextLine.startsWith(CVS_REVISION_DELIM)) {
757
758                 if (multiLine) {
759                     message += NEW_LINE;
760                 } else {
761                     multiLine = true;
762                 }
763                 message += nextLine;
764
765                 //Go to the next line.
766
nextLine = reader.readLine();
767             }
768
769             Modification nextModification = new Modification("cvs");
770             nextModification.revision = revision;
771
772             int lastSlashIndex = workingFileName.lastIndexOf("/");
773
774             String JavaDoc fileName, folderName = null;
775             fileName = workingFileName.substring(lastSlashIndex + 1);
776             if (lastSlashIndex != -1) {
777                 folderName = workingFileName.substring(0, lastSlashIndex);
778             }
779             Modification.ModifiedFile modfile = nextModification.createModifiedFile(fileName, folderName);
780             modfile.revision = nextModification.revision;
781
782             try {
783                 if (newCVSVersion) {
784                     nextModification.modifiedTime = DateUtil.parseCVSDate(
785                             dateStamp + " " + timeStamp + " GMT");
786                 } else {
787                     nextModification.modifiedTime = logDateFormatter.parse(dateStamp + " " + timeStamp + " GMT");
788                 }
789             } catch (ParseException JavaDoc pe) {
790                 log.error("Error parsing cvs log for date and time", pe);
791                 return null;
792             }
793
794             nextModification.userName = authorName;
795
796             String JavaDoc address = (String JavaDoc) mailAliases.get(authorName);
797             if (address != null) {
798                 nextModification.emailAddress = address;
799             }
800
801             nextModification.comment = (message != null ? message : "");
802
803             if (stateKeyword.equalsIgnoreCase(CVS_REVISION_DEAD)
804                     && message.indexOf("was initially added on branch") != -1) {
805                 log.debug("skipping branch addition activity for " + nextModification);
806                 //this prevents additions to a branch from showing up as action "deleted" from head
807
continue;
808             }
809
810             if (stateKeyword.equalsIgnoreCase(CVS_REVISION_DEAD)) {
811                 modfile.action = "deleted";
812                 if (propertyOnDelete != null) {
813                     properties.put(propertyOnDelete, "true");
814                 }
815             } else if (isAdded) {
816                 modfile.action = "added";
817             } else {
818                 modfile.action = "modified";
819             }
820             if (property != null) {
821                 properties.put(property, "true");
822             }
823             mods.add(nextModification);
824         }
825         return mods;
826     }
827
828     /**
829      * Find the CVS branch revision name, when the tag is not HEAD
830      * The reader will consume all lines up to the next description.
831      *
832      * @param reader the reader
833      * @param tag may be null
834      * @return the branch revision name, or <code>null</code> if not applicable or none was found.
835      * @throws IOException
836      */

837     private static String JavaDoc parseBranchRevisionName(BufferedReader JavaDoc reader, final String JavaDoc tag) throws IOException JavaDoc {
838         String JavaDoc branchRevisionName = null;
839
840         if (tag != null && !tag.equals(CVS_HEAD_TAG)) {
841             // Look for the revision of the form "tag: *.(0.)y ". this doesn't work for HEAD
842
// get line with branch revision on it.
843

844             String JavaDoc branchRevisionLine = readToNotPast(reader, "\t" + tag + ": ", CVS_DESCRIPTION);
845
846             if (branchRevisionLine != null) {
847                 // Look for the revision of the form "tag: *.(0.)y ", return "*.y"
848
branchRevisionName = branchRevisionLine.substring(tag.length() + 3);
849                 if (branchRevisionName.charAt(branchRevisionName.lastIndexOf(".") - 1) == '0') {
850                     branchRevisionName =
851                             branchRevisionName.substring(0, branchRevisionName.lastIndexOf(".") - 2)
852                                     + branchRevisionName.substring(branchRevisionName.lastIndexOf("."));
853                 }
854             }
855         }
856         return branchRevisionName;
857     }
858
859     /**
860      * This method will consume lines from the reader up to the line that begins
861      * with the String specified but not past a line that begins with the
862      * notPast String. If the line that begins with the beginsWith String is
863      * found then it will be returned. Otherwise null is returned.
864      *
865      * @param reader Reader to read lines from.
866      * @param beginsWith String to match to the beginning of a line.
867      * @param notPast String which indicates that lines should stop being consumed,
868      * even if the begins with match has not been found. Pass null to this
869      * method to ignore this string.
870      * @return String that begin as indicated, or null if none matched to the end
871      * of the reader or the notPast line was found.
872      * @throws IOException
873      */

874     private static String JavaDoc readToNotPast(BufferedReader JavaDoc reader, String JavaDoc beginsWith, String JavaDoc notPast)
875             throws IOException JavaDoc {
876         boolean checkingNotPast = notPast != null;
877
878         String JavaDoc nextLine = reader.readLine();
879         while (nextLine != null && !nextLine.startsWith(beginsWith)) {
880             if (checkingNotPast && nextLine.startsWith(notPast)) {
881                 return null;
882             }
883             nextLine = reader.readLine();
884         }
885
886         return nextLine;
887     }
888
889 }
890
Popular Tags