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: 2008/04/02 11:22: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.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.logging.Logger;
38  import java.util.regex.Pattern;
39  
40  import net.sf.statcvs.model.Author;
41  import net.sf.statcvs.model.Directory;
42  import net.sf.statcvs.model.Repository;
43  import net.sf.statcvs.model.SymbolicName;
44  import net.sf.statcvs.model.VersionedFile;
45  import net.sf.statcvs.output.ConfigurationOptions;
46  import net.sf.statcvs.util.FilePatternMatcher;
47  import net.sf.statcvs.util.FileUtils;
48  
49  /**
50   * <p>Helps building the {@link net.sf.statcvs.model.Repository} from a CVS
51   * log. The <tt>Builder</tt> is fed by some CVS history data source, for
52   * example a CVS log parser. The <tt>Repository</tt> can be retrieved
53   * using the {@link #createCvsContent} method.</p>
54   * 
55   * <p>The class also takes care of the creation of <tt>Author</tt> and 
56   * </tt>Directory</tt> objects and makes sure that there's only one of these
57   * for each author name and path. It also provides LOC count services.</p>
58   * 
59   * @author Richard Cyganiak <richard@cyganiak.de>
60   * @version $Id: Builder.java,v 1.40 2008/04/02 11:22:14 benoitx Exp $
61   */
62  public class Builder implements CvsLogBuilder {
63      private static Logger logger = Logger.getLogger(Builder.class.getName());
64  
65      private final RepositoryFileManager repositoryFileManager;
66      private final FilePatternMatcher includePattern;
67      private final FilePatternMatcher excludePattern;
68      private final Pattern tagsPattern;
69  
70      private final Map authors = new HashMap();
71      private final Map directories = new HashMap();
72      private final Map symbolicNames = new HashMap();
73  
74      private final List fileBuilders = new ArrayList();
75      private final Set atticFileNames = new HashSet();
76  
77      private FileBuilder currentFileBuilder = null;
78      private Date startDate = null;
79      private String projectName = null;
80  
81      private int countRejectedByExclude = 0;
82      private int countAcceptedByExclude = 0;
83      private int countRejectedByInclude = 0;
84      private int countAcceptedByInclude = 0;
85      private boolean flagOutOfSync = false;
86      private boolean flagHasLocalCVSMetadata = false;
87      private int countFoundLocalFiles = 0;
88      private int countNotFoundLocalFiles = 0;
89  
90      /**
91       * Creates a new <tt>Builder</tt>
92       * @param repositoryFileManager the {@link RepositoryFileManager} that
93       * 								can be used to retrieve LOC counts for
94       * 								the files that this builder will create
95       * @param includePattern a list of Ant-style wildcard patterns, seperated
96       *                       by : or ;
97       * @param excludePattern a list of Ant-style wildcard patterns, seperated
98       *                       by : or ;
99       * @param tagsPattern A regular expression; matching symbolic names are recorded
100      */
101     public Builder(final RepositoryFileManager repositoryFileManager, final FilePatternMatcher includePattern, final FilePatternMatcher excludePattern,
102             final Pattern tagsPattern) {
103         this.repositoryFileManager = repositoryFileManager;
104         this.includePattern = includePattern;
105         this.excludePattern = excludePattern;
106         this.tagsPattern = tagsPattern;
107         directories.put("", Directory.createRoot());
108     }
109 
110     /**
111      * Starts building the module.
112      * 
113      * @param moduleName name of the module
114      */
115     public void buildModule(final String moduleName) {
116         this.projectName = moduleName;
117     }
118 
119     /**
120      * Starts building a new file. The files are not expected to be created
121      * in any particular order.
122      * @param filename the file's name with path, for example "path/file.txt"
123      * @param isBinary <tt>true</tt> if it's a binary file
124      * @param isInAttic <tt>true</tt> if the file is dead on the main branch
125      * @param revBySymnames maps revision (string) by symbolic name (string)
126      */
127     public void buildFile(final String filename, final boolean isBinary, final boolean isInAttic, final Map revBySymnames) {
128         if (currentFileBuilder != null) {
129             fileBuilders.add(currentFileBuilder);
130         }
131         currentFileBuilder = new FileBuilder(this, filename, isBinary, revBySymnames);
132         if (isInAttic) {
133             atticFileNames.add(filename);
134         }
135     }
136 
137     /**
138      * Adds a revision to the current file. The revisions must be added in
139      * CVS logfile order, that is starting with the most recent one.
140      * 
141      * @param data the revision
142      */
143     public void buildRevision(final RevisionData data) {
144         currentFileBuilder.addRevisionData(data);
145         if (startDate == null || startDate.compareTo(data.getDate()) > 0) {
146             startDate = data.getDate();
147         }
148     }
149 
150     /**
151      * Returns a Repository object of all files.
152      * 
153      * @return Repository a Repository object
154      */
155     public Repository createCvsContent() {
156         if (currentFileBuilder != null) {
157             fileBuilders.add(currentFileBuilder);
158             currentFileBuilder = null;
159         }
160 
161         final Repository result = new Repository();
162         final Iterator it = fileBuilders.iterator();
163         while (it.hasNext()) {
164             final FileBuilder fileBuilder = (FileBuilder) it.next();
165             final VersionedFile file = fileBuilder.createFile(startDate);
166             if (file == null) {
167                 continue;
168             }
169             if (fileBuilder.hasUnexpectedLocalRevision()) {
170                 this.flagOutOfSync = true;
171             }
172             if (fileBuilder.hasLocalCVSMetadata()) {
173                 this.flagHasLocalCVSMetadata = true;
174             }
175             if (fileBuilder.hasLocalFileNotFound()) {
176                 this.countNotFoundLocalFiles++;
177                 this.flagOutOfSync = true;
178             } else if (file.getCurrentLinesOfCode() > 0) {
179                 this.countFoundLocalFiles++;
180             }
181             result.addFile(file);
182             logger.finer("adding " + file.getFilenameWithPath() + " (" + file.getRevisions().size() + " revisions)");
183         }
184 
185         // Uh oh...
186         final SortedSet revisions = result.getRevisions();
187         final List commits = new CommitListBuilder(revisions).createCommitList();
188         result.setCommits(commits);
189         result.setSymbolicNames(getMatchingSymbolicNames());
190         return result;
191     }
192 
193     public String getProjectName() {
194         return projectName;
195     }
196 
197     /**
198      * Returns the <tt>Set</tt> of filenames that are "in the attic".
199      * @return a <tt>Set</tt> of <tt>String</tt>s
200      */
201     public Set getAtticFileNames() {
202         return atticFileNames;
203     }
204 
205     /**
206      * @return <tt>true</tt> if there was an exclude pattern, and it rejected all files
207      */
208     public boolean allRejectedByExcludePattern() {
209         return this.countRejectedByExclude > 0 && this.countAcceptedByExclude == 0;
210     }
211 
212     /**
213      * @return <tt>true</tt> if there was an include pattern, and it rejected all files
214      */
215     public boolean allRejectedByIncludePattern() {
216         return this.countRejectedByInclude > 0 && this.countAcceptedByInclude == 0;
217     }
218 
219     /**
220      * Returns <tt>true</tt> if the local working copy is out of
221      * sync with the log. The current implementation spots if
222      * local files have been deleted and not yet committed, or
223      * if the log file was generated before the latest commit.
224      */
225     public boolean isLogAndLocalFilesOutOfSync() {
226         return this.flagHasLocalCVSMetadata && this.flagOutOfSync;
227     }
228 
229     /**
230      * Returns <tt>true</tt> if no local copy was found for
231      * the majority of files in the log. This is a strong indication
232      * that the log is not for the specified local working copy. 
233      */
234     public boolean isLocalFilesNotFound() {
235         return this.countNotFoundLocalFiles > this.countFoundLocalFiles;
236     }
237 
238     /**
239      * Returns <tt>true</tt> if at least some local files have matching
240      * entries in local CVS metada directories. If this is not the case,
241      * then the local copy is probably just an export, not a checkout,
242      * and we can't check if the log and working copy are in sync.
243      */
244     public boolean hasLocalCVSMetadata() {
245         return this.flagHasLocalCVSMetadata;
246     }
247 
248     /**
249      * returns the <tt>Author</tt> of the given name or creates it
250      * if it does not yet exist. Author names are handled as case-insensitive.
251      * @param name the author's name
252      * @return a corresponding <tt>Author</tt> object
253      */
254     public Author getAuthor(final String name) {
255         if (this.authors.containsKey(name.toLowerCase())) {
256             return (Author) this.authors.get(name.toLowerCase());
257         }
258         final Properties p = ConfigurationOptions.getConfigProperties();
259         final Author newAuthor = new Author(name);
260         this.authors.put(name.toLowerCase(), newAuthor);
261         if (p != null) {
262             newAuthor.setRealName(p.getProperty("user." + name.toLowerCase() + ".realName"));
263             newAuthor.setHomePageUrl(p.getProperty("user." + name.toLowerCase() + ".url"));
264             newAuthor.setImageUrl(p.getProperty("user." + name.toLowerCase() + ".image"));
265             newAuthor.setEmail(p.getProperty("user." + name.toLowerCase() + ".email"));
266         }
267         return newAuthor;
268     }
269 
270     /**
271      * Returns the <tt>Directory</tt> of the given filename or creates it
272      * if it does not yet exist.
273      * @param filename 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      * Returns the {@link SymbolicName} with the given name or creates it
286      * if it does not yet exist.
287      * 
288      * @param name the symbolic name's name
289      * @return the corresponding symbolic name object
290      */
291     public SymbolicName getSymbolicName(final String name) {
292         SymbolicName sym = (SymbolicName) symbolicNames.get(name);
293 
294         if (sym != null) {
295             return sym;
296         } else {
297             sym = new SymbolicName(name);
298             symbolicNames.put(name, sym);
299 
300             return sym;
301         }
302     }
303 
304     public int getLOC(final String filename) throws NoLineCountException {
305         if (repositoryFileManager == null) {
306             throw new NoLineCountException("no RepositoryFileManager");
307         }
308         return repositoryFileManager.getLinesOfCode(filename);
309     }
310 
311     /**
312      * @see RepositoryFileManager#getRevision(String)
313      */
314     public String getRevision(final String filename) throws IOException {
315         if (repositoryFileManager == null) {
316             throw new IOException("no RepositoryFileManager");
317         }
318         return repositoryFileManager.getRevision(filename);
319     }
320 
321     /**
322      * Matches a filename against the include and exclude patterns. If no
323      * include pattern was specified, all files will be included. If no
324      * exclude pattern was specified, no files will be excluded.
325      * @param filename a filename
326      * @return <tt>true</tt> if the filename matches one of the include
327      *         patterns and does not match any of the exclude patterns.
328      *         If it matches an include and an exclude pattern, <tt>false</tt>
329      *         will be returned.
330      */
331     public boolean matchesPatterns(final String filename) {
332         if (excludePattern != null) {
333             if (excludePattern.matches(filename)) {
334                 this.countRejectedByExclude++;
335                 return false;
336             } else {
337                 this.countAcceptedByExclude++;
338             }
339         }
340         if (includePattern != null) {
341             if (includePattern.matches(filename)) {
342                 this.countAcceptedByInclude++;
343             } else {
344                 this.countRejectedByInclude++;
345                 return false;
346             }
347         }
348         return true;
349     }
350 
351     /**
352      * @param path for example "src/net/sf/statcvs/"
353      * @return the <tt>Directory</tt> corresponding to <tt>statcvs</tt>
354      */
355     private Directory getDirectoryForPath(final String path) {
356         if (directories.containsKey(path)) {
357             return (Directory) directories.get(path);
358         }
359         final Directory parent = getDirectoryForPath(FileUtils.getParentDirectoryPath(path));
360         final Directory newDirectory = parent.createSubdirectory(FileUtils.getDirectoryName(path));
361         directories.put(path, newDirectory);
362         return newDirectory;
363     }
364 
365     private SortedSet getMatchingSymbolicNames() {
366         final TreeSet result = new TreeSet();
367         if (this.tagsPattern == null) {
368             return result;
369         }
370         for (final Iterator it = this.symbolicNames.values().iterator(); it.hasNext();) {
371             final SymbolicName sn = (SymbolicName) it.next();
372             if (sn.getDate() != null && this.tagsPattern.matcher(sn.getName()).matches()) {
373                 result.add(sn);
374             }
375         }
376         return result;
377     }
378 }