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 */ 64 public class Builder implements CvsLogBuilder { 65 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 private final Pattern tagsPattern; 71 72 private final Map authors = new HashMap(); 73 private final Map directories = new HashMap(); 74 private final Map symbolicNames = new HashMap(); 75 76 private final List fileBuilders = new ArrayList(); 77 private final Set atticFileNames = new HashSet(); 78 79 private FileBuilder currentFileBuilder = null; 80 private Date startDate = null; 81 private String projectName = null; 82 83 private int countRejectedByExclude = 0; 84 private int countAcceptedByExclude = 0; 85 private int countRejectedByInclude = 0; 86 private int countAcceptedByInclude = 0; 87 private boolean flagOutOfSync = false; 88 private boolean flagHasLocalCVSMetadata = false; 89 private int countFoundLocalFiles = 0; 90 private int countNotFoundLocalFiles = 0; 91 clean()92 public void clean() { 93 authors.clear(); 94 directories.clear(); 95 symbolicNames.clear(); 96 fileBuilders.clear(); 97 atticFileNames.clear(); 98 } 99 100 /** 101 * Creates a new <tt>Builder</tt> 102 * @param repositoryFileManager the {@link RepositoryFileManager} that 103 * can be used to retrieve LOC counts for 104 * the files that this builder will create 105 * @param includePattern a list of Ant-style wildcard patterns, seperated 106 * by : or ; 107 * @param excludePattern a list of Ant-style wildcard patterns, seperated 108 * by : or ; 109 * @param tagsPattern A regular expression; matching symbolic names are recorded 110 */ Builder(final RepositoryFileManager repositoryFileManager, final FilePatternMatcher includePattern, final FilePatternMatcher excludePattern, final Pattern tagsPattern)111 public Builder(final RepositoryFileManager repositoryFileManager, final FilePatternMatcher includePattern, final FilePatternMatcher excludePattern, 112 final Pattern tagsPattern) { 113 this.repositoryFileManager = repositoryFileManager; 114 this.includePattern = includePattern; 115 this.excludePattern = excludePattern; 116 this.tagsPattern = tagsPattern; 117 directories.put("", Directory.createRoot()); 118 } 119 120 /** 121 * Starts building the module. 122 * 123 * @param moduleName name of the module 124 */ buildModule(final String moduleName)125 public void buildModule(final String moduleName) { 126 this.projectName = moduleName; 127 } 128 129 /** 130 * Starts building a new file. The files are not expected to be created 131 * in any particular order. 132 * @param filename the file's name with path, for example "path/file.txt" 133 * @param isBinary <tt>true</tt> if it's a binary file 134 * @param isInAttic <tt>true</tt> if the file is dead on the main branch 135 * @param revBySymnames maps revision (string) by symbolic name (string) 136 */ buildFile(final String filename, final boolean isBinary, final boolean isInAttic, final Map revBySymnames)137 public void buildFile(final String filename, final boolean isBinary, final boolean isInAttic, final Map revBySymnames) { 138 if (currentFileBuilder != null) { 139 fileBuilders.add(currentFileBuilder); 140 } 141 currentFileBuilder = new FileBuilder(this, filename, isBinary, revBySymnames); 142 if (isInAttic) { 143 atticFileNames.add(filename); 144 } 145 } 146 147 /** 148 * Adds a revision to the current file. The revisions must be added in 149 * CVS logfile order, that is starting with the most recent one. 150 * 151 * @param data the revision 152 */ buildRevision(final RevisionData data)153 public void buildRevision(final RevisionData data) { 154 currentFileBuilder.addRevisionData(data); 155 if (startDate == null || startDate.compareTo(data.getDate()) > 0) { 156 startDate = data.getDate(); 157 } 158 } 159 160 /** 161 * Returns a Repository object of all files. 162 * 163 * @return Repository a Repository object 164 */ createCvsContent()165 public Repository createCvsContent() { 166 if (currentFileBuilder != null) { 167 fileBuilders.add(currentFileBuilder); 168 currentFileBuilder = null; 169 } 170 171 final Repository result = new Repository(); 172 final Iterator it = fileBuilders.iterator(); 173 while (it.hasNext()) { 174 final FileBuilder fileBuilder = (FileBuilder) it.next(); 175 final VersionedFile file = fileBuilder.createFile(startDate); 176 if (file == null) { 177 continue; 178 } 179 if (fileBuilder.hasUnexpectedLocalRevision()) { 180 this.flagOutOfSync = true; 181 } 182 if (fileBuilder.hasLocalCVSMetadata()) { 183 this.flagHasLocalCVSMetadata = true; 184 } 185 if (fileBuilder.hasLocalFileNotFound()) { 186 this.countNotFoundLocalFiles++; 187 this.flagOutOfSync = true; 188 } else if (file.getCurrentLinesOfCode() > 0) { 189 this.countFoundLocalFiles++; 190 } 191 result.addFile(file); 192 logger.finer("adding " + file.getFilenameWithPath() + " (" + file.getRevisions().size() + " revisions)"); 193 } 194 195 // Uh oh... 196 final SortedSet revisions = result.getRevisions(); 197 final List commits = new CommitListBuilder(revisions).createCommitList(); 198 result.setCommits(commits); 199 result.setSymbolicNames(getMatchingSymbolicNames()); 200 return result; 201 } 202 getProjectName()203 public String getProjectName() { 204 return projectName; 205 } 206 207 /** 208 * Returns the <tt>Set</tt> of filenames that are "in the attic". 209 * @return a <tt>Set</tt> of <tt>String</tt>s 210 */ getAtticFileNames()211 public Set getAtticFileNames() { 212 return atticFileNames; 213 } 214 215 /** 216 * @return <tt>true</tt> if there was an exclude pattern, and it rejected all files 217 */ allRejectedByExcludePattern()218 public boolean allRejectedByExcludePattern() { 219 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 */ allRejectedByIncludePattern()225 public boolean allRejectedByIncludePattern() { 226 return this.countRejectedByInclude > 0 && this.countAcceptedByInclude == 0; 227 } 228 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 */ isLogAndLocalFilesOutOfSync()235 public boolean isLogAndLocalFilesOutOfSync() { 236 return this.flagHasLocalCVSMetadata && this.flagOutOfSync; 237 } 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 */ isLocalFilesNotFound()244 public boolean isLocalFilesNotFound() { 245 return this.countNotFoundLocalFiles > this.countFoundLocalFiles; 246 } 247 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 */ hasLocalCVSMetadata()254 public boolean hasLocalCVSMetadata() { 255 return this.flagHasLocalCVSMetadata; 256 } 257 258 /** 259 * returns the <tt>Author</tt> of the given name or creates it 260 * if it does not yet exist. Author names are handled as case-insensitive. 261 * @param name the author's name 262 * @return a corresponding <tt>Author</tt> object 263 */ getAuthor(final String name)264 public Author getAuthor(final String name) { 265 String nameForConfig = name.toLowerCase(Locale.getDefault()); 266 if (this.authors.containsKey(nameForConfig)) { 267 return (Author) this.authors.get(nameForConfig); 268 } 269 final Properties p = ConfigurationOptions.getConfigProperties(); 270 Author newAuthor = new Author(name); 271 if (p != null) { 272 String replacementUser = p.getProperty("user." + nameForConfig + ".replacedBy"); 273 274 if (StringUtils.isNotEmpty(replacementUser)) { 275 replacementUser = replacementUser.toLowerCase(); 276 if (this.authors.containsKey(replacementUser)) { 277 return (Author) this.authors.get(replacementUser); 278 } 279 nameForConfig = replacementUser; 280 newAuthor = new Author(nameForConfig); 281 } 282 283 newAuthor.setRealName(p.getProperty("user." + nameForConfig + ".realName")); 284 newAuthor.setHomePageUrl(p.getProperty("user." + nameForConfig + ".url")); 285 newAuthor.setImageUrl(p.getProperty("user." + nameForConfig + ".image")); 286 newAuthor.setEmail(p.getProperty("user." + nameForConfig + ".email")); 287 newAuthor.setTwitterUserName(p.getProperty("user." + nameForConfig + ".twitterUsername")); 288 newAuthor.setTwitterUserId(p.getProperty("user." + nameForConfig + ".twitterUserId")); 289 String val = p.getProperty("user." + nameForConfig + ".twitterIncludeFlash"); 290 if (StringUtils.isNotEmpty(val)) { 291 newAuthor.setTwitterIncludeFlash(Boolean.valueOf(val).booleanValue()); 292 } 293 val = p.getProperty("user." + nameForConfig + ".twitterIncludeHtml"); 294 if (StringUtils.isNotEmpty(val)) { 295 newAuthor.setTwitterIncludeHtml(Boolean.valueOf(val).booleanValue()); 296 } 297 } 298 this.authors.put(nameForConfig, newAuthor); 299 return newAuthor; 300 } 301 302 /** 303 * Returns the <tt>Directory</tt> of the given filename or creates it 304 * if it does not yet exist. 305 * @param filename the name and path of a file, for example "src/Main.java" 306 * @return a corresponding <tt>Directory</tt> object 307 */ getDirectory(final String filename)308 public Directory getDirectory(final String filename) { 309 final int lastSlash = filename.lastIndexOf('/'); 310 if (lastSlash == -1) { 311 return getDirectoryForPath(""); 312 } 313 return getDirectoryForPath(filename.substring(0, lastSlash + 1)); 314 } 315 316 /** 317 * Returns the {@link SymbolicName} with the given name or creates it 318 * if it does not yet exist. 319 * 320 * @param name the symbolic name's name 321 * @return the corresponding symbolic name object 322 */ getSymbolicName(final String name)323 public SymbolicName getSymbolicName(final String name) { 324 SymbolicName sym = (SymbolicName) symbolicNames.get(name); 325 326 if (sym != null) { 327 return sym; 328 } else { 329 sym = new SymbolicName(name); 330 symbolicNames.put(name, sym); 331 332 return sym; 333 } 334 } 335 getLOC(final String filename)336 public int getLOC(final String filename) throws NoLineCountException { 337 if (repositoryFileManager == null) { 338 throw new NoLineCountException("no RepositoryFileManager"); 339 } 340 return repositoryFileManager.getLinesOfCode(filename); 341 } 342 343 /** 344 * @see RepositoryFileManager#getRevision(String) 345 */ getRevision(final String filename)346 public String getRevision(final String filename) throws IOException { 347 if (repositoryFileManager == null) { 348 throw new IOException("no RepositoryFileManager"); 349 } 350 return repositoryFileManager.getRevision(filename); 351 } 352 353 /** 354 * Matches a filename against the include and exclude patterns. If no 355 * include pattern was specified, all files will be included. If no 356 * exclude pattern was specified, no files will be excluded. 357 * @param filename a filename 358 * @return <tt>true</tt> if the filename matches one of the include 359 * patterns and does not match any of the exclude patterns. 360 * If it matches an include and an exclude pattern, <tt>false</tt> 361 * will be returned. 362 */ matchesPatterns(final String filename)363 public boolean matchesPatterns(final String filename) { 364 if (excludePattern != null) { 365 if (excludePattern.matches(filename)) { 366 this.countRejectedByExclude++; 367 return false; 368 } else { 369 this.countAcceptedByExclude++; 370 } 371 } 372 if (includePattern != null) { 373 if (includePattern.matches(filename)) { 374 this.countAcceptedByInclude++; 375 } else { 376 this.countRejectedByInclude++; 377 return false; 378 } 379 } 380 return true; 381 } 382 383 /** 384 * @param path for example "src/net/sf/statcvs/" 385 * @return the <tt>Directory</tt> corresponding to <tt>statcvs</tt> 386 */ getDirectoryForPath(final String path)387 private Directory getDirectoryForPath(final String path) { 388 if (directories.containsKey(path)) { 389 return (Directory) directories.get(path); 390 } 391 final Directory parent = getDirectoryForPath(FileUtils.getParentDirectoryPath(path)); 392 final Directory newDirectory = parent.createSubdirectory(FileUtils.getDirectoryName(path)); 393 directories.put(path, newDirectory); 394 return newDirectory; 395 } 396 getMatchingSymbolicNames()397 private SortedSet getMatchingSymbolicNames() { 398 final TreeSet result = new TreeSet(); 399 if (this.tagsPattern == null) { 400 return result; 401 } 402 for (final Iterator it = this.symbolicNames.values().iterator(); it.hasNext();) { 403 final SymbolicName sn = (SymbolicName) it.next(); 404 if (sn.getDate() != null && this.tagsPattern.matcher(sn.getName()).matches()) { 405 result.add(sn); 406 } 407 } 408 return result; 409 } 410 }