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 XsltXPathConformanceTestSuite()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 main(String[] args)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 suite()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 */ suite(File catalogXml)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 */ create(File base, Element testCaseElement)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 */ findFile(File directory, String name)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 MisspecifiedTest(String message)296 MisspecifiedTest(String message) { 297 super("test"); 298 this.message = message; 299 } 300 test()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 XsltTest(String category, String id, String purpose, String spec, File principalData, File principalStylesheet, File principal, String operation, String compareAs)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 XsltTest(File principalData, File principalStylesheet, File principal)344 XsltTest(File principalData, File principalStylesheet, File principal) { 345 this("standalone", "test", "", "", 346 principalData, principalStylesheet, principal, "standard", "XML"); 347 } 348 test()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 getName()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 */ assertNodesAreEquivalent(File expected, Node actual)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 */ fileToResultNode(File file)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 nodeToNormalizedString(Node node)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 emitNode(XmlSerializer serializer, Node node)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 emitAttributes(XmlSerializer serializer, Node node)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 emitChildren(XmlSerializer serializer, Node node)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 elementsOf(NodeList nodeList)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 */ fileToString(File file)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 warning(TransformerException exception)617 public void warning(TransformerException exception) { 618 if (this.warning == null) { 619 this.warning = exception; 620 } 621 } 622 error(TransformerException exception)623 public void error(TransformerException exception) { 624 if (this.error == null) { 625 this.error = exception; 626 } 627 } 628 fatalError(TransformerException exception)629 public void fatalError(TransformerException exception) { 630 if (this.error == null) { 631 this.error = exception; 632 } 633 } 634 } 635 } 636