1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
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
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
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 }