1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
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 */
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 */
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 */
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 */
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 */
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 */
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
220 final SortedSet revisions = result.getRevisions();
221 final List commits = new CommitListBuilder(revisions).createCommitList();
222 result.setCommits(commits);
223
224
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 */
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 */
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
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 */
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 */
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 */
339 public Map getFileBuilders() {
340 return fileBuilders;
341 }
342
343 /**
344 * @see RepositoryFileManager#getLinesOfCode(String)
345 */
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
354 public String getProjectName() {
355 return projectName;
356 }
357
358 /**
359 * @see RepositoryFileManager#getRevision(String)
360 */
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 */
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 */
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 */
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 */
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 */
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 {
460 private AuthorAnonymizingProvider() {
461
462 }
463
464 private static int count = 0;
465
466 static synchronized String getNewName() {
467 return "author" + (String.valueOf(++count));
468 }
469
470 }
471 }