View Javadoc

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 				// 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 
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 		// 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 	 */
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 }