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 17package libcore.xml; 18 19import java.io.BufferedInputStream; 20import java.io.File; 21import java.io.FileInputStream; 22import java.io.FileNotFoundException; 23import java.io.IOException; 24import java.io.InputStream; 25import java.io.InputStreamReader; 26import java.io.Reader; 27import java.io.StringReader; 28import java.io.StringWriter; 29import java.util.ArrayList; 30import java.util.Collections; 31import java.util.Comparator; 32import java.util.List; 33import javax.xml.parsers.DocumentBuilder; 34import javax.xml.parsers.DocumentBuilderFactory; 35import javax.xml.parsers.ParserConfigurationException; 36import javax.xml.transform.ErrorListener; 37import javax.xml.transform.Result; 38import javax.xml.transform.Source; 39import javax.xml.transform.Transformer; 40import javax.xml.transform.TransformerConfigurationException; 41import javax.xml.transform.TransformerException; 42import javax.xml.transform.TransformerFactory; 43import javax.xml.transform.dom.DOMResult; 44import javax.xml.transform.stream.StreamResult; 45import javax.xml.transform.stream.StreamSource; 46import junit.framework.Assert; 47import junit.framework.AssertionFailedError; 48import junit.framework.Test; 49import junit.framework.TestCase; 50import junit.framework.TestSuite; 51import org.w3c.dom.Attr; 52import org.w3c.dom.Document; 53import org.w3c.dom.Element; 54import org.w3c.dom.EntityReference; 55import org.w3c.dom.NamedNodeMap; 56import org.w3c.dom.Node; 57import org.w3c.dom.NodeList; 58import org.w3c.dom.ProcessingInstruction; 59import org.xml.sax.InputSource; 60import org.xml.sax.SAXException; 61import org.xml.sax.SAXParseException; 62import org.xmlpull.v1.XmlPullParserException; 63import org.xmlpull.v1.XmlPullParserFactory; 64import 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 */ 90public 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