/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package libcore.xml; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.ErrorListener; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMResult; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import junit.framework.Assert; import junit.framework.AssertionFailedError; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.EntityReference; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.ProcessingInstruction; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlSerializer; /** * The <a href="http://www.oasis-open.org/committees/tc_home.php?wg_abbrev=xslt">OASIS * XSLT conformance test suite</a>, adapted for use by JUnit. To run these tests * on a device: * <ul> * <li>Obtain the <a href="http://www.oasis-open.org/committees/download.php/12171/XSLT-testsuite-04.ZIP">test * suite zip file from the OASIS project site.</li> * <li>Unzip. * <li>Copy the files to a device: <code>adb shell mkdir /data/oasis ; * adb push ./XSLT-Conformance-TC /data/oasis</code>. * <li>Invoke this class' main method, passing the on-device path to the test * suite's <code>catalog.xml</code> file as an argument. * </ul> * * <p>Unfortunately, some of the tests in the OASIS suite will fail when * executed outside of their original development environment: * <ul> * <li>The tests assume case insensitive filesystems. Some will fail with * "Couldn't open file" errors due to a mismatch in file name casing. * <li>The tests assume certain network hosts will exist and serve * stylesheet files. In particular, "http://webxtest/" isn't generally * available. * </ul> */ public class XsltXPathConformanceTestSuite { private static final String defaultCatalogFile = "/home/dalvik-prebuild/OASIS/XSLT-Conformance-TC/TESTS/catalog.xml"; /** Orders element attributes by optional URI and name. */ private static final Comparator<Attr> orderByName = new Comparator<Attr>() { public int compare(Attr a, Attr b) { int result = compareNullsFirst(a.getNamespaceURI(), b.getNamespaceURI()); return result == 0 ? result : compareNullsFirst(a.getName(), b.getName()); } <T extends Comparable<T>> int compareNullsFirst(T a, T b) { return (a == b) ? 0 : (a == null) ? -1 : (b == null) ? 1 : a.compareTo(b); } }; private final DocumentBuilder documentBuilder; private final TransformerFactory transformerFactory; private final XmlPullParserFactory xmlPullParserFactory; public XsltXPathConformanceTestSuite() throws ParserConfigurationException, XmlPullParserException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); factory.setCoalescing(true); documentBuilder = factory.newDocumentBuilder(); transformerFactory = TransformerFactory.newInstance(); xmlPullParserFactory = XmlPullParserFactory.newInstance(); } public static void main(String[] args) throws Exception { if (args.length != 1) { System.out.println("Usage: XsltXPathConformanceTestSuite <catalog-xml>"); System.out.println(); System.out.println(" catalog-xml: an XML file describing an OASIS test suite"); System.out.println(" such as: " + defaultCatalogFile); return; } File catalogXml = new File(args[0]); // TestRunner.run(suite(catalogXml)); android-changed } public static Test suite() throws Exception { return suite(new File(defaultCatalogFile)); } /** * Returns a JUnit test suite for the tests described by the given document. */ public static Test suite(File catalogXml) throws Exception { XsltXPathConformanceTestSuite suite = new XsltXPathConformanceTestSuite(); /* * Extract the tests from an XML document with the following structure: * * <test-suite> * <test-catalog submitter="Lotus"> * <creator>Lotus/IBM</creator> * <major-path>Xalan_Conformance_Tests</major-path> * <date>2001-11-16</date> * <test-case ...> ... </test-case> * <test-case ...> ... </test-case> * <test-case ...> ... </test-case> * </test-catalog> * </test-suite> */ Document document = DocumentBuilderFactory.newInstance() .newDocumentBuilder().parse(catalogXml); Element testSuiteElement = document.getDocumentElement(); TestSuite result = new TestSuite(); for (Element testCatalog : elementsOf(testSuiteElement.getElementsByTagName("test-catalog"))) { Element majorPathElement = (Element) testCatalog.getElementsByTagName("major-path").item(0); String majorPath = majorPathElement.getTextContent(); File base = new File(catalogXml.getParentFile(), majorPath); for (Element testCaseElement : elementsOf(testCatalog.getElementsByTagName("test-case"))) { result.addTest(suite.create(base, testCaseElement)); } } return result; } /** * Returns a JUnit test for the test described by the given element. */ private TestCase create(File base, Element testCaseElement) { /* * Extract the XSLT test from a DOM entity with the following structure: * * <test-case category="XSLT-Result-Tree" id="attribset_attribset01"> * <file-path>attribset</file-path> * <creator>Paul Dick</creator> * <date>2001-11-08</date> * <purpose>Set attribute of a LRE from single attribute set.</purpose> * <spec-citation place="7.1.4" type="section" version="1.0" spec="xslt"/> * <scenario operation="standard"> * <input-file role="principal-data">attribset01.xml</input-file> * <input-file role="principal-stylesheet">attribset01.xsl</input-file> * <output-file role="principal" compare="XML">attribset01.out</output-file> * </scenario> * </test-case> */ Element filePathElement = (Element) testCaseElement.getElementsByTagName("file-path").item(0); Element purposeElement = (Element) testCaseElement.getElementsByTagName("purpose").item(0); Element specCitationElement = (Element) testCaseElement.getElementsByTagName("spec-citation").item(0); Element scenarioElement = (Element) testCaseElement.getElementsByTagName("scenario").item(0); String category = testCaseElement.getAttribute("category"); String id = testCaseElement.getAttribute("id"); String name = category + "." + id; String purpose = purposeElement != null ? purposeElement.getTextContent() : ""; String spec = "place=" + specCitationElement.getAttribute("place") + " type" + specCitationElement.getAttribute("type") + " version=" + specCitationElement.getAttribute("version") + " spec=" + specCitationElement.getAttribute("spec"); String operation = scenarioElement.getAttribute("operation"); Element principalDataElement = null; Element principalStylesheetElement = null; Element principalElement = null; for (Element element : elementsOf(scenarioElement.getChildNodes())) { String role = element.getAttribute("role"); if (role.equals("principal-data")) { principalDataElement = element; } else if (role.equals("principal-stylesheet")) { principalStylesheetElement = element; } else if (role.equals("principal")) { principalElement = element; } else if (!role.equals("supplemental-stylesheet") && !role.equals("supplemental-data")) { return new MisspecifiedTest("Unexpected element at " + name); } } String testDirectory = filePathElement.getTextContent(); File inBase = new File(base, testDirectory); File outBase = new File(new File(base, "REF_OUT"), testDirectory); if (principalDataElement == null || principalStylesheetElement == null) { return new MisspecifiedTest("Expected <scenario> to have " + "principal=data and principal-stylesheet elements at " + name); } try { File principalData = findFile(inBase, principalDataElement.getTextContent()); File principalStylesheet = findFile(inBase, principalStylesheetElement.getTextContent()); final File principal; final String compareAs; if (!operation.equals("execution-error")) { if (principalElement == null) { return new MisspecifiedTest("Expected <scenario> to have principal element at " + name); } principal = findFile(outBase, principalElement.getTextContent()); compareAs = principalElement.getAttribute("compare"); } else { principal = null; compareAs = null; } return new XsltTest(category, id, purpose, spec, principalData, principalStylesheet, principal, operation, compareAs); } catch (FileNotFoundException e) { return new MisspecifiedTest(e.getMessage() + " at " + name); } } /** * Finds the named file in the named directory. This tries extra hard to * avoid case-insensitive-naming problems, where the requested file is * available in a different casing. */ private File findFile(File directory, String name) throws FileNotFoundException { File file = new File(directory, name); if (file.exists()) { return file; } for (String child : directory.list()) { if (child.equalsIgnoreCase(name)) { return new File(directory, child); } } throw new FileNotFoundException("Missing file: " + file); } /** * Placeholder for a test that couldn't be configured to run properly. */ public class MisspecifiedTest extends TestCase { private final String message; MisspecifiedTest(String message) { super("test"); this.message = message; } public void test() { fail(message); } } /** * Processes an input XML file with an input XSLT stylesheet and compares * the result to an expected output file. */ public class XsltTest extends TestCase { private final String category; private final String id; private final String purpose; private final String spec; private final File principalData; private final File principalStylesheet; private final File principal; /** either "standard" or "execution-error" */ private final String operation; /** * The syntax to compare the output file using, such as "XML", "HTML", * "manual", or null for expected execution errors. */ private final String compareAs; XsltTest(String category, String id, String purpose, String spec, File principalData, File principalStylesheet, File principal, String operation, String compareAs) { super("test"); this.category = category; this.id = id; this.purpose = purpose; this.spec = spec; this.principalData = principalData; this.principalStylesheet = principalStylesheet; this.principal = principal; this.operation = operation; this.compareAs = compareAs; } XsltTest(File principalData, File principalStylesheet, File principal) { this("standalone", "test", "", "", principalData, principalStylesheet, principal, "standard", "XML"); } public void test() throws Exception { if (purpose != null) { System.out.println("Purpose: " + purpose); } if (spec != null) { System.out.println("Spec: " + spec); } Result result; if ("XML".equals(compareAs)) { DOMResult domResult = new DOMResult(); domResult.setNode(documentBuilder.newDocument().createElementNS("", "result")); result = domResult; } else { result = new StreamResult(new StringWriter()); } ErrorRecorder errorRecorder = new ErrorRecorder(); transformerFactory.setErrorListener(errorRecorder); Transformer transformer; try { Source xslt = new StreamSource(principalStylesheet); transformer = transformerFactory.newTransformer(xslt); if (errorRecorder.error == null) { transformer.setErrorListener(errorRecorder); transformer.transform(new StreamSource(principalData), result); } } catch (TransformerConfigurationException e) { errorRecorder.fatalError(e); } if (operation.equals("standard")) { if (errorRecorder.error != null) { throw errorRecorder.error; } } else if (operation.equals("execution-error")) { if (errorRecorder.error != null) { return; } fail("Expected " + operation + ", but transform completed normally." + " (Warning=" + errorRecorder.warning + ")"); } else { throw new UnsupportedOperationException("Unexpected operation: " + operation); } if ("XML".equals(compareAs)) { assertNodesAreEquivalent(principal, ((DOMResult) result).getNode()); } else { // TODO: implement support for comparing HTML etc. throw new UnsupportedOperationException("Cannot compare as " + compareAs); } } @Override public String getName() { return category + "." + id; } } /** * Ensures both XML documents represent the same semantic data. Non-semantic * data such as namespace prefixes, comments, and whitespace is ignored. * * @param actual an XML document whose root is a {@code <result>} element. * @param expected a file containing an XML document fragment. */ private void assertNodesAreEquivalent(File expected, Node actual) throws ParserConfigurationException, IOException, SAXException, XmlPullParserException { Node expectedNode = fileToResultNode(expected); String expectedString = nodeToNormalizedString(expectedNode); String actualString = nodeToNormalizedString(actual); Assert.assertEquals("Expected XML to match file " + expected, expectedString, actualString); } /** * Returns the given file's XML fragment as a single node, wrapped in * {@code <result>} tags. This takes care of normalizing the following * conditions: * * <ul> * <li>Files containing XML document fragments with multiple elements: * {@code <SPAN style="color=blue">Smurfs!</SPAN><br />} * * <li>Files containing XML document fragments with no elements: * {@code Smurfs!} * * <li>Files containing proper XML documents with a single element and an * XML declaration: * {@code <?xml version="1.0"?><doc />} * * <li>Files prefixed with a byte order mark header, such as 0xEFBBBF. * </ul> */ private Node fileToResultNode(File file) throws IOException, SAXException { String rawContents = fileToString(file); String fragment = rawContents; // If the file had an XML declaration, strip that. Otherwise wrapping // it in <result> tags would result in a malformed XML document. if (fragment.startsWith("<?xml")) { int declarationEnd = fragment.indexOf("?>"); fragment = fragment.substring(declarationEnd + 2); } // Parse it as document fragment wrapped in <result> tags. try { fragment = "<result>" + fragment + "</result>"; return documentBuilder.parse(new InputSource(new StringReader(fragment))) .getDocumentElement(); } catch (SAXParseException e) { Error error = new AssertionFailedError( "Failed to parse XML: " + file + "\n" + rawContents); error.initCause(e); throw error; } } private String nodeToNormalizedString(Node node) throws XmlPullParserException, IOException { StringWriter writer = new StringWriter(); XmlSerializer xmlSerializer = xmlPullParserFactory.newSerializer(); xmlSerializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); xmlSerializer.setOutput(writer); emitNode(xmlSerializer, node); xmlSerializer.flush(); return writer.toString(); } private void emitNode(XmlSerializer serializer, Node node) throws IOException { if (node == null) { throw new UnsupportedOperationException("Cannot emit null nodes"); } else if (node.getNodeType() == Node.ELEMENT_NODE) { Element element = (Element) node; serializer.startTag(element.getNamespaceURI(), element.getLocalName()); emitAttributes(serializer, element); emitChildren(serializer, element); serializer.endTag(element.getNamespaceURI(), element.getLocalName()); } else if (node.getNodeType() == Node.TEXT_NODE || node.getNodeType() == Node.CDATA_SECTION_NODE) { // TODO: is it okay to trim whitespace in general? This may cause // false positives for elements like HTML's <pre> tag String trimmed = node.getTextContent().trim(); if (trimmed.length() > 0) { serializer.text(trimmed); } } else if (node.getNodeType() == Node.DOCUMENT_NODE) { Document document = (Document) node; serializer.startDocument("UTF-8", true); emitNode(serializer, document.getDocumentElement()); serializer.endDocument(); } else if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) { ProcessingInstruction processingInstruction = (ProcessingInstruction) node; String data = processingInstruction.getData(); String target = processingInstruction.getTarget(); serializer.processingInstruction(target + " " + data); } else if (node.getNodeType() == Node.COMMENT_NODE) { // ignore! } else if (node.getNodeType() == Node.ENTITY_REFERENCE_NODE) { EntityReference entityReference = (EntityReference) node; serializer.entityRef(entityReference.getNodeName()); } else { throw new UnsupportedOperationException( "Cannot emit " + node + " of type " + node.getNodeType()); } } private void emitAttributes(XmlSerializer serializer, Node node) throws IOException { NamedNodeMap map = node.getAttributes(); if (map == null) { return; } List<Attr> attributes = new ArrayList<Attr>(); for (int i = 0; i < map.getLength(); i++) { attributes.add((Attr) map.item(i)); } Collections.sort(attributes, orderByName); for (Attr attr : attributes) { if ("xmlns".equals(attr.getPrefix()) || "xmlns".equals(attr.getLocalName())) { /* * Omit namespace declarations because they aren't considered * data. Ie. <foo:a xmlns:bar="http://google.com"> is semantically * equal to <bar:a xmlns:bar="http://google.com"> since the * prefix doesn't matter, only the URI it points to. * * When we omit the prefix, our XML serializer will still * generate one for us, using a predictable pattern. */ } else { serializer.attribute(attr.getNamespaceURI(), attr.getLocalName(), attr.getValue()); } } } private void emitChildren(XmlSerializer serializer, Node node) throws IOException { NodeList childNodes = node.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { emitNode(serializer, childNodes.item(i)); } } private static List<Element> elementsOf(NodeList nodeList) { List<Element> result = new ArrayList<Element>(); for (int i = 0; i < nodeList.getLength(); i++) { Node node = nodeList.item(i); if (node instanceof Element) { result.add((Element) node); } } return result; } /** * Reads the given file into a string. If the file contains a byte order * mark, the corresponding character set will be used. Otherwise the system * default charset will be used. */ private String fileToString(File file) throws IOException { InputStream in = new BufferedInputStream(new FileInputStream(file), 1024); // Read the byte order mark to determine the charset. // TODO: use a built-in API for this... Reader reader; in.mark(3); int byte1 = in.read(); int byte2 = in.read(); if (byte1 == 0xFF && byte2 == 0xFE) { reader = new InputStreamReader(in, "UTF-16LE"); } else if (byte1 == 0xFF && byte2 == 0xFF) { reader = new InputStreamReader(in, "UTF-16BE"); } else { int byte3 = in.read(); if (byte1 == 0xEF && byte2 == 0xBB && byte3 == 0xBF) { reader = new InputStreamReader(in, "UTF-8"); } else { in.reset(); reader = new InputStreamReader(in); } } StringWriter out = new StringWriter(); char[] buffer = new char[1024]; int count; while ((count = reader.read(buffer)) != -1) { out.write(buffer, 0, count); } in.close(); return out.toString(); } static class ErrorRecorder implements ErrorListener { Exception warning; Exception error; public void warning(TransformerException exception) { if (this.warning == null) { this.warning = exception; } } public void error(TransformerException exception) { if (this.error == null) { this.error = exception; } } public void fatalError(TransformerException exception) { if (this.error == null) { this.error = exception; } } } }