View Javadoc

1   /*
2   	StatCvs - CVS statistics generation 
3   	Copyright (C) 2002  Lukasz Pekacki <lukasz@pekacki.de>
4   	http://statcvs.sf.net/
5       
6   	This library is free software; you can redistribute it and/or
7   	modify it under the terms of the GNU Lesser General Public
8   	License as published by the Free Software Foundation; either
9   	version 2.1 of the License, or (at your option) any later version.
10  
11  	This library is distributed in the hope that it will be useful,
12  	but WITHOUT ANY WARRANTY; without even the implied warranty of
13  	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  	Lesser General Public License for more details.
15  
16  	You should have received a copy of the GNU Lesser General Public
17  	License along with this library; if not, write to the Free Software
18  	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19      
20  	$RCSfile: FileBuilder.java,v $
21  	$Date: 2008/04/02 11:22:14 $
22  */
23  package net.sf.statcvs.input;
24  
25  import java.io.IOException;
26  import java.util.ArrayList;
27  import java.util.Date;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.SortedSet;
32  import java.util.TreeSet;
33  import java.util.logging.Logger;
34  
35  import net.sf.statcvs.model.Revision;
36  import net.sf.statcvs.model.VersionedFile;
37  
38  /**
39   * <p>Builds a {@link VersionedFile} with {@link Revision}s from logging data.
40   * This class is responsible for deciding if a file or revisions will be
41   * included in the report, for translating from CVS logfile data structures
42   * to the data structures in the <tt>net.sf.statcvs.model</tt> package, and
43   * for calculating the LOC history for the file.</p>
44   * 
45   * <p>A main goal of this class is to delay the creation of the <tt>VersionedFile</tt>
46   * object until all revisions of the file have been collected from the log.
47   * We could simply create <tt>VersionedFile</tt> and <tt>Revision</tt>s on the fly
48   * as we parse through the log, but this creates a problem if we decide not
49   * to include the file after reading several revisions. The creation of a
50   * <tt>VersionedFile</tt> or <tt>Revision</tt> can cause many more objects to
51   * be created (<tt>Author</tt>, <tt>Directory</tt>, <tt>Commit</tt>), and
52   * it would be very hard to get rid of them if we don't want the file. This
53   * problem is solved by first collecting all information about one file in
54   * this class, and then, with all information present, deciding if we want
55   * to create the model instances or not.</p>
56   * 
57   * @author Richard Cyganiak <richard@cyganiak.de>
58   * @author Tammo van Lessen
59   * @version $Id: FileBuilder.java,v 1.18 2008/04/02 11:22:14 benoitx Exp $
60   */
61  public class FileBuilder {
62      private static Logger logger = Logger.getLogger(FileBuilder.class.getName());
63  
64      private final Builder builder;
65      private final String name;
66      private final boolean isBinary;
67      private final List revisions = new ArrayList();
68      private RevisionData lastAdded = null;
69      private final Map revBySymnames;
70  
71      private int locDelta;
72      private boolean flagLocalFileNotFound = false;
73      private boolean flagUnexpectedLocalRevision = false;
74      private boolean flagNoLocalCVSMetadata = false;
75  
76      /**
77       * Creates a new <tt>FileBuilder</tt>.
78       * 
79       * @param builder a <tt>Builder</tt> that provides factory services for
80       * author and directory instances and line counts.
81       * @param name the filename
82       * @param isBinary Is this a binary file or not?
83       */
84      public FileBuilder(final Builder builder, final String name, final boolean isBinary, final Map revBySymnames) {
85          this.builder = builder;
86          this.name = name;
87          this.isBinary = isBinary;
88          this.revBySymnames = revBySymnames;
89  
90          logger.fine("logging " + name);
91      }
92  
93      /**
94       * Adds a revision to the file. The revisions must be added in the
95       * same order as they appear in the CVS logfile, that is, most recent
96       * first.
97       * 
98       * @param data the revision
99       */
100     public void addRevisionData(final RevisionData data) {
101         if (!data.isOnTrunk()) {
102             return;
103         }
104         if (isBinary && !data.isCreation()) {
105             data.setLines(0, 0);
106         }
107         this.revisions.add(data);
108         lastAdded = data;
109 
110         locDelta += getLOCChange(data);
111     }
112 
113     /**
114      * Creates and returns a {@link VersionedFile} representation of the file.
115      * <tt>null</tt> is returned if the file does not meet certain criteria,
116      * for example if its filename meets an exclude filter or if it was dead
117      * during the entire logging timespan.
118      * 
119      * @param beginOfLogDate the date of the begin of the log
120      * @return a <tt>VersionedFile</tt> representation of the file.
121      */
122     public VersionedFile createFile(final Date beginOfLogDate) {
123         if (isFilteredFile() || !fileExistsInLogPeriod()) {
124             return null;
125         }
126         if (revisions.size() == 1 && lastAdded.isAddOnSubbranch()) {
127             return null;
128         }
129 
130         final VersionedFile file = new VersionedFile(name, builder.getDirectory(name));
131 
132         if (revisions.isEmpty()) {
133             buildBeginOfLogRevision(file, beginOfLogDate, getFinalLOC(), null);
134             return file;
135         }
136 
137         final Iterator it = revisions.iterator();
138         RevisionData currentData = (RevisionData) it.next();
139         int currentLOC = getFinalLOC();
140         RevisionData previousData;
141         int previousLOC;
142         SortedSet symbolicNames;
143 
144         while (it.hasNext()) {
145             previousData = currentData;
146             previousLOC = currentLOC;
147             currentData = (RevisionData) it.next();
148             currentLOC = previousLOC - getLOCChange(previousData);
149 
150             // symbolic names for previousData
151             symbolicNames = createSymbolicNamesCollection(previousData);
152 
153             if (previousData.isChangeOrRestore()) {
154                 if (currentData.isDeletion() || currentData.isAddOnSubbranch()) {
155                     buildCreationRevision(file, previousData, previousLOC, symbolicNames);
156                 } else {
157                     buildChangeRevision(file, previousData, previousLOC, symbolicNames);
158                 }
159             } else if (previousData.isDeletion()) {
160                 buildDeletionRevision(file, previousData, previousLOC, symbolicNames);
161             } else {
162                 logger.warning("illegal state in " + file.getFilenameWithPath() + ":" + previousData.getRevisionNumber());
163             }
164         }
165 
166         // symbolic names for currentData
167         symbolicNames = createSymbolicNamesCollection(currentData);
168 
169         final int nextLinesOfCode = currentLOC - getLOCChange(currentData);
170         if (currentData.isCreation()) {
171             buildCreationRevision(file, currentData, currentLOC, symbolicNames);
172         } else if (currentData.isDeletion()) {
173             buildDeletionRevision(file, currentData, currentLOC, symbolicNames);
174             buildBeginOfLogRevision(file, beginOfLogDate, nextLinesOfCode, symbolicNames);
175         } else if (currentData.isChangeOrRestore()) {
176             buildChangeRevision(file, currentData, currentLOC, symbolicNames);
177             buildBeginOfLogRevision(file, beginOfLogDate, nextLinesOfCode, symbolicNames);
178         } else if (currentData.isAddOnSubbranch()) {
179             // ignore
180         } else {
181             logger.warning("illegal state in " + file.getFilenameWithPath() + ":" + currentData.getRevisionNumber());
182         }
183         return file;
184     }
185 
186     public boolean hasUnexpectedLocalRevision() {
187         return this.flagUnexpectedLocalRevision;
188     }
189 
190     public boolean hasLocalFileNotFound() {
191         return this.flagLocalFileNotFound;
192     }
193 
194     public boolean hasLocalCVSMetadata() {
195         return !this.flagNoLocalCVSMetadata;
196     }
197 
198     /**
199      * Gets a LOC count for the file's most recent revision. If the file
200      * exists in the local checkout, we ask the {@link RepositoryFileManager}
201      * to count its lines of code. If not (that is, it is dead), return
202      * an approximated LOC value for its last non-dead revision.
203      *  
204      * @return the LOC count for the file's most recent revision.
205      */
206     private int getFinalLOC() {
207         if (isBinary) {
208             return 0;
209         } else if (lastAdded != null && lastAdded.isAddOnSubbranch()) {
210             return locDelta;
211         }
212 
213         String revision = null;
214         try {
215             revision = builder.getRevision(name);
216         } catch (final IOException e) {
217             if (!finalRevisionIsDead()) {
218                 logger.info(e.getMessage());
219                 this.flagNoLocalCVSMetadata = true;
220             }
221             revision = "???";
222         }
223 
224         try {
225             if ("1.1".equals(revision)) {
226                 return builder.getLOC(name) + locDelta;
227             } else {
228                 if (!revisions.isEmpty()) {
229                     final RevisionData firstAdded = (RevisionData) revisions.get(0);
230                     if (!finalRevisionIsDead() && !firstAdded.getRevisionNumber().equals(revision)) {
231                         if (!"???".equals(revision)) {
232                             logger.info(this.name + " should be at " + firstAdded.getRevisionNumber() + " but is at " + revision);
233                         }
234                         this.flagUnexpectedLocalRevision = true;
235                     }
236                 }
237                 return builder.getLOC(name);
238             }
239         } catch (final NoLineCountException e) {
240             if (!finalRevisionIsDead()) {
241                 logger.info("No line count for " + this.name);
242                 this.flagLocalFileNotFound = true;
243             }
244             return approximateFinalLOC();
245         }
246     }
247 
248     /**
249      * Returns <tt>true</tt> if the file's most recent revision is dead.
250      * 
251      * @return <tt>true</tt> if the file is dead.
252      */
253     private boolean finalRevisionIsDead() {
254         if (revisions.isEmpty()) {
255             return false;
256         }
257         return ((RevisionData) revisions.get(0)).isDeletion();
258     }
259 
260     /**
261      * Approximates the LOC count for files that are not present in the
262      * local checkout. If a file was deleted at some point in history, then
263      * we can't count its final lines of code. This algorithm calculates
264      * a lower bound for the file's LOC prior to deletion by following the
265      * ups and downs of the revisions.
266      * 
267      * @return a lower bound for the file's LOC before it was deleted
268      */
269     private int approximateFinalLOC() {
270         int max = 0;
271         int current = 0;
272         final Iterator it = revisions.iterator();
273         while (it.hasNext()) {
274             final RevisionData data = (RevisionData) it.next();
275             current += data.getLinesAdded();
276             max = Math.max(current, max);
277             current -= data.getLinesRemoved();
278         }
279         return max;
280     }
281 
282     /**
283      * Returns the change in LOC count caused by a revision. If there were
284      * 10 lines added and 3 lines removed, 7 would be returned. This does
285      * not take into account file deletion and creation.
286      * 
287      * @param data a revision
288      * @return the change in LOC count
289      */
290     private int getLOCChange(final RevisionData data) {
291         return data.getLinesAdded() - data.getLinesRemoved();
292     }
293 
294     private void buildCreationRevision(final VersionedFile file, final RevisionData data, final int loc, final SortedSet symbolicNames) {
295         file.addInitialRevision(data.getRevisionNumber(), builder.getAuthor(data.getLoginName()), data.getDate(), data.getComment(), loc, symbolicNames);
296     }
297 
298     private void buildChangeRevision(final VersionedFile file, final RevisionData data, final int loc, final SortedSet symbolicNames) {
299         file.addChangeRevision(data.getRevisionNumber(), builder.getAuthor(data.getLoginName()), data.getDate(), data.getComment(), loc, data.getLinesAdded()
300                 - data.getLinesRemoved(), Math.min(data.getLinesAdded(), data.getLinesRemoved()), symbolicNames);
301     }
302 
303     private void buildDeletionRevision(final VersionedFile file, final RevisionData data, final int loc, final SortedSet symbolicNames) {
304         file.addDeletionRevision(data.getRevisionNumber(), builder.getAuthor(data.getLoginName()), data.getDate(), data.getComment(), loc, symbolicNames);
305     }
306 
307     private void buildBeginOfLogRevision(final VersionedFile file, final Date beginOfLogDate, final int loc, final SortedSet symbolicNames) {
308         final Date date = new Date(beginOfLogDate.getTime() - 60000);
309         file.addBeginOfLogRevision(date, loc, symbolicNames);
310     }
311 
312     /**
313      * Takes a filename and checks if it should be processed or not.
314      * Can be used to filter out unwanted files.
315      * 
316      * @return <tt>true</tt> if this file should not be processed
317      */
318     private boolean isFilteredFile() {
319         return !this.builder.matchesPatterns(this.name);
320     }
321 
322     /**
323      * Returns <tt>false</tt> if the file did never exist in the timespan
324      * covered by the log. For our purposes, a file is non-existant if it
325      * has no revisions and does not exists in the module checkout.
326      * Note: A file with no revisions
327      * must be included in the report if it does exist in the module checkout.
328      * This happens if it was created before the log started, and not changed
329      * before the log ended.
330      * @return <tt>true</tt> if the file did exist at some point in the log period.
331      */
332     private boolean fileExistsInLogPeriod() {
333         if (revisions.size() > 0) {
334             return true;
335         }
336         try {
337             builder.getLOC(name);
338             return true;
339         } catch (final NoLineCountException fileDoesNotExistInTimespan) {
340             return false;
341         }
342     }
343 
344     /**
345      * Creates a sorted set containing all symbolic name objects affected by 
346      * this revision.
347      * If this revision has no symbolic names, this method returns null.
348      * 
349      * @param revisionData this revision
350      * @return the sorted set or null
351      */
352     private SortedSet createSymbolicNamesCollection(final RevisionData revisionData) {
353         SortedSet symbolicNames = null;
354 
355         final Iterator symIt = revBySymnames.keySet().iterator();
356         while (symIt.hasNext()) {
357             final String symName = (String) symIt.next();
358             final String rev = (String) revBySymnames.get(symName);
359             if (revisionData.getRevisionNumber().equals(rev)) {
360                 if (symbolicNames == null) {
361                     symbolicNames = new TreeSet();
362                 }
363                 logger.fine("adding revision " + name + "," + revisionData.getRevisionNumber() + " to symname " + symName);
364                 symbolicNames.add(builder.getSymbolicName(symName));
365             }
366         }
367 
368         return symbolicNames;
369     }
370 }