1/* 2 * Copyright (C) 2007 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 com.google.mockwebserver.MockResponse; 20import com.google.mockwebserver.MockWebServer; 21import java.io.ByteArrayInputStream; 22import java.io.IOException; 23import java.io.InputStream; 24import java.io.Reader; 25import java.io.StringReader; 26import java.util.ArrayList; 27import java.util.Arrays; 28import java.util.HashMap; 29import java.util.List; 30import java.util.Map; 31import junit.framework.Assert; 32import junit.framework.TestCase; 33import org.apache.harmony.xml.ExpatReader; 34import org.xml.sax.Attributes; 35import org.xml.sax.ContentHandler; 36import org.xml.sax.InputSource; 37import org.xml.sax.Locator; 38import org.xml.sax.SAXException; 39import org.xml.sax.XMLReader; 40import org.xml.sax.ext.DefaultHandler2; 41import org.xml.sax.helpers.DefaultHandler; 42 43public class ExpatSaxParserTest extends TestCase { 44 45 private static final String SNIPPET = "<dagny dad=\"bob\">hello</dagny>"; 46 47 public void testGlobalReferenceTableOverflow() throws Exception { 48 // We used to use a JNI global reference per interned string. 49 // Framework apps have a limit of 2000 JNI global references per VM. 50 StringBuilder xml = new StringBuilder(); 51 xml.append("<root>"); 52 for (int i = 0; i < 4000; ++i) { 53 xml.append("<tag" + i + ">"); 54 xml.append("</tag" + i + ">"); 55 } 56 xml.append("</root>"); 57 parse(xml.toString(), new DefaultHandler()); 58 } 59 60 public void testExceptions() { 61 // From startElement(). 62 ContentHandler contentHandler = new DefaultHandler() { 63 @Override 64 public void startElement(String uri, String localName, 65 String qName, Attributes attributes) 66 throws SAXException { 67 throw new SAXException(); 68 } 69 }; 70 try { 71 parse(SNIPPET, contentHandler); 72 fail(); 73 } catch (SAXException checked) { /* expected */ } 74 75 // From endElement(). 76 contentHandler = new DefaultHandler() { 77 @Override 78 public void endElement(String uri, String localName, 79 String qName) 80 throws SAXException { 81 throw new SAXException(); 82 } 83 }; 84 try { 85 parse(SNIPPET, contentHandler); 86 fail(); 87 } catch (SAXException checked) { /* expected */ } 88 89 // From characters(). 90 contentHandler = new DefaultHandler() { 91 @Override 92 public void characters(char ch[], int start, int length) 93 throws SAXException { 94 throw new SAXException(); 95 } 96 }; 97 try { 98 parse(SNIPPET, contentHandler); 99 fail(); 100 } catch (SAXException checked) { /* expected */ } 101 } 102 103 public void testSax() { 104 try { 105 // Parse String. 106 TestHandler handler = new TestHandler(); 107 parse(SNIPPET, handler); 108 validate(handler); 109 110 // Parse Reader. 111 handler = new TestHandler(); 112 parse(new StringReader(SNIPPET), handler); 113 validate(handler); 114 115 // Parse InputStream. 116 handler = new TestHandler(); 117 parse(new ByteArrayInputStream(SNIPPET.getBytes()), 118 Encoding.UTF_8, handler); 119 validate(handler); 120 } catch (SAXException e) { 121 throw new RuntimeException(e); 122 } catch (IOException e) { 123 throw new RuntimeException(e); 124 } 125 } 126 127 static void validate(TestHandler handler) { 128 assertEquals("dagny", handler.startElementName); 129 assertEquals("dagny", handler.endElementName); 130 assertEquals("hello", handler.text.toString()); 131 } 132 133 static class TestHandler extends DefaultHandler { 134 135 String startElementName; 136 String endElementName; 137 StringBuilder text = new StringBuilder(); 138 139 @Override 140 public void startElement(String uri, String localName, String qName, 141 Attributes attributes) throws SAXException { 142 143 assertNull(this.startElementName); 144 this.startElementName = localName; 145 146 // Validate attributes. 147 assertEquals(1, attributes.getLength()); 148 assertEquals("", attributes.getURI(0)); 149 assertEquals("dad", attributes.getLocalName(0)); 150 assertEquals("bob", attributes.getValue(0)); 151 assertEquals(0, attributes.getIndex("", "dad")); 152 assertEquals("bob", attributes.getValue("", "dad")); 153 } 154 155 @Override 156 public void endElement(String uri, String localName, String qName) 157 throws SAXException { 158 assertNull(this.endElementName); 159 this.endElementName = localName; 160 } 161 162 @Override 163 public void characters(char ch[], int start, int length) 164 throws SAXException { 165 this.text.append(ch, start, length); 166 } 167 } 168 169 static final String XML = 170 "<one xmlns='ns:default' xmlns:n1='ns:1' a='b'>\n" 171 + " <n1:two c='d' n1:e='f' xmlns:n2='ns:2'>text</n1:two>\n" 172 + "</one>"; 173 174 public void testNamespaces() { 175 try { 176 NamespaceHandler handler = new NamespaceHandler(); 177 parse(XML, handler); 178 handler.validate(); 179 } catch (SAXException e) { 180 throw new RuntimeException(e); 181 } 182 } 183 184 static class NamespaceHandler implements ContentHandler { 185 186 Locator locator; 187 boolean documentStarted; 188 boolean documentEnded; 189 Map<String, String> prefixMappings = new HashMap<String, String>(); 190 191 boolean oneStarted; 192 boolean twoStarted; 193 boolean oneEnded; 194 boolean twoEnded; 195 196 public void validate() { 197 assertTrue(documentEnded); 198 } 199 200 public void setDocumentLocator(Locator locator) { 201 this.locator = locator; 202 } 203 204 public void startDocument() throws SAXException { 205 documentStarted = true; 206 assertNotNull(locator); 207 assertEquals(0, prefixMappings.size()); 208 assertFalse(documentEnded); 209 } 210 211 public void endDocument() throws SAXException { 212 assertTrue(documentStarted); 213 assertTrue(oneEnded); 214 assertTrue(twoEnded); 215 assertEquals(0, prefixMappings.size()); 216 documentEnded = true; 217 } 218 219 public void startPrefixMapping(String prefix, String uri) 220 throws SAXException { 221 prefixMappings.put(prefix, uri); 222 } 223 224 public void endPrefixMapping(String prefix) throws SAXException { 225 assertNotNull(prefixMappings.remove(prefix)); 226 } 227 228 public void startElement(String uri, String localName, String qName, 229 Attributes atts) throws SAXException { 230 231 if (localName == "one") { 232 assertEquals(2, prefixMappings.size()); 233 234 assertEquals(1, locator.getLineNumber()); 235 236 assertFalse(oneStarted); 237 assertFalse(twoStarted); 238 assertFalse(oneEnded); 239 assertFalse(twoEnded); 240 241 oneStarted = true; 242 243 assertSame("ns:default", uri); 244 assertEquals("one", qName); 245 246 // Check atts. 247 assertEquals(1, atts.getLength()); 248 249 assertSame("", atts.getURI(0)); 250 assertSame("a", atts.getLocalName(0)); 251 assertEquals("b", atts.getValue(0)); 252 assertEquals(0, atts.getIndex("", "a")); 253 assertEquals("b", atts.getValue("", "a")); 254 255 return; 256 } 257 258 if (localName == "two") { 259 assertEquals(3, prefixMappings.size()); 260 261 assertTrue(oneStarted); 262 assertFalse(twoStarted); 263 assertFalse(oneEnded); 264 assertFalse(twoEnded); 265 266 twoStarted = true; 267 268 assertSame("ns:1", uri); 269 Assert.assertEquals("n1:two", qName); 270 271 // Check atts. 272 assertEquals(2, atts.getLength()); 273 274 assertSame("", atts.getURI(0)); 275 assertSame("c", atts.getLocalName(0)); 276 assertEquals("d", atts.getValue(0)); 277 assertEquals(0, atts.getIndex("", "c")); 278 assertEquals("d", atts.getValue("", "c")); 279 280 assertSame("ns:1", atts.getURI(1)); 281 assertSame("e", atts.getLocalName(1)); 282 assertEquals("f", atts.getValue(1)); 283 assertEquals(1, atts.getIndex("ns:1", "e")); 284 assertEquals("f", atts.getValue("ns:1", "e")); 285 286 // We shouldn't find these. 287 assertEquals(-1, atts.getIndex("ns:default", "e")); 288 assertEquals(null, atts.getValue("ns:default", "e")); 289 290 return; 291 } 292 293 fail(); 294 } 295 296 public void endElement(String uri, String localName, String qName) 297 throws SAXException { 298 if (localName == "one") { 299 assertEquals(3, locator.getLineNumber()); 300 301 assertTrue(oneStarted); 302 assertTrue(twoStarted); 303 assertTrue(twoEnded); 304 assertFalse(oneEnded); 305 306 oneEnded = true; 307 308 assertSame("ns:default", uri); 309 assertEquals("one", qName); 310 311 return; 312 } 313 314 if (localName == "two") { 315 assertTrue(oneStarted); 316 assertTrue(twoStarted); 317 assertFalse(twoEnded); 318 assertFalse(oneEnded); 319 320 twoEnded = true; 321 322 assertSame("ns:1", uri); 323 assertEquals("n1:two", qName); 324 325 return; 326 } 327 328 fail(); 329 } 330 331 public void characters(char ch[], int start, int length) 332 throws SAXException { 333 String s = new String(ch, start, length).trim(); 334 335 if (!s.equals("")) { 336 assertTrue(oneStarted); 337 assertTrue(twoStarted); 338 assertFalse(oneEnded); 339 assertFalse(twoEnded); 340 assertEquals("text", s); 341 } 342 } 343 344 public void ignorableWhitespace(char ch[], int start, int length) 345 throws SAXException { 346 fail(); 347 } 348 349 public void processingInstruction(String target, String data) 350 throws SAXException { 351 fail(); 352 } 353 354 public void skippedEntity(String name) throws SAXException { 355 fail(); 356 } 357 } 358 359 private TestDtdHandler runDtdTest(String s) throws Exception { 360 Reader in = new StringReader(s); 361 ExpatReader reader = new ExpatReader(); 362 TestDtdHandler handler = new TestDtdHandler(); 363 reader.setContentHandler(handler); 364 reader.setDTDHandler(handler); 365 reader.setLexicalHandler(handler); 366 reader.parse(new InputSource(in)); 367 return handler; 368 } 369 370 public void testDtdDoctype() throws Exception { 371 TestDtdHandler handler = runDtdTest("<?xml version=\"1.0\"?><!DOCTYPE foo PUBLIC 'bar' 'tee'><a></a>"); 372 assertEquals("foo", handler.name); 373 assertEquals("bar", handler.publicId); 374 assertEquals("tee", handler.systemId); 375 assertTrue(handler.ended); 376 } 377 378 public void testDtdUnparsedEntity_system() throws Exception { 379 TestDtdHandler handler = runDtdTest("<?xml version=\"1.0\"?><!DOCTYPE foo PUBLIC 'bar' 'tee' [ <!ENTITY ent SYSTEM 'blah' NDATA poop> ]><a></a>"); 380 assertEquals("ent", handler.ueName); 381 assertEquals(null, handler.uePublicId); 382 assertEquals("blah", handler.ueSystemId); 383 assertEquals("poop", handler.ueNotationName); 384 } 385 386 public void testDtdUnparsedEntity_public() throws Exception { 387 TestDtdHandler handler = runDtdTest("<?xml version=\"1.0\"?><!DOCTYPE foo PUBLIC 'bar' 'tee' [ <!ENTITY ent PUBLIC 'a' 'b' NDATA poop> ]><a></a>"); 388 assertEquals("ent", handler.ueName); 389 assertEquals("a", handler.uePublicId); 390 assertEquals("b", handler.ueSystemId); 391 assertEquals("poop", handler.ueNotationName); 392 } 393 394 public void testDtdNotation_system() throws Exception { 395 TestDtdHandler handler = runDtdTest("<?xml version=\"1.0\"?><!DOCTYPE foo PUBLIC 'bar' 'tee' [ <!NOTATION sn SYSTEM 'nf2'> ]><a></a>"); 396 assertEquals("sn", handler.ndName); 397 assertEquals(null, handler.ndPublicId); 398 assertEquals("nf2", handler.ndSystemId); 399 } 400 401 public void testDtdNotation_public() throws Exception { 402 TestDtdHandler handler = runDtdTest("<?xml version=\"1.0\"?><!DOCTYPE foo PUBLIC 'bar' 'tee' [ <!NOTATION pn PUBLIC 'nf1'> ]><a></a>"); 403 assertEquals("pn", handler.ndName); 404 assertEquals("nf1", handler.ndPublicId); 405 assertEquals(null, handler.ndSystemId); 406 } 407 408 static class TestDtdHandler extends DefaultHandler2 { 409 410 String name; 411 String publicId; 412 String systemId; 413 String ndName; 414 String ndPublicId; 415 String ndSystemId; 416 String ueName; 417 String uePublicId; 418 String ueSystemId; 419 String ueNotationName; 420 421 boolean ended; 422 423 Locator locator; 424 425 @Override 426 public void startDTD(String name, String publicId, String systemId) { 427 this.name = name; 428 this.publicId = publicId; 429 this.systemId = systemId; 430 } 431 432 @Override 433 public void endDTD() { 434 ended = true; 435 } 436 437 @Override 438 public void setDocumentLocator(Locator locator) { 439 this.locator = locator; 440 } 441 442 @Override 443 public void notationDecl(String name, String publicId, String systemId) { 444 this.ndName = name; 445 this.ndPublicId = publicId; 446 this.ndSystemId = systemId; 447 } 448 449 @Override 450 public void unparsedEntityDecl(String entityName, String publicId, String systemId, String notationName) { 451 this.ueName = entityName; 452 this.uePublicId = publicId; 453 this.ueSystemId = systemId; 454 this.ueNotationName = notationName; 455 } 456 } 457 458 public void testCdata() throws Exception { 459 Reader in = new StringReader( 460 "<a><![CDATA[<b></b>]]> <![CDATA[<c></c>]]></a>"); 461 462 ExpatReader reader = new ExpatReader(); 463 TestCdataHandler handler = new TestCdataHandler(); 464 reader.setContentHandler(handler); 465 reader.setLexicalHandler(handler); 466 467 reader.parse(new InputSource(in)); 468 469 assertEquals(2, handler.startCdata); 470 assertEquals(2, handler.endCdata); 471 assertEquals("<b></b> <c></c>", handler.buffer.toString()); 472 } 473 474 static class TestCdataHandler extends DefaultHandler2 { 475 476 int startCdata, endCdata; 477 StringBuffer buffer = new StringBuffer(); 478 479 @Override 480 public void characters(char ch[], int start, int length) { 481 buffer.append(ch, start, length); 482 } 483 484 @Override 485 public void startCDATA() throws SAXException { 486 startCdata++; 487 } 488 489 @Override 490 public void endCDATA() throws SAXException { 491 endCdata++; 492 } 493 } 494 495 public void testProcessingInstructions() throws IOException, SAXException { 496 Reader in = new StringReader( 497 "<?bob lee?><a></a>"); 498 499 ExpatReader reader = new ExpatReader(); 500 TestProcessingInstrutionHandler handler 501 = new TestProcessingInstrutionHandler(); 502 reader.setContentHandler(handler); 503 504 reader.parse(new InputSource(in)); 505 506 assertEquals("bob", handler.target); 507 assertEquals("lee", handler.data); 508 } 509 510 static class TestProcessingInstrutionHandler extends DefaultHandler2 { 511 512 String target; 513 String data; 514 515 @Override 516 public void processingInstruction(String target, String data) { 517 this.target = target; 518 this.data = data; 519 } 520 } 521 522 public void testExternalEntity() throws IOException, SAXException { 523 class Handler extends DefaultHandler { 524 525 List<String> elementNames = new ArrayList<String>(); 526 StringBuilder text = new StringBuilder(); 527 528 public InputSource resolveEntity(String publicId, String systemId) 529 throws IOException, SAXException { 530 if (publicId.equals("publicA") && systemId.equals("systemA")) { 531 return new InputSource(new StringReader("<a/>")); 532 } else if (publicId.equals("publicB") 533 && systemId.equals("systemB")) { 534 /* 535 * Explicitly set the encoding here or else the parser will 536 * try to use the parent parser's encoding which is utf-16. 537 */ 538 InputSource inputSource = new InputSource( 539 new ByteArrayInputStream("bob".getBytes("utf-8"))); 540 inputSource.setEncoding("utf-8"); 541 return inputSource; 542 } 543 544 throw new AssertionError(); 545 } 546 547 @Override 548 public void startElement(String uri, String localName, String qName, 549 Attributes attributes) throws SAXException { 550 elementNames.add(localName); 551 } 552 553 @Override 554 public void endElement(String uri, String localName, String qName) 555 throws SAXException { 556 elementNames.add("/" + localName); 557 } 558 559 @Override 560 public void characters(char ch[], int start, int length) 561 throws SAXException { 562 text.append(ch, start, length); 563 } 564 } 565 566 Reader in = new StringReader("<?xml version=\"1.0\"?>\n" 567 + "<!DOCTYPE foo [\n" 568 + " <!ENTITY a PUBLIC 'publicA' 'systemA'>\n" 569 + " <!ENTITY b PUBLIC 'publicB' 'systemB'>\n" 570 + "]>\n" 571 + "<foo>\n" 572 + " &a;<b>&b;</b></foo>"); 573 574 ExpatReader reader = new ExpatReader(); 575 Handler handler = new Handler(); 576 reader.setContentHandler(handler); 577 reader.setEntityResolver(handler); 578 579 reader.parse(new InputSource(in)); 580 581 assertEquals(Arrays.asList("foo", "a", "/a", "b", "/b", "/foo"), 582 handler.elementNames); 583 assertEquals("bob", handler.text.toString().trim()); 584 } 585 586 public void testExternalEntityDownload() throws IOException, SAXException { 587 final MockWebServer server = new MockWebServer(); 588 server.enqueue(new MockResponse().setBody("<bar></bar>")); 589 server.play(); 590 591 class Handler extends DefaultHandler { 592 final List<String> elementNames = new ArrayList<String>(); 593 594 @Override public InputSource resolveEntity(String publicId, String systemId) 595 throws IOException { 596 // The parser should have resolved the systemId. 597 assertEquals(server.getUrl("/systemBar").toString(), systemId); 598 return new InputSource(systemId); 599 } 600 601 @Override public void startElement(String uri, String localName, String qName, 602 Attributes attributes) { 603 elementNames.add(localName); 604 } 605 606 @Override public void endElement(String uri, String localName, String qName) { 607 elementNames.add("/" + localName); 608 } 609 } 610 611 // 'systemBar', the external entity, is relative to 'systemFoo': 612 Reader in = new StringReader("<?xml version=\"1.0\"?>\n" 613 + "<!DOCTYPE foo [\n" 614 + " <!ENTITY bar SYSTEM 'systemBar'>\n" 615 + "]>\n" 616 + "<foo>&bar;</foo>"); 617 ExpatReader reader = new ExpatReader(); 618 Handler handler = new Handler(); 619 reader.setContentHandler(handler); 620 reader.setEntityResolver(handler); 621 InputSource source = new InputSource(in); 622 source.setSystemId(server.getUrl("/systemFoo").toString()); 623 reader.parse(source); 624 assertEquals(Arrays.asList("foo", "bar", "/bar", "/foo"), handler.elementNames); 625 } 626 627 /** 628 * Parses the given xml string and fires events on the given SAX handler. 629 */ 630 private static void parse(String xml, ContentHandler contentHandler) 631 throws SAXException { 632 try { 633 XMLReader reader = new ExpatReader(); 634 reader.setContentHandler(contentHandler); 635 reader.parse(new InputSource(new StringReader(xml))); 636 } catch (IOException e) { 637 throw new AssertionError(e); 638 } 639 } 640 641 /** 642 * Parses xml from the given reader and fires events on the given SAX 643 * handler. 644 */ 645 private static void parse(Reader in, ContentHandler contentHandler) 646 throws IOException, SAXException { 647 XMLReader reader = new ExpatReader(); 648 reader.setContentHandler(contentHandler); 649 reader.parse(new InputSource(in)); 650 } 651 652 /** 653 * Parses xml from the given input stream and fires events on the given SAX 654 * handler. 655 */ 656 private static void parse(InputStream in, Encoding encoding, 657 ContentHandler contentHandler) throws IOException, SAXException { 658 try { 659 XMLReader reader = new ExpatReader(); 660 reader.setContentHandler(contentHandler); 661 InputSource source = new InputSource(in); 662 source.setEncoding(encoding.expatName); 663 reader.parse(source); 664 } catch (IOException e) { 665 throw new AssertionError(e); 666 } 667 } 668 669 /** 670 * Supported character encodings. 671 */ 672 private enum Encoding { 673 674 US_ASCII("US-ASCII"), 675 UTF_8("UTF-8"), 676 UTF_16("UTF-16"), 677 ISO_8859_1("ISO-8859-1"); 678 679 final String expatName; 680 681 Encoding(String expatName) { 682 this.expatName = expatName; 683 } 684 } 685} 686