View Javadoc

1   ////////////////////////////////////////////////////////////////////////////////
2   //
3   //                  ObjectLab is sponsoring QALab
4   // 
5   // Based in London, we are world leaders in the design and development 
6   // of bespoke applications for the Securities Financing markets.
7   // 
8   // <a href="http://www.objectlab.co.uk/open">Click here to learn more</a>
9   //           ___  _     _           _   _          _
10  //          / _ \| |__ (_) ___  ___| |_| |    __ _| |__
11  //         | | | | '_ \| |/ _ \/ __| __| |   / _` | '_ \
12  //         | |_| | |_) | |  __/ (__| |_| |__| (_| | |_) |
13  //          \___/|_.__// |\___|\___|\__|_____\__,_|_.__/
14  //                   |__/
15  //
16  //                   http://www.ObjectLab.co.uk
17  // ---------------------------------------------------------------------------
18  //
19  //QALab is released under the GNU General Public License.
20  //
21  //QALab: Collects QA Statistics from your build over time.
22  //2005+, ObjectLab Ltd
23  //
24  //This library is free software; you can redistribute it and/or
25  //modify it under the terms of the GNU General Public
26  //License as published by the Free Software Foundation; either
27  //version 2.1 of the License, or (at your option) any later version.
28  //
29  //This library is distributed in the hope that it will be useful,
30  //but WITHOUT ANY WARRANTY; without even the implied warranty of
31  //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
32  //General Public License for more details.
33  //
34  //You should have received a copy of the GNU General Public
35  //License along with this library; if not, write to the Free Software
36  //Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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         // now check the timeStamp
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                 // create the new file.
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         // Create the builder and parse the file
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                 // now handle the change... from null or whatever to
282                 // XML_VERSION.
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                     // now in the results, check the timestamp
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                     // now in the results, check the timestamp
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             // replace DTD!
457             StringWriter sw = new StringWriter();
458             XMLSerializer serial = new XMLSerializer(sw, format);
459 
460             serial.asDOMSerializer(); // As a DOM Serializer
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(); // As a DOM Serializer
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         // first get rid of the summary
526         NodeList list = compiledStats.getElementsByTagName(QALabTags.SUMMARY_TAG);
527         clearChildren(list, timestampStr, type);
528 
529         // first get rid of the files
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                 // get rid of it!
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  *                   ObjectLab is sponsoring QALab
578  * 
579  * Based in London, we are world leaders in the design and development 
580  * of bespoke applications for the securities financing markets.
581  * 
582  * <a href="http://www.objectlab.co.uk/open">Click here to learn more about us</a>
583  *           ___  _     _           _   _          _
584  *          / _ \| |__ (_) ___  ___| |_| |    __ _| |__
585  *         | | | | '_ \| |/ _ \/ __| __| |   / _` | '_ \
586  *         | |_| | |_) | |  __/ (__| |_| |__| (_| | |_) |
587  *          \___/|_.__// |\___|\___|\__|_____\__,_|_.__/
588  *                   |__/
589  *
590  *                     www.ObjectLab.co.uk
591  */