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: 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  
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      */
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      */
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      */
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      */
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      */
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 
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      */
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      */
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      */
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      */
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      */
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      */
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      */
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      */
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      */
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 
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      */
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      */
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      */
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 
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 }