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