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: 2004/12/14 13:38:13 $ 22 */ 23 package net.sf.statsvn.input; 24 25 import java.io.IOException; 26 import java.util.Date; 27 import java.util.HashMap; 28 import java.util.HashSet; 29 import java.util.Iterator; 30 import java.util.List; 31 import java.util.Locale; 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.regex.Pattern; 38 39 import net.sf.statcvs.Messages; 40 import net.sf.statcvs.input.CommitListBuilder; 41 import net.sf.statcvs.input.NoLineCountException; 42 import net.sf.statcvs.model.Author; 43 import net.sf.statcvs.model.Directory; 44 import net.sf.statcvs.model.Repository; 45 import net.sf.statcvs.model.SymbolicName; 46 import net.sf.statcvs.model.VersionedFile; 47 import net.sf.statcvs.output.ConfigurationOptions; 48 import net.sf.statcvs.util.FilePatternMatcher; 49 import net.sf.statcvs.util.FileUtils; 50 import net.sf.statcvs.util.StringUtils; 51 import net.sf.statsvn.output.SvnConfigurationOptions; 52 53 /** 54 * <p> 55 * Helps building the {@link net.sf.statsvn.model.Repository} from a SVN log. The <tt>Builder</tt> is fed by some SVN history data source, for example a SVN 56 * log parser. The <tt>Repository</tt> can be retrieved using the {@link #createRepository} method. 57 * </p> 58 * 59 * <p> 60 * The class also takes care of the creation of <tt>Author</tt> and </tt>Directory</tt> objects and makes sure that there's only one of these for each 61 * author name and path. It also provides LOC count services. 62 * </p> 63 * 64 * @author Richard Cyganiak <richard@cyganiak.de> 65 * @author Jason Kealey <jkealey@shade.ca> 66 * @author Gunter Mussbacher <gunterm@site.uottawa.ca> 67 * 68 * @version $Id: Builder.java 389 2009-05-27 18:17:59Z benoitx $ 69 * 70 */ 71 public class Builder implements SvnLogBuilder { 72 private final Set atticFileNames = new HashSet(); 73 74 private final Map authors = new HashMap(); 75 76 private FileBuilder currentFileBuilder = null; 77 78 private final Map directories = new HashMap(); 79 80 private final FilePatternMatcher excludePattern; 81 82 private final Map fileBuilders = new HashMap(); 83 84 private final FilePatternMatcher includePattern; 85 86 private String projectName = null; 87 88 private final RepositoryFileManager repositoryFileManager; 89 90 private Date startDate = null; 91 92 private final Map symbolicNames = new HashMap(); 93 94 private final Pattern tagsPattern; 95 clean()96 public void clean() { 97 atticFileNames.clear(); 98 authors.clear(); 99 directories.clear(); 100 fileBuilders.clear(); 101 symbolicNames.clear(); 102 } 103 104 /** 105 * Creates a new <tt>Builder</tt> 106 * 107 * @param repositoryFileManager 108 * the {@link RepositoryFileManager} that can be used to retrieve LOC counts for the files that this builder will create 109 * @param includePattern 110 * a list of Ant-style wildcard patterns, seperated by : or ; 111 * @param excludePattern 112 * a list of Ant-style wildcard patterns, seperated by : or ; 113 */ Builder(final RepositoryFileManager repositoryFileManager, final FilePatternMatcher includePattern, final FilePatternMatcher excludePattern, final Pattern tagsPattern)114 public Builder(final RepositoryFileManager repositoryFileManager, final FilePatternMatcher includePattern, final FilePatternMatcher excludePattern, 115 final Pattern tagsPattern) { 116 this.repositoryFileManager = repositoryFileManager; 117 this.includePattern = includePattern; 118 this.excludePattern = excludePattern; 119 this.tagsPattern = tagsPattern; 120 directories.put("", Directory.createRoot()); 121 } 122 123 /** 124 * Adds a file to the attic. This method should only be called if our first invocation to (@link #buildFile(String, boolean, boolean, Map)) was given an 125 * invalid isInAttic field. 126 * 127 * This is a hack to handle post-processing of implicit deletions at the same time as the implicit additions that can be found in Subversion. 128 * 129 * @param filename 130 * the filename to add to the attic. 131 */ addToAttic(final String filename)132 public void addToAttic(final String filename) { 133 if (!atticFileNames.contains(filename)) { 134 atticFileNames.add(filename); 135 } 136 } 137 138 /** 139 * <p> 140 * Starts building a new file. The files are not expected to be created in any particular order. Subsequent calls to (@link #buildRevision(RevisionData)) 141 * will add revisions to this file. 142 * </p> 143 * 144 * <p> 145 * New in StatSVN: If the method has already been invoked with the same filename, the original file will be re-loaded and the other arguments are ignored. 146 * </p> 147 * 148 * @param filename 149 * the file's name with path, for example "path/file.txt" 150 * @param isBinary 151 * <tt>true</tt> if it's a binary file 152 * @param isInAttic 153 * <tt>true</tt> if the file is dead on the main branch 154 * @param revBySymnames 155 * maps revision (string) by symbolic name (string) 156 * @param dateBySymnames 157 * maps date (date) by symbolic name (string) 158 */ buildFile(final String filename, final boolean isBinary, final boolean isInAttic, final Map revBySymnames, final Map dateBySymnames)159 public void buildFile(final String filename, final boolean isBinary, final boolean isInAttic, final Map revBySymnames, final Map dateBySymnames) { 160 if (fileBuilders.containsKey(filename)) { 161 currentFileBuilder = (FileBuilder) fileBuilders.get(filename); 162 } else { 163 currentFileBuilder = new FileBuilder(this, filename, isBinary, revBySymnames, dateBySymnames); 164 fileBuilders.put(filename, currentFileBuilder); 165 if (isInAttic) { 166 addToAttic(filename); 167 } 168 } 169 } 170 171 /** 172 * Starts building the module. 173 * 174 * @param moduleName 175 * name of the module 176 */ buildModule(final String moduleName)177 public void buildModule(final String moduleName) { 178 this.projectName = moduleName; 179 } 180 181 /** 182 * Adds a revision to the current file. The revisions must be added in SVN logfile order, that is starting with the most recent one. 183 * 184 * @param data 185 * the revision 186 */ buildRevision(final RevisionData data)187 public void buildRevision(final RevisionData data) { 188 189 currentFileBuilder.addRevisionData(data); 190 191 if (startDate == null || startDate.compareTo(data.getDate()) > 0) { 192 startDate = data.getDate(); 193 } 194 } 195 196 /** 197 * Returns a Repository object of all files. 198 * 199 * @return Repository a Repository object 200 */ createRepository()201 public Repository createRepository() { 202 203 if (startDate == null) { 204 return new Repository(); 205 } 206 207 final Repository result = new Repository(); 208 final Iterator it = fileBuilders.values().iterator(); 209 while (it.hasNext()) { 210 final FileBuilder fileBuilder = (FileBuilder) it.next(); 211 final VersionedFile file = fileBuilder.createFile(startDate); 212 if (file == null) { 213 continue; 214 } 215 result.addFile(file); 216 SvnConfigurationOptions.getTaskLogger().log("adding " + file.getFilenameWithPath() + " (" + file.getRevisions().size() + " revisions)"); 217 } 218 219 // Uh oh... 220 final SortedSet revisions = result.getRevisions(); 221 final List commits = new CommitListBuilder(revisions).createCommitList(); 222 result.setCommits(commits); 223 224 // result.setSymbolicNames(new TreeSet(symbolicNames.values())); 225 result.setSymbolicNames(getMatchingSymbolicNames()); 226 227 SvnConfigurationOptions.getTaskLogger().log("SYMBOLIC NAMES - " + symbolicNames); 228 229 return result; 230 } 231 232 /** 233 * Returns the <tt>Set</tt> of filenames that are "in the attic". 234 * 235 * @return a <tt>Set</tt> of <tt>String</tt>s 236 */ getAtticFileNames()237 public Set getAtticFileNames() { 238 return atticFileNames; 239 } 240 241 /** 242 * returns the <tt>Author</tt> of the given name or creates it if it does not yet exist. Author names are handled as case-insensitive. 243 * 244 * @param name 245 * the author's name 246 * @return a corresponding <tt>Author</tt> object 247 */ getAuthor(String name)248 public Author getAuthor(String name) { 249 if (name == null || name.length() == 0) { 250 name = Messages.getString("AUTHOR_UNKNOWN"); 251 } 252 253 String lowerCaseName = name.toLowerCase(Locale.getDefault()); 254 final boolean bAnon = SvnConfigurationOptions.isAnonymize(); 255 if (this.authors.containsKey(lowerCaseName)) { 256 return (Author) this.authors.get(lowerCaseName); 257 } 258 259 Author newAuthor; 260 if (bAnon) { 261 // The first time a particular name is encountered, create an anonymized name. 262 newAuthor = new Author(AuthorAnonymizingProvider.getNewName()); 263 } else { 264 newAuthor = new Author(name); 265 } 266 267 final Properties p = ConfigurationOptions.getConfigProperties(); 268 269 if (p != null) { 270 String replacementUser = p.getProperty("user." + lowerCaseName + ".replacedBy"); 271 272 if (StringUtils.isNotEmpty(replacementUser)) { 273 replacementUser = replacementUser.toLowerCase(); 274 if (this.authors.containsKey(replacementUser)) { 275 return (Author) this.authors.get(replacementUser); 276 } 277 lowerCaseName = replacementUser; 278 newAuthor = new Author(lowerCaseName); 279 } 280 } 281 282 if (p != null && !bAnon) { 283 newAuthor.setRealName(p.getProperty("user." + lowerCaseName + ".realName")); 284 newAuthor.setHomePageUrl(p.getProperty("user." + lowerCaseName + ".url")); 285 newAuthor.setImageUrl(p.getProperty("user." + lowerCaseName + ".image")); 286 newAuthor.setEmail(p.getProperty("user." + lowerCaseName + ".email")); 287 newAuthor.setTwitterUserName(p.getProperty("user." + name.toLowerCase() + ".twitterUsername")); 288 newAuthor.setTwitterUserId(p.getProperty("user." + name.toLowerCase() + ".twitterUserId")); 289 String val = p.getProperty("user." + name.toLowerCase() + ".twitterIncludeFlash"); 290 if (val != null && val.length() > 0) { 291 newAuthor.setTwitterIncludeFlash(Boolean.valueOf(val).booleanValue()); 292 } 293 val = p.getProperty("user." + name.toLowerCase() + ".twitterIncludeHtml"); 294 if (val != null && val.length() > 0) { 295 newAuthor.setTwitterIncludeHtml(Boolean.valueOf(val).booleanValue()); 296 } 297 } 298 this.authors.put(lowerCaseName, newAuthor); 299 return newAuthor; 300 } 301 302 /** 303 * Returns the <tt>Directory</tt> of the given filename or creates it if it does not yet exist. 304 * 305 * @param filename 306 * the name and path of a file, for example "src/Main.java" 307 * @return a corresponding <tt>Directory</tt> object 308 */ getDirectory(final String filename)309 public Directory getDirectory(final String filename) { 310 final int lastSlash = filename.lastIndexOf('/'); 311 if (lastSlash == -1) { 312 return getDirectoryForPath(""); 313 } 314 return getDirectoryForPath(filename.substring(0, lastSlash + 1)); 315 } 316 317 /** 318 * @param path 319 * for example "src/net/sf/statcvs/" 320 * @return the <tt>Directory</tt> corresponding to <tt>statcvs</tt> 321 */ getDirectoryForPath(final String path)322 private Directory getDirectoryForPath(final String path) { 323 if (directories.containsKey(path)) { 324 return (Directory) directories.get(path); 325 } 326 final Directory parent = getDirectoryForPath(FileUtils.getParentDirectoryPath(path)); 327 final Directory newDirectory = parent.createSubdirectory(FileUtils.getDirectoryName(path)); 328 directories.put(path, newDirectory); 329 return newDirectory; 330 } 331 332 /** 333 * New in StatSVN: We need to have access to FileBuilders after they have been created to populate them with version numbers later on. 334 * 335 * @todo Beef up this interface to better encapsulate the data structure. 336 * 337 * @return this builder's contained (@link FileBuilder)s. 338 */ getFileBuilders()339 public Map getFileBuilders() { 340 return fileBuilders; 341 } 342 343 /** 344 * @see RepositoryFileManager#getLinesOfCode(String) 345 */ getLOC(final String filename)346 public int getLOC(final String filename) throws NoLineCountException { 347 if (repositoryFileManager == null) { 348 throw new NoLineCountException("no RepositoryFileManager"); 349 } 350 351 return repositoryFileManager.getLinesOfCode(filename); 352 } 353 getProjectName()354 public String getProjectName() { 355 return projectName; 356 } 357 358 /** 359 * @see RepositoryFileManager#getRevision(String) 360 */ getRevision(final String filename)361 public String getRevision(final String filename) throws IOException { 362 if (repositoryFileManager == null) { 363 throw new IOException("no RepositoryFileManager"); 364 } 365 return repositoryFileManager.getRevision(filename); 366 } 367 368 /** 369 * Returns the {@link SymbolicName} with the given name or creates it if it does not yet exist. 370 * 371 * @param name 372 * the symbolic name's name 373 * @return the corresponding symbolic name object 374 */ getSymbolicName(final String name, final Date date)375 public SymbolicName getSymbolicName(final String name, final Date date) { 376 SymbolicName sym = (SymbolicName) symbolicNames.get(name); 377 378 if (sym != null) { 379 return sym; 380 } else { 381 sym = new SymbolicName(name, date); 382 symbolicNames.put(name, sym); 383 384 return sym; 385 } 386 } 387 388 /** 389 * Matches a filename against the include and exclude patterns. If no include pattern was specified, all files will be included. If no exclude pattern was 390 * specified, no files will be excluded. 391 * 392 * @param filename 393 * a filename 394 * @return <tt>true</tt> if the filename matches one of the include patterns and does not match any of the exclude patterns. If it matches an include and 395 * an exclude pattern, <tt>false</tt> will be returned. 396 */ matchesPatterns(final String filename)397 public boolean matchesPatterns(final String filename) { 398 if (excludePattern != null && excludePattern.matches(filename)) { 399 return false; 400 } 401 if (includePattern != null) { 402 return includePattern.matches(filename); 403 } 404 return true; 405 } 406 407 /** 408 * Matches a tag against the tag patterns. 409 * 410 * @param tag 411 * a tag 412 * @return <tt>true</tt> if the tag matches the tag pattern. 413 */ matchesTagPatterns(final String tag)414 public boolean matchesTagPatterns(final String tag) { 415 if (tagsPattern != null) { 416 return tagsPattern.matcher(tag).matches(); 417 } 418 return false; 419 } 420 421 /** 422 * New in StatSVN: Updates a particular revision for a file with new line count information. If the file or revision does not exist, action will do nothing. 423 * 424 * Necessary because line counts are not given in the log file and hence can only be added in a second pass. 425 * 426 * @param filename 427 * the file to be updated 428 * @param revisionNumber 429 * the revision number to be updated 430 * @param linesAdded 431 * the lines that were added 432 * @param linesRemoved 433 * the lines that were removed 434 */ updateRevision(final String filename, final String revisionNumber, final int linesAdded, final int linesRemoved)435 public synchronized void updateRevision(final String filename, final String revisionNumber, final int linesAdded, final int linesRemoved) { 436 final FileBuilder fb = (FileBuilder) fileBuilders.get(filename); 437 if (fb != null) { 438 fb.updateRevision(revisionNumber, linesAdded, linesRemoved); 439 } 440 } 441 442 /** 443 * return only a set of matching tag names (from a list on the command line). 444 */ getMatchingSymbolicNames()445 private SortedSet getMatchingSymbolicNames() { 446 final TreeSet result = new TreeSet(); 447 if (this.tagsPattern == null) { 448 return result; 449 } 450 for (final Iterator it = this.symbolicNames.values().iterator(); it.hasNext();) { 451 final SymbolicName sn = (SymbolicName) it.next(); 452 if (sn.getDate() != null && this.tagsPattern.matcher(sn.getName()).matches()) { 453 result.add(sn); 454 } 455 } 456 return result; 457 } 458 459 private static final class AuthorAnonymizingProvider { AuthorAnonymizingProvider()460 private AuthorAnonymizingProvider() { 461 // no access 462 } 463 464 private static int count = 0; 465 getNewName()466 static synchronized String getNewName() { 467 return "author" + (String.valueOf(++count)); 468 } 469 470 } 471 }