/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.harmony.xml; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.net.URI; import java.net.URL; import java.net.URLConnection; import libcore.io.IoUtils; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.DTDHandler; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.ext.LexicalHandler; /** * Adapts SAX API to the Expat native XML parser. Not intended for reuse * across documents. * * @see org.apache.harmony.xml.ExpatReader */ class ExpatParser { private static final int BUFFER_SIZE = 8096; // in bytes /** Pointer to XML_Parser instance. */ private int pointer; private boolean inStartElement = false; private int attributeCount = -1; private int attributePointer = 0; private final Locator locator = new ExpatLocator(); private final ExpatReader xmlReader; private final String publicId; private final String systemId; private final String encoding; private final ExpatAttributes attributes = new CurrentAttributes(); private static final String OUTSIDE_START_ELEMENT = "Attributes can only be used within the scope of startElement()."; /** We default to UTF-8 when the user doesn't specify an encoding. */ private static final String DEFAULT_ENCODING = "UTF-8"; /** Encoding used for Java chars, used to parse Readers and Strings */ /*package*/ static final String CHARACTER_ENCODING = "UTF-16"; /** Timeout for HTTP connections (in ms) */ private static final int TIMEOUT = 20 * 1000; /** * Constructs a new parser with the specified encoding. */ /*package*/ ExpatParser(String encoding, ExpatReader xmlReader, boolean processNamespaces, String publicId, String systemId) { this.publicId = publicId; this.systemId = systemId; this.xmlReader = xmlReader; /* * TODO: Let Expat try to guess the encoding instead of defaulting. * Unfortunately, I don't know how to tell which encoding Expat picked, * so I won't know how to encode "<externalEntity>" below. The solution * I think is to fix Expat to not require the "<externalEntity>" * workaround. */ this.encoding = encoding == null ? DEFAULT_ENCODING : encoding; this.pointer = initialize( this.encoding, processNamespaces ); } /** * Used by {@link EntityParser}. */ private ExpatParser(String encoding, ExpatReader xmlReader, int pointer, String publicId, String systemId) { this.encoding = encoding; this.xmlReader = xmlReader; this.pointer = pointer; this.systemId = systemId; this.publicId = publicId; } /** * Initializes native resources. * * @return the pointer to the native parser */ private native int initialize(String encoding, boolean namespacesEnabled); /** * Called at the start of an element. * * @param uri namespace URI of element or "" if namespace processing is * disabled * @param localName local name of element or "" if namespace processing is * disabled * @param qName qualified name or "" if namespace processing is enabled * @param attributePointer pointer to native attribute char*--we keep * a separate pointer so we can detach it from the parser instance * @param attributeCount number of attributes */ /*package*/ void startElement(String uri, String localName, String qName, int attributePointer, int attributeCount) throws SAXException { ContentHandler contentHandler = xmlReader.contentHandler; if (contentHandler == null) { return; } try { inStartElement = true; this.attributePointer = attributePointer; this.attributeCount = attributeCount; contentHandler.startElement( uri, localName, qName, this.attributes); } finally { inStartElement = false; this.attributeCount = -1; this.attributePointer = 0; } } /*package*/ void endElement(String uri, String localName, String qName) throws SAXException { ContentHandler contentHandler = xmlReader.contentHandler; if (contentHandler != null) { contentHandler.endElement(uri, localName, qName); } } /*package*/ void text(char[] text, int length) throws SAXException { ContentHandler contentHandler = xmlReader.contentHandler; if (contentHandler != null) { contentHandler.characters(text, 0, length); } } /*package*/ void comment(char[] text, int length) throws SAXException { LexicalHandler lexicalHandler = xmlReader.lexicalHandler; if (lexicalHandler != null) { lexicalHandler.comment(text, 0, length); } } /*package*/ void startCdata() throws SAXException { LexicalHandler lexicalHandler = xmlReader.lexicalHandler; if (lexicalHandler != null) { lexicalHandler.startCDATA(); } } /*package*/ void endCdata() throws SAXException { LexicalHandler lexicalHandler = xmlReader.lexicalHandler; if (lexicalHandler != null) { lexicalHandler.endCDATA(); } } /*package*/ void startNamespace(String prefix, String uri) throws SAXException { ContentHandler contentHandler = xmlReader.contentHandler; if (contentHandler != null) { contentHandler.startPrefixMapping(prefix, uri); } } /*package*/ void endNamespace(String prefix) throws SAXException { ContentHandler contentHandler = xmlReader.contentHandler; if (contentHandler != null) { contentHandler.endPrefixMapping(prefix); } } /*package*/ void startDtd(String name, String publicId, String systemId) throws SAXException { LexicalHandler lexicalHandler = xmlReader.lexicalHandler; if (lexicalHandler != null) { lexicalHandler.startDTD(name, publicId, systemId); } } /*package*/ void endDtd() throws SAXException { LexicalHandler lexicalHandler = xmlReader.lexicalHandler; if (lexicalHandler != null) { lexicalHandler.endDTD(); } } /*package*/ void processingInstruction(String target, String data) throws SAXException { ContentHandler contentHandler = xmlReader.contentHandler; if (contentHandler != null) { contentHandler.processingInstruction(target, data); } } /*package*/ void notationDecl(String name, String publicId, String systemId) throws SAXException { DTDHandler dtdHandler = xmlReader.dtdHandler; if (dtdHandler != null) { dtdHandler.notationDecl(name, publicId, systemId); } } /*package*/ void unparsedEntityDecl(String name, String publicId, String systemId, String notationName) throws SAXException { DTDHandler dtdHandler = xmlReader.dtdHandler; if (dtdHandler != null) { dtdHandler.unparsedEntityDecl(name, publicId, systemId, notationName); } } /** * Handles an external entity. * * @param context to be passed back to Expat when we parse the entity * @param publicId the publicId of the entity * @param systemId the systemId of the entity */ /*package*/ void handleExternalEntity(String context, String publicId, String systemId) throws SAXException, IOException { EntityResolver entityResolver = xmlReader.entityResolver; if (entityResolver == null) { return; } /* * The spec. is terribly under-specified here. It says that if the * systemId is a URL, we should try to resolve it, but it doesn't * specify how to tell whether or not the systemId is a URL let alone * how to resolve it. * * Other implementations do various insane things. We try to keep it * simple: if the systemId parses as a URI and it's relative, we try to * resolve it against the parent document's systemId. If anything goes * wrong, we go with the original systemId. If crazybob had designed * the API, he would have left all resolving to the EntityResolver. */ if (this.systemId != null) { try { URI systemUri = new URI(systemId); if (!systemUri.isAbsolute() && !systemUri.isOpaque()) { // It could be relative (or it may not be a URI at all!) URI baseUri = new URI(this.systemId); systemUri = baseUri.resolve(systemUri); // Replace systemId w/ resolved URI systemId = systemUri.toString(); } } catch (Exception e) { System.logI("Could not resolve '" + systemId + "' relative to" + " '" + this.systemId + "' at " + locator, e); } } InputSource inputSource = entityResolver.resolveEntity( publicId, systemId); if (inputSource == null) { /* * The spec. actually says that we should try to treat systemId * as a URL and download and parse its contents here, but an * entity resolver can easily accomplish the same by returning * new InputSource(systemId). * * Downloading external entities by default would result in several * unwanted DTD downloads, not to mention pose a security risk * when parsing untrusted XML -- see for example * http://archive.cert.uni-stuttgart.de/bugtraq/2002/10/msg00421.html -- * so we just do nothing instead. This also enables the user to * opt out of entity parsing when using * {@link org.xml.sax.helpers.DefaultHandler}, something that * wouldn't be possible otherwise. */ return; } String encoding = pickEncoding(inputSource); int pointer = createEntityParser(this.pointer, context); try { EntityParser entityParser = new EntityParser(encoding, xmlReader, pointer, inputSource.getPublicId(), inputSource.getSystemId()); parseExternalEntity(entityParser, inputSource); } finally { releaseParser(pointer); } } /** * Picks an encoding for an external entity. Defaults to UTF-8. */ private String pickEncoding(InputSource inputSource) { Reader reader = inputSource.getCharacterStream(); if (reader != null) { return CHARACTER_ENCODING; } String encoding = inputSource.getEncoding(); return encoding == null ? DEFAULT_ENCODING : encoding; } /** * Parses the the external entity provided by the input source. */ private void parseExternalEntity(ExpatParser entityParser, InputSource inputSource) throws IOException, SAXException { /* * Expat complains if the external entity isn't wrapped with a root * element so we add one and ignore it later on during parsing. */ // Try the character stream. Reader reader = inputSource.getCharacterStream(); if (reader != null) { try { entityParser.append("<externalEntity>"); entityParser.parseFragment(reader); entityParser.append("</externalEntity>"); } finally { IoUtils.closeQuietly(reader); } return; } // Try the byte stream. InputStream in = inputSource.getByteStream(); if (in != null) { try { entityParser.append("<externalEntity>" .getBytes(entityParser.encoding)); entityParser.parseFragment(in); entityParser.append("</externalEntity>" .getBytes(entityParser.encoding)); } finally { IoUtils.closeQuietly(in); } return; } // Make sure we use the user-provided systemId. String systemId = inputSource.getSystemId(); if (systemId == null) { // TODO: We could just try our systemId here. throw new ParseException("No input specified.", locator); } // Try the system id. in = openUrl(systemId); try { entityParser.append("<externalEntity>" .getBytes(entityParser.encoding)); entityParser.parseFragment(in); entityParser.append("</externalEntity>" .getBytes(entityParser.encoding)); } finally { IoUtils.closeQuietly(in); } } /** * Creates a native entity parser. * * @param parentPointer pointer to parent Expat parser * @param context passed to {@link #handleExternalEntity} * @return pointer to native parser */ private static native int createEntityParser(int parentPointer, String context); /** * Appends part of an XML document. This parser will parse the given XML to * the extent possible and dispatch to the appropriate methods. * * @param xml a whole or partial snippet of XML * @throws SAXException if an error occurs during parsing */ /*package*/ void append(String xml) throws SAXException { try { appendString(this.pointer, xml, false); } catch (ExpatException e) { throw new ParseException(e.getMessage(), this.locator); } } private native void appendString(int pointer, String xml, boolean isFinal) throws SAXException, ExpatException; /** * Appends part of an XML document. This parser will parse the given XML to * the extent possible and dispatch to the appropriate methods. * * @param xml a whole or partial snippet of XML * @param offset into the char[] * @param length of characters to use * @throws SAXException if an error occurs during parsing */ /*package*/ void append(char[] xml, int offset, int length) throws SAXException { try { appendChars(this.pointer, xml, offset, length); } catch (ExpatException e) { throw new ParseException(e.getMessage(), this.locator); } } private native void appendChars(int pointer, char[] xml, int offset, int length) throws SAXException, ExpatException; /** * Appends part of an XML document. This parser will parse the given XML to * the extent possible and dispatch to the appropriate methods. * * @param xml a whole or partial snippet of XML * @throws SAXException if an error occurs during parsing */ /*package*/ void append(byte[] xml) throws SAXException { append(xml, 0, xml.length); } /** * Appends part of an XML document. This parser will parse the given XML to * the extent possible and dispatch to the appropriate methods. * * @param xml a whole or partial snippet of XML * @param offset into the byte[] * @param length of bytes to use * @throws SAXException if an error occurs during parsing */ /*package*/ void append(byte[] xml, int offset, int length) throws SAXException { try { appendBytes(this.pointer, xml, offset, length); } catch (ExpatException e) { throw new ParseException(e.getMessage(), this.locator); } } private native void appendBytes(int pointer, byte[] xml, int offset, int length) throws SAXException, ExpatException; /** * Parses an XML document from the given input stream. */ /*package*/ void parseDocument(InputStream in) throws IOException, SAXException { startDocument(); parseFragment(in); finish(); endDocument(); } /** * Parses an XML Document from the given reader. */ /*package*/ void parseDocument(Reader in) throws IOException, SAXException { startDocument(); parseFragment(in); finish(); endDocument(); } /** * Parses XML from the given Reader. */ private void parseFragment(Reader in) throws IOException, SAXException { char[] buffer = new char[BUFFER_SIZE / 2]; int length; while ((length = in.read(buffer)) != -1) { try { appendChars(this.pointer, buffer, 0, length); } catch (ExpatException e) { throw new ParseException(e.getMessage(), locator); } } } /** * Parses XML from the given input stream. */ private void parseFragment(InputStream in) throws IOException, SAXException { byte[] buffer = new byte[BUFFER_SIZE]; int length; while ((length = in.read(buffer)) != -1) { try { appendBytes(this.pointer, buffer, 0, length); } catch (ExpatException e) { throw new ParseException(e.getMessage(), this.locator); } } } private void startDocument() throws SAXException { ContentHandler contentHandler = xmlReader.contentHandler; if (contentHandler != null) { contentHandler.setDocumentLocator(this.locator); contentHandler.startDocument(); } } private void endDocument() throws SAXException { ContentHandler contentHandler; contentHandler = xmlReader.contentHandler; if (contentHandler != null) { contentHandler.endDocument(); } } /** * Indicate that we're finished parsing. * * @throws SAXException if the xml is incomplete */ /*package*/ void finish() throws SAXException { try { appendString(this.pointer, "", true); } catch (ExpatException e) { throw new ParseException(e.getMessage(), this.locator); } } @Override protected synchronized void finalize() throws Throwable { try { if (this.pointer != 0) { release(this.pointer); this.pointer = 0; } } finally { super.finalize(); } } /** * Releases all native objects. */ private native void release(int pointer); /** * Releases native parser only. */ private static native void releaseParser(int pointer); /** * Initialize static resources. */ private static native void staticInitialize(String emptyString); static { staticInitialize(""); } /** * Gets the current line number within the XML file. */ private int line() { return line(this.pointer); } private static native int line(int pointer); /** * Gets the current column number within the XML file. */ private int column() { return column(this.pointer); } private static native int column(int pointer); /** * Clones the current attributes so they can be used outside of * startElement(). */ /*package*/ Attributes cloneAttributes() { if (!inStartElement) { throw new IllegalStateException(OUTSIDE_START_ELEMENT); } if (attributeCount == 0) { return ClonedAttributes.EMPTY; } int clonePointer = cloneAttributes(this.attributePointer, this.attributeCount); return new ClonedAttributes(pointer, clonePointer, attributeCount); } private static native int cloneAttributes(int pointer, int attributeCount); /** * Used for cloned attributes. */ private static class ClonedAttributes extends ExpatAttributes { private static final Attributes EMPTY = new ClonedAttributes(0, 0, 0); private final int parserPointer; private int pointer; private final int length; /** * Constructs a Java wrapper for native attributes. * * @param parserPointer pointer to the parse, can be 0 if length is 0. * @param pointer pointer to the attributes array, can be 0 if the * length is 0. * @param length number of attributes */ private ClonedAttributes(int parserPointer, int pointer, int length) { this.parserPointer = parserPointer; this.pointer = pointer; this.length = length; } @Override public int getParserPointer() { return this.parserPointer; } @Override public int getPointer() { return pointer; } @Override public int getLength() { return length; } @Override protected synchronized void finalize() throws Throwable { try { if (pointer != 0) { freeAttributes(pointer); pointer = 0; } } finally { super.finalize(); } } } private class ExpatLocator implements Locator { public String getPublicId() { return publicId; } public String getSystemId() { return systemId; } public int getLineNumber() { return line(); } public int getColumnNumber() { return column(); } @Override public String toString() { return "Locator[publicId: " + publicId + ", systemId: " + systemId + ", line: " + getLineNumber() + ", column: " + getColumnNumber() + "]"; } } /** * Attributes that are only valid during startElement(). */ private class CurrentAttributes extends ExpatAttributes { @Override public int getParserPointer() { return pointer; } @Override public int getPointer() { if (!inStartElement) { throw new IllegalStateException(OUTSIDE_START_ELEMENT); } return attributePointer; } @Override public int getLength() { if (!inStartElement) { throw new IllegalStateException(OUTSIDE_START_ELEMENT); } return attributeCount; } } /** * Includes line and column in the message. */ private static class ParseException extends SAXParseException { private ParseException(String message, Locator locator) { super(makeMessage(message, locator), locator); } private static String makeMessage(String message, Locator locator) { return makeMessage(message, locator.getLineNumber(), locator.getColumnNumber()); } private static String makeMessage( String message, int line, int column) { return "At line " + line + ", column " + column + ": " + message; } } /** * Opens an InputStream for the given URL. */ /*package*/ static InputStream openUrl(String url) throws IOException { try { URLConnection urlConnection = new URL(url).openConnection(); urlConnection.setConnectTimeout(TIMEOUT); urlConnection.setReadTimeout(TIMEOUT); urlConnection.setDoInput(true); urlConnection.setDoOutput(false); return urlConnection.getInputStream(); } catch (Exception e) { IOException ioe = new IOException("Couldn't open " + url); ioe.initCause(e); throw ioe; } } /** * Parses an external entity. */ private static class EntityParser extends ExpatParser { private int depth = 0; private EntityParser(String encoding, ExpatReader xmlReader, int pointer, String publicId, String systemId) { super(encoding, xmlReader, pointer, publicId, systemId); } @Override void startElement(String uri, String localName, String qName, int attributePointer, int attributeCount) throws SAXException { /* * Skip topmost element generated by our workaround in * {@link #handleExternalEntity}. */ if (depth++ > 0) { super.startElement(uri, localName, qName, attributePointer, attributeCount); } } @Override void endElement(String uri, String localName, String qName) throws SAXException { if (--depth > 0) { super.endElement(uri, localName, qName); } } @Override @SuppressWarnings("FinalizeDoesntCallSuperFinalize") protected synchronized void finalize() throws Throwable { /* * Don't release our native resources. We do so explicitly in * {@link #handleExternalEntity} and we don't want to release the * parsing context--our parent is using it. */ } } }