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