1 package net.sf.statsvn.input;
2
3 import java.util.Collection;
4 import java.util.HashMap;
5 import java.util.Iterator;
6 import java.util.Map;
7
8 import javax.xml.parsers.DocumentBuilder;
9 import javax.xml.parsers.DocumentBuilderFactory;
10 import javax.xml.parsers.ParserConfigurationException;
11
12 import net.sf.statcvs.output.ConfigurationOptions;
13 import net.sf.statsvn.output.SvnConfigurationOptions;
14
15 import org.w3c.dom.Document;
16 import org.w3c.dom.Element;
17 import org.w3c.dom.NodeList;
18
19 /**
20 * <p>
21 * CVS log files include lines modified for each commit and binary status of a
22 * file while SVN log files do not offer this additional information.
23 * </p>
24 *
25 * <p>
26 * StatSVN must query the Subversion repository for line counts using svn diff.
27 * However, this is very costly, performance-wise. Therefore, the decision was
28 * taken to persist this information in an XML file. This class receives
29 * information from (@link net.sf.statsvn.input.SvnXmlLineCountsFileHandler) to
30 * build a DOM-based xml structure. It also forwards line counts to the
31 * appropriate (@link net.sf.statsvn.input.FileBuilder).
32 * </p>
33 *
34 * @author Gunter Mussbacher <gunterm@site.uottawa.ca>
35 * @version $Id: CacheBuilder.java 351 2008-03-28 18:46:26Z benoitx $
36 */
37 public class CacheBuilder {
38 private final SvnLogBuilder builder;
39
40 private final RepositoryFileManager repositoryFileManager;
41
42 private Element currentPath = null;
43
44 private Document document = null;
45
46 private String currentFilename;
47
48 private Element cache = null;
49
50 /**
51 * Constructs the LineCountsBuilder by giving it a reference to the builder
52 * currently in use.
53 *
54 * @param builder
55 * the SvnLogBuilder which contains all the FileBuilders.
56 */
57 public CacheBuilder(final SvnLogBuilder builder, final RepositoryFileManager repositoryFileManager) {
58 this.builder = builder;
59 this.repositoryFileManager = repositoryFileManager;
60 }
61
62 /**
63 * Adds a path in the DOM. To be followed by invocations to (@link
64 * #addRevision(String, String, String))
65 *
66 * @param name
67 * the filename
68 * @param latestRevision
69 * the latest revision of the file for which the binary status is
70 * known
71 * @param binaryStatus
72 * binary status of latest revision
73 */
74 private void addDOMPath(final String name, final String latestRevision, final String binaryStatus) {
75 currentPath = document.createElement(CacheConfiguration.PATH);
76 currentPath.setAttribute(CacheConfiguration.NAME, name);
77 currentPath.setAttribute(CacheConfiguration.LATEST_REVISION, latestRevision);
78 currentPath.setAttribute(CacheConfiguration.BINARY_STATUS, binaryStatus);
79 cache.appendChild(currentPath);
80 }
81
82 /**
83 * Updates the BINARY_STATUS and LATEST_REVISION attributes of a path in the
84 * DOM. Updates only if the revisionNumber is higher than current
85 * LATEST_REVISION of the path.
86 *
87 * @param path
88 * the path to be updated
89 * @param isBinary
90 * indicates if the revision is binary or not
91 * @param revisionNumber
92 * the revision number for which the binary status is valid
93 */
94 private void updateDOMPath(final Element path, final boolean isBinary, final String revisionNumber) {
95 int oldRevision = 0;
96 int newRevision = -1;
97 try {
98 oldRevision = Integer.parseInt(path.getAttribute(CacheConfiguration.LATEST_REVISION));
99 newRevision = Integer.parseInt(revisionNumber);
100 } catch (final NumberFormatException e) {
101 SvnConfigurationOptions.getTaskLogger().log(
102 "Ignoring invalid revision number " + revisionNumber + " for " + path.getAttribute(CacheConfiguration.NAME));
103 newRevision = -1;
104 }
105 String binaryStatus = CacheConfiguration.NOT_BINARY;
106 if (isBinary) {
107 binaryStatus = CacheConfiguration.BINARY;
108 }
109 if (newRevision >= oldRevision) {
110 path.setAttribute(CacheConfiguration.LATEST_REVISION, revisionNumber);
111 path.setAttribute(CacheConfiguration.BINARY_STATUS, binaryStatus);
112 }
113 }
114
115 /**
116 * Finds a path in the DOM.
117 *
118 * @param name
119 * the filename
120 * @return the path or null if the path does not exist
121 */
122 private Element findDOMPath(final String name) {
123 if (currentPath != null && name.equals(currentPath.getAttribute(CacheConfiguration.NAME))) {
124 return currentPath;
125 }
126 final NodeList paths = cache.getChildNodes();
127 for (int i = 0; i < paths.getLength(); i++) {
128 final Element path = (Element) paths.item(i);
129 if (name.equals(path.getAttribute(CacheConfiguration.NAME))) {
130 return path;
131 }
132 }
133 return null;
134 }
135
136 /**
137 * Adds a revision to the current path in the DOM. To be preceeded by (@link
138 * #addPath(String))
139 *
140 * @param number
141 * the revision number
142 * @param added
143 * the number of lines that were added
144 * @param removed
145 * the number of lines that were removed
146 */
147 private void addDOMRevision(final String number, final String added, final String removed, final String binaryStatus) {
148 final Element revision = document.createElement(CacheConfiguration.REVISION);
149 revision.setAttribute(CacheConfiguration.NUMBER, number);
150 revision.setAttribute(CacheConfiguration.ADDED, added);
151 revision.setAttribute(CacheConfiguration.REMOVED, removed);
152 revision.setAttribute(CacheConfiguration.BINARY_STATUS, binaryStatus);
153 currentPath.appendChild(revision);
154 }
155
156 /**
157 * Initializes the builder for subsequent invocations of (@link
158 * #buildRevision(String, String, String)).
159 *
160 * @param name
161 * the filename
162 */
163 public void buildPath(final String name, final String revision, final String binaryStatus) {
164 currentFilename = repositoryFileManager.absoluteToRelativePath(name);
165 addDOMPath(name, revision, binaryStatus);
166
167 }
168
169 /**
170 * Given the file specified by the preceeding invocation to (@link
171 * #buildPath(String)), set the line counts for the given revision.
172 *
173 * If the path given in the preceeding invocation to (@link
174 * #buildPath(String)) is not used by the (@link SvnLogBuilder), this call
175 * does nothing.
176 *
177 * @param number
178 * the revision number
179 * @param added
180 * the number of lines added
181 * @param removed
182 * the number of lines removed.
183 */
184 public void buildRevision(final String number, final String added, final String removed, final String binaryStatus) {
185 if (!added.equals("-1") && !removed.equals("-1")) {
186 addDOMRevision(number, added, removed, binaryStatus);
187 builder.updateRevision(currentFilename, number, Integer.parseInt(added), Integer.parseInt(removed));
188 }
189 }
190
191 /**
192 * Builds the DOM root.
193 *
194 * @throws ParserConfigurationException
195 */
196 public void buildRoot() throws ParserConfigurationException {
197 final DocumentBuilderFactory factoryDOM = DocumentBuilderFactory.newInstance();
198 DocumentBuilder builderDOM;
199 builderDOM = factoryDOM.newDocumentBuilder();
200 document = builderDOM.newDocument();
201 cache = document.createElement(CacheConfiguration.CACHE);
202 cache.setAttribute(CacheConfiguration.PROJECT, ConfigurationOptions.getProjectName());
203 cache.setAttribute(CacheConfiguration.XML_VERSION, "1.0");
204 document.appendChild(cache);
205 }
206
207 /**
208 * Returns the DOM object when building is complete.
209 *
210 * @return the DOM document.
211 */
212 public Document getDocument() {
213 return document;
214 }
215
216 /**
217 * Adds a revision to the DOM.
218 *
219 * Encapsulates calls to (@link #buildRoot()), (@link #buildPath(String)),
220 * and (@link #buildRevision(String, String, String)) into one easy to use
221 * interface.
222 *
223 *
224 * @param name
225 * the filename
226 * @param number
227 * the revision number
228 * @param added
229 * the number of lines added
230 * @param removed
231 * the number of lines removed
232 */
233 public synchronized void newRevision(String name, final String number, final String added, final String removed, final boolean binaryStatus) {
234 name = repositoryFileManager.relativeToAbsolutePath(name);
235 checkDocument();
236 if (document != null) {
237 currentPath = findDOMPath(name);
238 if (currentPath == null) {
239
240 addDOMPath(name, "0", CacheConfiguration.UNKNOWN);
241 }
242 String sBinaryStatus = CacheConfiguration.NOT_BINARY;
243 if (binaryStatus) {
244 sBinaryStatus = CacheConfiguration.BINARY;
245 }
246 addDOMRevision(number, added, removed, sBinaryStatus);
247 }
248 }
249
250 private void checkDocument() {
251 if (document == null) {
252 try {
253 buildRoot();
254 } catch (final ParserConfigurationException e) {
255 document = null;
256 }
257 }
258 }
259
260 /**
261 * Updates all paths in the DOM structure with the latest binary status
262 * information from the working folder.
263 *
264 * @param name
265 * the filename
266 * @param number
267 * the revision number
268 * @param added
269 * the number of lines added
270 * @param removed
271 * the number of lines removed
272 */
273 public void updateBinaryStatus(final Collection fileBuilders, final String revisionNumber) {
274
275 final Map mFileBuilders = new HashMap();
276 for (final Iterator iter = fileBuilders.iterator(); iter.hasNext();) {
277 final FileBuilder fileBuilder = (FileBuilder) iter.next();
278 mFileBuilders.put(fileBuilder.getName(), fileBuilder);
279 }
280 if (!mFileBuilders.isEmpty()) {
281
282
283
284
285 checkDocument();
286 final NodeList paths = cache.getChildNodes();
287 for (int i = 0; i < paths.getLength(); i++) {
288 final Element path = (Element) paths.item(i);
289 if (mFileBuilders.containsKey(repositoryFileManager.absoluteToRelativePath(path.getAttribute(CacheConfiguration.NAME)))) {
290 final FileBuilder fileBuilder = (FileBuilder) mFileBuilders.get(repositoryFileManager.absoluteToRelativePath(path
291 .getAttribute(CacheConfiguration.NAME)));
292 updateDOMPath(path, fileBuilder.isBinary(), revisionNumber);
293 mFileBuilders.remove(repositoryFileManager.absoluteToRelativePath(path.getAttribute(CacheConfiguration.NAME)));
294 }
295 }
296
297 final Collection cFileBuilders = mFileBuilders.values();
298 for (final Iterator iter = cFileBuilders.iterator(); iter.hasNext();) {
299 final FileBuilder fileBuilder = (FileBuilder) iter.next();
300 String binaryStatus = CacheConfiguration.NOT_BINARY;
301 if (fileBuilder.isBinary()) {
302 binaryStatus = CacheConfiguration.BINARY;
303 }
304 addDOMPath(repositoryFileManager.relativeToAbsolutePath(fileBuilder.getName()), revisionNumber, binaryStatus);
305 }
306 }
307
308 }
309
310 /**
311 * Checks the path's cached binary status.
312 *
313 * @param fileName
314 * the path to be checked
315 * @param revisionNumber
316 * the revision of the path to be checked
317 * @return true if the path's BINARY_STATUS is true and the revisionNumber
318 * is lower or equal to the path's LATEST_REVISION
319 */
320 public synchronized boolean isBinary(final String fileName, final String revisionNumber) {
321 int latestRevision = 0;
322 int revisionToCheck = -1;
323 checkDocument();
324 final Element path = findDOMPath(repositoryFileManager.relativeToAbsolutePath(fileName));
325 if (path == null) {
326 return false;
327 }
328 try {
329 latestRevision = Integer.parseInt(path.getAttribute(CacheConfiguration.LATEST_REVISION));
330 revisionToCheck = Integer.parseInt(revisionNumber);
331 } catch (final NumberFormatException e) {
332 SvnConfigurationOptions.getTaskLogger().log(
333 "Ignoring invalid revision number " + revisionNumber + " for " + path.getAttribute(CacheConfiguration.NAME));
334 revisionToCheck = -1;
335 }
336 if (latestRevision >= revisionToCheck) {
337 final String binaryStatus = path.getAttribute(CacheConfiguration.BINARY_STATUS);
338 if (binaryStatus.equals(CacheConfiguration.BINARY)) {
339 return true;
340 }
341 }
342 return false;
343 }
344 }