1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 package net.objectlab.qalab.exporter;
40
41 import java.io.File;
42 import java.io.FileInputStream;
43 import java.io.FileWriter;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.io.StringWriter;
47 import java.util.Locale;
48 import java.util.Properties;
49
50 import javax.xml.parsers.DocumentBuilderFactory;
51 import javax.xml.parsers.ParserConfigurationException;
52
53 import net.objectlab.qalab.interfaces.QALabExporter;
54 import net.objectlab.qalab.util.QALabTags;
55 import net.objectlab.qalab.util.TaskLogger;
56 import net.objectlab.qalab.util.Util;
57
58 import org.apache.tools.ant.BuildException;
59 import org.apache.xml.serialize.OutputFormat;
60 import org.apache.xml.serialize.XMLSerializer;
61 import org.w3c.dom.Document;
62 import org.w3c.dom.Element;
63 import org.w3c.dom.Node;
64 import org.w3c.dom.NodeList;
65 import org.xml.sax.InputSource;
66 import org.xml.sax.SAXException;
67
68 /**
69 * This is the main exporter to an XML file following the QALab DTD.
70 *
71 * @author Benoit Xhenseval
72 * @version $Revision$
73 */
74 public class QALabXMLExporter implements QALabExporter {
75 private static final String XML_VERSION = "1.1";
76
77 /**
78 * If no qalab.xml is found, this will be the basis of a new one.
79 */
80 private static final String DTD_DEFINITION = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
81 + System.getProperty("line.separator") + "<!DOCTYPE qalab [" + System.getProperty("line.separator")
82 + "<!ELEMENT qalab (summary, file*)>" + System.getProperty("line.separator")
83 + "<!ATTLIST qalab version CDATA #REQUIRED> " + System.getProperty("line.separator")
84 + "<!ELEMENT summary (summaryresult*)>" + System.getProperty("line.separator") + "<!ELEMENT file (result+)>"
85 + System.getProperty("line.separator") + "<!ATTLIST file id ID #REQUIRED" + System.getProperty("line.separator")
86 + " path CDATA #REQUIRED>" + System.getProperty("line.separator") + "<!ELEMENT result EMPTY>"
87 + System.getProperty("line.separator") + "<!ATTLIST result date CDATA #REQUIRED"
88 + System.getProperty("line.separator") + " statvalue CDATA #REQUIRED" + System.getProperty("line.separator")
89 + " type CDATA #REQUIRED>" + System.getProperty("line.separator") + "<!ELEMENT summaryresult EMPTY>"
90 + System.getProperty("line.separator") + "<!ATTLIST summaryresult date CDATA #REQUIRED"
91 + System.getProperty("line.separator") + " statvalue CDATA #REQUIRED" + System.getProperty("line.separator")
92 + " filecount CDATA #REQUIRED" + System.getProperty("line.separator") + " type CDATA #REQUIRED>"
93 + System.getProperty("line.separator") + "]>" + System.getProperty("line.separator");
94
95 private static final String EMPTY_FILE = DTD_DEFINITION + System.getProperty("line.separator") + "<qalab version=\""
96 + XML_VERSION + "\">" + " <summary/>" + "</qalab>";
97
98 /** current XML version * */
99 private String currentXmlVersion = null;
100
101 /** the compiled stats. */
102 private Document compiledStats = null;
103
104 /** given implementation for the logger. * */
105 private TaskLogger taskLogger = null;
106
107 /** if true, no debug information is logged. * */
108 private boolean quiet = true;
109
110 /** the merged properties file. */
111 private File outputFile = null;
112
113 /** the action for same type and timestamp replace (default)| new. */
114 private String action = "replace";
115
116 /** timestamp to use. */
117 private String timestampToUse = null;
118
119 /** type of stats we are interested in. */
120 private String typeToUse = null;
121
122 /**
123 * For instance for the XML Exporter.
124 * <ul>
125 * <li>qalab.outputfile = the output file name and path.</li>
126 * <li>qalab.merge.output.classname = alternatively, the XML could be a
127 * resource for this class</li>
128 * <li>qalab.merge.output.resourcename = the resource name for the
129 * qalab.merge.output.classname given</li>
130 * <li>qalab.merge.timestamp = the timestamp YYYY-MM-dd HH:mm:ss</li>
131 * <li>qalab.merge.type = the type of statistics being merged. (eg
132 * checkstyle,findbugs,simian, pmd)</li>
133 * <li>qalab.merge.action = the action to take in case of clash
134 * type/timetstamp</li>
135 * </ul>
136 *
137 * @param properties
138 * the properties.
139 * @see net.objectlab.qalab.interfaces.
140 * QALabExporter#configure(java.util.Properties)
141 */
142 public final void configure(final Properties properties) {
143 InputStream stream = null;
144
145 try {
146 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
147 factory.setValidating(false);
148
149 stream = parseOriginalDocument(properties, factory);
150
151 validateXmlVersion(compiledStats);
152
153 if (!quiet) {
154 getTaskLogger().log("Parsed Statistics.");
155 }
156
157 loadActionAndStamp(properties);
158
159 if ("replace".equals(action)) {
160 clearAll(timestampToUse, typeToUse);
161 if (!quiet) {
162 getTaskLogger().log("Finished clearing due to " + action);
163 }
164 }
165 } catch (IOException e) {
166 throw new BuildException("Cannot create file ", e);
167 } catch (SAXException e) {
168 throw new BuildException("Cannot parse file ", e);
169 } catch (ClassNotFoundException e) {
170 throw new BuildException("Cannot find class ", e);
171 } catch (ParserConfigurationException e) {
172 throw new BuildException("Issue with ParserConfiguration ", e);
173 } finally {
174 if (stream != null) {
175 try {
176 stream.close();
177 } catch (IOException e) {
178 throw new BuildException("Cannot close streams", e);
179 }
180 }
181 if (!quiet) {
182 getTaskLogger().log("Finished Configuring XML streams.");
183 }
184 }
185 }
186
187 /**
188 * @param properties
189 */
190 private void loadActionAndStamp(final Properties properties) {
191 final String actionToUse = properties.getProperty("qalab.merge.action");
192
193 if (actionToUse != null) {
194 action = actionToUse.toLowerCase(Locale.ENGLISH);
195 }
196
197 if (!quiet) {
198 getTaskLogger().log("ACTION: " + action);
199 }
200
201
202 timestampToUse = properties.getProperty("qalab.merge.output.timestamp");
203
204 if (!quiet) {
205 getTaskLogger().log("Timestamp to use:" + timestampToUse);
206 }
207
208 typeToUse = properties.getProperty("qalab.merge.type");
209 }
210
211 /**
212 * @param properties
213 * @param factory
214 * @return
215 * @throws IOException
216 * @throws FileNotFoundException
217 * @throws ClassNotFoundException
218 * @throws SAXException
219 * @throws ParserConfigurationException
220 */
221 private InputStream parseOriginalDocument(final Properties properties, DocumentBuilderFactory factory) throws IOException,
222 ClassNotFoundException, SAXException, ParserConfigurationException {
223 InputStream stream;
224 final String file = properties.getProperty("qalab.merge.output.file");
225
226 if (file != null) {
227 outputFile = new File(file);
228
229 if (!quiet) {
230 getTaskLogger().log("Parsing " + outputFile);
231 }
232
233 if (!outputFile.exists()) {
234
235 if (outputFile.createNewFile()) {
236 FileWriter stringOut = null;
237
238 try {
239 stringOut = new FileWriter(outputFile);
240 stringOut.write(EMPTY_FILE);
241 stringOut.flush();
242 } finally {
243 if (stringOut != null) {
244 stringOut.close();
245 }
246 }
247 } else {
248 throw new BuildException("Cannot create file " + outputFile.getAbsolutePath());
249 }
250 }
251
252 if (!quiet) {
253 getTaskLogger().log("Output file: " + outputFile.getAbsolutePath());
254 }
255
256 stream = new FileInputStream(outputFile);
257 } else {
258 final String resourceName = properties.getProperty("qalab.merge.output.resourcename");
259 final String className = properties.getProperty("qalab.merge.output.classname");
260
261 if (!quiet) {
262 getTaskLogger().log("Using Stream: " + resourceName + " for " + className);
263 }
264
265 stream = Class.forName(className).getResourceAsStream(resourceName);
266 }
267
268
269 compiledStats = factory.newDocumentBuilder().parse(new InputSource(stream));
270 return stream;
271 }
272
273 private void validateXmlVersion(Document doc) {
274 Element qalab = getElement(doc.getElementsByTagName("qalab"));
275
276 if (qalab != null) {
277 currentXmlVersion = qalab.getAttribute("version");
278 if (currentXmlVersion == null || !currentXmlVersion.equals(XML_VERSION)) {
279 getTaskLogger().log("Inadequate DTD, must change it to " + XML_VERSION);
280 qalab.setAttribute("version", XML_VERSION);
281
282
283 changeXmlToDtdV11(qalab);
284 }
285 }
286 }
287
288 /**
289 * @param qalab
290 */
291 private void changeXmlToDtdV11(Element qalab) {
292 NodeList list = qalab.getChildNodes();
293 for (int i = 0; i < list.getLength(); i++) {
294 Node childNode = list.item(i);
295 if (childNode.getNodeType() == Node.ELEMENT_NODE) {
296 if (QALabTags.FILE_TAG.equals(childNode.getNodeName())) {
297 Element fileNode = (Element) list.item(i);
298 String id = fileNode.getAttribute(QALabTags.ID_ATTRIBUTE);
299 fileNode.setAttribute(QALabTags.ID_ATTRIBUTE, id.replace('/', '_'));
300 fileNode.setAttribute(QALabTags.PATH_ATTRIBUTE, id);
301
302 NodeList results = fileNode.getChildNodes();
303 for (int u = 0; u < results.getLength(); u++) {
304 Node resultNode = results.item(u);
305 if (resultNode.getNodeType() == Node.ELEMENT_NODE
306 && QALabTags.RESULT_TAG.equals(resultNode.getNodeName())) {
307 Element resultElement = (Element) resultNode;
308 String timestamp = resultElement.getAttribute(QALabTags.DATE_ATTRIBUTE);
309 dropMidnight(resultElement, timestamp);
310 }
311 }
312 } else if (QALabTags.SUMMARY_TAG.equals(childNode.getNodeName())) {
313 Element summaryNode = (Element) list.item(i);
314
315 NodeList results = summaryNode.getChildNodes();
316 for (int u = 0; u < results.getLength(); u++) {
317 Node resultNode = results.item(u);
318 if (resultNode.getNodeType() == Node.ELEMENT_NODE
319 && QALabTags.SUMMARY_RESULT_TAG.equals(resultNode.getNodeName())) {
320 Element resultElement = (Element) resultNode;
321 String timestamp = resultElement.getAttribute(QALabTags.DATE_ATTRIBUTE);
322 dropMidnight(resultElement, timestamp);
323 }
324 }
325 }
326 }
327 }
328 }
329
330 /**
331 * Remove the 00:00:00 from timestamp.
332 *
333 * @param resultElement
334 * @param timestamp
335 */
336 private void dropMidnight(Element resultElement, String timestamp) {
337 final int indexOf = timestamp.indexOf(" 00:00:00");
338 if (indexOf > 0) {
339 resultElement.setAttribute(QALabTags.DATE_ATTRIBUTE, timestamp.substring(0, indexOf));
340 }
341 }
342
343 /**
344 * Add summary details for this merger of statistics for this type.
345 *
346 * @param violationCount
347 * total number of violations for this type
348 * @param fileCount
349 * total number of files affected by this type.
350 */
351 public final void addSummary(final int violationCount, final int fileCount) {
352 NodeList list = compiledStats.getElementsByTagName(QALabTags.SUMMARY_TAG);
353
354 for (int i = 0; i < list.getLength(); i++) {
355 Node childNode = list.item(i);
356
357 if (childNode.getNodeType() == Node.ELEMENT_NODE) {
358 Element summary = (Element) childNode;
359 Element foundSummaryResult = summary.getOwnerDocument().createElement(QALabTags.SUMMARY_RESULT_TAG);
360
361 foundSummaryResult.setAttribute(QALabTags.TYPE_ATTRIBUTE, typeToUse);
362 foundSummaryResult.setAttribute(QALabTags.FILECOUNT_ATTRIBUTE, "" + fileCount);
363 foundSummaryResult.setAttribute(QALabTags.STATVALUE_ATTRIBUTE, "" + violationCount);
364 foundSummaryResult.setAttribute(QALabTags.DATE_ATTRIBUTE, timestampToUse);
365
366 summary.appendChild(foundSummaryResult);
367
368 if (!isQuiet()) {
369 getTaskLogger().log("Summary:" + typeToUse + " file:" + fileCount + " issues:" + violationCount);
370 }
371
372 break;
373 }
374 }
375 }
376
377 /**
378 * Add a result entry for a given file name and type.
379 *
380 * @param violationCount
381 * total number of violations for this type and file.
382 * @param fileName
383 * file name
384 * @see net.objectlab.qalab.interfaces.QALabExporter#addFileResult(
385 * java.util.Date, int, java.lang.String, java.lang.String)
386 */
387 public final void addFileResult(final int violationCount, final String fileName) {
388 String id = fileName.replace('/', '_');
389 Element fileElement = compiledStats.getElementById(id);
390
391 if (!isQuiet()) {
392 taskLogger.log("* addNewResults for [" + fileName + "] Element:" + id);
393 }
394
395 if (fileElement == null) {
396 if (!isQuiet()) {
397 taskLogger.log("Create file new ENTRY......");
398 }
399
400 fileElement = compiledStats.createElement(QALabTags.FILE_TAG);
401 fileElement.setAttribute(QALabTags.ID_ATTRIBUTE, id);
402 fileElement.setAttribute(QALabTags.PATH_ATTRIBUTE, fileName);
403 compiledStats.getDocumentElement().appendChild(fileElement);
404 }
405 Element newResult = compiledStats.createElement(QALabTags.RESULT_TAG);
406
407 newResult.setAttribute(QALabTags.TYPE_ATTRIBUTE, typeToUse);
408 newResult.setAttribute(QALabTags.STATVALUE_ATTRIBUTE, "" + violationCount);
409 newResult.setAttribute(QALabTags.DATE_ATTRIBUTE, timestampToUse);
410 fileElement.appendChild(newResult);
411 }
412
413 /**
414 * Find the element in the children that has the same timestamp and type.
415 *
416 * @param startingElement
417 * the start element in the DOM
418 * @param timestampStr
419 * the timestamp
420 * @param type
421 * the type we are looking for.
422 * @return Element or null if not found
423 */
424 private Element findSameTypeAndTimestamp(final Element startingElement, final String timestampStr, final String type) {
425 Element newResult = null;
426 NodeList fileResults = startingElement.getChildNodes();
427
428 if ("replace".equals(action)) {
429 for (int u = 0; u < fileResults.getLength() && newResult == null; u++) {
430 Node resultNode = fileResults.item(u);
431
432 if (resultNode.getNodeType() == Node.ELEMENT_NODE) {
433 final String stamp = Util.getAttributeValue(resultNode.getAttributes(), "date", isQuiet(), getTaskLogger());
434 final String existingType = Util.getAttributeValue(resultNode.getAttributes(), "type", isQuiet(),
435 getTaskLogger());
436 if (timestampStr.equals(stamp) && type.equals(existingType)) {
437 newResult = (Element) resultNode;
438 }
439 }
440 }
441 }
442 return newResult;
443 }
444
445 /**
446 * Save the stats (called when the parsing of input statistics is
447 * completed).
448 *
449 * @see net.objectlab.qalab.interfaces.QALabExporter#save()
450 */
451 public final void save() throws IOException {
452 OutputFormat format = new OutputFormat(compiledStats, "UTF-8", true);
453 FileWriter stringOut = new FileWriter(outputFile);
454
455 if (!XML_VERSION.equals(currentXmlVersion)) {
456
457 StringWriter sw = new StringWriter();
458 XMLSerializer serial = new XMLSerializer(sw, format);
459
460 serial.asDOMSerializer();
461 serial.serialize(compiledStats);
462 sw.flush();
463 sw.close();
464 StringBuffer newXml = new StringBuffer(sw.getBuffer().substring(sw.getBuffer().indexOf("<qalab")));
465 newXml.insert(0, DTD_DEFINITION);
466 stringOut.write(newXml.toString());
467 } else {
468 XMLSerializer serial = new XMLSerializer(stringOut, format);
469
470 serial.asDOMSerializer();
471 serial.serialize(compiledStats);
472 }
473 stringOut.flush();
474 stringOut.close();
475 }
476
477 /**
478 * @return logger to use if not quiet.
479 */
480 protected final TaskLogger getTaskLogger() {
481 return taskLogger;
482 }
483
484 /**
485 * set the task logger, ie mechanism to log issues & debug info.
486 *
487 * @param task
488 * the logger to use
489 */
490 public final void setTaskLogger(final TaskLogger task) {
491 taskLogger = task;
492 }
493
494 /**
495 * @return true means that no debug info is logged.
496 */
497 public final boolean isQuiet() {
498 return quiet;
499 }
500
501 /**
502 * @param noLog
503 * true if no log required.
504 */
505 public final void setQuiet(final boolean noLog) {
506 this.quiet = noLog;
507 }
508
509 /**
510 * @return the compiled document.
511 */
512 public final Document getDocument() {
513 return compiledStats;
514 }
515
516 /**
517 * get rid of the element with same timestamp and type.
518 *
519 * @param timestampStr
520 * the timestamp to eliminate
521 * @param type
522 * the type to eliminate
523 */
524 private void clearAll(final String timestampStr, final String type) {
525
526 NodeList list = compiledStats.getElementsByTagName(QALabTags.SUMMARY_TAG);
527 clearChildren(list, timestampStr, type);
528
529
530 NodeList listFiles = compiledStats.getElementsByTagName(QALabTags.FILE_TAG);
531 clearChildren(listFiles, timestampStr, type);
532 }
533
534 /**
535 * get rid of the element with same timestamp and type.
536 *
537 * @param list
538 * list of nodes that may contain items with same timestamp and
539 * type.
540 * @param timestampStr
541 * the timestamp to eliminate
542 * @param type
543 * the type to eliminate
544 */
545 private void clearChildren(final NodeList list, final String timestampStr, final String type) {
546 for (int i = 0; i < list.getLength(); i++) {
547 Node childNode = list.item(i);
548
549 if (childNode.getNodeType() == Node.ELEMENT_NODE) {
550 Element summary = (Element) childNode;
551
552 Element foundSummaryResult = findSameTypeAndTimestamp(summary, timestampStr, type);
553
554
555 if (foundSummaryResult != null) {
556 if (!isQuiet()) {
557 getTaskLogger()
558 .log("Removing element due to same type and timestamp " + foundSummaryResult.getNodeName());
559 }
560 childNode.removeChild(foundSummaryResult);
561 }
562 }
563 }
564 }
565
566 private Element getElement(NodeList list) {
567 for (int i = 0; i < list.getLength(); i++) {
568 Node childNode = list.item(i);
569 if (childNode.getNodeType() == Node.ELEMENT_NODE) {
570 return (Element) childNode;
571 }
572 }
573 return null;
574 }
575 }
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591