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 	 */
CacheBuilder(final SvnLogBuilder builder, final RepositoryFileManager repositoryFileManager)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 	 */
addDOMPath(final String name, final String latestRevision, final String binaryStatus)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 	 */
updateDOMPath(final Element path, final boolean isBinary, final String revisionNumber)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 	 */
findDOMPath(final String name)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 	 */
addDOMRevision(final String number, final String added, final String removed, final String binaryStatus)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 	 */
buildPath(final String name, final String revision, final String binaryStatus)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 	 */
buildRevision(final String number, final String added, final String removed, final String binaryStatus)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 	 */
buildRoot()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 	 */
getDocument()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 	 */
newRevision(String name, final String number, final String added, final String removed, final boolean binaryStatus)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 				// changes currentPath to new one
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 
checkDocument()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 	 */
updateBinaryStatus(final Collection fileBuilders, final String revisionNumber)273 	public void updateBinaryStatus(final Collection fileBuilders, final String revisionNumber) {
274 		// change data structure to a more appropriate one for lookup
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 			// go through all the paths in the DOM and update their binary
282 			// status
283 			// remove the fileBuilder once its corresponding path in the DOM was
284 			// dealt with
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 			// go through remaining fileBuilders and add them to the DOM
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 	 */
isBinary(final String fileName, final String revisionNumber)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 }
345