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