001: /*
002:
003: Licensed to the Apache Software Foundation (ASF) under one or more
004: contributor license agreements. See the NOTICE file distributed with
005: this work for additional information regarding copyright ownership.
006: The ASF licenses this file to You under the Apache License, Version 2.0
007: (the "License"); you may not use this file except in compliance with
008: the License. You may obtain a copy of the License at
009:
010: http://www.apache.org/licenses/LICENSE-2.0
011:
012: Unless required by applicable law or agreed to in writing, software
013: distributed under the License is distributed on an "AS IS" BASIS,
014: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015: See the License for the specific language governing permissions and
016: limitations under the License.
017:
018: */
019: package org.apache.batik.test.xml;
020:
021: import java.io.File;
022: import java.io.FileInputStream;
023: import java.io.FileOutputStream;
024: import java.io.InputStream;
025: import java.io.OutputStream;
026: import java.io.BufferedInputStream;
027: import java.io.BufferedOutputStream;
028: import java.io.FileWriter;
029: import java.io.IOException;
030: import java.io.PrintWriter;
031: import java.io.StringWriter;
032: import java.io.Writer;
033:
034: import java.net.URL;
035:
036: import java.util.Calendar;
037:
038: import javax.xml.parsers.DocumentBuilderFactory;
039: import javax.xml.parsers.DocumentBuilder;
040:
041: import org.apache.batik.test.TestReport;
042: import org.apache.batik.test.TestReportProcessor;
043: import org.apache.batik.test.TestSuite;
044: import org.apache.batik.test.TestException;
045:
046: import org.apache.batik.util.XMLConstants;
047:
048: import org.w3c.dom.Attr;
049: import org.w3c.dom.Document;
050: import org.w3c.dom.Element;
051: import org.w3c.dom.NamedNodeMap;
052: import org.w3c.dom.Node;
053: import org.w3c.dom.NodeList;
054: import org.w3c.dom.DOMImplementation;
055:
056: /**
057: * This implementation of the <tt>TestReportProcessor</tt> interface
058: * converts the <tt>TestReports</tt> it processes into an
059: * XML document that it outputs in a directory. The directory
060: * used by the object can be configured at creation time.
061: * <br />
062: * The <tt>XMLTestReportProcessor</tt> can optionally notify a
063: * report consumer of the XML file it created.
064: *
065: * @author <a href="mailto:vhardy@apache.org">Vincent Hardy</a>
066: * @version $Id: XMLTestReportProcessor.java 482121 2006-12-04 10:00:39Z dvholten $
067: */
068: public class XMLTestReportProcessor implements TestReportProcessor,
069: XTRConstants, XMLConstants {
070: /**
071: * An <tt>XMLReportConsumer</tt> is notified every time a
072: * new report is generated by an <tt>XMLTestReportProcessor</tt>
073: */
074: public static interface XMLReportConsumer {
075: /**
076: * Invoked when new report has been generated.
077: * @param xmlReport file containing the xml report
078: * @param reportDirectory base directory where any resource relative
079: * to the report processing should be stored.
080: */
081: void onNewReport(File xmlReport, File reportDirectory)
082: throws Exception;
083: }
084:
085: /**
086: * Error message if report directory does not exist.
087: */
088: public static final String ERROR_REPORT_DIRECTORY_UNUSABLE = "xml.XMLTestReportProcessor.error.report.directory.unusable";
089:
090: /**
091: * Error message if report resources directory does not exist.
092: */
093: public static final String ERROR_REPORT_RESOURCES_DIRECTORY_UNUSABLE = "xml.XMLTestReportProcessor.error.report.resources.directory.unusable";
094:
095: /**
096: * Default report directory
097: */
098: public static final String XML_TEST_REPORT_DEFAULT_DIRECTORY = Messages
099: .formatMessage(
100: "XMLTestReportProcessor.config.xml.test.report.default.directory",
101: null);
102:
103: /**
104: * Directory where the XML report is created
105: */
106: public static final String XML_REPORT_DIRECTORY = Messages
107: .formatMessage(
108: "XMLTestReportProcessor.xml.report.directory", null);
109:
110: /**
111: * Directory where resources (e.g., images) referenced by the
112: * XML report are copied.
113: */
114: public static final String XML_RESOURCES_DIRECTORY = Messages
115: .formatMessage(
116: "XMLTestReportProcessor.xml.resources.directory",
117: null);
118:
119: /**
120: * Test report name
121: */
122: public static final String XML_TEST_REPORT_NAME = Messages
123: .formatMessage(
124: "XMLTestReportProcessor.config.xml.test.report.name",
125: null);
126:
127: /**
128: * The XMLReportConsumer instance is notified whenever
129: * this object generates a new report.
130: */
131: protected XMLReportConsumer consumer;
132:
133: /**
134: * String encoding the date the report was generated.
135: */
136: protected String reportDate;
137:
138: /**
139: * Directory into which this processor puts all files and resources.
140: */
141: protected File reportDirectory;
142:
143: /**
144: * Directory into which XML files are created
145: */
146: protected File xmlDirectory;
147:
148: /**
149: * Directory into whichr resources refered to by XML files are created
150: */
151: protected File xmlResourcesDirectory;
152:
153: /**
154: * Default constructor
155: */
156: public XMLTestReportProcessor() {
157: }
158:
159: /**
160: * @param consumer consumer for the XML report generated
161: * by this object. May be null.
162: */
163: public XMLTestReportProcessor(
164: XMLTestReportProcessor.XMLReportConsumer consumer) {
165: this .consumer = consumer;
166: }
167:
168: /**
169: * Recursively processes the input <tt>TestReport</tt> and
170: * any of its children.
171: */
172: public void processReport(TestReport report) throws TestException {
173:
174: /**
175: * First, create the directories for the
176: * report and report resources
177: */
178: initializeReportDirectories();
179:
180: try {
181:
182: /**
183: * Create a new document and build the root
184: * <testReport> element. Then, process the
185: * TestReports recursively.
186: */
187: DocumentBuilder docBuilder = DocumentBuilderFactory
188: .newInstance().newDocumentBuilder();
189: DOMImplementation impl = docBuilder.getDOMImplementation();
190:
191: Document document = null;
192:
193: if (report.getTest() instanceof TestSuite) {
194: document = impl.createDocument(XTR_NAMESPACE_URI,
195: XTR_TEST_SUITE_REPORT_TAG, null);
196: } else {
197: document = impl.createDocument(XTR_NAMESPACE_URI,
198: XTR_TEST_REPORT_TAG, null);
199: }
200:
201: Element root = document.getDocumentElement();
202:
203: root.setAttribute(XTR_DATE_ATTRIBUTE, reportDate);
204:
205: processReport(report, root, document);
206:
207: File xmlReport = serializeReport(root);
208:
209: if (consumer != null) {
210: consumer.onNewReport(xmlReport, getReportDirectory());
211: }
212:
213: } catch (Exception e) {
214: StringWriter sw = new StringWriter();
215: PrintWriter pw = new PrintWriter(sw);
216: e.printStackTrace(pw);
217: throw new TestException(INTERNAL_ERROR, new Object[] {
218: e.getClass().getName(), e.getMessage(),
219: sw.toString() }, e);
220: }
221: }
222:
223: /**
224: * Checks that the input File represents a directory that
225: * can be used. If the directory does not exist, this method
226: * will attempt to create it.
227: */
228: public void checkDirectory(File dir, String errorCode)
229: throws TestException {
230: boolean dirOK = false;
231: try {
232: if (!dir.exists()) {
233: dirOK = dir.mkdir();
234: } else if (dir.isDirectory()) {
235: dirOK = true;
236: }
237: } finally {
238: if (!dirOK) {
239: throw new TestException(errorCode, new Object[] { dir
240: .getAbsolutePath() }, null);
241:
242: }
243: }
244: }
245:
246: /**
247: * By default, the report directory is given by a configuration
248: * variable. Each test run will create a sub directory with
249: * the current date and time as the same. All the resources
250: * created by the report processor are then put into that
251: * "dated" directory.
252: */
253: public void initializeReportDirectories() throws TestException {
254: //
255: // Base report directory
256: //
257: File baseReportDir = new File(XML_TEST_REPORT_DEFAULT_DIRECTORY);
258: checkDirectory(baseReportDir, ERROR_REPORT_DIRECTORY_UNUSABLE);
259:
260: //
261: // Create sub-directory name based on date and time
262: //
263: Calendar c = Calendar.getInstance();
264: String dirName = "" + c.get(Calendar.YEAR) + "."
265: + makeTwoDigits(c.get(Calendar.MONTH) + 1) + "."
266: + makeTwoDigits(c.get(Calendar.DAY_OF_MONTH)) + "-"
267: + makeTwoDigits(c.get(Calendar.HOUR_OF_DAY)) + "h"
268: + makeTwoDigits(c.get(Calendar.MINUTE)) + "m"
269: + makeTwoDigits(c.get(Calendar.SECOND)) + "s";
270:
271: reportDate = dirName;
272: reportDirectory = new File(baseReportDir, dirName);
273: checkDirectory(reportDirectory, ERROR_REPORT_DIRECTORY_UNUSABLE);
274:
275: //
276: // Now, create a sub-directory for XML files and
277: // anotherone for resources
278: //
279: xmlDirectory = new File(reportDirectory, XML_REPORT_DIRECTORY);
280: checkDirectory(xmlDirectory, ERROR_REPORT_DIRECTORY_UNUSABLE);
281:
282: xmlResourcesDirectory = new File(xmlDirectory,
283: XML_RESOURCES_DIRECTORY);
284: checkDirectory(xmlResourcesDirectory,
285: ERROR_REPORT_DIRECTORY_UNUSABLE);
286: }
287:
288: /**
289: * Forces a two digit string
290: */
291: protected String makeTwoDigits(int i) {
292: if (i > 9) {
293: return "" + i;
294: } else {
295: return "0" + i;
296: }
297: }
298:
299: /**
300: * Returns the report directory
301: */
302: public File getReportDirectory() {
303: return reportDirectory;
304: }
305:
306: /**
307: * By default, the report resources directory is
308: * given by a configuration variable.
309: */
310: public File getReportResourcesDirectory() {
311: return xmlResourcesDirectory;
312: }
313:
314: /**
315: * Recursively processes the input <tt>TestReport</tt> adding
316: * the report information to the input element.
317: */
318: protected void processReport(TestReport report,
319: Element reportElement, Document reportDocument)
320: throws IOException {
321: if (report == null) {
322: throw new IllegalArgumentException();
323: }
324:
325: reportElement.setAttribute(XTR_TEST_NAME_ATTRIBUTE, report
326: .getTest().getName());
327:
328: String id = report.getTest().getQualifiedId();
329: if (!"".equals(id)) {
330: reportElement.setAttribute(XTR_ID_ATTRIBUTE, id);
331: }
332:
333: String status = report.hasPassed() ? XTR_PASSED_VALUE
334: : XTR_FAILED_VALUE;
335:
336: reportElement.setAttribute(XTR_STATUS_ATTRIBUTE, status);
337:
338: String className = report.getTest().getClass().getName();
339:
340: reportElement.setAttribute(XTR_CLASS_ATTRIBUTE, className);
341:
342: if (!report.hasPassed()) {
343: reportElement.setAttribute(XTR_ERROR_CODE_ATTRIBUTE, report
344: .getErrorCode());
345: }
346:
347: TestReport.Entry[] entries = report.getDescription();
348: int n = entries != null ? entries.length : 0;
349:
350: if (n > 0) {
351: Element descriptionElement = reportDocument
352: .createElementNS(null, XTR_DESCRIPTION_TAG);
353:
354: reportElement.appendChild(descriptionElement);
355:
356: for (int i = 0; i < n; i++) {
357: processEntry(entries[i], descriptionElement,
358: reportDocument);
359:
360: }
361: }
362: }
363:
364: protected void processEntry(TestReport.Entry entry,
365: Element descriptionElement, Document reportDocument)
366: throws IOException {
367:
368: Object value = entry.getValue();
369: String key = entry.getKey();
370:
371: if (value instanceof TestReport) {
372: TestReport report = (TestReport) value;
373:
374: Element reportElement = null;
375:
376: if (report.getTest() instanceof TestSuite) {
377: reportElement = reportDocument.createElementNS(
378: XTR_NAMESPACE_URI, XTR_TEST_SUITE_REPORT_TAG);
379: } else {
380: reportElement = reportDocument.createElementNS(
381: XTR_NAMESPACE_URI, XTR_TEST_REPORT_TAG);
382: }
383:
384: descriptionElement.appendChild(reportElement);
385: processReport((TestReport) entry.getValue(), reportElement,
386: reportDocument);
387: } else if (value instanceof URL) {
388: Element entryElement = reportDocument.createElementNS(
389: XTR_NAMESPACE_URI, XTR_URI_ENTRY_TAG);
390:
391: descriptionElement.appendChild(entryElement);
392:
393: entryElement
394: .setAttribute(XTR_KEY_ATTRIBUTE, key.toString());
395:
396: entryElement.setAttribute(XTR_VALUE_ATTRIBUTE, value
397: .toString());
398:
399: } else if (value instanceof File) {
400: //
401: // The entry is a potentially temporary File. Copy
402: // the file to the repository and create a file entry
403: // referencing that file copy.
404: //
405: File tmpFile = (File) value;
406:
407: File tmpFileCopy = createResourceFileForName(tmpFile
408: .getName());
409:
410: copy(tmpFile, tmpFileCopy);
411:
412: Element entryElement = reportDocument.createElementNS(
413: XTR_NAMESPACE_URI, XTR_FILE_ENTRY_TAG);
414:
415: descriptionElement.appendChild(entryElement);
416:
417: entryElement
418: .setAttribute(XTR_KEY_ATTRIBUTE, key.toString());
419:
420: entryElement.setAttribute(XTR_VALUE_ATTRIBUTE, tmpFileCopy
421: .toURL().toString());
422:
423: } else {
424:
425: Element entryElement = reportDocument.createElementNS(
426: XTR_NAMESPACE_URI, XTR_GENERIC_ENTRY_TAG);
427:
428: descriptionElement.appendChild(entryElement);
429:
430: entryElement
431: .setAttribute(XTR_KEY_ATTRIBUTE, key.toString());
432:
433: Attr a = reportDocument
434: .createAttribute(XTR_VALUE_ATTRIBUTE);
435: a.setValue(value != null ? value.toString() : "null");
436: entryElement.setAttributeNode(a);
437: }
438: }
439:
440: /**
441: * Untility method. Creates a file in the resources directory
442: * for the given name. If a file in that directory does not
443: * exist yet, then it is used. Otherwise, a file with the same
444: * name with a digit suffix is created. For example, if "myFile.png"
445: * is requested, then "myFile.png" is created or "myFile<n>.png"
446: * where <n> will be one or several digits.
447: */
448: protected File createResourceFileForName(String fileName) {
449: File r = new File(xmlResourcesDirectory, fileName);
450: if (!r.exists()) {
451: return r;
452: } else {
453: return createResourceFileForName(fileName, 1);
454: }
455: }
456:
457: protected File createResourceFileForName(String fileName,
458: int instance) {
459: // First, create a 'versioned' file name
460: int n = fileName.lastIndexOf('.');
461: String iFileName = fileName + instance;
462: if (n != -1) {
463: iFileName = fileName.substring(0, n) + instance
464: + fileName.substring(n, fileName.length());
465: }
466:
467: File r = new File(xmlResourcesDirectory, iFileName);
468: if (!r.exists()) {
469: return r;
470: } else {
471: return createResourceFileForName(fileName, instance + 1);
472: }
473: }
474:
475: /**
476: * Utility method. Copies in to out
477: */
478: protected void copy(File in, File out) throws IOException {
479: InputStream is = new BufferedInputStream(
480: new FileInputStream(in));
481: OutputStream os = new BufferedOutputStream(
482: new FileOutputStream(out));
483:
484: final byte[] b = new byte[1024];
485: int n = -1;
486: while ((n = is.read(b)) != -1) {
487: os.write(b, 0, n);
488: }
489:
490: is.close();
491: os.close();
492: }
493:
494: /**
495: * Saves the XML document into a file
496: */
497: protected File serializeReport(Element reportElement)
498: throws IOException {
499: //
500: // First, create a new File
501: //
502: File reportFile = new File(xmlDirectory, XML_TEST_REPORT_NAME);
503:
504: FileWriter fw = new FileWriter(reportFile);
505:
506: serializeElement(reportElement, "", fw);
507:
508: fw.close();
509:
510: return reportFile;
511: }
512:
513: private static String EOL;
514:
515: private static String PROPERTY_LINE_SEPARATOR = "line.separator";
516: private static String PROPERTY_LINE_SEPARATOR_DEFAULT = "\n";
517: static {
518: String temp;
519: try {
520: temp = System.getProperty(PROPERTY_LINE_SEPARATOR,
521: PROPERTY_LINE_SEPARATOR_DEFAULT);
522: } catch (SecurityException e) {
523: temp = PROPERTY_LINE_SEPARATOR_DEFAULT;
524: }
525: EOL = temp;
526: }
527:
528: protected void serializeElement(Element element, String prefix,
529: Writer writer) throws IOException {
530: writer.write(prefix);
531: writer.write(XML_OPEN_TAG_START);
532: writer.write(element.getTagName());
533:
534: serializeAttributes(element, writer);
535:
536: NodeList children = element.getChildNodes();
537: if (children != null && children.getLength() > 0) {
538: writer.write(XML_OPEN_TAG_END_CHILDREN);
539: writer.write(EOL);
540: int n = children.getLength();
541: for (int i = 0; i < n; i++) {
542: Node child = children.item(i);
543: if (child.getNodeType() == Node.ELEMENT_NODE) {
544: serializeElement((Element) child, prefix + XML_TAB,
545: writer);
546: }
547: }
548: writer.write(prefix);
549: writer.write(XML_CLOSE_TAG_START);
550: writer.write(element.getTagName());
551: writer.write(XML_CLOSE_TAG_END);
552: } else {
553: writer.write(XML_OPEN_TAG_END_NO_CHILDREN);
554: }
555:
556: writer.write(EOL);
557:
558: }
559:
560: protected void serializeAttributes(Element element, Writer writer)
561: throws IOException {
562: NamedNodeMap attributes = element.getAttributes();
563: if (attributes != null) {
564: int nAttr = attributes.getLength();
565: for (int i = 0; i < nAttr; i++) {
566: Attr attr = (Attr) attributes.item(i);
567: writer.write(XML_SPACE);
568: writer.write(attr.getName());
569: writer.write(XML_EQUAL_SIGN);
570: writer.write(XML_DOUBLE_QUOTE);
571: writer.write(encode(attr.getValue()));
572: writer.write(XML_DOUBLE_QUOTE);
573: }
574: }
575: }
576:
577: /**
578: * Poor way of replacing '<', '>', '"', '&' and '''
579: * in attribute values.
580: */
581: protected String encode(String attrValue) {
582: StringBuffer sb = new StringBuffer(attrValue);
583: replace(sb, XML_CHAR_AMP, XML_ENTITY_AMP);
584: replace(sb, XML_CHAR_LT, XML_ENTITY_LT);
585: replace(sb, XML_CHAR_GT, XML_ENTITY_GT);
586: replace(sb, XML_CHAR_QUOT, XML_ENTITY_QUOT);
587: replace(sb, XML_CHAR_APOS, XML_ENTITY_APOS);
588: return sb.toString();
589: }
590:
591: protected void replace(StringBuffer s, char c, String r) {
592: String v = s.toString() + 1;
593: int i = v.length();
594:
595: while ((i = v.lastIndexOf(c, --i)) != -1) {
596: s.deleteCharAt(i);
597: s.insert(i, r);
598: }
599: }
600: }
|