blob: c48962a308730c815d0e45a78633a138bb2cc9ef [file] [log] [blame]
/*
* 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 org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
/**
* Fast, partial XmlPullParser implementation based upon Expat. Does not
* support validation or {@code DOCTYPE} processing.
*/
public class ExpatPullParser implements XmlPullParser {
/**
* This feature is identified by http://xmlpull.org/v1/doc/features.html#relaxed
* If this feature is supported that means that XmlPull parser will be
* lenient when checking XML well formedness.
* NOTE: use it only if XML input is not well-formed and in general usage
* if this feature is discouraged
* NOTE: as there is no definition of what is relaxed XML parsing
* therefore what parser will do completely depends on implementation used
*/
public static final String FEATURE_RELAXED =
"http://xmlpull.org/v1/doc/features.html#relaxed";
private static final int BUFFER_SIZE = 8096;
private static final String NOT_A_START_TAG = "This is not a start tag.";
private Document document;
private boolean processNamespaces = false;
private boolean relaxed = false;
public void setFeature(String name, boolean state)
throws XmlPullParserException {
if (name == null) {
// Required by API.
throw new IllegalArgumentException("Null feature name");
}
if (name.equals(FEATURE_PROCESS_NAMESPACES)) {
processNamespaces = state;
return;
}
if (name.equals(FEATURE_RELAXED)) {
relaxed = true;
return;
}
// You're free to turn these features off because we don't support them.
if (!state && (name.equals(FEATURE_REPORT_NAMESPACE_ATTRIBUTES)
|| name.equals(FEATURE_PROCESS_DOCDECL)
|| name.equals(FEATURE_VALIDATION))) {
return;
}
throw new XmlPullParserException("Unsupported feature: " + name);
}
public boolean getFeature(String name) {
if (name == null) {
// Required by API.
throw new IllegalArgumentException("Null feature name");
}
// We always support namespaces, but no other features.
return name.equals(FEATURE_PROCESS_NAMESPACES) && processNamespaces;
}
/**
* Returns true if this parser processes namespaces.
*
* @see #setNamespaceProcessingEnabled(boolean)
*/
public boolean isNamespaceProcessingEnabled() {
return processNamespaces;
}
/**
* Enables or disables namespace processing. Set to false by default.
*
* @see #isNamespaceProcessingEnabled()
*/
public void setNamespaceProcessingEnabled(boolean processNamespaces) {
this.processNamespaces = processNamespaces;
}
public void setProperty(String name, Object value)
throws XmlPullParserException {
if (name == null) {
// Required by API.
throw new IllegalArgumentException("Null feature name");
}
// We don't support any properties.
throw new XmlPullParserException("Properties aren't supported.");
}
public Object getProperty(String name) {
return null;
}
public void setInput(Reader in) throws XmlPullParserException {
this.document = new CharDocument(in, processNamespaces);
}
public void setInput(InputStream in, String encodingName)
throws XmlPullParserException {
this.document = new ByteDocument(in, encodingName, processNamespaces);
}
public String getInputEncoding() {
return this.document.getEncoding();
}
/**
* Not supported.
*
* @throws UnsupportedOperationException always
*/
public void defineEntityReplacementText(String entityName,
String replacementText) throws XmlPullParserException {
throw new UnsupportedOperationException();
}
public int getNamespaceCount(int depth) throws XmlPullParserException {
return document.currentEvent.namespaceStack.countAt(depth);
}
public String getNamespacePrefix(int pos) throws XmlPullParserException {
String prefix = document.currentEvent.namespaceStack.prefixAt(pos);
@SuppressWarnings("StringEquality")
boolean hasPrefix = prefix != "";
return hasPrefix ? prefix : null;
}
public String getNamespaceUri(int pos) throws XmlPullParserException {
return document.currentEvent.namespaceStack.uriAt(pos);
}
public String getNamespace(String prefix) {
// In XmlPullParser API, null == default namespace.
if (prefix == null) {
// Internally, we use empty string instead of null.
prefix = "";
}
return document.currentEvent.namespaceStack.uriFor(prefix);
}
public int getDepth() {
return this.document.getDepth();
}
public String getPositionDescription() {
return "line " + getLineNumber() + ", column " + getColumnNumber();
}
/**
* Not supported.
*
* @return {@literal -1} always
*/
public int getLineNumber() {
// We would have to record the line number in each event.
return -1;
}
/**
* Not supported.
*
* @return {@literal -1} always
*/
public int getColumnNumber() {
// We would have to record the column number in each event.
return -1;
}
public boolean isWhitespace() throws XmlPullParserException {
if (getEventType() != TEXT) {
throw new XmlPullParserException("Not on text.");
}
String text = getText();
if (text.length() == 0) {
return true;
}
int length = text.length();
for (int i = 0; i < length; i++) {
if (!Character.isWhitespace(text.charAt(i))) {
return false;
}
}
return true;
}
public String getText() {
final StringBuilder builder = this.document.currentEvent.getText();
return builder == null ? null : builder.toString();
}
public char[] getTextCharacters(int[] holderForStartAndLength) {
final StringBuilder builder = this.document.currentEvent.getText();
final int length = builder.length();
char[] characters = new char[length];
builder.getChars(0, length, characters, 0);
holderForStartAndLength[0] = 0;
holderForStartAndLength[1] = length;
return characters;
}
public String getNamespace() {
return this.document.currentEvent.getNamespace();
}
public String getName() {
return this.document.currentEvent.getName();
}
/**
* Not supported.
*
* @throws UnsupportedOperationException always
*/
public String getPrefix() {
throw new UnsupportedOperationException();
}
public boolean isEmptyElementTag() throws XmlPullParserException {
return this.document.isCurrentElementEmpty();
}
public int getAttributeCount() {
return this.document.currentEvent.getAttributeCount();
}
public String getAttributeNamespace(int index) {
return this.document.currentEvent.getAttributeNamespace(index);
}
public String getAttributeName(int index) {
return this.document.currentEvent.getAttributeName(index);
}
/**
* Not supported.
*
* @throws UnsupportedOperationException always
*/
public String getAttributePrefix(int index) {
throw new UnsupportedOperationException();
}
public String getAttributeType(int index) {
return "CDATA";
}
public boolean isAttributeDefault(int index) {
return false;
}
public String getAttributeValue(int index) {
return this.document.currentEvent.getAttributeValue(index);
}
public String getAttributeValue(String namespace, String name) {
return this.document.currentEvent.getAttributeValue(namespace, name);
}
public int getEventType() throws XmlPullParserException {
return this.document.currentEvent.getType();
}
public int next() throws XmlPullParserException, IOException {
return this.document.dequeue();
}
/**
* Not supported.
*
* @throws UnsupportedOperationException always
*/
public int nextToken() throws XmlPullParserException, IOException {
throw new UnsupportedOperationException();
}
public void require(int type, String namespace, String name)
throws XmlPullParserException, IOException {
if (type != getEventType()
|| (namespace != null && !namespace.equals(getNamespace()))
|| (name != null && !name.equals(getName()))) {
throw new XmlPullParserException("expected "
+ TYPES[type] + getPositionDescription());
}
}
public String nextText() throws XmlPullParserException, IOException {
if (this.document.currentEvent.getType() != START_TAG)
throw new XmlPullParserException("Not on start tag.");
int next = this.document.dequeue();
switch (next) {
case TEXT: return getText();
case END_TAG: return "";
default: throw new XmlPullParserException(
"Unexpected event type: " + TYPES[next]);
}
}
public int nextTag() throws XmlPullParserException, IOException {
int eventType = next();
if (eventType == TEXT && isWhitespace()) {
eventType = next();
}
if (eventType != START_TAG && eventType != END_TAG) {
throw new XmlPullParserException(
"Expected start or end tag", this, null);
}
return eventType;
}
/**
* Immutable namespace stack. Pushing a new namespace on to the stack
* only results in one object allocation. Most operations are O(N) where
* N is the stack size. Accessing recently pushed namespaces, like those
* for the current element, is significantly faster.
*/
static class NamespaceStack {
/** An empty stack. */
static final NamespaceStack EMPTY = new NamespaceStack();
private final NamespaceStack parent;
private final String prefix;
private final String uri;
private final int index;
private final int depth;
/**
* Constructs an actual namespace stack node. Internally, the nodes
* and the stack are one in the same making for a very efficient
* implementation. The user just sees an immutable stack and the
* builder.
*/
private NamespaceStack(NamespaceStack parent, String prefix,
String uri, int depth) {
this.parent = parent;
this.prefix = prefix;
this.uri = uri;
this.index = parent.index + 1;
this.depth = depth;
}
/**
* Constructs a dummy node which only serves to point to the bottom
* of the stack. Using an actual node instead of null simplifies the
* code.
*/
private NamespaceStack() {
this.parent = null;
this.prefix = null;
this.uri = null;
// This node has an index of -1 since the actual first node in the
// stack has index 0.
this.index = -1;
// The actual first node will have a depth of 1.
this.depth = 0;
}
String uriFor(String prefix) {
for (NamespaceStack node = this; node.index >= 0;
node = node.parent) {
if (node.prefix.equals(prefix)) {
return node.uri;
}
}
// Not found.
return null;
}
/**
* Gets the prefix at the given index in the stack.
*/
String prefixAt(int index) {
return nodeAt(index).prefix;
}
/**
* Gets the URI at the given index in the stack.
*/
String uriAt(int index) {
return nodeAt(index).uri;
}
private NamespaceStack nodeAt(int index) {
if (index > this.index) {
throw new IndexOutOfBoundsException("Index > size.");
}
if (index < 0) {
throw new IndexOutOfBoundsException("Index < 0.");
}
NamespaceStack node = this;
while (index != node.index) {
node = node.parent;
}
return node;
}
/**
* Gets the size of the stack at the given element depth.
*/
int countAt(int depth) {
if (depth > this.depth) {
throw new IndexOutOfBoundsException("Depth > maximum.");
}
if (depth < 0) {
throw new IndexOutOfBoundsException("Depth < 0.");
}
NamespaceStack node = this;
while (depth < node.depth) {
node = node.parent;
}
return node.index + 1;
}
/** Builds a NamespaceStack. */
static class Builder {
NamespaceStack top = EMPTY;
/**
* Pushes a namespace onto the stack.
*
* @param depth of the element upon which the namespace was
* declared
*/
void push(String prefix, String uri, int depth) {
top = new NamespaceStack(top, prefix, uri, depth);
}
/**
* Pops all namespaces from the given element depth.
*/
void pop(int depth) {
// Remove all nodes at the specified depth.
while (top != null && top.depth == depth) {
top = top.parent;
}
}
/** Returns the current stack. */
NamespaceStack build() {
return top;
}
}
}
/**
* Base class for events. Implements event chaining and defines event API
* along with common implementations which can be overridden.
*/
static abstract class Event {
/** Element depth at the time of this event. */
final int depth;
/** The namespace stack at the time of this event. */
final NamespaceStack namespaceStack;
/** Next event in the queue. */
Event next = null;
Event(int depth, NamespaceStack namespaceStack) {
this.depth = depth;
this.namespaceStack = namespaceStack;
}
void setNext(Event next) {
this.next = next;
}
Event getNext() {
return next;
}
StringBuilder getText() {
return null;
}
String getNamespace() {
return null;
}
String getName() {
return null;
}
int getAttributeCount() {
return -1;
}
String getAttributeNamespace(int index) {
throw new IndexOutOfBoundsException(NOT_A_START_TAG);
}
String getAttributeName(int index) {
throw new IndexOutOfBoundsException(NOT_A_START_TAG);
}
String getAttributeValue(int index) {
throw new IndexOutOfBoundsException(NOT_A_START_TAG);
}
abstract int getType();
String getAttributeValue(String namespace, String name) {
throw new IndexOutOfBoundsException(NOT_A_START_TAG);
}
public int getDepth() {
return this.depth;
}
}
static class StartDocumentEvent extends Event {
public StartDocumentEvent() {
super(0, NamespaceStack.EMPTY);
}
@Override
int getType() {
return START_DOCUMENT;
}
}
static class StartTagEvent extends Event {
final String name;
final String namespace;
final Attributes attributes;
final boolean processNamespaces;
StartTagEvent(String namespace,
String name,
ExpatParser expatParser,
int depth,
NamespaceStack namespaceStack,
boolean processNamespaces) {
super(depth, namespaceStack);
this.namespace = namespace;
this.name = name;
this.attributes = expatParser.cloneAttributes();
this.processNamespaces = processNamespaces;
}
@Override
String getNamespace() {
return namespace;
}
@Override
String getName() {
return name;
}
@Override
int getAttributeCount() {
return attributes.getLength();
}
@Override
String getAttributeNamespace(int index) {
return attributes.getURI(index);
}
@Override
String getAttributeName(int index) {
return processNamespaces ? attributes.getLocalName(index)
: attributes.getQName(index);
}
@Override
String getAttributeValue(int index) {
return attributes.getValue(index);
}
@Override
String getAttributeValue(String namespace, String name) {
if (namespace == null) {
namespace = "";
}
return attributes.getValue(namespace, name);
}
@Override
int getType() {
return START_TAG;
}
}
static class EndTagEvent extends Event {
final String namespace;
final String localName;
EndTagEvent(String namespace, String localName, int depth,
NamespaceStack namespaceStack) {
super(depth, namespaceStack);
this.namespace = namespace;
this.localName = localName;
}
@Override
String getName() {
return this.localName;
}
@Override
String getNamespace() {
return this.namespace;
}
@Override
int getType() {
return END_TAG;
}
}
static class TextEvent extends Event {
final StringBuilder builder;
public TextEvent(int initialCapacity, int depth,
NamespaceStack namespaceStack) {
super(depth, namespaceStack);
this.builder = new StringBuilder(initialCapacity);
}
@Override
int getType() {
return TEXT;
}
@Override
StringBuilder getText() {
return this.builder;
}
void append(char[] text, int start, int length) {
builder.append(text, start, length);
}
}
static class EndDocumentEvent extends Event {
EndDocumentEvent() {
super(0, NamespaceStack.EMPTY);
}
@Override
Event getNext() {
throw new IllegalStateException("End of document.");
}
@Override
void setNext(Event next) {
throw new IllegalStateException("End of document.");
}
@Override
int getType() {
return END_DOCUMENT;
}
}
/**
* Encapsulates the parsing context of the current document.
*/
abstract class Document {
final String encoding;
final ExpatParser parser;
final boolean processNamespaces;
TextEvent textEvent = null;
boolean finished = false;
Document(String encoding, boolean processNamespaces) {
this.encoding = encoding;
this.processNamespaces = processNamespaces;
ExpatReader xmlReader = new ExpatReader();
xmlReader.setContentHandler(new SaxHandler());
this.parser = new ExpatParser(
encoding, xmlReader, processNamespaces, null, null);
}
/** Namespace stack builder. */
NamespaceStack.Builder namespaceStackBuilder
= new NamespaceStack.Builder();
Event currentEvent = new StartDocumentEvent();
Event last = currentEvent;
/**
* Sends some more XML to the parser.
*/
void pump() throws IOException, XmlPullParserException {
if (this.finished) {
return;
}
int length = buffer();
// End of document.
if (length == -1) {
this.finished = true;
if (!relaxed) {
try {
parser.finish();
} catch (SAXException e) {
throw new XmlPullParserException(
"Premature end of document.", ExpatPullParser.this, e);
}
}
add(new EndDocumentEvent());
return;
}
if (length == 0) {
return;
}
flush(parser, length);
}
/**
* Reads data into the buffer.
*
* @return the length of data buffered or {@code -1} if we've reached
* the end of the data.
*/
abstract int buffer() throws IOException;
/**
* Sends buffered data to the parser.
*
* @param parser the parser to flush to
* @param length of data buffered
*/
abstract void flush(ExpatParser parser, int length)
throws XmlPullParserException;
/**
* Adds an event.
*/
void add(Event event) {
// Flush pre-exising text event if necessary.
if (textEvent != null) {
last.setNext(textEvent);
last = textEvent;
textEvent = null;
}
last.setNext(event);
last = event;
}
/**
* Moves to the next event in the queue.
*
* @return type of next event
*/
int dequeue() throws XmlPullParserException, IOException {
Event next;
while ((next = currentEvent.getNext()) == null) {
pump();
}
currentEvent.next = null;
currentEvent = next;
return currentEvent.getType();
}
String getEncoding() {
return this.encoding;
}
int getDepth() {
return currentEvent.getDepth();
}
/**
* Returns true if we're on a start element and the next event is
* its corresponding end element.
*
* @throws XmlPullParserException if we aren't on a start element
*/
boolean isCurrentElementEmpty() throws XmlPullParserException {
if (currentEvent.getType() != START_TAG) {
throw new XmlPullParserException(NOT_A_START_TAG);
}
Event next;
try {
while ((next = currentEvent.getNext()) == null) {
pump();
}
} catch (IOException ex) {
throw new XmlPullParserException(ex.toString());
}
return next.getType() == END_TAG;
}
private class SaxHandler implements ContentHandler {
int depth = 0;
public void startPrefixMapping(String prefix, String uri)
throws SAXException {
// Depth + 1--we aren't actually in the element yet.
namespaceStackBuilder.push(prefix, uri, depth + 1);
}
public void startElement(String uri, String localName, String qName,
Attributes attributes) {
String name = processNamespaces ? localName : qName;
add(new StartTagEvent(uri, name, parser, ++this.depth,
namespaceStackBuilder.build(), processNamespaces));
}
public void endElement(String uri, String localName, String qName) {
String name = processNamespaces ? localName : qName;
int depth = this.depth--;
add(new EndTagEvent(uri, name, depth,
namespaceStackBuilder.build()));
namespaceStackBuilder.pop(depth);
}
public void characters(char[] ch, int start, int length) {
// Ignore empty strings.
if (length == 0) {
return;
}
// Start a new text event if necessary.
if (textEvent == null) {
textEvent = new TextEvent(length, this.depth,
namespaceStackBuilder.build());
}
// Append to an existing text event.
textEvent.append(ch, start, length);
}
public void setDocumentLocator(Locator locator) {}
public void startDocument() throws SAXException {}
public void endDocument() throws SAXException {}
public void endPrefixMapping(String prefix) throws SAXException {}
public void ignorableWhitespace(char[] ch, int start, int length)
throws SAXException {}
public void processingInstruction(String target, String data)
throws SAXException {}
public void skippedEntity(String name) throws SAXException {}
}
}
class CharDocument extends Document {
final char[] buffer = new char[BUFFER_SIZE / 2];
final Reader in;
CharDocument(Reader in, boolean processNamespaces) {
super("UTF-16", processNamespaces);
this.in = in;
}
@Override
int buffer() throws IOException {
return in.read(buffer);
}
@Override
void flush(ExpatParser parser, int length)
throws XmlPullParserException {
try {
parser.append(buffer, 0, length);
} catch (SAXException e) {
throw new XmlPullParserException(
"Error parsing document.", ExpatPullParser.this, e);
}
}
}
class ByteDocument extends Document {
final byte[] buffer = new byte[BUFFER_SIZE];
final InputStream in;
ByteDocument(InputStream in, String encoding,
boolean processNamespaces) {
super(encoding, processNamespaces);
this.in = in;
}
@Override
int buffer() throws IOException {
return in.read(buffer);
}
@Override
void flush(ExpatParser parser, int length)
throws XmlPullParserException {
try {
parser.append(buffer, 0, length);
} catch (SAXException e) {
throw new XmlPullParserException(
"Error parsing document.", ExpatPullParser.this, e);
}
}
}
}