View Javadoc

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      
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         // 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      */
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             // 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      */
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             // no access
462         }
463 
464         private static int count = 0;
465 
466         static synchronized String getNewName() {
467             return "author" + (String.valueOf(++count));
468         }
469 
470     }
471 }