1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
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 }