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