Coverage Report - net.sf.statcvs.input.FileBuilder
 
Classes in this File Line Coverage Branch Coverage Complexity
FileBuilder
91%
118/129
84%
59/70
3.412
 
 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  48
     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  228
     private final List revisions = new ArrayList();
 68  228
     private RevisionData lastAdded = null;
 69  
     private final Map revBySymnames;
 70  
 
 71  
     private int locDelta;
 72  228
     private boolean flagLocalFileNotFound = false;
 73  228
     private boolean flagUnexpectedLocalRevision = false;
 74  228
     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  228
     public FileBuilder(final Builder builder, final String name, final boolean isBinary, final Map revBySymnames) {
 85  228
         this.builder = builder;
 86  228
         this.name = name;
 87  228
         this.isBinary = isBinary;
 88  228
         this.revBySymnames = revBySymnames;
 89  
 
 90  228
         logger.fine("logging " + name);
 91  228
     }
 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  270
         if (!data.isOnTrunk()) {
 102  18
             return;
 103  
         }
 104  252
         if (isBinary && !data.isCreation()) {
 105  18
             data.setLines(0, 0);
 106  
         }
 107  252
         this.revisions.add(data);
 108  252
         lastAdded = data;
 109  
 
 110  252
         locDelta += getLOCChange(data);
 111  252
     }
 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  198
         if (isFilteredFile() || !fileExistsInLogPeriod()) {
 124  24
             return null;
 125  
         }
 126  174
         if (revisions.size() == 1 && lastAdded.isAddOnSubbranch()) {
 127  18
             return null;
 128  
         }
 129  
 
 130  156
         final VersionedFile file = new VersionedFile(name, builder.getDirectory(name));
 131  
 
 132  156
         if (revisions.isEmpty()) {
 133  18
             buildBeginOfLogRevision(file, beginOfLogDate, getFinalLOC(), null);
 134  18
             return file;
 135  
         }
 136  
 
 137  138
         final Iterator it = revisions.iterator();
 138  138
         RevisionData currentData = (RevisionData) it.next();
 139  138
         int currentLOC = getFinalLOC();
 140  
         RevisionData previousData;
 141  
         int previousLOC;
 142  
         SortedSet symbolicNames;
 143  
 
 144  240
         while (it.hasNext()) {
 145  102
             previousData = currentData;
 146  102
             previousLOC = currentLOC;
 147  102
             currentData = (RevisionData) it.next();
 148  102
             currentLOC = previousLOC - getLOCChange(previousData);
 149  
 
 150  
             // symbolic names for previousData
 151  102
             symbolicNames = createSymbolicNamesCollection(previousData);
 152  
 
 153  102
             if (previousData.isChangeOrRestore()) {
 154  78
                 if (currentData.isDeletion() || currentData.isAddOnSubbranch()) {
 155  12
                     buildCreationRevision(file, previousData, previousLOC, symbolicNames);
 156  
                 } else {
 157  66
                     buildChangeRevision(file, previousData, previousLOC, symbolicNames);
 158  
                 }
 159  24
             } else if (previousData.isDeletion()) {
 160  24
                 buildDeletionRevision(file, previousData, previousLOC, symbolicNames);
 161  
             } else {
 162  0
                 logger.warning("illegal state in " + file.getFilenameWithPath() + ":" + previousData.getRevisionNumber());
 163  
             }
 164  
         }
 165  
 
 166  
         // symbolic names for currentData
 167  138
         symbolicNames = createSymbolicNamesCollection(currentData);
 168  
 
 169  138
         final int nextLinesOfCode = currentLOC - getLOCChange(currentData);
 170  138
         if (currentData.isCreation()) {
 171  114
             buildCreationRevision(file, currentData, currentLOC, symbolicNames);
 172  24
         } else if (currentData.isDeletion()) {
 173  0
             buildDeletionRevision(file, currentData, currentLOC, symbolicNames);
 174  0
             buildBeginOfLogRevision(file, beginOfLogDate, nextLinesOfCode, symbolicNames);
 175  24
         } else if (currentData.isChangeOrRestore()) {
 176  18
             buildChangeRevision(file, currentData, currentLOC, symbolicNames);
 177  18
             buildBeginOfLogRevision(file, beginOfLogDate, nextLinesOfCode, symbolicNames);
 178  6
         } else if (currentData.isAddOnSubbranch()) {
 179  
             // ignore
 180  
         } else {
 181  0
             logger.warning("illegal state in " + file.getFilenameWithPath() + ":" + currentData.getRevisionNumber());
 182  
         }
 183  138
         return file;
 184  
     }
 185  
 
 186  
     public boolean hasUnexpectedLocalRevision() {
 187  126
         return this.flagUnexpectedLocalRevision;
 188  
     }
 189  
 
 190  
     public boolean hasLocalFileNotFound() {
 191  126
         return this.flagLocalFileNotFound;
 192  
     }
 193  
 
 194  
     public boolean hasLocalCVSMetadata() {
 195  126
         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  156
         if (isBinary) {
 208  24
             return 0;
 209  132
         } else if (lastAdded != null && lastAdded.isAddOnSubbranch()) {
 210  6
             return locDelta;
 211  
         }
 212  
 
 213  126
         String revision = null;
 214  
         try {
 215  126
             revision = builder.getRevision(name);
 216  126
         } catch (final IOException e) {
 217  126
             if (!finalRevisionIsDead()) {
 218  114
                 logger.info(e.getMessage());
 219  114
                 this.flagNoLocalCVSMetadata = true;
 220  
             }
 221  126
             revision = "???";
 222  0
         }
 223  
 
 224  
         try {
 225  126
             if ("1.1".equals(revision)) {
 226  0
                 return builder.getLOC(name) + locDelta;
 227  
             } else {
 228  126
                 if (!revisions.isEmpty()) {
 229  108
                     final RevisionData firstAdded = (RevisionData) revisions.get(0);
 230  108
                     if (!finalRevisionIsDead() && !firstAdded.getRevisionNumber().equals(revision)) {
 231  96
                         if (!"???".equals(revision)) {
 232  0
                             logger.info(this.name + " should be at " + firstAdded.getRevisionNumber() + " but is at " + revision);
 233  
                         }
 234  96
                         this.flagUnexpectedLocalRevision = true;
 235  
                     }
 236  
                 }
 237  126
                 return builder.getLOC(name);
 238  
             }
 239  72
         } catch (final NoLineCountException e) {
 240  72
             if (!finalRevisionIsDead()) {
 241  60
                 logger.info("No line count for " + this.name);
 242  60
                 this.flagLocalFileNotFound = true;
 243  
             }
 244  72
             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  306
         if (revisions.isEmpty()) {
 255  18
             return false;
 256  
         }
 257  288
         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  72
         int max = 0;
 271  72
         int current = 0;
 272  72
         final Iterator it = revisions.iterator();
 273  216
         while (it.hasNext()) {
 274  144
             final RevisionData data = (RevisionData) it.next();
 275  144
             current += data.getLinesAdded();
 276  144
             max = Math.max(current, max);
 277  144
             current -= data.getLinesRemoved();
 278  120
         }
 279  72
         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  492
         return data.getLinesAdded() - data.getLinesRemoved();
 292  
     }
 293  
 
 294  
     private void buildCreationRevision(final VersionedFile file, final RevisionData data, final int loc, final SortedSet symbolicNames) {
 295  126
         file.addInitialRevision(data.getRevisionNumber(), builder.getAuthor(data.getLoginName()), data.getDate(), data.getComment(), loc, symbolicNames);
 296  126
     }
 297  
 
 298  
     private void buildChangeRevision(final VersionedFile file, final RevisionData data, final int loc, final SortedSet symbolicNames) {
 299  84
         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  84
     }
 302  
 
 303  
     private void buildDeletionRevision(final VersionedFile file, final RevisionData data, final int loc, final SortedSet symbolicNames) {
 304  24
         file.addDeletionRevision(data.getRevisionNumber(), builder.getAuthor(data.getLoginName()), data.getDate(), data.getComment(), loc, symbolicNames);
 305  24
     }
 306  
 
 307  
     private void buildBeginOfLogRevision(final VersionedFile file, final Date beginOfLogDate, final int loc, final SortedSet symbolicNames) {
 308  36
         final Date date = new Date(beginOfLogDate.getTime() - 60000);
 309  36
         file.addBeginOfLogRevision(date, loc, symbolicNames);
 310  36
     }
 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  198
         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  198
         if (revisions.size() > 0) {
 334  156
             return true;
 335  
         }
 336  
         try {
 337  42
             builder.getLOC(name);
 338  18
             return true;
 339  24
         } catch (final NoLineCountException fileDoesNotExistInTimespan) {
 340  24
             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  240
         SortedSet symbolicNames = null;
 354  
 
 355  240
         final Iterator symIt = revBySymnames.keySet().iterator();
 356  276
         while (symIt.hasNext()) {
 357  36
             final String symName = (String) symIt.next();
 358  36
             final String rev = (String) revBySymnames.get(symName);
 359  36
             if (revisionData.getRevisionNumber().equals(rev)) {
 360  0
                 if (symbolicNames == null) {
 361  0
                     symbolicNames = new TreeSet();
 362  
                 }
 363  0
                 logger.fine("adding revision " + name + "," + revisionData.getRevisionNumber() + " to symname " + symName);
 364  0
                 symbolicNames.add(builder.getSymbolicName(symName));
 365  
             }
 366  30
         }
 367  
 
 368  240
         return symbolicNames;
 369  
     }
 370  
 }