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.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
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 }