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.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 		// Uh oh...
211 		final SortedSet revisions = result.getRevisions();
212 		final List commits = new CommitListBuilder(revisions).createCommitList();
213 		result.setCommits(commits);
214 
215 		//        result.setSymbolicNames(new TreeSet(symbolicNames.values()));
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 			// The first time a particular name is encountered, create an anonymized name.
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 			// no access
429 		}
430 
431 		private static int count = 0;
432 
433 		static synchronized String getNewName() {
434 			return "author" + (String.valueOf(++count));
435 		}
436 
437 	}
438 }