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.StringReader;
20import java.util.Arrays;
21import java.util.List;
22import javax.xml.parsers.SAXParser;
23import javax.xml.parsers.SAXParserFactory;
24import junit.framework.AssertionFailedError;
25import junit.framework.TestCase;
26import org.xml.sax.Attributes;
27import org.xml.sax.ContentHandler;
28import org.xml.sax.InputSource;
29import org.xml.sax.SAXParseException;
30import org.xml.sax.XMLReader;
31import org.xml.sax.helpers.DefaultHandler;
32
33/**
34 * Initiate and observe a SAX parse session.
35 */
36public final class SaxTest extends TestCase {
37
38    public void testNoPrefixesNoNamespaces() throws Exception {
39        parse(false, false, "<foo bar=\"baz\"/>", new DefaultHandler() {
40            @Override public void startElement(String uri, String localName,
41                    String qName, Attributes attributes) {
42                assertEquals("", uri);
43                assertEquals("", localName);
44                assertEquals("foo", qName);
45                assertEquals(1, attributes.getLength());
46                assertEquals("", attributes.getURI(0));
47                assertOneOf("bar", "", attributes.getLocalName(0));
48                assertEquals("bar", attributes.getQName(0));
49            }
50        });
51
52        parse(false, false, "<a:foo a:bar=\"baz\"/>", new DefaultHandler() {
53            @Override public void startElement(String uri, String localName,
54                    String qName, Attributes attributes) {
55                assertEquals("", uri);
56                assertEquals("", localName);
57                assertEquals("a:foo", qName);
58                assertEquals(1, attributes.getLength());
59                assertEquals("", attributes.getURI(0));
60                assertOneOf("a:bar", "", attributes.getLocalName(0));
61                assertEquals("a:bar", attributes.getQName(0));
62            }
63        });
64    }
65
66    public void testNoPrefixesYesNamespaces() throws Exception {
67        parse(false, true, "<foo bar=\"baz\"/>", new DefaultHandler() {
68            @Override public void startElement(String uri, String localName,
69                    String qName, Attributes attributes) {
70                assertEquals("", uri);
71                assertEquals("foo", localName);
72                assertEquals("foo", qName);
73                assertEquals(1, attributes.getLength());
74                assertEquals("", attributes.getURI(0));
75                assertEquals("bar", attributes.getLocalName(0));
76                assertEquals("bar", attributes.getQName(0));
77            }
78        });
79
80        parse(false, true, "<a:foo a:bar=\"baz\" xmlns:a=\"http://quux\"/>", new DefaultHandler() {
81            @Override public void startElement(String uri, String localName,
82                    String qName, Attributes attributes) {
83                assertEquals("http://quux", uri);
84                assertEquals("foo", localName);
85                assertEquals("a:foo", qName);
86                assertEquals(1, attributes.getLength());
87                assertEquals("http://quux", attributes.getURI(0));
88                assertEquals("bar", attributes.getLocalName(0));
89                assertEquals("a:bar", attributes.getQName(0));
90            }
91        });
92    }
93
94    /**
95     * Android's Expat-based SAX parser fails this test because Expat doesn't
96     * supply us with our much desired {@code xmlns="http://..."} attributes.
97     */
98    public void testYesPrefixesYesNamespaces() throws Exception {
99        parse(true, true, "<foo bar=\"baz\"/>", new DefaultHandler() {
100            @Override public void startElement(String uri, String localName,
101                    String qName, Attributes attributes) {
102                assertEquals("", uri);
103                assertEquals("foo", localName);
104                assertEquals("foo", qName);
105                assertEquals(1, attributes.getLength());
106                assertEquals("", attributes.getURI(0));
107                assertEquals("bar", attributes.getLocalName(0));
108                assertEquals("bar", attributes.getQName(0));
109            }
110        });
111
112        parse(true, true, "<a:foo a:bar=\"baz\" xmlns:a=\"http://quux\"/>", new DefaultHandler() {
113            @Override public void startElement(String uri, String localName,
114                    String qName, Attributes attributes) {
115                assertEquals("http://quux", uri);
116                assertEquals("foo", localName);
117                assertEquals("a:foo", qName);
118                assertEquals(2, attributes.getLength());
119                assertEquals("http://quux", attributes.getURI(0));
120                assertEquals("bar", attributes.getLocalName(0));
121                assertEquals("a:bar", attributes.getQName(0));
122                assertEquals("", attributes.getURI(1));
123                assertEquals("", attributes.getLocalName(1));
124                assertEquals("xmlns:a", attributes.getQName(1));
125            }
126        });
127    }
128
129    public void testYesPrefixesNoNamespaces() throws Exception {
130        parse(true, false, "<foo bar=\"baz\"/>", new DefaultHandler() {
131            @Override public void startElement(String uri, String localName,
132                    String qName, Attributes attributes) {
133                assertEquals("", uri);
134                assertEquals("", localName);
135                assertEquals("foo", qName);
136                assertEquals(1, attributes.getLength());
137                assertEquals("", attributes.getURI(0));
138                assertOneOf("bar", "", attributes.getLocalName(0));
139                assertEquals("bar", attributes.getQName(0));
140            }
141        });
142
143        parse(true, false, "<a:foo a:bar=\"baz\"/>", new DefaultHandler() {
144            @Override public void startElement(String uri, String localName,
145                    String qName, Attributes attributes) {
146                assertEquals("", uri);
147                assertEquals("", localName);
148                assertEquals("a:foo", qName);
149                assertEquals(1, attributes.getLength());
150                assertEquals("", attributes.getURI(0));
151                assertOneOf("a:bar", "", attributes.getLocalName(0));
152                assertEquals("a:bar", attributes.getQName(0));
153            }
154        });
155    }
156
157    /**
158     * Test that the external-general-entities feature can be disabled.
159     * http://code.google.com/p/android/issues/detail?id=9493
160     */
161    public void testDisableExternalGeneralEntities() throws Exception {
162        String xml = "<!DOCTYPE foo ["
163                + "  <!ENTITY bar SYSTEM \"/no-such-document.xml\">"
164                + "]>"
165                + "<foo>&bar;</foo>";
166        testDisableExternalEntities("http://xml.org/sax/features/external-general-entities", xml);
167    }
168
169    /**
170     * Test that the external-parameter-entities feature can be disabled.
171     * http://code.google.com/p/android/issues/detail?id=9493
172     */
173    public void testDisableExternalParameterEntities() throws Exception {
174        String xml = "<!DOCTYPE foo ["
175                + "  <!ENTITY % bar SYSTEM \"/no-such-document.xml\">"
176                + "  %bar;"
177                + "]>"
178                + "<foo/>";
179        testDisableExternalEntities("http://xml.org/sax/features/external-parameter-entities", xml);
180    }
181
182    /**
183     * Disables the named feature and then parses the supplied XML. The content
184     * is expected to be equivalent to "<foo/>".
185     */
186    private void testDisableExternalEntities(String feature, String xml) throws Exception {
187        SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
188        XMLReader reader = parser.getXMLReader();
189        reader.setFeature(feature, false);
190        assertFalse(reader.getFeature(feature));
191        reader.setContentHandler(new ThrowingHandler() {
192            @Override public void startElement(
193                    String uri, String localName, String qName, Attributes attributes) {
194                assertEquals("foo", qName);
195            }
196            @Override public void endElement(String uri, String localName, String qName) {
197                assertEquals("foo", qName);
198            }
199        });
200        reader.parse(new InputSource(new StringReader(xml)));
201    }
202
203    private void parse(boolean prefixes, boolean namespaces, String xml,
204            ContentHandler handler) throws Exception {
205        SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
206        XMLReader reader = parser.getXMLReader();
207        reader.setFeature("http://xml.org/sax/features/namespace-prefixes", prefixes);
208        reader.setFeature("http://xml.org/sax/features/namespaces", namespaces);
209        reader.setContentHandler(handler);
210        reader.parse(new InputSource(new StringReader(xml)));
211    }
212
213    /**
214     * @param expected an optional value that may or may have not been supplied
215     * @param sentinel a marker value that means the expected value was omitted
216     */
217    private void assertOneOf(String expected, String sentinel, String actual) {
218        List<String> optionsList = Arrays.asList(sentinel, expected);
219        assertTrue("Expected one of " + optionsList + " but was " + actual,
220                optionsList.contains(actual));
221    }
222
223    /**
224     * This SAX handler throws on everything but startDocument, endDocument,
225     * and setDocumentLocator(). Override the methods that are expected to be
226     * called.
227     */
228    static class ThrowingHandler extends DefaultHandler {
229        @Override public InputSource resolveEntity(String publicId, String systemId) {
230            throw new AssertionFailedError();
231        }
232        @Override public void notationDecl(String name, String publicId, String systemId) {
233            throw new AssertionFailedError();
234        }
235        @Override public void unparsedEntityDecl(
236                String name, String publicId, String systemId, String notationName) {
237            throw new AssertionFailedError();
238        }
239        @Override public void startPrefixMapping(String prefix, String uri) {
240            throw new AssertionFailedError();
241        }
242        @Override public void endPrefixMapping(String prefix) {
243            throw new AssertionFailedError();
244        }
245        @Override public void startElement(
246                String uri, String localName, String qName, Attributes attributes) {
247            throw new AssertionFailedError();
248        }
249        @Override public void endElement(String uri, String localName, String qName) {
250            throw new AssertionFailedError();
251        }
252        @Override public void characters(char[] ch, int start, int length) {
253            throw new AssertionFailedError();
254        }
255        @Override public void ignorableWhitespace(char[] ch, int start, int length) {
256            throw new AssertionFailedError();
257        }
258        @Override public void processingInstruction(String target, String data) {
259            throw new AssertionFailedError();
260        }
261        @Override public void skippedEntity(String name) {
262            throw new AssertionFailedError();
263        }
264        @Override public void warning(SAXParseException e) {
265            throw new AssertionFailedError();
266        }
267        @Override public void error(SAXParseException e) {
268            throw new AssertionFailedError();
269        }
270        @Override public void fatalError(SAXParseException e) {
271            throw new AssertionFailedError();
272        }
273    }
274}
275