Home | History | Annotate | Download | only in xml
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package libcore.xml;
     18 
     19 import java.io.BufferedInputStream;
     20 import java.io.File;
     21 import java.io.FileInputStream;
     22 import java.io.FileNotFoundException;
     23 import java.io.IOException;
     24 import java.io.InputStream;
     25 import java.io.InputStreamReader;
     26 import java.io.Reader;
     27 import java.io.StringReader;
     28 import java.io.StringWriter;
     29 import java.util.ArrayList;
     30 import java.util.Collections;
     31 import java.util.Comparator;
     32 import java.util.List;
     33 import javax.xml.parsers.DocumentBuilder;
     34 import javax.xml.parsers.DocumentBuilderFactory;
     35 import javax.xml.parsers.ParserConfigurationException;
     36 import javax.xml.transform.ErrorListener;
     37 import javax.xml.transform.Result;
     38 import javax.xml.transform.Source;
     39 import javax.xml.transform.Transformer;
     40 import javax.xml.transform.TransformerConfigurationException;
     41 import javax.xml.transform.TransformerException;
     42 import javax.xml.transform.TransformerFactory;
     43 import javax.xml.transform.dom.DOMResult;
     44 import javax.xml.transform.stream.StreamResult;
     45 import javax.xml.transform.stream.StreamSource;
     46 import junit.framework.Assert;
     47 import junit.framework.AssertionFailedError;
     48 import junit.framework.Test;
     49 import junit.framework.TestCase;
     50 import junit.framework.TestSuite;
     51 import org.w3c.dom.Attr;
     52 import org.w3c.dom.Document;
     53 import org.w3c.dom.Element;
     54 import org.w3c.dom.EntityReference;
     55 import org.w3c.dom.NamedNodeMap;
     56 import org.w3c.dom.Node;
     57 import org.w3c.dom.NodeList;
     58 import org.w3c.dom.ProcessingInstruction;
     59 import org.xml.sax.InputSource;
     60 import org.xml.sax.SAXException;
     61 import org.xml.sax.SAXParseException;
     62 import org.xmlpull.v1.XmlPullParserException;
     63 import org.xmlpull.v1.XmlPullParserFactory;
     64 import org.xmlpull.v1.XmlSerializer;
     65 
     66 /**
     67  * The <a href="http://www.oasis-open.org/committees/tc_home.php?wg_abbrev=xslt">OASIS
     68  * XSLT conformance test suite</a>, adapted for use by JUnit. To run these tests
     69  * on a device:
     70  * <ul>
     71  *     <li>Obtain the <a href="http://www.oasis-open.org/committees/download.php/12171/XSLT-testsuite-04.ZIP">test
     72  *         suite zip file from the OASIS project site.</li>
     73  *     <li>Unzip.
     74  *     <li>Copy the files to a device: <code>adb shell mkdir /data/oasis ;
     75  *         adb push ./XSLT-Conformance-TC /data/oasis</code>.
     76  *     <li>Invoke this class' main method, passing the on-device path to the test
     77  *         suite's <code>catalog.xml</code> file as an argument.
     78  * </ul>
     79  *
     80  * <p>Unfortunately, some of the tests in the OASIS suite will fail when
     81  * executed outside of their original development environment:
     82  * <ul>
     83  *     <li>The tests assume case insensitive filesystems. Some will fail with
     84  *        "Couldn't open file" errors due to a mismatch in file name casing.
     85  *     <li>The tests assume certain network hosts will exist and serve
     86  *         stylesheet files. In particular, "http://webxtest/" isn't generally
     87  *         available.
     88  * </ul>
     89  */
     90 public class XsltXPathConformanceTestSuite {
     91 
     92     private static final String defaultCatalogFile
     93             = "/home/dalvik-prebuild/OASIS/XSLT-Conformance-TC/TESTS/catalog.xml";
     94 
     95     /** Orders element attributes by optional URI and name. */
     96     private static final Comparator<Attr> orderByName = new Comparator<Attr>() {
     97         public int compare(Attr a, Attr b) {
     98             int result = compareNullsFirst(a.getNamespaceURI(), b.getNamespaceURI());
     99             return result == 0 ? result
    100                     : compareNullsFirst(a.getName(), b.getName());
    101         }
    102 
    103         <T extends Comparable<T>> int compareNullsFirst(T a, T b) {
    104             return (a == b) ? 0
    105                     : (a == null) ? -1
    106                     : (b == null) ? 1
    107                     : a.compareTo(b);
    108         }
    109     };
    110 
    111     private final DocumentBuilder documentBuilder;
    112     private final TransformerFactory transformerFactory;
    113     private final XmlPullParserFactory xmlPullParserFactory;
    114 
    115     public XsltXPathConformanceTestSuite()
    116             throws ParserConfigurationException, XmlPullParserException {
    117         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    118         factory.setNamespaceAware(true);
    119         factory.setCoalescing(true);
    120         documentBuilder = factory.newDocumentBuilder();
    121 
    122         transformerFactory = TransformerFactory.newInstance();
    123         xmlPullParserFactory = XmlPullParserFactory.newInstance();
    124     }
    125 
    126     public static void main(String[] args) throws Exception {
    127         if (args.length != 1) {
    128             System.out.println("Usage: XsltXPathConformanceTestSuite <catalog-xml>");
    129             System.out.println();
    130             System.out.println("  catalog-xml: an XML file describing an OASIS test suite");
    131             System.out.println("               such as: " + defaultCatalogFile);
    132             return;
    133         }
    134 
    135         File catalogXml = new File(args[0]);
    136         // TestRunner.run(suite(catalogXml)); android-changed
    137     }
    138 
    139     public static Test suite() throws Exception {
    140         return suite(new File(defaultCatalogFile));
    141     }
    142 
    143     /**
    144      * Returns a JUnit test suite for the tests described by the given document.
    145      */
    146     public static Test suite(File catalogXml) throws Exception {
    147         XsltXPathConformanceTestSuite suite = new XsltXPathConformanceTestSuite();
    148 
    149         /*
    150          * Extract the tests from an XML document with the following structure:
    151          *
    152          *  <test-suite>
    153          *    <test-catalog submitter="Lotus">
    154          *      <creator>Lotus/IBM</creator>
    155          *      <major-path>Xalan_Conformance_Tests</major-path>
    156          *      <date>2001-11-16</date>
    157          *      <test-case ...> ... </test-case>
    158          *      <test-case ...> ... </test-case>
    159          *      <test-case ...> ... </test-case>
    160          *    </test-catalog>
    161          *  </test-suite>
    162          */
    163 
    164         Document document = DocumentBuilderFactory.newInstance()
    165                 .newDocumentBuilder().parse(catalogXml);
    166         Element testSuiteElement = document.getDocumentElement();
    167         TestSuite result = new TestSuite();
    168         for (Element testCatalog : elementsOf(testSuiteElement.getElementsByTagName("test-catalog"))) {
    169             Element majorPathElement = (Element) testCatalog.getElementsByTagName("major-path").item(0);
    170             String majorPath = majorPathElement.getTextContent();
    171             File base = new File(catalogXml.getParentFile(), majorPath);
    172 
    173             for (Element testCaseElement : elementsOf(testCatalog.getElementsByTagName("test-case"))) {
    174                 result.addTest(suite.create(base, testCaseElement));
    175             }
    176         }
    177 
    178         return result;
    179     }
    180 
    181     /**
    182      * Returns a JUnit test for the test described by the given element.
    183      */
    184     private TestCase create(File base, Element testCaseElement) {
    185 
    186         /*
    187          * Extract the XSLT test from a DOM entity with the following structure:
    188          *
    189          *   <test-case category="XSLT-Result-Tree" id="attribset_attribset01">
    190          *       <file-path>attribset</file-path>
    191          *       <creator>Paul Dick</creator>
    192          *       <date>2001-11-08</date>
    193          *       <purpose>Set attribute of a LRE from single attribute set.</purpose>
    194          *       <spec-citation place="7.1.4" type="section" version="1.0" spec="xslt"/>
    195          *        <scenario operation="standard">
    196          *           <input-file role="principal-data">attribset01.xml</input-file>
    197          *           <input-file role="principal-stylesheet">attribset01.xsl</input-file>
    198          *           <output-file role="principal" compare="XML">attribset01.out</output-file>
    199          *       </scenario>
    200          *   </test-case>
    201          */
    202 
    203         Element filePathElement = (Element) testCaseElement.getElementsByTagName("file-path").item(0);
    204         Element purposeElement = (Element) testCaseElement.getElementsByTagName("purpose").item(0);
    205         Element specCitationElement = (Element) testCaseElement.getElementsByTagName("spec-citation").item(0);
    206         Element scenarioElement = (Element) testCaseElement.getElementsByTagName("scenario").item(0);
    207 
    208         String category = testCaseElement.getAttribute("category");
    209         String id = testCaseElement.getAttribute("id");
    210         String name = category + "." + id;
    211         String purpose = purposeElement != null ? purposeElement.getTextContent() : "";
    212         String spec = "place=" + specCitationElement.getAttribute("place")
    213                 + " type" + specCitationElement.getAttribute("type")
    214                 + " version=" + specCitationElement.getAttribute("version")
    215                 + " spec=" + specCitationElement.getAttribute("spec");
    216         String operation = scenarioElement.getAttribute("operation");
    217 
    218         Element principalDataElement = null;
    219         Element principalStylesheetElement = null;
    220         Element principalElement = null;
    221 
    222         for (Element element : elementsOf(scenarioElement.getChildNodes())) {
    223             String role = element.getAttribute("role");
    224             if (role.equals("principal-data")) {
    225                 principalDataElement = element;
    226             } else if (role.equals("principal-stylesheet")) {
    227                 principalStylesheetElement = element;
    228             } else if (role.equals("principal")) {
    229                 principalElement = element;
    230             } else if (!role.equals("supplemental-stylesheet")
    231                     && !role.equals("supplemental-data")) {
    232                 return new MisspecifiedTest("Unexpected element at " + name);
    233             }
    234         }
    235 
    236         String testDirectory = filePathElement.getTextContent();
    237         File inBase = new File(base, testDirectory);
    238         File outBase = new File(new File(base, "REF_OUT"), testDirectory);
    239 
    240         if (principalDataElement == null || principalStylesheetElement == null) {
    241             return new MisspecifiedTest("Expected <scenario> to have "
    242                     + "principal=data and principal-stylesheet elements at " + name);
    243         }
    244 
    245         try {
    246             File principalData = findFile(inBase, principalDataElement.getTextContent());
    247             File principalStylesheet = findFile(inBase, principalStylesheetElement.getTextContent());
    248 
    249             final File principal;
    250             final String compareAs;
    251             if (!operation.equals("execution-error")) {
    252                 if (principalElement == null) {
    253                     return new MisspecifiedTest("Expected <scenario> to have principal element at " + name);
    254                 }
    255 
    256                 principal = findFile(outBase, principalElement.getTextContent());
    257                 compareAs = principalElement.getAttribute("compare");
    258             } else {
    259                 principal = null;
    260                 compareAs = null;
    261             }
    262 
    263             return new XsltTest(category, id, purpose, spec, principalData,
    264                     principalStylesheet, principal, operation, compareAs);
    265         } catch (FileNotFoundException e) {
    266             return new MisspecifiedTest(e.getMessage() + " at " + name);
    267         }
    268     }
    269 
    270     /**
    271      * Finds the named file in the named directory. This tries extra hard to
    272      * avoid case-insensitive-naming problems, where the requested file is
    273      * available in a different casing.
    274      */
    275     private File findFile(File directory, String name) throws FileNotFoundException {
    276         File file = new File(directory, name);
    277         if (file.exists()) {
    278             return file;
    279         }
    280 
    281         for (String child : directory.list()) {
    282             if (child.equalsIgnoreCase(name)) {
    283                 return new File(directory, child);
    284             }
    285         }
    286 
    287         throw new FileNotFoundException("Missing file: " + file);
    288     }
    289 
    290     /**
    291      * Placeholder for a test that couldn't be configured to run properly.
    292      */
    293     public class MisspecifiedTest extends TestCase {
    294         private final String message;
    295 
    296         MisspecifiedTest(String message) {
    297             super("test");
    298             this.message = message;
    299         }
    300 
    301         public void test() {
    302             fail(message);
    303         }
    304     }
    305 
    306     /**
    307      * Processes an input XML file with an input XSLT stylesheet and compares
    308      * the result to an expected output file.
    309      */
    310     public class XsltTest extends TestCase {
    311         private final String category;
    312         private final String id;
    313         private final String purpose;
    314         private final String spec;
    315 
    316         private final File principalData;
    317         private final File principalStylesheet;
    318         private final File principal;
    319 
    320         /** either "standard" or "execution-error" */
    321         private final String operation;
    322 
    323         /**
    324          * The syntax to compare the output file using, such as "XML", "HTML",
    325          * "manual", or null for expected execution errors.
    326          */
    327         private final String compareAs;
    328 
    329         XsltTest(String category, String id, String purpose, String spec,
    330                 File principalData, File principalStylesheet, File principal,
    331                 String operation, String compareAs) {
    332             super("test");
    333             this.category = category;
    334             this.id = id;
    335             this.purpose = purpose;
    336             this.spec = spec;
    337             this.principalData = principalData;
    338             this.principalStylesheet = principalStylesheet;
    339             this.principal = principal;
    340             this.operation = operation;
    341             this.compareAs = compareAs;
    342         }
    343 
    344         XsltTest(File principalData, File principalStylesheet, File principal) {
    345             this("standalone", "test", "", "",
    346                     principalData, principalStylesheet, principal, "standard", "XML");
    347         }
    348 
    349         public void test() throws Exception {
    350             if (purpose != null) {
    351                 System.out.println("Purpose: " + purpose);
    352             }
    353             if (spec != null) {
    354                 System.out.println("Spec: " + spec);
    355             }
    356 
    357             Result result;
    358             if ("XML".equals(compareAs)) {
    359                 DOMResult domResult = new DOMResult();
    360                 domResult.setNode(documentBuilder.newDocument().createElementNS("", "result"));
    361                 result = domResult;
    362             } else {
    363                 result = new StreamResult(new StringWriter());
    364             }
    365 
    366             ErrorRecorder errorRecorder = new ErrorRecorder();
    367             transformerFactory.setErrorListener(errorRecorder);
    368 
    369             Transformer transformer;
    370             try {
    371                 Source xslt = new StreamSource(principalStylesheet);
    372                 transformer = transformerFactory.newTransformer(xslt);
    373                 if (errorRecorder.error == null) {
    374                     transformer.setErrorListener(errorRecorder);
    375                     transformer.transform(new StreamSource(principalData), result);
    376                 }
    377             } catch (TransformerConfigurationException e) {
    378                 errorRecorder.fatalError(e);
    379             }
    380 
    381             if (operation.equals("standard")) {
    382                 if (errorRecorder.error != null) {
    383                     throw errorRecorder.error;
    384                 }
    385             } else if (operation.equals("execution-error")) {
    386                 if (errorRecorder.error != null) {
    387                     return;
    388                 }
    389                 fail("Expected " + operation + ", but transform completed normally."
    390                         + " (Warning=" + errorRecorder.warning + ")");
    391             } else {
    392                 throw new UnsupportedOperationException("Unexpected operation: " + operation);
    393             }
    394 
    395             if ("XML".equals(compareAs)) {
    396                 assertNodesAreEquivalent(principal, ((DOMResult) result).getNode());
    397             } else {
    398                 // TODO: implement support for comparing HTML etc.
    399                 throw new UnsupportedOperationException("Cannot compare as " + compareAs);
    400             }
    401         }
    402 
    403         @Override public String getName() {
    404             return category + "." + id;
    405         }
    406     }
    407 
    408     /**
    409      * Ensures both XML documents represent the same semantic data. Non-semantic
    410      * data such as namespace prefixes, comments, and whitespace is ignored.
    411      *
    412      * @param actual an XML document whose root is a {@code <result>} element.
    413      * @param expected a file containing an XML document fragment.
    414      */
    415     private void assertNodesAreEquivalent(File expected, Node actual)
    416             throws ParserConfigurationException, IOException, SAXException,
    417             XmlPullParserException {
    418 
    419         Node expectedNode = fileToResultNode(expected);
    420         String expectedString = nodeToNormalizedString(expectedNode);
    421         String actualString = nodeToNormalizedString(actual);
    422 
    423         Assert.assertEquals("Expected XML to match file " + expected,
    424                 expectedString, actualString);
    425     }
    426 
    427     /**
    428      * Returns the given file's XML fragment as a single node, wrapped in
    429      * {@code <result>} tags. This takes care of normalizing the following
    430      * conditions:
    431      *
    432      * <ul>
    433      * <li>Files containing XML document fragments with multiple elements:
    434      * {@code <SPAN style="color=blue">Smurfs!</SPAN><br />}
    435      *
    436      * <li>Files containing XML document fragments with no elements:
    437      * {@code Smurfs!}
    438      *
    439      * <li>Files containing proper XML documents with a single element and an
    440      * XML declaration:
    441      * {@code <?xml version="1.0"?><doc />}
    442      *
    443      * <li>Files prefixed with a byte order mark header, such as 0xEFBBBF.
    444      * </ul>
    445      */
    446     private Node fileToResultNode(File file) throws IOException, SAXException {
    447         String rawContents = fileToString(file);
    448         String fragment = rawContents;
    449 
    450         // If the file had an XML declaration, strip that. Otherwise wrapping
    451         // it in <result> tags would result in a malformed XML document.
    452         if (fragment.startsWith("<?xml")) {
    453             int declarationEnd = fragment.indexOf("?>");
    454             fragment = fragment.substring(declarationEnd + 2);
    455         }
    456 
    457         // Parse it as document fragment wrapped in <result> tags.
    458         try {
    459             fragment = "<result>" + fragment + "</result>";
    460             return documentBuilder.parse(new InputSource(new StringReader(fragment)))
    461                     .getDocumentElement();
    462         } catch (SAXParseException e) {
    463             Error error = new AssertionFailedError(
    464                     "Failed to parse XML: " + file + "\n" + rawContents);
    465             error.initCause(e);
    466             throw error;
    467         }
    468     }
    469 
    470     private String nodeToNormalizedString(Node node)
    471             throws XmlPullParserException, IOException {
    472         StringWriter writer = new StringWriter();
    473         XmlSerializer xmlSerializer = xmlPullParserFactory.newSerializer();
    474         xmlSerializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
    475         xmlSerializer.setOutput(writer);
    476         emitNode(xmlSerializer, node);
    477         xmlSerializer.flush();
    478         return writer.toString();
    479     }
    480 
    481     private void emitNode(XmlSerializer serializer, Node node) throws IOException {
    482         if (node == null) {
    483             throw new UnsupportedOperationException("Cannot emit null nodes");
    484 
    485         } else if (node.getNodeType() == Node.ELEMENT_NODE) {
    486             Element element = (Element) node;
    487             serializer.startTag(element.getNamespaceURI(), element.getLocalName());
    488             emitAttributes(serializer, element);
    489             emitChildren(serializer, element);
    490             serializer.endTag(element.getNamespaceURI(), element.getLocalName());
    491 
    492         } else if (node.getNodeType() == Node.TEXT_NODE
    493                 || node.getNodeType() == Node.CDATA_SECTION_NODE) {
    494             // TODO: is it okay to trim whitespace in general? This may cause
    495             //     false positives for elements like HTML's <pre> tag
    496             String trimmed = node.getTextContent().trim();
    497             if (trimmed.length() > 0) {
    498                 serializer.text(trimmed);
    499             }
    500 
    501         } else if (node.getNodeType() == Node.DOCUMENT_NODE) {
    502             Document document = (Document) node;
    503             serializer.startDocument("UTF-8", true);
    504             emitNode(serializer, document.getDocumentElement());
    505             serializer.endDocument();
    506 
    507         } else if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) {
    508             ProcessingInstruction processingInstruction = (ProcessingInstruction) node;
    509             String data = processingInstruction.getData();
    510             String target = processingInstruction.getTarget();
    511             serializer.processingInstruction(target + " " + data);
    512 
    513         } else if (node.getNodeType() == Node.COMMENT_NODE) {
    514             // ignore!
    515 
    516         } else if (node.getNodeType() == Node.ENTITY_REFERENCE_NODE) {
    517             EntityReference entityReference = (EntityReference) node;
    518             serializer.entityRef(entityReference.getNodeName());
    519 
    520         } else {
    521             throw new UnsupportedOperationException(
    522                     "Cannot emit " + node + " of type " + node.getNodeType());
    523         }
    524     }
    525 
    526     private void emitAttributes(XmlSerializer serializer, Node node)
    527             throws IOException {
    528         NamedNodeMap map = node.getAttributes();
    529         if (map == null) {
    530             return;
    531         }
    532 
    533         List<Attr> attributes = new ArrayList<Attr>();
    534         for (int i = 0; i < map.getLength(); i++) {
    535             attributes.add((Attr) map.item(i));
    536         }
    537         Collections.sort(attributes, orderByName);
    538 
    539         for (Attr attr : attributes) {
    540             if ("xmlns".equals(attr.getPrefix()) || "xmlns".equals(attr.getLocalName())) {
    541                 /*
    542                  * Omit namespace declarations because they aren't considered
    543                  * data. Ie. <foo:a xmlns:bar="http://google.com"> is semantically
    544                  * equal to <bar:a xmlns:bar="http://google.com"> since the
    545                  * prefix doesn't matter, only the URI it points to.
    546                  *
    547                  * When we omit the prefix, our XML serializer will still
    548                  * generate one for us, using a predictable pattern.
    549                  */
    550             } else {
    551                 serializer.attribute(attr.getNamespaceURI(), attr.getLocalName(), attr.getValue());
    552             }
    553         }
    554     }
    555 
    556     private void emitChildren(XmlSerializer serializer, Node node)
    557             throws IOException {
    558         NodeList childNodes = node.getChildNodes();
    559         for (int i = 0; i < childNodes.getLength(); i++) {
    560             emitNode(serializer, childNodes.item(i));
    561         }
    562     }
    563 
    564     private static List<Element> elementsOf(NodeList nodeList) {
    565         List<Element> result = new ArrayList<Element>();
    566         for (int i = 0; i < nodeList.getLength(); i++) {
    567             Node node = nodeList.item(i);
    568             if (node instanceof Element) {
    569                 result.add((Element) node);
    570             }
    571         }
    572         return result;
    573     }
    574 
    575     /**
    576      * Reads the given file into a string. If the file contains a byte order
    577      * mark, the corresponding character set will be used. Otherwise the system
    578      * default charset will be used.
    579      */
    580     private String fileToString(File file) throws IOException {
    581         InputStream in = new BufferedInputStream(new FileInputStream(file), 1024);
    582 
    583         // Read the byte order mark to determine the charset.
    584         // TODO: use a built-in API for this...
    585         Reader reader;
    586         in.mark(3);
    587         int byte1 = in.read();
    588         int byte2 = in.read();
    589         if (byte1 == 0xFF && byte2 == 0xFE) {
    590             reader = new InputStreamReader(in, "UTF-16LE");
    591         } else if (byte1 == 0xFF && byte2 == 0xFF) {
    592             reader = new InputStreamReader(in, "UTF-16BE");
    593         } else {
    594             int byte3 = in.read();
    595             if (byte1 == 0xEF && byte2 == 0xBB && byte3 == 0xBF) {
    596                 reader = new InputStreamReader(in, "UTF-8");
    597             } else {
    598                 in.reset();
    599                 reader = new InputStreamReader(in);
    600             }
    601         }
    602 
    603         StringWriter out = new StringWriter();
    604         char[] buffer = new char[1024];
    605         int count;
    606         while ((count = reader.read(buffer)) != -1) {
    607             out.write(buffer, 0, count);
    608         }
    609         in.close();
    610         return out.toString();
    611     }
    612 
    613     static class ErrorRecorder implements ErrorListener {
    614         Exception warning;
    615         Exception error;
    616 
    617         public void warning(TransformerException exception) {
    618             if (this.warning == null) {
    619                 this.warning = exception;
    620             }
    621         }
    622 
    623         public void error(TransformerException exception) {
    624             if (this.error == null) {
    625                 this.error = exception;
    626             }
    627         }
    628 
    629         public void fatalError(TransformerException exception) {
    630             if (this.error == null) {
    631                 this.error = exception;
    632             }
    633         }
    634     }
    635 }
    636