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 
17 package org.apache.harmony.xml;
18 
19 import org.xml.sax.Attributes;
20 import org.xml.sax.ContentHandler;
21 import org.xml.sax.DTDHandler;
22 import org.xml.sax.EntityResolver;
23 import org.xml.sax.InputSource;
24 import org.xml.sax.Locator;
25 import org.xml.sax.SAXException;
26 import org.xml.sax.SAXParseException;
27 import org.xml.sax.ext.LexicalHandler;
28 
29 import android.compat.annotation.UnsupportedAppUsage;
30 
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.io.Reader;
34 import java.net.URI;
35 import java.net.URL;
36 import java.net.URLConnection;
37 import libcore.io.IoUtils;
38 
39 import dalvik.annotation.optimization.ReachabilitySensitive;
40 
41 /**
42  * Adapts SAX API to the Expat native XML parser. Not intended for reuse
43  * across documents.
44  *
45  * @see org.apache.harmony.xml.ExpatReader
46  */
47 class ExpatParser {
48 
49     private static final int BUFFER_SIZE = 8096; // in bytes
50 
51     /** Pointer to XML_Parser instance. */
52     // A few native methods taking the pointer value are static; @ReachabilitySensitive is
53     // necessary to ensure the Java object is kept reachable sufficiently long in these cases.
54     @ReachabilitySensitive
55     private long pointer;
56 
57     private boolean inStartElement = false;
58     private int attributeCount = -1;
59     private long attributePointer = 0;
60 
61     private final Locator locator = new ExpatLocator();
62 
63     @UnsupportedAppUsage
64     private final ExpatReader xmlReader;
65 
66     private final String publicId;
67     private final String systemId;
68 
69     private final String encoding;
70 
71     @UnsupportedAppUsage
72     private final ExpatAttributes attributes = new CurrentAttributes();
73 
74     private static final String OUTSIDE_START_ELEMENT
75             = "Attributes can only be used within the scope of startElement().";
76 
77     /** We default to UTF-8 when the user doesn't specify an encoding. */
78     private static final String DEFAULT_ENCODING = "UTF-8";
79 
80     /** Encoding used for Java chars, used to parse Readers and Strings */
81     /*package*/ static final String CHARACTER_ENCODING = "UTF-16";
82 
83     /** Timeout for HTTP connections (in ms) */
84     private static final int TIMEOUT = 20 * 1000;
85 
86     /**
87      * Constructs a new parser with the specified encoding.
88      */
89     @UnsupportedAppUsage
ExpatParser(String encoding, ExpatReader xmlReader, boolean processNamespaces, String publicId, String systemId)90     /*package*/ ExpatParser(String encoding, ExpatReader xmlReader,
91             boolean processNamespaces, String publicId, String systemId) {
92         this.publicId = publicId;
93         this.systemId = systemId;
94 
95         this.xmlReader = xmlReader;
96 
97         /*
98          * TODO: Let Expat try to guess the encoding instead of defaulting.
99          * Unfortunately, I don't know how to tell which encoding Expat picked,
100          * so I won't know how to encode "<externalEntity>" below. The solution
101          * I think is to fix Expat to not require the "<externalEntity>"
102          * workaround.
103          */
104         this.encoding = encoding == null ? DEFAULT_ENCODING : encoding;
105         this.pointer = initialize(
106             this.encoding,
107             processNamespaces
108         );
109     }
110 
111     /**
112      * Used by {@link EntityParser}.
113      */
ExpatParser(String encoding, ExpatReader xmlReader, long pointer, String publicId, String systemId)114     private ExpatParser(String encoding, ExpatReader xmlReader, long pointer,
115             String publicId, String systemId) {
116         this.encoding = encoding;
117         this.xmlReader = xmlReader;
118         this.pointer = pointer;
119         this.systemId = systemId;
120         this.publicId = publicId;
121     }
122 
123     /**
124      * Initializes native resources.
125      *
126      * @return the pointer to the native parser
127      */
initialize(String encoding, boolean namespacesEnabled)128     private native long initialize(String encoding, boolean namespacesEnabled);
129 
130     /**
131      * Called at the start of an element.
132      *
133      * @param uri namespace URI of element or "" if namespace processing is
134      *  disabled
135      * @param localName local name of element or "" if namespace processing is
136      *  disabled
137      * @param qName qualified name or "" if namespace processing is enabled
138      * @param attributePointer pointer to native attribute char*--we keep
139      *  a separate pointer so we can detach it from the parser instance
140      * @param attributeCount number of attributes
141      */
startElement(String uri, String localName, String qName, long attributePointer, int attributeCount)142     /*package*/ void startElement(String uri, String localName, String qName,
143             long attributePointer, int attributeCount) throws SAXException {
144         ContentHandler contentHandler = xmlReader.contentHandler;
145         if (contentHandler == null) {
146             return;
147         }
148 
149         try {
150             inStartElement = true;
151             this.attributePointer = attributePointer;
152             this.attributeCount = attributeCount;
153 
154             contentHandler.startElement(
155                     uri, localName, qName, this.attributes);
156         } finally {
157             inStartElement = false;
158             this.attributeCount = -1;
159             this.attributePointer = 0;
160         }
161     }
162 
endElement(String uri, String localName, String qName)163     /*package*/ void endElement(String uri, String localName, String qName)
164             throws SAXException {
165         ContentHandler contentHandler = xmlReader.contentHandler;
166         if (contentHandler != null) {
167             contentHandler.endElement(uri, localName, qName);
168         }
169     }
170 
text(char[] text, int length)171     /*package*/ void text(char[] text, int length) throws SAXException {
172         ContentHandler contentHandler = xmlReader.contentHandler;
173         if (contentHandler != null) {
174             contentHandler.characters(text, 0, length);
175         }
176     }
177 
comment(char[] text, int length)178     /*package*/ void comment(char[] text, int length) throws SAXException {
179         LexicalHandler lexicalHandler = xmlReader.lexicalHandler;
180         if (lexicalHandler != null) {
181             lexicalHandler.comment(text, 0, length);
182         }
183     }
184 
startCdata()185     /*package*/ void startCdata() throws SAXException {
186         LexicalHandler lexicalHandler = xmlReader.lexicalHandler;
187         if (lexicalHandler != null) {
188             lexicalHandler.startCDATA();
189         }
190     }
191 
endCdata()192     /*package*/ void endCdata() throws SAXException {
193         LexicalHandler lexicalHandler = xmlReader.lexicalHandler;
194         if (lexicalHandler != null) {
195             lexicalHandler.endCDATA();
196         }
197     }
198 
startNamespace(String prefix, String uri)199     /*package*/ void startNamespace(String prefix, String uri)
200             throws SAXException {
201         ContentHandler contentHandler = xmlReader.contentHandler;
202         if (contentHandler != null) {
203             contentHandler.startPrefixMapping(prefix, uri);
204         }
205     }
206 
endNamespace(String prefix)207     /*package*/ void endNamespace(String prefix) throws SAXException {
208         ContentHandler contentHandler = xmlReader.contentHandler;
209         if (contentHandler != null) {
210             contentHandler.endPrefixMapping(prefix);
211         }
212     }
213 
startDtd(String name, String publicId, String systemId)214     /*package*/ void startDtd(String name, String publicId, String systemId)
215             throws SAXException {
216         LexicalHandler lexicalHandler = xmlReader.lexicalHandler;
217         if (lexicalHandler != null) {
218             lexicalHandler.startDTD(name, publicId, systemId);
219         }
220     }
221 
endDtd()222     /*package*/ void endDtd() throws SAXException {
223         LexicalHandler lexicalHandler = xmlReader.lexicalHandler;
224         if (lexicalHandler != null) {
225             lexicalHandler.endDTD();
226         }
227     }
228 
processingInstruction(String target, String data)229     /*package*/ void processingInstruction(String target, String data)
230             throws SAXException {
231         ContentHandler contentHandler = xmlReader.contentHandler;
232         if (contentHandler != null) {
233             contentHandler.processingInstruction(target, data);
234         }
235     }
236 
notationDecl(String name, String publicId, String systemId)237     /*package*/ void notationDecl(String name, String publicId, String systemId) throws SAXException {
238         DTDHandler dtdHandler = xmlReader.dtdHandler;
239         if (dtdHandler != null) {
240             dtdHandler.notationDecl(name, publicId, systemId);
241         }
242     }
243 
unparsedEntityDecl(String name, String publicId, String systemId, String notationName)244     /*package*/ void unparsedEntityDecl(String name, String publicId, String systemId, String notationName) throws SAXException {
245         DTDHandler dtdHandler = xmlReader.dtdHandler;
246         if (dtdHandler != null) {
247             dtdHandler.unparsedEntityDecl(name, publicId, systemId, notationName);
248         }
249     }
250 
251     /**
252      * Handles an external entity.
253      *
254      * @param context to be passed back to Expat when we parse the entity
255      * @param publicId the publicId of the entity
256      * @param systemId the systemId of the entity
257      */
handleExternalEntity(String context, String publicId, String systemId)258     /*package*/ void handleExternalEntity(String context, String publicId,
259             String systemId) throws SAXException, IOException {
260         EntityResolver entityResolver = xmlReader.entityResolver;
261         if (entityResolver == null) {
262             return;
263         }
264 
265         /*
266          * The spec. is terribly under-specified here. It says that if the
267          * systemId is a URL, we should try to resolve it, but it doesn't
268          * specify how to tell whether or not the systemId is a URL let alone
269          * how to resolve it.
270          *
271          * Other implementations do various dangerous things. We try to keep it
272          * simple: if the systemId parses as a URI and it's relative, we try to
273          * resolve it against the parent document's systemId. If anything goes
274          * wrong, we go with the original systemId. If crazybob had designed
275          * the API, he would have left all resolving to the EntityResolver.
276          */
277         if (this.systemId != null) {
278             try {
279                 URI systemUri = new URI(systemId);
280                 if (!systemUri.isAbsolute() && !systemUri.isOpaque()) {
281                     // It could be relative (or it may not be a URI at all!)
282                     URI baseUri = new URI(this.systemId);
283                     systemUri = baseUri.resolve(systemUri);
284 
285                     // Replace systemId w/ resolved URI
286                     systemId = systemUri.toString();
287                 }
288             } catch (Exception e) {
289                 System.logI("Could not resolve '" + systemId + "' relative to"
290                         + " '" + this.systemId + "' at " + locator, e);
291             }
292         }
293 
294         InputSource inputSource = entityResolver.resolveEntity(
295                 publicId, systemId);
296         if (inputSource == null) {
297             /*
298              * The spec. actually says that we should try to treat systemId
299              * as a URL and download and parse its contents here, but an
300              * entity resolver can easily accomplish the same by returning
301              * new InputSource(systemId).
302              *
303              * Downloading external entities by default would result in several
304              * unwanted DTD downloads, not to mention pose a security risk
305              * when parsing untrusted XML -- see for example
306              * http://archive.cert.uni-stuttgart.de/bugtraq/2002/10/msg00421.html --
307              * so we just do nothing instead. This also enables the user to
308              * opt out of entity parsing when using
309              * {@link org.xml.sax.helpers.DefaultHandler}, something that
310              * wouldn't be possible otherwise.
311              */
312             return;
313         }
314 
315         String encoding = pickEncoding(inputSource);
316         long pointer = createEntityParser(this.pointer, context);
317         try {
318             EntityParser entityParser = new EntityParser(encoding, xmlReader,
319                     pointer, inputSource.getPublicId(),
320                     inputSource.getSystemId());
321 
322             parseExternalEntity(entityParser, inputSource);
323         } finally {
324             releaseParser(pointer);
325         }
326     }
327 
328     /**
329      * Picks an encoding for an external entity. Defaults to UTF-8.
330      */
pickEncoding(InputSource inputSource)331     private String pickEncoding(InputSource inputSource) {
332         Reader reader = inputSource.getCharacterStream();
333         if (reader != null) {
334             return CHARACTER_ENCODING;
335         }
336 
337         String encoding = inputSource.getEncoding();
338         return encoding == null ? DEFAULT_ENCODING : encoding;
339     }
340 
341     /**
342      * Parses the the external entity provided by the input source.
343      */
parseExternalEntity(ExpatParser entityParser, InputSource inputSource)344     private void parseExternalEntity(ExpatParser entityParser,
345             InputSource inputSource) throws IOException, SAXException {
346         /*
347          * Expat complains if the external entity isn't wrapped with a root
348          * element so we add one and ignore it later on during parsing.
349          */
350 
351         // Try the character stream.
352         Reader reader = inputSource.getCharacterStream();
353         if (reader != null) {
354             try {
355                 entityParser.append("<externalEntity>");
356                 entityParser.parseFragment(reader);
357                 entityParser.append("</externalEntity>");
358             } finally {
359                 IoUtils.closeQuietly(reader);
360             }
361             return;
362         }
363 
364         // Try the byte stream.
365         InputStream in = inputSource.getByteStream();
366         if (in != null) {
367             try {
368                 entityParser.append("<externalEntity>"
369                         .getBytes(entityParser.encoding));
370                 entityParser.parseFragment(in);
371                 entityParser.append("</externalEntity>"
372                         .getBytes(entityParser.encoding));
373             } finally {
374                 IoUtils.closeQuietly(in);
375             }
376             return;
377         }
378 
379         // Make sure we use the user-provided systemId.
380         String systemId = inputSource.getSystemId();
381         if (systemId == null) {
382             // TODO: We could just try our systemId here.
383             throw new ParseException("No input specified.", locator);
384         }
385 
386         // Try the system id.
387         in = openUrl(systemId);
388         try {
389             entityParser.append("<externalEntity>"
390                     .getBytes(entityParser.encoding));
391             entityParser.parseFragment(in);
392             entityParser.append("</externalEntity>"
393                     .getBytes(entityParser.encoding));
394         } finally {
395             IoUtils.closeQuietly(in);
396         }
397     }
398 
399     /**
400      * Creates a native entity parser.
401      *
402      * @param parentPointer pointer to parent Expat parser
403      * @param context passed to {@link #handleExternalEntity}
404      * @return pointer to native parser
405      */
createEntityParser(long parentPointer, String context)406     private static native long createEntityParser(long parentPointer, String context);
407 
408     /**
409      * Appends part of an XML document. This parser will parse the given XML to
410      * the extent possible and dispatch to the appropriate methods.
411      *
412      * @param xml a whole or partial snippet of XML
413      * @throws SAXException if an error occurs during parsing
414      */
append(String xml)415     /*package*/ void append(String xml) throws SAXException {
416         try {
417             appendString(this.pointer, xml, false);
418         } catch (ExpatException e) {
419             throw new ParseException(e.getMessage(), this.locator);
420         }
421     }
422 
appendString(long pointer, String xml, boolean isFinal)423     private native void appendString(long pointer, String xml, boolean isFinal)
424             throws SAXException, ExpatException;
425 
426     /**
427      * Appends part of an XML document. This parser will parse the given XML to
428      * the extent possible and dispatch to the appropriate methods.
429      *
430      * @param xml a whole or partial snippet of XML
431      * @param offset into the char[]
432      * @param length of characters to use
433      * @throws SAXException if an error occurs during parsing
434      */
435     @UnsupportedAppUsage
append(char[] xml, int offset, int length)436     /*package*/ void append(char[] xml, int offset, int length)
437             throws SAXException {
438         try {
439             appendChars(this.pointer, xml, offset, length);
440         } catch (ExpatException e) {
441             throw new ParseException(e.getMessage(), this.locator);
442         }
443     }
444 
appendChars(long pointer, char[] xml, int offset, int length)445     private native void appendChars(long pointer, char[] xml, int offset,
446             int length) throws SAXException, ExpatException;
447 
448     /**
449      * Appends part of an XML document. This parser will parse the given XML to
450      * the extent possible and dispatch to the appropriate methods.
451      *
452      * @param xml a whole or partial snippet of XML
453      * @throws SAXException if an error occurs during parsing
454      */
append(byte[] xml)455     /*package*/ void append(byte[] xml) throws SAXException {
456         append(xml, 0, xml.length);
457     }
458 
459     /**
460      * Appends part of an XML document. This parser will parse the given XML to
461      * the extent possible and dispatch to the appropriate methods.
462      *
463      * @param xml a whole or partial snippet of XML
464      * @param offset into the byte[]
465      * @param length of bytes to use
466      * @throws SAXException if an error occurs during parsing
467      */
468     @UnsupportedAppUsage
append(byte[] xml, int offset, int length)469     /*package*/ void append(byte[] xml, int offset, int length)
470             throws SAXException {
471         try {
472             appendBytes(this.pointer, xml, offset, length);
473         } catch (ExpatException e) {
474             throw new ParseException(e.getMessage(), this.locator);
475         }
476     }
477 
appendBytes(long pointer, byte[] xml, int offset, int length)478     private native void appendBytes(long pointer, byte[] xml, int offset,
479             int length) throws SAXException, ExpatException;
480 
481     /**
482      * Parses an XML document from the given input stream.
483      */
parseDocument(InputStream in)484     /*package*/ void parseDocument(InputStream in) throws IOException,
485             SAXException {
486         startDocument();
487         parseFragment(in);
488         finish();
489         endDocument();
490     }
491 
492     /**
493      * Parses an XML Document from the given reader.
494      */
parseDocument(Reader in)495     /*package*/ void parseDocument(Reader in) throws IOException, SAXException {
496         startDocument();
497         parseFragment(in);
498         finish();
499         endDocument();
500     }
501 
502     /**
503      * Parses XML from the given Reader.
504      */
parseFragment(Reader in)505     private void parseFragment(Reader in) throws IOException, SAXException {
506         char[] buffer = new char[BUFFER_SIZE / 2];
507         int length;
508         while ((length = in.read(buffer)) != -1) {
509             try {
510                 appendChars(this.pointer, buffer, 0, length);
511             } catch (ExpatException e) {
512                 throw new ParseException(e.getMessage(), locator);
513             }
514         }
515     }
516 
517     /**
518      * Parses XML from the given input stream.
519      */
parseFragment(InputStream in)520     private void parseFragment(InputStream in)
521             throws IOException, SAXException {
522         byte[] buffer = new byte[BUFFER_SIZE];
523         int length;
524         while ((length = in.read(buffer)) != -1) {
525             try {
526                 appendBytes(this.pointer, buffer, 0, length);
527             } catch (ExpatException e) {
528                 throw new ParseException(e.getMessage(), this.locator);
529             }
530         }
531     }
532 
startDocument()533     private void startDocument() throws SAXException {
534         ContentHandler contentHandler = xmlReader.contentHandler;
535         if (contentHandler != null) {
536             contentHandler.setDocumentLocator(this.locator);
537             contentHandler.startDocument();
538         }
539     }
540 
endDocument()541     private void endDocument() throws SAXException {
542         ContentHandler contentHandler;
543         contentHandler = xmlReader.contentHandler;
544         if (contentHandler != null) {
545             contentHandler.endDocument();
546         }
547     }
548 
549     /**
550      * Indicate that we're finished parsing.
551      *
552      * @throws SAXException if the xml is incomplete
553      */
554     @UnsupportedAppUsage
finish()555     /*package*/ void finish() throws SAXException {
556         try {
557             appendString(this.pointer, "", true);
558         } catch (ExpatException e) {
559             throw new ParseException(e.getMessage(), this.locator);
560         }
561     }
562 
finalize()563     @Override protected synchronized void finalize() throws Throwable {
564         try {
565             if (this.pointer != 0) {
566                 release(this.pointer);
567                 this.pointer = 0;
568             }
569         } finally {
570             super.finalize();
571         }
572     }
573 
574     /**
575      * Releases all native objects.
576      */
release(long pointer)577     private native void release(long pointer);
578 
579     /**
580      * Releases native parser only.
581      */
releaseParser(long pointer)582     private static native void releaseParser(long pointer);
583 
584     /**
585      * Initialize static resources.
586      */
staticInitialize(String emptyString)587     private static native void staticInitialize(String emptyString);
588 
589     static {
590         staticInitialize("");
591     }
592 
593     /**
594      * Gets the current line number within the XML file.
595      */
line()596     private int line() {
597         return line(this.pointer);
598     }
599 
line(long pointer)600     private static native int line(long pointer);
601 
602     /**
603      * Gets the current column number within the XML file.
604      */
column()605     private int column() {
606         return column(this.pointer);
607     }
608 
column(long pointer)609     private static native int column(long pointer);
610 
611     /**
612      * Clones the current attributes so they can be used outside of
613      * startElement().
614      */
615     @UnsupportedAppUsage
cloneAttributes()616     /*package*/ Attributes cloneAttributes() {
617         if (!inStartElement) {
618             throw new IllegalStateException(OUTSIDE_START_ELEMENT);
619         }
620 
621         if (attributeCount == 0) {
622             return ClonedAttributes.EMPTY;
623         }
624 
625         long clonePointer
626                 = cloneAttributes(this.attributePointer, this.attributeCount);
627         return new ClonedAttributes(pointer, clonePointer, attributeCount);
628     }
629 
cloneAttributes(long pointer, int attributeCount)630     private static native long cloneAttributes(long pointer, int attributeCount);
631 
632     /**
633      * Used for cloned attributes.
634      */
635     private static class ClonedAttributes extends ExpatAttributes {
636     // TODO: Can we please remove this? It appears unused, and the finalizer
637     // asynchronously invalidates the result returned by getPointer() at a
638     // largely unpredictable time. b/70989581
639 
640         private static final Attributes EMPTY = new ClonedAttributes(0, 0, 0);
641 
642         private final long parserPointer;
643         private long pointer;
644         private final int length;
645 
646         /**
647          * Constructs a Java wrapper for native attributes.
648          *
649          * @param parserPointer pointer to the parse, can be 0 if length is 0.
650          * @param pointer pointer to the attributes array, can be 0 if the
651          *  length is 0.
652          * @param length number of attributes
653          */
ClonedAttributes(long parserPointer, long pointer, int length)654         private ClonedAttributes(long parserPointer, long pointer, int length) {
655             this.parserPointer = parserPointer;
656             this.pointer = pointer;
657             this.length = length;
658         }
659 
660         @Override
getParserPointer()661         public long getParserPointer() {
662             return this.parserPointer;
663         }
664 
665         @Override
getPointer()666         public long getPointer() {
667             return pointer;
668         }
669 
670         @Override
getLength()671         public int getLength() {
672             return length;
673         }
674 
finalize()675         @Override protected synchronized void finalize() throws Throwable {
676             try {
677                 if (pointer != 0) {
678                     freeAttributes(pointer);
679                     pointer = 0;
680                 }
681             } finally {
682                 super.finalize();
683             }
684         }
685     }
686 
687     private class ExpatLocator implements Locator {
688 
getPublicId()689         public String getPublicId() {
690             return publicId;
691         }
692 
getSystemId()693         public String getSystemId() {
694             return systemId;
695         }
696 
getLineNumber()697         public int getLineNumber() {
698             return line();
699         }
700 
getColumnNumber()701         public int getColumnNumber() {
702             return column();
703         }
704 
705         @Override
toString()706         public String toString() {
707             return "Locator[publicId: " + publicId + ", systemId: " + systemId
708                 + ", line: " + getLineNumber()
709                 + ", column: " + getColumnNumber() + "]";
710         }
711     }
712 
713     /**
714      * Attributes that are only valid during startElement().
715      */
716     private class CurrentAttributes extends ExpatAttributes {
717 
718         @Override
getParserPointer()719         public long getParserPointer() {
720             return pointer;
721         }
722 
723         @Override
getPointer()724         public long getPointer() {
725             if (!inStartElement) {
726                 throw new IllegalStateException(OUTSIDE_START_ELEMENT);
727             }
728             return attributePointer;
729         }
730 
731         @Override
getLength()732         public int getLength() {
733             if (!inStartElement) {
734                 throw new IllegalStateException(OUTSIDE_START_ELEMENT);
735             }
736             return attributeCount;
737         }
738     }
739 
740     /**
741      * Includes line and column in the message.
742      */
743     private static class ParseException extends SAXParseException {
744 
ParseException(String message, Locator locator)745         private ParseException(String message, Locator locator) {
746             super(makeMessage(message, locator), locator);
747         }
748 
makeMessage(String message, Locator locator)749         private static String makeMessage(String message, Locator locator) {
750             return makeMessage(message, locator.getLineNumber(),
751                     locator.getColumnNumber());
752         }
753 
makeMessage( String message, int line, int column)754         private static String makeMessage(
755                 String message, int line, int column) {
756             return "At line " + line + ", column "
757                     + column + ": " + message;
758         }
759     }
760 
761     /**
762      * Opens an InputStream for the given URL.
763      */
openUrl(String url)764     /*package*/ static InputStream openUrl(String url) throws IOException {
765         try {
766             URLConnection urlConnection = new URL(url).openConnection();
767             urlConnection.setConnectTimeout(TIMEOUT);
768             urlConnection.setReadTimeout(TIMEOUT);
769             urlConnection.setDoInput(true);
770             urlConnection.setDoOutput(false);
771             return urlConnection.getInputStream();
772         } catch (Exception e) {
773             IOException ioe = new IOException("Couldn't open " + url);
774             ioe.initCause(e);
775             throw ioe;
776         }
777     }
778 
779     /**
780      * Parses an external entity.
781      */
782     private static class EntityParser extends ExpatParser {
783 
784         @UnsupportedAppUsage
785         private int depth = 0;
786 
EntityParser(String encoding, ExpatReader xmlReader, long pointer, String publicId, String systemId)787         private EntityParser(String encoding, ExpatReader xmlReader,
788                 long pointer, String publicId, String systemId) {
789             super(encoding, xmlReader, pointer, publicId, systemId);
790         }
791 
792         @Override
startElement(String uri, String localName, String qName, long attributePointer, int attributeCount)793         void startElement(String uri, String localName, String qName,
794                 long attributePointer, int attributeCount) throws SAXException {
795             /*
796              * Skip topmost element generated by our workaround in
797              * {@link #handleExternalEntity}.
798              */
799             if (depth++ > 0) {
800                 super.startElement(uri, localName, qName, attributePointer,
801                         attributeCount);
802             }
803         }
804 
805         @Override
endElement(String uri, String localName, String qName)806         void endElement(String uri, String localName, String qName)
807                 throws SAXException {
808             if (--depth > 0) {
809                 super.endElement(uri, localName, qName);
810             }
811         }
812 
813         @Override
814         @SuppressWarnings("FinalizeDoesntCallSuperFinalize")
finalize()815         protected synchronized void finalize() throws Throwable {
816             /*
817              * Don't release our native resources. We do so explicitly in
818              * {@link #handleExternalEntity} and we don't want to release the
819              * parsing context--our parent is using it.
820              */
821         }
822     }
823 }
824