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/03/18 16:25:47 $
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.44 2009/03/18 16:25:47 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      /**
93       * Creates a new <tt>Builder</tt>
94       * @param repositoryFileManager the {@link RepositoryFileManager} that
95       * 								can be used to retrieve LOC counts for
96       * 								the files that this builder will create
97       * @param includePattern a list of Ant-style wildcard patterns, seperated
98       *                       by : or ;
99       * @param excludePattern a list of Ant-style wildcard patterns, seperated
100      *                       by : or ;
101      * @param tagsPattern A regular expression; matching symbolic names are recorded
102      */
103     public Builder(final RepositoryFileManager repositoryFileManager, final FilePatternMatcher includePattern, final FilePatternMatcher excludePattern,
104             final Pattern tagsPattern) {
105         this.repositoryFileManager = repositoryFileManager;
106         this.includePattern = includePattern;
107         this.excludePattern = excludePattern;
108         this.tagsPattern = tagsPattern;
109         directories.put("", Directory.createRoot());
110     }
111 
112     /**
113      * Starts building the module.
114      * 
115      * @param moduleName name of the module
116      */
117     public void buildModule(final String moduleName) {
118         this.projectName = moduleName;
119     }
120 
121     /**
122      * Starts building a new file. The files are not expected to be created
123      * in any particular order.
124      * @param filename the file's name with path, for example "path/file.txt"
125      * @param isBinary <tt>true</tt> if it's a binary file
126      * @param isInAttic <tt>true</tt> if the file is dead on the main branch
127      * @param revBySymnames maps revision (string) by symbolic name (string)
128      */
129     public void buildFile(final String filename, final boolean isBinary, final boolean isInAttic, final Map revBySymnames) {
130         if (currentFileBuilder != null) {
131             fileBuilders.add(currentFileBuilder);
132         }
133         currentFileBuilder = new FileBuilder(this, filename, isBinary, revBySymnames);
134         if (isInAttic) {
135             atticFileNames.add(filename);
136         }
137     }
138 
139     /**
140      * Adds a revision to the current file. The revisions must be added in
141      * CVS logfile order, that is starting with the most recent one.
142      * 
143      * @param data the revision
144      */
145     public void buildRevision(final RevisionData data) {
146         currentFileBuilder.addRevisionData(data);
147         if (startDate == null || startDate.compareTo(data.getDate()) > 0) {
148             startDate = data.getDate();
149         }
150     }
151 
152     /**
153      * Returns a Repository object of all files.
154      * 
155      * @return Repository a Repository object
156      */
157     public Repository createCvsContent() {
158         if (currentFileBuilder != null) {
159             fileBuilders.add(currentFileBuilder);
160             currentFileBuilder = null;
161         }
162 
163         final Repository result = new Repository();
164         final Iterator it = fileBuilders.iterator();
165         while (it.hasNext()) {
166             final FileBuilder fileBuilder = (FileBuilder) it.next();
167             final VersionedFile file = fileBuilder.createFile(startDate);
168             if (file == null) {
169                 continue;
170             }
171             if (fileBuilder.hasUnexpectedLocalRevision()) {
172                 this.flagOutOfSync = true;
173             }
174             if (fileBuilder.hasLocalCVSMetadata()) {
175                 this.flagHasLocalCVSMetadata = true;
176             }
177             if (fileBuilder.hasLocalFileNotFound()) {
178                 this.countNotFoundLocalFiles++;
179                 this.flagOutOfSync = true;
180             } else if (file.getCurrentLinesOfCode() > 0) {
181                 this.countFoundLocalFiles++;
182             }
183             result.addFile(file);
184             logger.finer("adding " + file.getFilenameWithPath() + " (" + file.getRevisions().size() + " revisions)");
185         }
186 
187         // Uh oh...
188         final SortedSet revisions = result.getRevisions();
189         final List commits = new CommitListBuilder(revisions).createCommitList();
190         result.setCommits(commits);
191         result.setSymbolicNames(getMatchingSymbolicNames());
192         return result;
193     }
194 
195     public String getProjectName() {
196         return projectName;
197     }
198 
199     /**
200      * Returns the <tt>Set</tt> of filenames that are "in the attic".
201      * @return a <tt>Set</tt> of <tt>String</tt>s
202      */
203     public Set getAtticFileNames() {
204         return atticFileNames;
205     }
206 
207     /**
208      * @return <tt>true</tt> if there was an exclude pattern, and it rejected all files
209      */
210     public boolean allRejectedByExcludePattern() {
211         return this.countRejectedByExclude > 0 && this.countAcceptedByExclude == 0;
212     }
213 
214     /**
215      * @return <tt>true</tt> if there was an include pattern, and it rejected all files
216      */
217     public boolean allRejectedByIncludePattern() {
218         return this.countRejectedByInclude > 0 && this.countAcceptedByInclude == 0;
219     }
220 
221     /**
222      * Returns <tt>true</tt> if the local working copy is out of
223      * sync with the log. The current implementation spots if
224      * local files have been deleted and not yet committed, or
225      * if the log file was generated before the latest commit.
226      */
227     public boolean isLogAndLocalFilesOutOfSync() {
228         return this.flagHasLocalCVSMetadata && this.flagOutOfSync;
229     }
230 
231     /**
232      * Returns <tt>true</tt> if no local copy was found for
233      * the majority of files in the log. This is a strong indication
234      * that the log is not for the specified local working copy. 
235      */
236     public boolean isLocalFilesNotFound() {
237         return this.countNotFoundLocalFiles > this.countFoundLocalFiles;
238     }
239 
240     /**
241      * Returns <tt>true</tt> if at least some local files have matching
242      * entries in local CVS metada directories. If this is not the case,
243      * then the local copy is probably just an export, not a checkout,
244      * and we can't check if the log and working copy are in sync.
245      */
246     public boolean hasLocalCVSMetadata() {
247         return this.flagHasLocalCVSMetadata;
248     }
249 
250     /**
251      * returns the <tt>Author</tt> of the given name or creates it
252      * if it does not yet exist. Author names are handled as case-insensitive.
253      * @param name the author's name
254      * @return a corresponding <tt>Author</tt> object
255      */
256     public Author getAuthor(final String name) {
257         String nameForConfig = name.toLowerCase(Locale.getDefault());
258         if (this.authors.containsKey(nameForConfig)) {
259             return (Author) this.authors.get(nameForConfig);
260         }
261         final Properties p = ConfigurationOptions.getConfigProperties();
262         Author newAuthor = new Author(name);
263         if (p != null) {
264             String replacementUser = p.getProperty("user." + nameForConfig + ".replacedBy");
265 
266             if (StringUtils.isNotEmpty(replacementUser)) {
267                 replacementUser = replacementUser.toLowerCase();
268                 if (this.authors.containsKey(replacementUser)) {
269                     return (Author) this.authors.get(replacementUser);
270                 }
271                 nameForConfig = replacementUser;
272                 newAuthor = new Author(nameForConfig);
273             }
274 
275             newAuthor.setRealName(p.getProperty("user." + nameForConfig + ".realName"));
276             newAuthor.setHomePageUrl(p.getProperty("user." + nameForConfig + ".url"));
277             newAuthor.setImageUrl(p.getProperty("user." + nameForConfig + ".image"));
278             newAuthor.setEmail(p.getProperty("user." + nameForConfig + ".email"));
279             newAuthor.setTwitterUserName(p.getProperty("user." + nameForConfig + ".twitterUsername"));
280             newAuthor.setTwitterUserId(p.getProperty("user." + nameForConfig + ".twitterUserId"));
281             String val = p.getProperty("user." + nameForConfig + ".twitterIncludeFlash");
282             if (StringUtils.isNotEmpty(val)) {
283                 newAuthor.setTwitterIncludeFlash(Boolean.valueOf(val).booleanValue());
284             }
285             val = p.getProperty("user." + nameForConfig + ".twitterIncludeHtml");
286             if (StringUtils.isNotEmpty(val)) {
287                 newAuthor.setTwitterIncludeHtml(Boolean.valueOf(val).booleanValue());
288             }
289         }
290         this.authors.put(nameForConfig, newAuthor);
291         return newAuthor;
292     }
293 
294     /**
295      * Returns the <tt>Directory</tt> of the given filename or creates it
296      * if it does not yet exist.
297      * @param filename the name and path of a file, for example "src/Main.java"
298      * @return a corresponding <tt>Directory</tt> object
299      */
300     public Directory getDirectory(final String filename) {
301         final int lastSlash = filename.lastIndexOf('/');
302         if (lastSlash == -1) {
303             return getDirectoryForPath("");
304         }
305         return getDirectoryForPath(filename.substring(0, lastSlash + 1));
306     }
307 
308     /**
309      * Returns the {@link SymbolicName} with the given name or creates it
310      * if it does not yet exist.
311      * 
312      * @param name the symbolic name's name
313      * @return the corresponding symbolic name object
314      */
315     public SymbolicName getSymbolicName(final String name) {
316         SymbolicName sym = (SymbolicName) symbolicNames.get(name);
317 
318         if (sym != null) {
319             return sym;
320         } else {
321             sym = new SymbolicName(name);
322             symbolicNames.put(name, sym);
323 
324             return sym;
325         }
326     }
327 
328     public int getLOC(final String filename) throws NoLineCountException {
329         if (repositoryFileManager == null) {
330             throw new NoLineCountException("no RepositoryFileManager");
331         }
332         return repositoryFileManager.getLinesOfCode(filename);
333     }
334 
335     /**
336      * @see RepositoryFileManager#getRevision(String)
337      */
338     public String getRevision(final String filename) throws IOException {
339         if (repositoryFileManager == null) {
340             throw new IOException("no RepositoryFileManager");
341         }
342         return repositoryFileManager.getRevision(filename);
343     }
344 
345     /**
346      * Matches a filename against the include and exclude patterns. If no
347      * include pattern was specified, all files will be included. If no
348      * exclude pattern was specified, no files will be excluded.
349      * @param filename a filename
350      * @return <tt>true</tt> if the filename matches one of the include
351      *         patterns and does not match any of the exclude patterns.
352      *         If it matches an include and an exclude pattern, <tt>false</tt>
353      *         will be returned.
354      */
355     public boolean matchesPatterns(final String filename) {
356         if (excludePattern != null) {
357             if (excludePattern.matches(filename)) {
358                 this.countRejectedByExclude++;
359                 return false;
360             } else {
361                 this.countAcceptedByExclude++;
362             }
363         }
364         if (includePattern != null) {
365             if (includePattern.matches(filename)) {
366                 this.countAcceptedByInclude++;
367             } else {
368                 this.countRejectedByInclude++;
369                 return false;
370             }
371         }
372         return true;
373     }
374 
375     /**
376      * @param path for example "src/net/sf/statcvs/"
377      * @return the <tt>Directory</tt> corresponding to <tt>statcvs</tt>
378      */
379     private Directory getDirectoryForPath(final String path) {
380         if (directories.containsKey(path)) {
381             return (Directory) directories.get(path);
382         }
383         final Directory parent = getDirectoryForPath(FileUtils.getParentDirectoryPath(path));
384         final Directory newDirectory = parent.createSubdirectory(FileUtils.getDirectoryName(path));
385         directories.put(path, newDirectory);
386         return newDirectory;
387     }
388 
389     private SortedSet getMatchingSymbolicNames() {
390         final TreeSet result = new TreeSet();
391         if (this.tagsPattern == null) {
392             return result;
393         }
394         for (final Iterator it = this.symbolicNames.values().iterator(); it.hasNext();) {
395             final SymbolicName sn = (SymbolicName) it.next();
396             if (sn.getDate() != null && this.tagsPattern.matcher(sn.getName()).matches()) {
397                 result.add(sn);
398             }
399         }
400         return result;
401     }
402 }