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