Coverage Report - net.sf.statcvs.input.Builder
 
Classes in this File Line Coverage Branch Coverage Complexity
Builder
85%
104/123
62%
41/66
2.65
 
 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: Builder.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.HashMap;
 29  
 import java.util.HashSet;
 30  
 import java.util.Iterator;
 31  
 import java.util.List;
 32  
 import java.util.Map;
 33  
 import java.util.Properties;
 34  
 import java.util.Set;
 35  
 import java.util.SortedSet;
 36  
 import java.util.TreeSet;
 37  
 import java.util.logging.Logger;
 38  
 import java.util.regex.Pattern;
 39  
 
 40  
 import net.sf.statcvs.model.Author;
 41  
 import net.sf.statcvs.model.Directory;
 42  
 import net.sf.statcvs.model.Repository;
 43  
 import net.sf.statcvs.model.SymbolicName;
 44  
 import net.sf.statcvs.model.VersionedFile;
 45  
 import net.sf.statcvs.output.ConfigurationOptions;
 46  
 import net.sf.statcvs.util.FilePatternMatcher;
 47  
 import net.sf.statcvs.util.FileUtils;
 48  
 
 49  
 /**
 50  
  * <p>Helps building the {@link net.sf.statcvs.model.Repository} from a CVS
 51  
  * log. The <tt>Builder</tt> is fed by some CVS history data source, for
 52  
  * example a CVS log parser. The <tt>Repository</tt> can be retrieved
 53  
  * using the {@link #createCvsContent} method.</p>
 54  
  * 
 55  
  * <p>The class also takes care of the creation of <tt>Author</tt> and 
 56  
  * </tt>Directory</tt> objects and makes sure that there's only one of these
 57  
  * for each author name and path. It also provides LOC count services.</p>
 58  
  * 
 59  
  * @author Richard Cyganiak <richard@cyganiak.de>
 60  
  * @version $Id: Builder.java,v 1.40 2008/04/02 11:22:14 benoitx Exp $
 61  
  */
 62  
 public class Builder implements CvsLogBuilder {
 63  32
     private static Logger logger = Logger.getLogger(Builder.class.getName());
 64  
 
 65  
     private final RepositoryFileManager repositoryFileManager;
 66  
     private final FilePatternMatcher includePattern;
 67  
     private final FilePatternMatcher excludePattern;
 68  
     private final Pattern tagsPattern;
 69  
 
 70  156
     private final Map authors = new HashMap();
 71  156
     private final Map directories = new HashMap();
 72  156
     private final Map symbolicNames = new HashMap();
 73  
 
 74  156
     private final List fileBuilders = new ArrayList();
 75  156
     private final Set atticFileNames = new HashSet();
 76  
 
 77  156
     private FileBuilder currentFileBuilder = null;
 78  156
     private Date startDate = null;
 79  156
     private String projectName = null;
 80  
 
 81  156
     private int countRejectedByExclude = 0;
 82  156
     private int countAcceptedByExclude = 0;
 83  156
     private int countRejectedByInclude = 0;
 84  156
     private int countAcceptedByInclude = 0;
 85  156
     private boolean flagOutOfSync = false;
 86  156
     private boolean flagHasLocalCVSMetadata = false;
 87  156
     private int countFoundLocalFiles = 0;
 88  156
     private int countNotFoundLocalFiles = 0;
 89  
 
 90  
     /**
 91  
      * Creates a new <tt>Builder</tt>
 92  
      * @param repositoryFileManager the {@link RepositoryFileManager} that
 93  
      *                                                                 can be used to retrieve LOC counts for
 94  
      *                                                                 the files that this builder will create
 95  
      * @param includePattern a list of Ant-style wildcard patterns, seperated
 96  
      *                       by : or ;
 97  
      * @param excludePattern a list of Ant-style wildcard patterns, seperated
 98  
      *                       by : or ;
 99  
      * @param tagsPattern A regular expression; matching symbolic names are recorded
 100  
      */
 101  
     public Builder(final RepositoryFileManager repositoryFileManager, final FilePatternMatcher includePattern, final FilePatternMatcher excludePattern,
 102  156
             final Pattern tagsPattern) {
 103  156
         this.repositoryFileManager = repositoryFileManager;
 104  156
         this.includePattern = includePattern;
 105  156
         this.excludePattern = excludePattern;
 106  156
         this.tagsPattern = tagsPattern;
 107  156
         directories.put("", Directory.createRoot());
 108  156
     }
 109  
 
 110  
     /**
 111  
      * Starts building the module.
 112  
      * 
 113  
      * @param moduleName name of the module
 114  
      */
 115  
     public void buildModule(final String moduleName) {
 116  60
         this.projectName = moduleName;
 117  60
     }
 118  
 
 119  
     /**
 120  
      * Starts building a new file. The files are not expected to be created
 121  
      * in any particular order.
 122  
      * @param filename the file's name with path, for example "path/file.txt"
 123  
      * @param isBinary <tt>true</tt> if it's a binary file
 124  
      * @param isInAttic <tt>true</tt> if the file is dead on the main branch
 125  
      * @param revBySymnames maps revision (string) by symbolic name (string)
 126  
      */
 127  
     public void buildFile(final String filename, final boolean isBinary, final boolean isInAttic, final Map revBySymnames) {
 128  124
         if (currentFileBuilder != null) {
 129  40
             fileBuilders.add(currentFileBuilder);
 130  
         }
 131  124
         currentFileBuilder = new FileBuilder(this, filename, isBinary, revBySymnames);
 132  124
         if (isInAttic) {
 133  12
             atticFileNames.add(filename);
 134  
         }
 135  124
     }
 136  
 
 137  
     /**
 138  
      * Adds a revision to the current file. The revisions must be added in
 139  
      * CVS logfile order, that is starting with the most recent one.
 140  
      * 
 141  
      * @param data the revision
 142  
      */
 143  
     public void buildRevision(final RevisionData data) {
 144  156
         currentFileBuilder.addRevisionData(data);
 145  156
         if (startDate == null || startDate.compareTo(data.getDate()) > 0) {
 146  144
             startDate = data.getDate();
 147  
         }
 148  156
     }
 149  
 
 150  
     /**
 151  
      * Returns a Repository object of all files.
 152  
      * 
 153  
      * @return Repository a Repository object
 154  
      */
 155  
     public Repository createCvsContent() {
 156  84
         if (currentFileBuilder != null) {
 157  76
             fileBuilders.add(currentFileBuilder);
 158  76
             currentFileBuilder = null;
 159  
         }
 160  
 
 161  84
         final Repository result = new Repository();
 162  84
         final Iterator it = fileBuilders.iterator();
 163  188
         while (it.hasNext()) {
 164  104
             final FileBuilder fileBuilder = (FileBuilder) it.next();
 165  104
             final VersionedFile file = fileBuilder.createFile(startDate);
 166  104
             if (file == null) {
 167  20
                 continue;
 168  
             }
 169  84
             if (fileBuilder.hasUnexpectedLocalRevision()) {
 170  52
                 this.flagOutOfSync = true;
 171  
             }
 172  84
             if (fileBuilder.hasLocalCVSMetadata()) {
 173  28
                 this.flagHasLocalCVSMetadata = true;
 174  
             }
 175  84
             if (fileBuilder.hasLocalFileNotFound()) {
 176  40
                 this.countNotFoundLocalFiles++;
 177  40
                 this.flagOutOfSync = true;
 178  44
             } else if (file.getCurrentLinesOfCode() > 0) {
 179  20
                 this.countFoundLocalFiles++;
 180  
             }
 181  84
             result.addFile(file);
 182  84
             logger.finer("adding " + file.getFilenameWithPath() + " (" + file.getRevisions().size() + " revisions)");
 183  84
         }
 184  
 
 185  
         // Uh oh...
 186  84
         final SortedSet revisions = result.getRevisions();
 187  84
         final List commits = new CommitListBuilder(revisions).createCommitList();
 188  84
         result.setCommits(commits);
 189  84
         result.setSymbolicNames(getMatchingSymbolicNames());
 190  84
         return result;
 191  
     }
 192  
 
 193  
     public String getProjectName() {
 194  4
         return projectName;
 195  
     }
 196  
 
 197  
     /**
 198  
      * Returns the <tt>Set</tt> of filenames that are "in the attic".
 199  
      * @return a <tt>Set</tt> of <tt>String</tt>s
 200  
      */
 201  
     public Set getAtticFileNames() {
 202  12
         return atticFileNames;
 203  
     }
 204  
 
 205  
     /**
 206  
      * @return <tt>true</tt> if there was an exclude pattern, and it rejected all files
 207  
      */
 208  
     public boolean allRejectedByExcludePattern() {
 209  0
         return this.countRejectedByExclude > 0 && this.countAcceptedByExclude == 0;
 210  
     }
 211  
 
 212  
     /**
 213  
      * @return <tt>true</tt> if there was an include pattern, and it rejected all files
 214  
      */
 215  
     public boolean allRejectedByIncludePattern() {
 216  0
         return this.countRejectedByInclude > 0 && this.countAcceptedByInclude == 0;
 217  
     }
 218  
 
 219  
     /**
 220  
      * Returns <tt>true</tt> if the local working copy is out of
 221  
      * sync with the log. The current implementation spots if
 222  
      * local files have been deleted and not yet committed, or
 223  
      * if the log file was generated before the latest commit.
 224  
      */
 225  
     public boolean isLogAndLocalFilesOutOfSync() {
 226  0
         return this.flagHasLocalCVSMetadata && this.flagOutOfSync;
 227  
     }
 228  
 
 229  
     /**
 230  
      * Returns <tt>true</tt> if no local copy was found for
 231  
      * the majority of files in the log. This is a strong indication
 232  
      * that the log is not for the specified local working copy. 
 233  
      */
 234  
     public boolean isLocalFilesNotFound() {
 235  0
         return this.countNotFoundLocalFiles > this.countFoundLocalFiles;
 236  
     }
 237  
 
 238  
     /**
 239  
      * Returns <tt>true</tt> if at least some local files have matching
 240  
      * entries in local CVS metada directories. If this is not the case,
 241  
      * then the local copy is probably just an export, not a checkout,
 242  
      * and we can't check if the log and working copy are in sync.
 243  
      */
 244  
     public boolean hasLocalCVSMetadata() {
 245  0
         return this.flagHasLocalCVSMetadata;
 246  
     }
 247  
 
 248  
     /**
 249  
      * returns the <tt>Author</tt> of the given name or creates it
 250  
      * if it does not yet exist. Author names are handled as case-insensitive.
 251  
      * @param name the author's name
 252  
      * @return a corresponding <tt>Author</tt> object
 253  
      */
 254  
     public Author getAuthor(final String name) {
 255  164
         if (this.authors.containsKey(name.toLowerCase())) {
 256  64
             return (Author) this.authors.get(name.toLowerCase());
 257  
         }
 258  100
         final Properties p = ConfigurationOptions.getConfigProperties();
 259  100
         final Author newAuthor = new Author(name);
 260  100
         this.authors.put(name.toLowerCase(), newAuthor);
 261  100
         if (p != null) {
 262  100
             newAuthor.setRealName(p.getProperty("user." + name.toLowerCase() + ".realName"));
 263  100
             newAuthor.setHomePageUrl(p.getProperty("user." + name.toLowerCase() + ".url"));
 264  100
             newAuthor.setImageUrl(p.getProperty("user." + name.toLowerCase() + ".image"));
 265  100
             newAuthor.setEmail(p.getProperty("user." + name.toLowerCase() + ".email"));
 266  
         }
 267  100
         return newAuthor;
 268  
     }
 269  
 
 270  
     /**
 271  
      * Returns the <tt>Directory</tt> of the given filename or creates it
 272  
      * if it does not yet exist.
 273  
      * @param filename the name and path of a file, for example "src/Main.java"
 274  
      * @return a corresponding <tt>Directory</tt> object
 275  
      */
 276  
     public Directory getDirectory(final String filename) {
 277  116
         final int lastSlash = filename.lastIndexOf('/');
 278  116
         if (lastSlash == -1) {
 279  92
             return getDirectoryForPath("");
 280  
         }
 281  24
         return getDirectoryForPath(filename.substring(0, lastSlash + 1));
 282  
     }
 283  
 
 284  
     /**
 285  
      * Returns the {@link SymbolicName} with the given name or creates it
 286  
      * if it does not yet exist.
 287  
      * 
 288  
      * @param name the symbolic name's name
 289  
      * @return the corresponding symbolic name object
 290  
      */
 291  
     public SymbolicName getSymbolicName(final String name) {
 292  0
         SymbolicName sym = (SymbolicName) symbolicNames.get(name);
 293  
 
 294  0
         if (sym != null) {
 295  0
             return sym;
 296  
         } else {
 297  0
             sym = new SymbolicName(name);
 298  0
             symbolicNames.put(name, sym);
 299  
 
 300  0
             return sym;
 301  
         }
 302  
     }
 303  
 
 304  
     public int getLOC(final String filename) throws NoLineCountException {
 305  80
         if (repositoryFileManager == null) {
 306  24
             throw new NoLineCountException("no RepositoryFileManager");
 307  
         }
 308  56
         return repositoryFileManager.getLinesOfCode(filename);
 309  
     }
 310  
 
 311  
     /**
 312  
      * @see RepositoryFileManager#getRevision(String)
 313  
      */
 314  
     public String getRevision(final String filename) throws IOException {
 315  84
         if (repositoryFileManager == null) {
 316  44
             throw new IOException("no RepositoryFileManager");
 317  
         }
 318  40
         return repositoryFileManager.getRevision(filename);
 319  
     }
 320  
 
 321  
     /**
 322  
      * Matches a filename against the include and exclude patterns. If no
 323  
      * include pattern was specified, all files will be included. If no
 324  
      * exclude pattern was specified, no files will be excluded.
 325  
      * @param filename a filename
 326  
      * @return <tt>true</tt> if the filename matches one of the include
 327  
      *         patterns and does not match any of the exclude patterns.
 328  
      *         If it matches an include and an exclude pattern, <tt>false</tt>
 329  
      *         will be returned.
 330  
      */
 331  
     public boolean matchesPatterns(final String filename) {
 332  144
         if (excludePattern != null) {
 333  12
             if (excludePattern.matches(filename)) {
 334  8
                 this.countRejectedByExclude++;
 335  8
                 return false;
 336  
             } else {
 337  4
                 this.countAcceptedByExclude++;
 338  
             }
 339  
         }
 340  136
         if (includePattern != null) {
 341  4
             if (includePattern.matches(filename)) {
 342  4
                 this.countAcceptedByInclude++;
 343  
             } else {
 344  0
                 this.countRejectedByInclude++;
 345  0
                 return false;
 346  
             }
 347  
         }
 348  136
         return true;
 349  
     }
 350  
 
 351  
     /**
 352  
      * @param path for example "src/net/sf/statcvs/"
 353  
      * @return the <tt>Directory</tt> corresponding to <tt>statcvs</tt>
 354  
      */
 355  
     private Directory getDirectoryForPath(final String path) {
 356  160
         if (directories.containsKey(path)) {
 357  116
             return (Directory) directories.get(path);
 358  
         }
 359  44
         final Directory parent = getDirectoryForPath(FileUtils.getParentDirectoryPath(path));
 360  44
         final Directory newDirectory = parent.createSubdirectory(FileUtils.getDirectoryName(path));
 361  44
         directories.put(path, newDirectory);
 362  44
         return newDirectory;
 363  
     }
 364  
 
 365  
     private SortedSet getMatchingSymbolicNames() {
 366  84
         final TreeSet result = new TreeSet();
 367  84
         if (this.tagsPattern == null) {
 368  84
             return result;
 369  
         }
 370  0
         for (final Iterator it = this.symbolicNames.values().iterator(); it.hasNext();) {
 371  0
             final SymbolicName sn = (SymbolicName) it.next();
 372  0
             if (sn.getDate() != null && this.tagsPattern.matcher(sn.getName()).matches()) {
 373  0
                 result.add(sn);
 374  
             }
 375  0
         }
 376  0
         return result;
 377  
     }
 378  
 }