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