diff --git a/src/main/java/android/content/res/XmlResourceParser.java b/src/main/java/android/content/res/XmlResourceParser.java new file mode 100644 index 00000000..55ac439a --- /dev/null +++ b/src/main/java/android/content/res/XmlResourceParser.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2006 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 android.content.res; + +import android.util.AttributeSet; +import org.xmlpull.v1.XmlPullParser; + +public interface XmlResourceParser extends XmlPullParser, AttributeSet, AutoCloseable { + String getAttributeNamespace (int index); + public void close(); +} + diff --git a/src/main/java/android/util/AttributeSet.java b/src/main/java/android/util/AttributeSet.java new file mode 100644 index 00000000..7c73df73 --- /dev/null +++ b/src/main/java/android/util/AttributeSet.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2006 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 android.util; + +public interface AttributeSet { + public int getAttributeCount(); + default String getAttributeNamespace (int index) { + return null; + } + public String getAttributeName(int index); + public String getAttributeValue(int index); + public String getAttributeValue(String namespace, String name); + public String getPositionDescription(); + public int getAttributeNameResource(int index); + public int getAttributeListValue(String namespace, String attribute, String[] options, int defaultValue); + public boolean getAttributeBooleanValue(String namespace, String attribute, boolean defaultValue); + public int getAttributeResourceValue(String namespace, String attribute, int defaultValue); + public int getAttributeIntValue(String namespace, String attribute, int defaultValue); + public int getAttributeUnsignedIntValue(String namespace, String attribute, int defaultValue); + public float getAttributeFloatValue(String namespace, String attribute, float defaultValue); + public int getAttributeListValue(int index, String[] options, int defaultValue); + public boolean getAttributeBooleanValue(int index, boolean defaultValue); + public int getAttributeResourceValue(int index, int defaultValue); + public int getAttributeIntValue(int index, int defaultValue); + public int getAttributeUnsignedIntValue(int index, int defaultValue); + public float getAttributeFloatValue(int index, float defaultValue); + public String getIdAttribute(); + public String getClassAttribute(); + public int getIdAttributeResourceValue(int defaultValue); + public int getStyleAttribute(); +} diff --git a/src/main/java/com/android/org/kxml2/io/KXmlParser.java b/src/main/java/com/android/org/kxml2/io/KXmlParser.java new file mode 100644 index 00000000..1e99341e --- /dev/null +++ b/src/main/java/com/android/org/kxml2/io/KXmlParser.java @@ -0,0 +1,2138 @@ +/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. */ + +// Contributors: Paul Hackenberger (unterminated entity handling in relaxed mode) + +package com.android.org.kxml2.io; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.HashMap; +import java.util.Map; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + + +public class KXmlParser implements XmlPullParser, Closeable { + + private static final String PROPERTY_XMLDECL_VERSION + = "http://xmlpull.org/v1/doc/properties.html#xmldecl-version"; + private static final String PROPERTY_XMLDECL_STANDALONE + = "http://xmlpull.org/v1/doc/properties.html#xmldecl-standalone"; + private static final String PROPERTY_LOCATION = "http://xmlpull.org/v1/doc/properties.html#location"; + private static final String FEATURE_RELAXED = "http://xmlpull.org/v1/doc/features.html#relaxed"; + + private static final Map DEFAULT_ENTITIES = new HashMap(); + static { + DEFAULT_ENTITIES.put("lt", "<"); + DEFAULT_ENTITIES.put("gt", ">"); + DEFAULT_ENTITIES.put("amp", "&"); + DEFAULT_ENTITIES.put("apos", "'"); + DEFAULT_ENTITIES.put("quot", "\""); + } + + private static final int ELEMENTDECL = 11; + private static final int ENTITYDECL = 12; + private static final int ATTLISTDECL = 13; + private static final int NOTATIONDECL = 14; + private static final int PARAMETER_ENTITY_REF = 15; + private static final char[] START_COMMENT = { '<', '!', '-', '-' }; + private static final char[] END_COMMENT = { '-', '-', '>' }; + private static final char[] COMMENT_DOUBLE_DASH = { '-', '-' }; + private static final char[] START_CDATA = { '<', '!', '[', 'C', 'D', 'A', 'T', 'A', '[' }; + private static final char[] END_CDATA = { ']', ']', '>' }; + private static final char[] START_PROCESSING_INSTRUCTION = { '<', '?' }; + private static final char[] END_PROCESSING_INSTRUCTION = { '?', '>' }; + private static final char[] START_DOCTYPE = { '<', '!', 'D', 'O', 'C', 'T', 'Y', 'P', 'E' }; + private static final char[] SYSTEM = { 'S', 'Y', 'S', 'T', 'E', 'M' }; + private static final char[] PUBLIC = { 'P', 'U', 'B', 'L', 'I', 'C' }; + private static final char[] START_ELEMENT = { '<', '!', 'E', 'L', 'E', 'M', 'E', 'N', 'T' }; + private static final char[] START_ATTLIST = { '<', '!', 'A', 'T', 'T', 'L', 'I', 'S', 'T' }; + private static final char[] START_ENTITY = { '<', '!', 'E', 'N', 'T', 'I', 'T', 'Y' }; + private static final char[] START_NOTATION = { '<', '!', 'N', 'O', 'T', 'A', 'T', 'I', 'O', 'N' }; + private static final char[] EMPTY = new char[] { 'E', 'M', 'P', 'T', 'Y' }; + private static final char[] ANY = new char[]{ 'A', 'N', 'Y' }; + private static final char[] NDATA = new char[]{ 'N', 'D', 'A', 'T', 'A' }; + private static final char[] NOTATION = new char[]{ 'N', 'O', 'T', 'A', 'T', 'I', 'O', 'N' }; + private static final char[] REQUIRED = new char[] { 'R', 'E', 'Q', 'U', 'I', 'R', 'E', 'D' }; + private static final char[] IMPLIED = new char[] { 'I', 'M', 'P', 'L', 'I', 'E', 'D' }; + private static final char[] FIXED = new char[] { 'F', 'I', 'X', 'E', 'D' }; + + static final private String UNEXPECTED_EOF = "Unexpected EOF"; + static final private String ILLEGAL_TYPE = "Wrong event type"; + static final private int XML_DECLARATION = 998; + + // general + private String location; + + private String version; + private Boolean standalone; + private String rootElementName; + private String systemId; + private String publicId; + + /** + * True if the {@code } contents are handled. The DTD defines + * entity values and default attribute values. These values are parsed at + * inclusion time and may contain both tags and entity references. + * + *

If this is false, the user must {@link #defineEntityReplacementText + * define entity values manually}. Such entity values are literal strings + * and will not be parsed. There is no API to define default attributes + * manually. + */ + private boolean processDocDecl; + private boolean processNsp; + private boolean relaxed; + private boolean keepNamespaceAttributes; + + /** + * If non-null, the contents of the read buffer must be copied into this + * string builder before the read buffer is overwritten. This is used to + * capture the raw DTD text while parsing the DTD. + */ + private StringBuilder bufferCapture; + + /** + * Entities defined in or for this document. This map is created lazily. + */ + private Map documentEntities; + + /** + * Default attributes in this document. The outer map's key is the element + * name; the inner map's key is the attribute name. Both keys should be + * without namespace adjustments. This map is created lazily. + */ + private Map> defaultAttributes; + + + private int depth; + private String[] elementStack = new String[16]; + private String[] nspStack = new String[8]; + private int[] nspCounts = new int[4]; + + // source + + private Reader reader; + private String encoding; + private ContentSource nextContentSource; + private char[] buffer = new char[8192]; + private int position = 0; + private int limit = 0; + + /* + * Track the number of newlines and columns preceding the current buffer. To + * compute the line and column of a position in the buffer, compute the line + * and column in the buffer and add the preceding values. + */ + private int bufferStartLine; + private int bufferStartColumn; + + // the current token + + private int type; + private boolean isWhitespace; + private String namespace; + private String prefix; + private String name; + private String text; + + private boolean degenerated; + private int attributeCount; + + // true iff. we've encountered the START_TAG of an XML element at depth == 0; + private boolean parsedTopLevelStartTag; + + /* + * The current element's attributes arranged in groups of 4: + * i + 0 = attribute namespace URI + * i + 1 = attribute namespace prefix + * i + 2 = attribute qualified name (may contain ":", as in "html:h1") + * i + 3 = attribute value + */ + private String[] attributes = new String[16]; + + private String error; + + private boolean unresolved; + + public final LibCoreStringPool stringPool = new LibCoreStringPool(); + + /** + * Retains namespace attributes like {@code xmlns="http://foo"} or {@code xmlns:foo="http:foo"} + * in pulled elements. Most applications will only be interested in the effective namespaces of + * their elements, so these attributes aren't useful. But for structure preserving wrappers like + * DOM, it is necessary to keep the namespace data around. + */ + public void keepNamespaceAttributes() { + this.keepNamespaceAttributes = true; + } + + private boolean adjustNsp() throws XmlPullParserException { + boolean any = false; + + for (int i = 0; i < attributeCount << 2; i += 4) { + String attrName = attributes[i + 2]; + int cut = attrName.indexOf(':'); + String prefix; + + if (cut != -1) { + prefix = attrName.substring(0, cut); + attrName = attrName.substring(cut + 1); + } else if (attrName.equals("xmlns")) { + prefix = attrName; + attrName = null; + } else { + continue; + } + + if (!prefix.equals("xmlns")) { + any = true; + } else { + int j = (nspCounts[depth]++) << 1; + + nspStack = ensureCapacity(nspStack, j + 2); + nspStack[j] = attrName; + nspStack[j + 1] = attributes[i + 3]; + + if (attrName != null && attributes[i + 3].isEmpty()) { + checkRelaxed("illegal empty namespace"); + } + + if (keepNamespaceAttributes) { + // explicitly set the namespace for unprefixed attributes + // such as xmlns="http://foo" + attributes[i] = "http://www.w3.org/2000/xmlns/"; + any = true; + } else { + System.arraycopy( + attributes, + i + 4, + attributes, + i, + ((--attributeCount) << 2) - i); + + i -= 4; + } + } + } + + if (any) { + for (int i = (attributeCount << 2) - 4; i >= 0; i -= 4) { + + String attrName = attributes[i + 2]; + int cut = attrName.indexOf(':'); + + if (cut == 0 && !relaxed) { + throw new RuntimeException( + "illegal attribute name: " + attrName + " at " + this); + } else if (cut != -1) { + String attrPrefix = attrName.substring(0, cut); + + attrName = attrName.substring(cut + 1); + + String attrNs = getNamespace(attrPrefix); + + if (attrNs == null && !relaxed) { + throw new RuntimeException( + "Undefined Prefix: " + attrPrefix + " in " + this); + } + + attributes[i] = attrNs; + attributes[i + 1] = attrPrefix; + attributes[i + 2] = attrName; + } + } + } + + int cut = name.indexOf(':'); + + if (cut == 0) { + checkRelaxed("illegal tag name: " + name); + } + + if (cut != -1) { + prefix = name.substring(0, cut); + name = name.substring(cut + 1); + } + + this.namespace = getNamespace(prefix); + + if (this.namespace == null) { + if (prefix != null) { + checkRelaxed("undefined prefix: " + prefix); + } + this.namespace = NO_NAMESPACE; + } + + return any; + } + + private String[] ensureCapacity(String[] arr, int required) { + if (arr.length >= required) { + return arr; + } + String[] bigger = new String[required + 16]; + System.arraycopy(arr, 0, bigger, 0, arr.length); + return bigger; + } + + private void checkRelaxed(String errorMessage) throws XmlPullParserException { + if (!relaxed) { + throw new XmlPullParserException(errorMessage, this, null); + } + if (error == null) { + error = "Error: " + errorMessage; + } + } + + public int next() throws XmlPullParserException, IOException { + return next(false); + } + + public int nextToken() throws XmlPullParserException, IOException { + return next(true); + } + + private int next(boolean justOneToken) throws IOException, XmlPullParserException { + if (reader == null) { + throw new XmlPullParserException("setInput() must be called first.", this, null); + } + + if (type == END_TAG) { + depth--; + } + + // degenerated needs to be handled before error because of possible + // processor expectations(!) + + if (degenerated) { + degenerated = false; + type = END_TAG; + return type; + } + + if (error != null) { + if (justOneToken) { + text = error; + type = COMMENT; + error = null; + return type; + } else { + error = null; + } + } + + type = peekType(false); + + if (type == XML_DECLARATION) { + readXmlDeclaration(); + type = peekType(false); + } + + text = null; + isWhitespace = true; + prefix = null; + name = null; + namespace = null; + attributeCount = -1; + boolean throwOnResolveFailure = !justOneToken; + + while (true) { + switch (type) { + + /* + * Return immediately after encountering a start tag, end tag, or + * the end of the document. + */ + case START_TAG: + parseStartTag(false, throwOnResolveFailure); + return type; + case END_TAG: + readEndTag(); + return type; + case END_DOCUMENT: + return type; + + /* + * Return after any text token when we're looking for a single + * token. Otherwise concatenate all text between tags. + */ + case ENTITY_REF: + if (justOneToken) { + StringBuilder entityTextBuilder = new StringBuilder(); + readEntity(entityTextBuilder, true, throwOnResolveFailure, ValueContext.TEXT); + text = entityTextBuilder.toString(); + break; + } + // fall-through + case TEXT: + text = readValue('<', !justOneToken, throwOnResolveFailure, ValueContext.TEXT); + if (depth == 0 && isWhitespace) { + type = IGNORABLE_WHITESPACE; + } + break; + case CDSECT: + read(START_CDATA); + text = readUntil(END_CDATA, true); + break; + + /* + * Comments, processing instructions and declarations are returned + * when we're looking for a single token. Otherwise they're skipped. + */ + case COMMENT: + String commentText = readComment(justOneToken); + if (justOneToken) { + text = commentText; + } + break; + case PROCESSING_INSTRUCTION: + read(START_PROCESSING_INSTRUCTION); + String processingInstruction = readUntil(END_PROCESSING_INSTRUCTION, justOneToken); + if (justOneToken) { + text = processingInstruction; + } + break; + case DOCDECL: + readDoctype(justOneToken); + if (parsedTopLevelStartTag) { + throw new XmlPullParserException("Unexpected token", this, null); + } + break; + + default: + throw new XmlPullParserException("Unexpected token", this, null); + } + + if (depth == 0 && (type == ENTITY_REF || type == TEXT || type == CDSECT)) { + throw new XmlPullParserException("Unexpected token", this, null); + } + + if (justOneToken) { + return type; + } + + if (type == IGNORABLE_WHITESPACE) { + text = null; + } + + /* + * We've read all that we can of a non-empty text block. Always + * report this as text, even if it was a CDATA block or entity + * reference. + */ + int peek = peekType(false); + if (text != null && !text.isEmpty() && peek < TEXT) { + type = TEXT; + return type; + } + + type = peek; + } + } + + /** + * Reads text until the specified delimiter is encountered. Consumes the + * text and the delimiter. + * + * @param returnText true to return the read text excluding the delimiter; + * false to return null. + */ + private String readUntil(char[] delimiter, boolean returnText) + throws IOException, XmlPullParserException { + int start = position; + StringBuilder result = null; + + if (returnText && text != null) { + result = new StringBuilder(); + result.append(text); + } + + search: + while (true) { + if (position + delimiter.length > limit) { + if (start < position && returnText) { + if (result == null) { + result = new StringBuilder(); + } + result.append(buffer, start, position - start); + } + if (!fillBuffer(delimiter.length)) { + checkRelaxed(UNEXPECTED_EOF); + type = COMMENT; + return null; + } + start = position; + } + + // TODO: replace with Arrays.equals(buffer, position, delimiter, 0, delimiter.length) + // when the VM has better method inlining + for (int i = 0; i < delimiter.length; i++) { + if (buffer[position + i] != delimiter[i]) { + position++; + continue search; + } + } + + break; + } + + int end = position; + position += delimiter.length; + + if (!returnText) { + return null; + } else if (result == null) { + return stringPool.get(buffer, start, end - start); + } else { + result.append(buffer, start, end - start); + return result.toString(); + } + } + + /** + * Returns true if an XML declaration was read. + */ + private void readXmlDeclaration() throws IOException, XmlPullParserException { + if (bufferStartLine != 0 || bufferStartColumn != 0 || position != 0) { + checkRelaxed("processing instructions must not start with xml"); + } + + read(START_PROCESSING_INSTRUCTION); + parseStartTag(true, true); + + if (attributeCount < 1 || !"version".equals(attributes[2])) { + checkRelaxed("version expected"); + } + + version = attributes[3]; + + int pos = 1; + + if (pos < attributeCount && "encoding".equals(attributes[2 + 4])) { + encoding = attributes[3 + 4]; + pos++; + } + + if (pos < attributeCount && "standalone".equals(attributes[4 * pos + 2])) { + String st = attributes[3 + 4 * pos]; + if ("yes".equals(st)) { + standalone = Boolean.TRUE; + } else if ("no".equals(st)) { + standalone = Boolean.FALSE; + } else { + checkRelaxed("illegal standalone value: " + st); + } + pos++; + } + + if (pos != attributeCount) { + checkRelaxed("unexpected attributes in XML declaration"); + } + + isWhitespace = true; + text = null; + } + + private String readComment(boolean returnText) throws IOException, XmlPullParserException { + read(START_COMMENT); + + if (relaxed) { + return readUntil(END_COMMENT, returnText); + } + + String commentText = readUntil(COMMENT_DOUBLE_DASH, returnText); + if (peekCharacter() != '>') { + throw new XmlPullParserException("Comments may not contain --", this, null); + } + position++; + return commentText; + } + + /** + * Read the document's DTD. Although this parser is non-validating, the DTD + * must be parsed to capture entity values and default attribute values. + */ + private void readDoctype(boolean saveDtdText) throws IOException, XmlPullParserException { + read(START_DOCTYPE); + + int startPosition = -1; + if (saveDtdText) { + bufferCapture = new StringBuilder(); + startPosition = position; + } + try { + skip(); + rootElementName = readName(); + readExternalId(true, true); + skip(); + if (peekCharacter() == '[') { + readInternalSubset(); + } + skip(); + } finally { + if (saveDtdText) { + bufferCapture.append(buffer, 0, position); + bufferCapture.delete(0, startPosition); + text = bufferCapture.toString(); + bufferCapture = null; + } + } + + read('>'); + skip(); + } + + /** + * Reads an external ID of one of these two forms: + * SYSTEM "quoted system name" + * PUBLIC "quoted public id" "quoted system name" + * + * If the system name is not required, this also supports lone public IDs of + * this form: + * PUBLIC "quoted public id" + * + * Returns true if any ID was read. + */ + private boolean readExternalId(boolean requireSystemName, boolean assignFields) + throws IOException, XmlPullParserException { + skip(); + int c = peekCharacter(); + + if (c == 'S') { + read(SYSTEM); + } else if (c == 'P') { + read(PUBLIC); + skip(); + if (assignFields) { + publicId = readQuotedId(true); + } else { + readQuotedId(false); + } + } else { + return false; + } + + skip(); + + if (!requireSystemName) { + int delimiter = peekCharacter(); + if (delimiter != '"' && delimiter != '\'') { + return true; // no system name! + } + } + + if (assignFields) { + systemId = readQuotedId(true); + } else { + readQuotedId(false); + } + return true; + } + + private static final char[] SINGLE_QUOTE = new char[] { '\'' }; + private static final char[] DOUBLE_QUOTE = new char[] { '"' }; + + /** + * Reads a quoted string, performing no entity escaping of the contents. + */ + private String readQuotedId(boolean returnText) throws IOException, XmlPullParserException { + int quote = peekCharacter(); + char[] delimiter; + if (quote == '"') { + delimiter = DOUBLE_QUOTE; + } else if (quote == '\'') { + delimiter = SINGLE_QUOTE; + } else { + throw new XmlPullParserException("Expected a quoted string", this, null); + } + position++; + return readUntil(delimiter, returnText); + } + + private void readInternalSubset() throws IOException, XmlPullParserException { + read('['); + + while (true) { + skip(); + if (peekCharacter() == ']') { + position++; + return; + } + + int declarationType = peekType(true); + switch (declarationType) { + case ELEMENTDECL: + readElementDeclaration(); + break; + + case ATTLISTDECL: + readAttributeListDeclaration(); + break; + + case ENTITYDECL: + readEntityDeclaration(); + break; + + case NOTATIONDECL: + readNotationDeclaration(); + break; + + case PROCESSING_INSTRUCTION: + read(START_PROCESSING_INSTRUCTION); + readUntil(END_PROCESSING_INSTRUCTION, false); + break; + + case COMMENT: + readComment(false); + break; + + case PARAMETER_ENTITY_REF: + throw new XmlPullParserException( + "Parameter entity references are not supported", this, null); + + default: + throw new XmlPullParserException("Unexpected token", this, null); + } + } + } + + /** + * Read an element declaration. This contains a name and a content spec. + * + * + * + */ + private void readElementDeclaration() throws IOException, XmlPullParserException { + read(START_ELEMENT); + skip(); + readName(); + readContentSpec(); + skip(); + read('>'); + } + + /** + * Read an element content spec. This is a regular expression-like pattern + * of names or other content specs. The following operators are supported: + * sequence: (a,b,c) + * choice: (a|b|c) + * optional: a? + * one or more: a+ + * any number: a* + * + * The special name '#PCDATA' is permitted but only if it is the first + * element of the first group: + * (#PCDATA|a|b) + * + * The top-level element must be either a choice, a sequence, or one of the + * special names EMPTY and ANY. + */ + private void readContentSpec() throws IOException, XmlPullParserException { + // this implementation is very lenient; it scans for balanced parens only + skip(); + int c = peekCharacter(); + if (c == '(') { + int depth = 0; + do { + if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + } else if (c == -1) { + throw new XmlPullParserException( + "Unterminated element content spec", this, null); + } + position++; + c = peekCharacter(); + } while (depth > 0); + + if (c == '*' || c == '?' || c == '+') { + position++; + } + } else if (c == EMPTY[0]) { + read(EMPTY); + } else if (c == ANY[0]) { + read(ANY); + } else { + throw new XmlPullParserException("Expected element content spec", this, null); + } + } + + /** + * Reads an attribute list declaration such as the following: + * + * + * Each attribute has a name, type and default. + * + * Types are one of the built-in types (CDATA, ID, IDREF, IDREFS, ENTITY, + * ENTITIES, NMTOKEN, or NMTOKENS), an enumerated type "(list|of|options)" + * or NOTATION followed by an enumerated type. + * + * The default is either #REQUIRED, #IMPLIED, #FIXED, a quoted value, or + * #FIXED with a quoted value. + */ + private void readAttributeListDeclaration() throws IOException, XmlPullParserException { + read(START_ATTLIST); + skip(); + String elementName = readName(); + + while (true) { + skip(); + int c = peekCharacter(); + if (c == '>') { + position++; + return; + } + + // attribute name + String attributeName = readName(); + + // attribute type + skip(); + if (position + 1 >= limit && !fillBuffer(2)) { + throw new XmlPullParserException("Malformed attribute list", this, null); + } + if (buffer[position] == NOTATION[0] && buffer[position + 1] == NOTATION[1]) { + read(NOTATION); + skip(); + } + c = peekCharacter(); + if (c == '(') { + position++; + while (true) { + skip(); + readName(); + skip(); + c = peekCharacter(); + if (c == ')') { + position++; + break; + } else if (c == '|') { + position++; + } else { + throw new XmlPullParserException("Malformed attribute type", this, null); + } + } + } else { + readName(); + } + + // default value + skip(); + c = peekCharacter(); + if (c == '#') { + position++; + c = peekCharacter(); + if (c == 'R') { + read(REQUIRED); + } else if (c == 'I') { + read(IMPLIED); + } else if (c == 'F') { + read(FIXED); + } else { + throw new XmlPullParserException("Malformed attribute type", this, null); + } + skip(); + c = peekCharacter(); + } + if (c == '"' || c == '\'') { + position++; + // TODO: does this do escaping correctly? + String value = readValue((char) c, true, true, ValueContext.ATTRIBUTE); + if (peekCharacter() == c) { + position++; + } + defineAttributeDefault(elementName, attributeName, value); + } + } + } + + private void defineAttributeDefault(String elementName, String attributeName, String value) { + if (defaultAttributes == null) { + defaultAttributes = new HashMap>(); + } + Map elementAttributes = defaultAttributes.get(elementName); + if (elementAttributes == null) { + elementAttributes = new HashMap(); + defaultAttributes.put(elementName, elementAttributes); + } + elementAttributes.put(attributeName, value); + } + + /** + * Read an entity declaration. The value of internal entities are inline: + * + * + * The values of external entities must be retrieved by URL or path: + * + * + * + * + * Entities may be general or parameterized. Parameterized entities are + * marked by a percent sign. Such entities may only be used in the DTD: + * + */ + private void readEntityDeclaration() throws IOException, XmlPullParserException { + read(START_ENTITY); + boolean generalEntity = true; + + skip(); + if (peekCharacter() == '%') { + generalEntity = false; + position++; + skip(); + } + + String name = readName(); + + skip(); + int quote = peekCharacter(); + String entityValue; + if (quote == '"' || quote == '\'') { + position++; + entityValue = readValue((char) quote, true, false, ValueContext.ENTITY_DECLARATION); + if (peekCharacter() == quote) { + position++; + } + } else if (readExternalId(true, false)) { + /* + * Map external entities to the empty string. This is dishonest, + * but it's consistent with Android's Expat pull parser. + */ + entityValue = ""; + skip(); + if (peekCharacter() == NDATA[0]) { + read(NDATA); + skip(); + readName(); + } + } else { + throw new XmlPullParserException("Expected entity value or external ID", this, null); + } + + if (generalEntity && processDocDecl) { + if (documentEntities == null) { + documentEntities = new HashMap(); + } + documentEntities.put(name, entityValue.toCharArray()); + } + + skip(); + read('>'); + } + + private void readNotationDeclaration() throws IOException, XmlPullParserException { + read(START_NOTATION); + skip(); + readName(); + if (!readExternalId(false, false)) { + throw new XmlPullParserException( + "Expected external ID or public ID for notation", this, null); + } + skip(); + read('>'); + } + + private void readEndTag() throws IOException, XmlPullParserException { + read('<'); + read('/'); + name = readName(); // TODO: pass the expected name in as a hint? + skip(); + read('>'); + + int sp = (depth - 1) * 4; + + if (depth == 0) { + checkRelaxed("read end tag " + name + " with no tags open"); + type = COMMENT; + return; + } + + if (name.equals(elementStack[sp + 3])) { + namespace = elementStack[sp]; + prefix = elementStack[sp + 1]; + name = elementStack[sp + 2]; + } else if (!relaxed) { + throw new XmlPullParserException( + "expected: /" + elementStack[sp + 3] + " read: " + name, this, null); + } + } + + /** + * Returns the type of the next token. + */ + private int peekType(boolean inDeclaration) throws IOException, XmlPullParserException { + if (position >= limit && !fillBuffer(1)) { + return END_DOCUMENT; + } + + switch (buffer[position]) { + case '&': + return ENTITY_REF; // & + case '<': + if (position + 3 >= limit && !fillBuffer(4)) { + throw new XmlPullParserException("Dangling <", this, null); + } + + switch (buffer[position + 1]) { + case '/': + return END_TAG; // = limit && !fillBuffer(1)) { + checkRelaxed(UNEXPECTED_EOF); + return; + } + + int c = buffer[position]; + + if (xmldecl) { + if (c == '?') { + position++; + read('>'); + return; + } + } else { + if (c == '/') { + degenerated = true; + position++; + skip(); + read('>'); + break; + } else if (c == '>') { + position++; + break; + } + } + + String attrName = readName(); + + int i = (attributeCount++) * 4; + attributes = ensureCapacity(attributes, i + 4); + attributes[i] = ""; + attributes[i + 1] = null; + attributes[i + 2] = attrName; + + skip(); + if (position >= limit && !fillBuffer(1)) { + checkRelaxed(UNEXPECTED_EOF); + return; + } + + if (buffer[position] == '=') { + position++; + + skip(); + if (position >= limit && !fillBuffer(1)) { + checkRelaxed(UNEXPECTED_EOF); + return; + } + char delimiter = buffer[position]; + + if (delimiter == '\'' || delimiter == '"') { + position++; + } else if (relaxed) { + delimiter = ' '; + } else { + throw new XmlPullParserException("attr value delimiter missing!", this, null); + } + + attributes[i + 3] = readValue(delimiter, true, throwOnResolveFailure, + ValueContext.ATTRIBUTE); + + if (delimiter != ' ' && peekCharacter() == delimiter) { + position++; // end quote + } + } else if (relaxed) { + attributes[i + 3] = attrName; + } else { + checkRelaxed("Attr.value missing f. " + attrName); + attributes[i + 3] = attrName; + } + } + + int sp = depth++ * 4; + if (depth == 1) { + parsedTopLevelStartTag = true; + } + elementStack = ensureCapacity(elementStack, sp + 4); + elementStack[sp + 3] = name; + + if (depth >= nspCounts.length) { + int[] bigger = new int[depth + 4]; + System.arraycopy(nspCounts, 0, bigger, 0, nspCounts.length); + nspCounts = bigger; + } + + nspCounts[depth] = nspCounts[depth - 1]; + + if (processNsp) { + adjustNsp(); + } else { + namespace = ""; + } + + // For consistency with Expat, add default attributes after fixing namespaces. + if (defaultAttributes != null) { + Map elementDefaultAttributes = defaultAttributes.get(name); + if (elementDefaultAttributes != null) { + for (Map.Entry entry : elementDefaultAttributes.entrySet()) { + if (getAttributeValue(null, entry.getKey()) != null) { + continue; // an explicit value overrides the default + } + + int i = (attributeCount++) * 4; + attributes = ensureCapacity(attributes, i + 4); + attributes[i] = ""; + attributes[i + 1] = null; + attributes[i + 2] = entry.getKey(); + attributes[i + 3] = entry.getValue(); + } + } + } + + elementStack[sp] = namespace; + elementStack[sp + 1] = prefix; + elementStack[sp + 2] = name; + } + + /** + * Reads an entity reference from the buffer, resolves it, and writes the + * resolved entity to {@code out}. If the entity cannot be read or resolved, + * {@code out} will contain the partial entity reference. + */ + private void readEntity(StringBuilder out, boolean isEntityToken, boolean throwOnResolveFailure, + ValueContext valueContext) throws IOException, XmlPullParserException { + int start = out.length(); + + if (buffer[position++] != '&') { + throw new AssertionError(); + } + + out.append('&'); + + while (true) { + int c = peekCharacter(); + + if (c == ';') { + out.append(';'); + position++; + break; + + } else if (c >= 128 + || (c >= '0' && c <= '9') + || (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || c == '_' + || c == '-' + || c == '#') { + position++; + out.append((char) c); + + } else if (relaxed) { + // intentionally leave the partial reference in 'out' + return; + + } else { + throw new XmlPullParserException("unterminated entity ref", this, null); + } + } + + String code = out.substring(start + 1, out.length() - 1); + + if (isEntityToken) { + name = code; + } + + if (code.startsWith("#")) { + try { + int c = code.startsWith("#x") + ? Integer.parseInt(code.substring(2), 16) + : Integer.parseInt(code.substring(1)); + out.delete(start, out.length()); + out.appendCodePoint(c); + unresolved = false; + return; + } catch (NumberFormatException notANumber) { + throw new XmlPullParserException("Invalid character reference: &" + code); + } catch (IllegalArgumentException invalidCodePoint) { + throw new XmlPullParserException("Invalid character reference: &" + code); + } + } + + if (valueContext == ValueContext.ENTITY_DECLARATION) { + // keep the unresolved &code; in the text to resolve later + return; + } + + String defaultEntity = DEFAULT_ENTITIES.get(code); + if (defaultEntity != null) { + out.delete(start, out.length()); + unresolved = false; + out.append(defaultEntity); + return; + } + + char[] resolved; + if (documentEntities != null && (resolved = documentEntities.get(code)) != null) { + out.delete(start, out.length()); + unresolved = false; + if (processDocDecl) { + pushContentSource(resolved); // parse the entity as XML + } else { + out.append(resolved); // include the entity value as text + } + return; + } + + /* + * The parser skipped an external DTD, and now we've encountered an + * unknown entity that could have been declared there. Map it to the + * empty string. This is dishonest, but it's consistent with Android's + * old ExpatPullParser. + */ + if (systemId != null) { + out.delete(start, out.length()); + return; + } + + // keep the unresolved entity "&code;" in the text for relaxed clients + unresolved = true; + if (throwOnResolveFailure) { + checkRelaxed("unresolved: &" + code + ";"); + } + } + + /** + * Where a value is found impacts how that value is interpreted. For + * example, in attributes, "\n" must be replaced with a space character. In + * text, "]]>" is forbidden. In entity declarations, named references are + * not resolved. + */ + enum ValueContext { + ATTRIBUTE, + TEXT, + ENTITY_DECLARATION + } + + /** + * Returns the current text or attribute value. This also has the side + * effect of setting isWhitespace to false if a non-whitespace character is + * encountered. + * + * @param delimiter {@code <} for text, {@code "} and {@code '} for quoted + * attributes, or a space for unquoted attributes. + */ + private String readValue(char delimiter, boolean resolveEntities, boolean throwOnResolveFailure, + ValueContext valueContext) throws IOException, XmlPullParserException { + + /* + * This method returns all of the characters from the current position + * through to an appropriate delimiter. + * + * If we're lucky (which we usually are), we'll return a single slice of + * the buffer. This fast path avoids allocating a string builder. + * + * There are 6 unlucky characters we could encounter: + * - "&": entities must be resolved. + * - "%": parameter entities are unsupported in entity values. + * - "<": this isn't permitted in attributes unless relaxed. + * - "]": this requires a lookahead to defend against the forbidden + * CDATA section delimiter "]]>". + * - "\r": If a "\r" is followed by a "\n", we discard the "\r". If it + * isn't followed by "\n", we replace "\r" with either a "\n" + * in text nodes or a space in attribute values. + * - "\n": In attribute values, "\n" must be replaced with a space. + * + * We could also get unlucky by needing to refill the buffer midway + * through the text. + */ + + int start = position; + StringBuilder result = null; + + // if a text section was already started, prefix the start + if (valueContext == ValueContext.TEXT && text != null) { + result = new StringBuilder(); + result.append(text); + } + + while (true) { + + /* + * Make sure we have at least a single character to read from the + * buffer. This mutates the buffer, so save the partial result + * to the slow path string builder first. + */ + if (position >= limit) { + if (start < position) { + if (result == null) { + result = new StringBuilder(); + } + result.append(buffer, start, position - start); + } + if (!fillBuffer(1)) { + return result != null ? result.toString() : ""; + } + start = position; + } + + char c = buffer[position]; + + if (c == delimiter + || (delimiter == ' ' && (c <= ' ' || c == '>')) + || c == '&' && !resolveEntities) { + break; + } + + if (c != '\r' + && (c != '\n' || valueContext != ValueContext.ATTRIBUTE) + && c != '&' + && c != '<' + && (c != ']' || valueContext != ValueContext.TEXT) + && (c != '%' || valueContext != ValueContext.ENTITY_DECLARATION)) { + isWhitespace &= (c <= ' '); + position++; + continue; + } + + /* + * We've encountered an unlucky character! Convert from fast + * path to slow path if we haven't done so already. + */ + if (result == null) { + result = new StringBuilder(); + } + result.append(buffer, start, position - start); + + if (c == '\r') { + if ((position + 1 < limit || fillBuffer(2)) && buffer[position + 1] == '\n') { + position++; + } + c = (valueContext == ValueContext.ATTRIBUTE) ? ' ' : '\n'; + + } else if (c == '\n') { + c = ' '; + + } else if (c == '&') { + isWhitespace = false; // TODO: what if the entity resolves to whitespace? + readEntity(result, false, throwOnResolveFailure, valueContext); + start = position; + continue; + + } else if (c == '<') { + if (valueContext == ValueContext.ATTRIBUTE) { + checkRelaxed("Illegal: \"<\" inside attribute value"); + } + isWhitespace = false; + + } else if (c == ']') { + if ((position + 2 < limit || fillBuffer(3)) + && buffer[position + 1] == ']' && buffer[position + 2] == '>') { + checkRelaxed("Illegal: \"]]>\" outside CDATA section"); + } + isWhitespace = false; + + } else if (c == '%') { + throw new XmlPullParserException("This parser doesn't support parameter entities", + this, null); + + } else { + throw new AssertionError(); + } + + position++; + result.append(c); + start = position; + } + + if (result == null) { + return stringPool.get(buffer, start, position - start); + } else { + result.append(buffer, start, position - start); + return result.toString(); + } + } + + private void read(char expected) throws IOException, XmlPullParserException { + int c = peekCharacter(); + if (c != expected) { + checkRelaxed("expected: '" + expected + "' actual: '" + ((char) c) + "'"); + if (c == -1) { + return; // On EOF, don't move position beyond limit + } + } + position++; + } + + private void read(char[] chars) throws IOException, XmlPullParserException { + if (position + chars.length > limit && !fillBuffer(chars.length)) { + checkRelaxed("expected: '" + new String(chars) + "' but was EOF"); + return; + } + + // TODO: replace with Arrays.equals(buffer, position, delimiter, 0, delimiter.length) + // when the VM has better method inlining + for (int i = 0; i < chars.length; i++) { + if (buffer[position + i] != chars[i]) { + checkRelaxed("expected: \"" + new String(chars) + "\" but was \"" + + new String(buffer, position, chars.length) + "...\""); + } + } + + position += chars.length; + } + + private int peekCharacter() throws IOException, XmlPullParserException { + if (position < limit || fillBuffer(1)) { + return buffer[position]; + } + return -1; + } + + /** + * Returns true once {@code limit - position >= minimum}. If the data is + * exhausted before that many characters are available, this returns + * false. + */ + private boolean fillBuffer(int minimum) throws IOException, XmlPullParserException { + // If we've exhausted the current content source, remove it + while (nextContentSource != null) { + if (position < limit) { + throw new XmlPullParserException("Unbalanced entity!", this, null); + } + popContentSource(); + if (limit - position >= minimum) { + return true; + } + } + + // Before clobbering the old characters, update where buffer starts + for (int i = 0; i < position; i++) { + if (buffer[i] == '\n') { + bufferStartLine++; + bufferStartColumn = 0; + } else { + bufferStartColumn++; + } + } + + if (bufferCapture != null) { + bufferCapture.append(buffer, 0, position); + } + + if (limit != position) { + limit -= position; + System.arraycopy(buffer, position, buffer, 0, limit); + } else { + limit = 0; + } + + position = 0; + int total; + while ((total = reader.read(buffer, limit, buffer.length - limit)) != -1) { + limit += total; + if (limit >= minimum) { + return true; + } + } + return false; + } + + /** + * Returns an element or attribute name. This is always non-empty for + * non-relaxed parsers. + */ + private String readName() throws IOException, XmlPullParserException { + if (position >= limit && !fillBuffer(1)) { + checkRelaxed("name expected"); + return ""; + } + + int start = position; + StringBuilder result = null; + + // read the first character + char c = buffer[position]; + if ((c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || c == '_' + || c == ':' + || c >= '\u00c0' // TODO: check the XML spec + || relaxed) { + position++; + } else { + checkRelaxed("name expected"); + return ""; + } + + while (true) { + /* + * Make sure we have at least a single character to read from the + * buffer. This mutates the buffer, so save the partial result + * to the slow path string builder first. + */ + if (position >= limit) { + if (result == null) { + result = new StringBuilder(); + } + result.append(buffer, start, position - start); + if (!fillBuffer(1)) { + return result.toString(); + } + start = position; + } + + // read another character + c = buffer[position]; + if ((c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') + || c == '_' + || c == '-' + || c == ':' + || c == '.' + || c >= '\u00b7') { // TODO: check the XML spec + position++; + continue; + } + + // we encountered a non-name character. done! + if (result == null) { + return stringPool.get(buffer, start, position - start); + } else { + result.append(buffer, start, position - start); + return result.toString(); + } + } + } + + private void skip() throws IOException, XmlPullParserException { + while (position < limit || fillBuffer(1)) { + int c = buffer[position]; + if (c > ' ') { + break; + } + position++; + } + } + + // public part starts here... + + public void setInput(Reader reader) throws XmlPullParserException { + this.reader = reader; + + type = START_DOCUMENT; + parsedTopLevelStartTag = false; + name = null; + namespace = null; + degenerated = false; + attributeCount = -1; + encoding = null; + version = null; + standalone = null; + + if (reader == null) { + return; + } + + position = 0; + limit = 0; + bufferStartLine = 0; + bufferStartColumn = 0; + depth = 0; + documentEntities = null; + } + + public void setInput(InputStream is, String charset) throws XmlPullParserException { + position = 0; + limit = 0; + boolean detectCharset = (charset == null); + + if (is == null) { + throw new IllegalArgumentException("is == null"); + } + + try { + if (detectCharset) { + // read the four bytes looking for an indication of the encoding in use + int firstFourBytes = 0; + while (limit < 4) { + int i = is.read(); + if (i == -1) { + break; + } + firstFourBytes = (firstFourBytes << 8) | i; + buffer[limit++] = (char) i; + } + + if (limit == 4) { + switch (firstFourBytes) { + case 0x00000FEFF: // UTF-32BE BOM + charset = "UTF-32BE"; + limit = 0; + break; + + case 0x0FFFE0000: // UTF-32LE BOM + charset = "UTF-32LE"; + limit = 0; + break; + + case 0x0000003c: // '<' in UTF-32BE + charset = "UTF-32BE"; + buffer[0] = '<'; + limit = 1; + break; + + case 0x03c000000: // '<' in UTF-32LE + charset = "UTF-32LE"; + buffer[0] = '<'; + limit = 1; + break; + + case 0x0003c003f: // "') { + String s = new String(buffer, 0, limit); + int i0 = s.indexOf("encoding"); + if (i0 != -1) { + while (s.charAt(i0) != '"' && s.charAt(i0) != '\'') { + i0++; + } + char deli = s.charAt(i0++); + int i1 = s.indexOf(deli, i0); + charset = s.substring(i0, i1); + } + break; + } + } + break; + + default: + // handle a byte order mark followed by something other than + * is still at character 0. + */ + if (!detectCharset && peekCharacter() == 0xfeff) { + limit--; + System.arraycopy(buffer, 1, buffer, 0, limit); + } + } catch (Exception e) { + throw new XmlPullParserException("Invalid stream or encoding: " + e, this, e); + } + } + + public void close() throws IOException { + if (reader != null) { + reader.close(); + } + } + + public boolean getFeature(String feature) { + if (XmlPullParser.FEATURE_PROCESS_NAMESPACES.equals(feature)) { + return processNsp; + } else if (FEATURE_RELAXED.equals(feature)) { + return relaxed; + } else if (FEATURE_PROCESS_DOCDECL.equals(feature)) { + return processDocDecl; + } else { + return false; + } + } + + public String getInputEncoding() { + return encoding; + } + + public void defineEntityReplacementText(String entity, String value) + throws XmlPullParserException { + if (processDocDecl) { + throw new IllegalStateException( + "Entity replacement text may not be defined with DOCTYPE processing enabled."); + } + if (reader == null) { + throw new IllegalStateException( + "Entity replacement text must be defined after setInput()"); + } + if (documentEntities == null) { + documentEntities = new HashMap(); + } + documentEntities.put(entity, value.toCharArray()); + } + + public Object getProperty(String property) { + if (property.equals(PROPERTY_XMLDECL_VERSION)) { + return version; + } else if (property.equals(PROPERTY_XMLDECL_STANDALONE)) { + return standalone; + } else if (property.equals(PROPERTY_LOCATION)) { + return location != null ? location : reader.toString(); + } else { + return null; + } + } + + /** + * Returns the root element's name if it was declared in the DTD. This + * equals the first tag's name for valid documents. + */ + public String getRootElementName() { + return rootElementName; + } + + /** + * Returns the document's system ID if it was declared. This is typically a + * string like {@code http://www.w3.org/TR/html4/strict.dtd}. + */ + public String getSystemId() { + return systemId; + } + + /** + * Returns the document's public ID if it was declared. This is typically a + * string like {@code -//W3C//DTD HTML 4.01//EN}. + */ + public String getPublicId() { + return publicId; + } + + public int getNamespaceCount(int depth) { + if (depth > this.depth) { + throw new IndexOutOfBoundsException(); + } + return nspCounts[depth]; + } + + public String getNamespacePrefix(int pos) { + return nspStack[pos * 2]; + } + + public String getNamespaceUri(int pos) { + return nspStack[(pos * 2) + 1]; + } + + public String getNamespace(String prefix) { + if ("xml".equals(prefix)) { + return "http://www.w3.org/XML/1998/namespace"; + } + if ("xmlns".equals(prefix)) { + return "http://www.w3.org/2000/xmlns/"; + } + + for (int i = (getNamespaceCount(depth) << 1) - 2; i >= 0; i -= 2) { + if (prefix == null) { + if (nspStack[i] == null) { + return nspStack[i + 1]; + } + } else if (prefix.equals(nspStack[i])) { + return nspStack[i + 1]; + } + } + return null; + } + + public int getDepth() { + return depth; + } + + public String getPositionDescription() { + StringBuilder buf = new StringBuilder(type < TYPES.length ? TYPES[type] : "unknown"); + buf.append(' '); + + if (type == START_TAG || type == END_TAG) { + if (degenerated) { + buf.append("(empty) "); + } + buf.append('<'); + if (type == END_TAG) { + buf.append('/'); + } + + if (prefix != null) { + buf.append("{" + namespace + "}" + prefix + ":"); + } + buf.append(name); + + int cnt = attributeCount * 4; + for (int i = 0; i < cnt; i += 4) { + buf.append(' '); + if (attributes[i + 1] != null) { + buf.append("{" + attributes[i] + "}" + attributes[i + 1] + ":"); + } + buf.append(attributes[i + 2] + "='" + attributes[i + 3] + "'"); + } + + buf.append('>'); + } else if (type == IGNORABLE_WHITESPACE) { + ; + } else if (type != TEXT) { + buf.append(getText()); + } else if (isWhitespace) { + buf.append("(whitespace)"); + } else { + String text = getText(); + if (text.length() > 16) { + text = text.substring(0, 16) + "..."; + } + buf.append(text); + } + + buf.append("@" + getLineNumber() + ":" + getColumnNumber()); + if (location != null) { + buf.append(" in "); + buf.append(location); + } else if (reader != null) { + buf.append(" in "); + buf.append(reader.toString()); + } + return buf.toString(); + } + + public int getLineNumber() { + int result = bufferStartLine; + for (int i = 0; i < position; i++) { + if (buffer[i] == '\n') { + result++; + } + } + return result + 1; // the first line is '1' + } + + public int getColumnNumber() { + int result = bufferStartColumn; + for (int i = 0; i < position; i++) { + if (buffer[i] == '\n') { + result = 0; + } else { + result++; + } + } + return result + 1; // the first column is '1' + } + + public boolean isWhitespace() throws XmlPullParserException { + if (type != TEXT && type != IGNORABLE_WHITESPACE && type != CDSECT) { + throw new XmlPullParserException(ILLEGAL_TYPE, this, null); + } + return isWhitespace; + } + + public String getText() { + if (type < TEXT || (type == ENTITY_REF && unresolved)) { + return null; + } else if (text == null) { + return ""; + } else { + return text; + } + } + + public char[] getTextCharacters(int[] poslen) { + String text = getText(); + if (text == null) { + poslen[0] = -1; + poslen[1] = -1; + return null; + } + char[] result = text.toCharArray(); + poslen[0] = 0; + poslen[1] = result.length; + return result; + } + + public String getNamespace() { + return namespace; + } + + public String getName() { + return name; + } + + public String getPrefix() { + return prefix; + } + + public boolean isEmptyElementTag() throws XmlPullParserException { + if (type != START_TAG) { + throw new XmlPullParserException(ILLEGAL_TYPE, this, null); + } + return degenerated; + } + + public int getAttributeCount() { + return attributeCount; + } + + public String getAttributeType(int index) { + return "CDATA"; + } + + public boolean isAttributeDefault(int index) { + return false; + } + + public String getAttributeNamespace(int index) { + if (index >= attributeCount) { + throw new IndexOutOfBoundsException(); + } + return attributes[index * 4]; + } + + public String getAttributeName(int index) { + if (index >= attributeCount) { + throw new IndexOutOfBoundsException(); + } + return attributes[(index * 4) + 2]; + } + + public String getAttributePrefix(int index) { + if (index >= attributeCount) { + throw new IndexOutOfBoundsException(); + } + return attributes[(index * 4) + 1]; + } + + public String getAttributeValue(int index) { + if (index >= attributeCount) { + throw new IndexOutOfBoundsException(); + } + return attributes[(index * 4) + 3]; + } + + public String getAttributeValue(String namespace, String name) { + for (int i = (attributeCount * 4) - 4; i >= 0; i -= 4) { + if (attributes[i + 2].equals(name) + && (namespace == null || attributes[i].equals(namespace))) { + return attributes[i + 3]; + } + } + + return null; + } + + public int getEventType() throws XmlPullParserException { + return type; + } + + // utility methods to make XML parsing easier ... + + public int nextTag() throws XmlPullParserException, IOException { + next(); + if (type == TEXT && isWhitespace) { + next(); + } + + if (type != END_TAG && type != START_TAG) { + throw new XmlPullParserException("unexpected type", this, null); + } + + return type; + } + + public void require(int type, String namespace, String name) + throws XmlPullParserException, IOException { + if (type != this.type + || (namespace != null && !namespace.equals(getNamespace())) + || (name != null && !name.equals(getName()))) { + throw new XmlPullParserException( + "expected: " + TYPES[type] + " {" + namespace + "}" + name, this, null); + } + } + + public String nextText() throws XmlPullParserException, IOException { + if (type != START_TAG) { + throw new XmlPullParserException("precondition: START_TAG", this, null); + } + + next(); + + String result; + if (type == TEXT) { + result = getText(); + next(); + } else { + result = ""; + } + + if (type != END_TAG) { + throw new XmlPullParserException("END_TAG expected", this, null); + } + + return result; + } + + public void setFeature(String feature, boolean value) throws XmlPullParserException { + if (XmlPullParser.FEATURE_PROCESS_NAMESPACES.equals(feature)) { + processNsp = value; + } else if (XmlPullParser.FEATURE_PROCESS_DOCDECL.equals(feature)) { + processDocDecl = value; + } else if (FEATURE_RELAXED.equals(feature)) { + relaxed = value; + } else { + throw new XmlPullParserException("unsupported feature: " + feature, this, null); + } + } + + public void setProperty(String property, Object value) throws XmlPullParserException { + if (property.equals(PROPERTY_LOCATION)) { + location = String.valueOf(value); + } else { + throw new XmlPullParserException("unsupported property: " + property); + } + } + static class ContentSource { + private final ContentSource next; + private final char[] buffer; + private final int position; + private final int limit; + ContentSource(ContentSource next, char[] buffer, int position, int limit) { + this.next = next; + this.buffer = buffer; + this.position = position; + this.limit = limit; + } + } + private void pushContentSource(char[] newBuffer) { + nextContentSource = new ContentSource(nextContentSource, buffer, position, limit); + buffer = newBuffer; + position = 0; + limit = newBuffer.length; + } + + /** + * Replaces the current exhausted buffer with the next buffer in the chain. + */ + private void popContentSource() { + buffer = nextContentSource.buffer; + position = nextContentSource.position; + limit = nextContentSource.limit; + nextContentSource = nextContentSource.next; + } +} diff --git a/src/main/java/com/android/org/kxml2/io/LibCoreStringPool.java b/src/main/java/com/android/org/kxml2/io/LibCoreStringPool.java new file mode 100644 index 00000000..715df45c --- /dev/null +++ b/src/main/java/com/android/org/kxml2/io/LibCoreStringPool.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010 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 com.android.org.kxml2.io; + +// Taken from libcore.internal.StringPool + +class LibCoreStringPool { + + private final String[] pool = new String[512]; + + public LibCoreStringPool() { + } + + private static boolean contentEquals(String s, char[] chars, int start, int length) { + if (s.length() != length) { + return false; + } + for (int i = 0; i < length; i++) { + if (chars[start + i] != s.charAt(i)) { + return false; + } + } + return true; + } + + /** + * Returns a string equal to {@code new String(array, start, length)}. + */ + public String get(char[] array, int start, int length) { + // Compute an arbitrary hash of the content + int hashCode = 0; + for (int i = start; i < start + length; i++) { + hashCode = (hashCode * 31) + array[i]; + } + + // Pick a bucket using Doug Lea's supplemental secondaryHash function (from HashMap) + hashCode ^= (hashCode >>> 20) ^ (hashCode >>> 12); + hashCode ^= (hashCode >>> 7) ^ (hashCode >>> 4); + int index = hashCode & (pool.length - 1); + + String pooled = pool[index]; + if (pooled != null && contentEquals(pooled, array, start, length)) { + return pooled; + } + + String result = new String(array, start, length); + pool[index] = result; + return result; + } + +} diff --git a/src/main/java/com/reandroid/apk/xmlencoder/XMLFileEncoder.java b/src/main/java/com/reandroid/apk/xmlencoder/XMLFileEncoder.java index a0bb3f55..3288516f 100644 --- a/src/main/java/com/reandroid/apk/xmlencoder/XMLFileEncoder.java +++ b/src/main/java/com/reandroid/apk/xmlencoder/XMLFileEncoder.java @@ -110,6 +110,8 @@ private void buildAttributes(XMLElement element, ResXmlElement resXmlElement){ entry =getAttributeBlock(attribute); if(entry !=null){ resourceId= entry.getResourceId(); + }else if(attribute.getNamePrefix()!=null){ + throw new EncodeException("No resource found for: "+attribute.getName()+": "+mCurrentPath); } } ResXmlAttribute xmlAttribute = diff --git a/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlAttribute.java b/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlAttribute.java index df5be99e..4627e71b 100755 --- a/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlAttribute.java +++ b/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlAttribute.java @@ -75,7 +75,7 @@ public String getNamePrefix(){ } return startNamespace.getPrefix(); } - @Deprecated + // WARN! Careful this is not real value public String getValueString(){ return getString(getValueStringReference()); } diff --git a/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlElement.java b/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlElement.java index 9ad7235c..f1bcc153 100755 --- a/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlElement.java +++ b/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlElement.java @@ -39,7 +39,7 @@ public class ResXmlElement extends ResXmlNode implements JSONConvert private final BlockList mBody; private final SingleBlockContainer mEndElementContainer; private final BlockList mEndNamespaceList; - private int mDepth; + private int mLevel; public ResXmlElement() { super(5); this.mStartNamespaceList = new BlockList<>(); @@ -103,6 +103,7 @@ public void setAttributesUnitSize(int size, boolean setToAll){ } } } + @Override public String getComment(){ return getStartElement().getComment(); } @@ -282,6 +283,20 @@ public String getTagPrefix(){ } return null; } + public int getAttributeCount() { + ResXmlStartElement startElement=getStartElement(); + if(startElement!=null){ + return startElement.getResXmlAttributeArray().childesCount(); + } + return 0; + } + public ResXmlAttribute getAttributeAt(int index){ + ResXmlStartElement startElement=getStartElement(); + if(startElement!=null){ + return startElement.getResXmlAttributeArray().get(index); + } + return null; + } public Collection listAttributes(){ ResXmlStartElement startElement=getStartElement(); if(startElement!=null){ @@ -313,11 +328,21 @@ public ResXmlDocument getParentDocument(){ return getParentInstance(ResXmlDocument.class); } + @Override public int getDepth(){ - return mDepth; + int depth = 0; + ResXmlElement parent = getParentResXmlElement(); + while (parent!=null){ + depth++; + parent = parent.getParentResXmlElement(); + } + return depth; + } + public int getLevel(){ + return mLevel; } - private void setDepth(int depth){ - mDepth=depth; + private void setLevel(int level){ + mLevel = level; } public void addElement(ResXmlElement element){ mBody.add(element); @@ -359,6 +384,12 @@ public void clearChildes(){ mBody.remove(xmlNode); } } + public ResXmlNode getResXmlNode(int position){ + return mBody.get(position); + } + public int countResXmlNodes(){ + return mBody.size(); + } public List listXmlNodes(){ return new ArrayList<>(getXmlNodes()); } @@ -412,14 +443,7 @@ public ResXmlElement getRootResXmlElement(){ return this; } public ResXmlElement getParentResXmlElement(){ - Block parent=getParent(); - while (parent!=null){ - if(parent instanceof ResXmlElement){ - return (ResXmlElement)parent; - } - parent=parent.getParent(); - } - return null; + return getParentInstance(ResXmlElement.class); } public ResXmlStartNamespace getStartNamespaceByUriRef(int uriRef){ if(uriRef<0){ @@ -669,7 +693,7 @@ private boolean readNext(BlockReader reader) throws IOException { } private void onFinishedRead(BlockReader reader, HeaderBlock headerBlock) throws IOException{ int avail=reader.available(); - if(avail>0 && getDepth()==0){ + if(avail>0 && getLevel()==0){ onFinishedUnexpected(reader); return; } @@ -692,7 +716,7 @@ private void onStartElement(BlockReader reader) throws IOException{ if(hasStartElement()){ ResXmlElement childElement=new ResXmlElement(); addElement(childElement); - childElement.setDepth(getDepth()+1); + childElement.setLevel(getLevel()+1); childElement.readBytes(reader); }else{ ResXmlStartElement startElement=new ResXmlStartElement(); diff --git a/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlNode.java b/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlNode.java index 388a7f7c..458ba044 100644 --- a/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlNode.java +++ b/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlNode.java @@ -25,6 +25,8 @@ public abstract class ResXmlNode extends FixedBlockContainer implements JSONCon } void onRemove(){ } + public abstract String getComment(); + public abstract int getDepth(); public static final String NAME_node_type="node_type"; } diff --git a/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlPullParser.java b/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlPullParser.java new file mode 100644 index 00000000..bfd54d9f --- /dev/null +++ b/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlPullParser.java @@ -0,0 +1,700 @@ + /* + * Copyright (C) 2022 github.com/REAndroid + * + * 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 com.reandroid.arsc.chunk.xml; + +import android.content.res.XmlResourceParser; +import com.reandroid.arsc.value.ValueType; +import org.xmlpull.v1.XmlPullParserException; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class ResXmlPullParser implements XmlResourceParser { + private ResXmlDocument mDocument; + private boolean mDocumentCreatedHere; + private ResXmlElement mCurrentElement; + private ResXmlTextNode mCurrentText; + private int mEvent = -1; + + + public ResXmlPullParser(){ + } + + public void setResXmlDocument(ResXmlDocument xmlDocument){ + closeDocument(); + this.mDocument = xmlDocument; + } + public ResXmlDocument getResXmlDocument() { + return mDocument; + } + + public void closeDocument(){ + mCurrentElement = null; + mCurrentText = null; + mEvent = -1; + destroyDocument(); + } + private void destroyDocument(){ + if(!mDocumentCreatedHere){ + return; + } + mDocumentCreatedHere = false; + if(this.mDocument == null){ + return; + } + this.mDocument.destroy(); + this.mDocument = null; + } + + @Override + public void close(){ + closeDocument(); + } + @Override + public int getAttributeCount() { + return mCurrentElement.getAttributeCount(); + } + @Override + public String getAttributeName(int index) { + ResXmlAttribute xmlAttribute = mCurrentElement.getAttributeAt(index); + if(xmlAttribute!=null){ + return xmlAttribute.getName(); + } + return null; + } + @Override + public String getAttributeValue(int index) { + ResXmlAttribute xmlAttribute = geResXmlAttributeAt(index); + if(xmlAttribute!=null){ + return xmlAttribute.getValueString(); + } + return null; + } + @Override + public String getAttributeValue(String namespace, String name) { + ResXmlAttribute attribute = getAttribute(namespace, name); + if(attribute != null){ + return attribute.getValueString(); + } + return null; + } + @Override + public String getPositionDescription() { + return null; + } + @Override + public int getAttributeNameResource(int index) { + ResXmlAttribute attribute = geResXmlAttributeAt(index); + if(attribute!=null){ + return attribute.getNameResourceID(); + } + return 0; + } + @Override + public int getAttributeListValue(String namespace, String attribute, String[] options, int defaultValue) { + ResXmlAttribute xmlAttribute = getAttribute(namespace, attribute); + if(xmlAttribute == null){ + return 0; + } + List list = Arrays.asList(options); + int index = list.indexOf(xmlAttribute.getValueString()); + if(index==-1){ + return defaultValue; + } + return index; + } + @Override + public boolean getAttributeBooleanValue(String namespace, String attribute, boolean defaultValue) { + ResXmlAttribute xmlAttribute = getAttribute(namespace, attribute); + if(xmlAttribute == null || xmlAttribute.getValueType() != ValueType.INT_BOOLEAN){ + return defaultValue; + } + return xmlAttribute.getValueAsBoolean(); + } + @Override + public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) { + ResXmlAttribute xmlAttribute = getAttribute(namespace, attribute); + if(xmlAttribute == null){ + return 0; + } + ValueType valueType=xmlAttribute.getValueType(); + if(valueType==ValueType.ATTRIBUTE + ||valueType==ValueType.REFERENCE + ||valueType==ValueType.DYNAMIC_ATTRIBUTE + ||valueType==ValueType.DYNAMIC_REFERENCE){ + return xmlAttribute.getData(); + } + return defaultValue; + } + @Override + public int getAttributeIntValue(String namespace, String attribute, int defaultValue) { + ResXmlAttribute xmlAttribute = getAttribute(namespace, attribute); + if(xmlAttribute == null){ + return 0; + } + ValueType valueType=xmlAttribute.getValueType(); + if(valueType==ValueType.INT_DEC + ||valueType==ValueType.INT_HEX){ + return xmlAttribute.getData(); + } + return defaultValue; + } + @Override + public int getAttributeUnsignedIntValue(String namespace, String attribute, int defaultValue) { + ResXmlAttribute xmlAttribute = getAttribute(namespace, attribute); + if(xmlAttribute == null){ + return 0; + } + ValueType valueType=xmlAttribute.getValueType(); + if(valueType==ValueType.INT_DEC){ + return xmlAttribute.getData(); + } + return defaultValue; + } + @Override + public float getAttributeFloatValue(String namespace, String attribute, float defaultValue) { + ResXmlAttribute xmlAttribute = getAttribute(namespace, attribute); + if(xmlAttribute == null){ + return 0; + } + ValueType valueType=xmlAttribute.getValueType(); + if(valueType==ValueType.FLOAT){ + return Float.intBitsToFloat(xmlAttribute.getData()); + } + return defaultValue; + } + + @Override + public int getAttributeListValue(int index, String[] options, int defaultValue) { + ResXmlAttribute xmlAttribute = geResXmlAttributeAt(index); + if(xmlAttribute == null){ + return 0; + } + List list = Arrays.asList(options); + int i = list.indexOf(xmlAttribute.getValueString()); + if(i==-1){ + return defaultValue; + } + return index; + } + @Override + public boolean getAttributeBooleanValue(int index, boolean defaultValue) { + ResXmlAttribute xmlAttribute = geResXmlAttributeAt(index); + if(xmlAttribute == null || xmlAttribute.getValueType() != ValueType.INT_BOOLEAN){ + return defaultValue; + } + return xmlAttribute.getValueAsBoolean(); + } + @Override + public int getAttributeResourceValue(int index, int defaultValue) { + ResXmlAttribute xmlAttribute = geResXmlAttributeAt(index); + if(xmlAttribute == null){ + return 0; + } + ValueType valueType=xmlAttribute.getValueType(); + if(valueType==ValueType.ATTRIBUTE + ||valueType==ValueType.REFERENCE + ||valueType==ValueType.DYNAMIC_ATTRIBUTE + ||valueType==ValueType.DYNAMIC_REFERENCE){ + return xmlAttribute.getData(); + } + return defaultValue; + } + @Override + public int getAttributeIntValue(int index, int defaultValue) { + ResXmlAttribute xmlAttribute = geResXmlAttributeAt(index); + if(xmlAttribute == null){ + return 0; + } + ValueType valueType=xmlAttribute.getValueType(); + if(valueType==ValueType.INT_DEC + ||valueType==ValueType.INT_HEX){ + return xmlAttribute.getData(); + } + return defaultValue; + } + @Override + public int getAttributeUnsignedIntValue(int index, int defaultValue) { + ResXmlAttribute xmlAttribute = geResXmlAttributeAt(index); + if(xmlAttribute == null){ + return 0; + } + ValueType valueType=xmlAttribute.getValueType(); + if(valueType==ValueType.INT_DEC){ + return xmlAttribute.getData(); + } + return defaultValue; + } + @Override + public float getAttributeFloatValue(int index, float defaultValue) { + ResXmlAttribute xmlAttribute = geResXmlAttributeAt(index); + if(xmlAttribute == null){ + return 0; + } + ValueType valueType=xmlAttribute.getValueType(); + if(valueType==ValueType.FLOAT){ + return Float.intBitsToFloat(xmlAttribute.getData()); + } + return defaultValue; + } + + @Override + public String getIdAttribute() { + ResXmlStartElement startElement = getResXmlStartElement(); + if(startElement!=null){ + ResXmlAttribute attribute = startElement.getIdAttribute(); + if(attribute!=null){ + return attribute.getName(); + } + } + return null; + } + @Override + public String getClassAttribute() { + ResXmlStartElement startElement = getResXmlStartElement(); + if(startElement!=null){ + ResXmlAttribute attribute = startElement.getClassAttribute(); + if(attribute!=null){ + return attribute.getName(); + } + } + return null; + } + @Override + public int getIdAttributeResourceValue(int defaultValue) { + ResXmlStartElement startElement = getResXmlStartElement(); + if(startElement!=null){ + ResXmlAttribute attribute = startElement.getIdAttribute(); + if(attribute!=null){ + return attribute.getNameResourceID(); + } + } + return 0; + } + @Override + public int getStyleAttribute() { + ResXmlStartElement startElement = getResXmlStartElement(); + if(startElement!=null){ + ResXmlAttribute attribute = startElement.getStyleAttribute(); + if(attribute!=null){ + return attribute.getNameResourceID(); + } + } + return 0; + } + + @Override + public void setFeature(String name, boolean state) throws XmlPullParserException { + } + @Override + public boolean getFeature(String name) { + return false; + } + @Override + public void setProperty(String name, Object value) throws XmlPullParserException { + } + @Override + public Object getProperty(String name) { + return null; + } + @Override + public void setInput(Reader in) throws XmlPullParserException { + InputStream inputStream = getFromLock(in); + if(inputStream == null){ + throw new XmlPullParserException("Can't parse binary xml from reader"); + } + setInput(inputStream, null); + } + @Override + public void setInput(InputStream inputStream, String inputEncoding) throws XmlPullParserException { + ResXmlDocument xmlDocument = new ResXmlDocument(); + try { + xmlDocument.readBytes(inputStream); + } catch (IOException exception) { + XmlPullParserException pullParserException = new XmlPullParserException(exception.getMessage()); + pullParserException.initCause(exception); + throw pullParserException; + } + setResXmlDocument(xmlDocument); + this.mDocumentCreatedHere = true; + } + @Override + public String getInputEncoding() { + return null; + } + @Override + public void defineEntityReplacementText(String entityName, String replacementText) throws XmlPullParserException { + } + @Override + public int getNamespaceCount(int depth) throws XmlPullParserException { + ResXmlElement element = getCurrentElement(); + while(element!=null && element.getDepth()>depth){ + element=element.getParentResXmlElement(); + } + if(element!=null){ + return element.getStartNamespaceList().size(); + } + return 0; + } + @Override + public String getNamespacePrefix(int pos) throws XmlPullParserException { + ResXmlAttribute attribute = mCurrentElement.getAttributeAt(pos); + if(attribute!=null){ + return attribute.getNamePrefix(); + } + return null; + } + @Override + public String getNamespaceUri(int pos) throws XmlPullParserException { + ResXmlAttribute attribute = mCurrentElement.getAttributeAt(pos); + if(attribute!=null){ + return attribute.getUri(); + } + return null; + } + @Override + public String getNamespace(String prefix) { + ResXmlStartNamespace startNamespace = mCurrentElement.getStartNamespaceByPrefix(prefix); + if(startNamespace!=null){ + return startNamespace.getUri(); + } + return null; + } + @Override + public int getDepth() { + int event = mEvent; + if(event == START_TAG || event == END_TAG){ + return mCurrentElement.getDepth(); + } + if(event == TEXT){ + return mCurrentText.getDepth(); + } + return 0; + } + @Override + public int getLineNumber() { + int event = mEvent; + if(event == START_TAG){ + ResXmlStartElement startElement = mCurrentElement.getStartElement(); + if(startElement!=null){ + return startElement.getLineNumber(); + } + return 0; + } + if(event == END_TAG){ + ResXmlEndElement endElement = mCurrentElement.getEndElement(); + if(endElement!=null){ + return endElement.getLineNumber(); + } + return 0; + } + if(event == TEXT){ + return mCurrentText.getLineNumber(); + } + return 0; + } + @Override + public int getColumnNumber() { + return 0; + } + @Override + public boolean isWhitespace() throws XmlPullParserException { + return false; + } + @Override + public String getText() { + int event = mEvent; + if(event == TEXT){ + return mCurrentText.getText(); + } + if(event == START_TAG || event == END_TAG){ + return mCurrentElement.getTag(); + } + return null; + } + @Override + public char[] getTextCharacters(int[] holderForStartAndLength) { + String text = getText(); + if (text == null) { + holderForStartAndLength[0] = -1; + holderForStartAndLength[1] = -1; + return null; + } + char[] result = text.toCharArray(); + holderForStartAndLength[0] = 0; + holderForStartAndLength[1] = result.length; + return result; + } + @Override + public String getNamespace() { + ResXmlElement element = getCurrentElement(); + if(element!=null){ + return element.getTagUri(); + } + return null; + } + @Override + public String getName() { + ResXmlElement element = getCurrentElement(); + if(element!=null){ + return element.getTag(); + } + return null; + } + @Override + public String getPrefix() { + ResXmlElement element = getCurrentElement(); + if(element!=null){ + return element.getTagPrefix(); + } + return null; + } + @Override + public boolean isEmptyElementTag() throws XmlPullParserException { + ResXmlElement element = getCurrentElement(); + if(element!=null){ + return element.countResXmlNodes() == 0 && element.getAttributeCount()==0; + } + return false; + } + @Override + public String getAttributeNamespace(int index) { + ResXmlAttribute attribute = geResXmlAttributeAt(index); + if(attribute != null){ + return attribute.getUri(); + } + return null; + } + @Override + public String getAttributePrefix(int index) { + ResXmlAttribute attribute = geResXmlAttributeAt(index); + if(attribute != null){ + return attribute.getNamePrefix(); + } + return null; + } + @Override + public String getAttributeType(int index) { + return "CDATA"; + } + @Override + public boolean isAttributeDefault(int index) { + return false; + } + private ResXmlAttribute geResXmlAttributeAt(int index){ + ResXmlElement element = getCurrentElement(); + if(element == null){ + return null; + } + return element.getAttributeAt(index); + } + private ResXmlAttribute getAttribute(String namespace, String name) { + ResXmlElement element = getCurrentElement(); + if(element == null){ + return null; + } + for(ResXmlAttribute attribute:element.listAttributes()){ + if(Objects.equals(namespace, attribute.getUri()) + && Objects.equals(name, attribute.getName())){ + return attribute; + } + } + return null; + } + private ResXmlStartElement getResXmlStartElement(){ + ResXmlElement element = getCurrentElement(); + if(element!=null){ + return element.getStartElement(); + } + return null; + } + private ResXmlElement getCurrentElement() { + int event = mEvent; + if(event!=START_TAG && event!=END_TAG){ + return null; + } + return mCurrentElement; + } + @Override + public int getEventType() throws XmlPullParserException { + return mEvent; + } + @Override + public int next() throws XmlPullParserException, IOException { + checkNotEnded(); + int event = calculateNextEvent(mEvent); + if(event == START_DOCUMENT){ + onStartDocument(); + }else if(event == END_DOCUMENT){ + onEndDocument(); + }else if(event == START_TAG){ + onStartTag(); + }else if(event == END_TAG){ + onEndTag(); + }else if(event == TEXT){ + onText(); + } + this.mEvent = event; + return event; + } + private void onEndTag() throws XmlPullParserException { + int previous = mEvent; + if(previous == END_TAG){ + mCurrentElement = mCurrentElement.getParentResXmlElement(); + } + mCurrentText = null; + } + private void onText() throws XmlPullParserException { + int previous = mEvent; + if(previous == END_TAG){ + int position = mCurrentElement.getIndex(); + ResXmlElement parent = mCurrentElement.getParentResXmlElement(); + position++; + mCurrentText = (ResXmlTextNode) parent.getResXmlNode(position); + mCurrentElement = parent; + }else if(previous == START_TAG){ + ResXmlElement parent = mCurrentElement; + mCurrentText = (ResXmlTextNode) parent.getResXmlNode(0); + }else if(previous == TEXT){ + int position = mCurrentText.getIndex(); + ResXmlElement parent = mCurrentElement; + position++; + mCurrentText = (ResXmlTextNode) parent.getResXmlNode(position); + mCurrentText = (ResXmlTextNode) parent.getResXmlNode(0); + }else { + throw new XmlPullParserException("Unknown state at onText() prev="+previous); + } + } + private void onStartTag() throws XmlPullParserException { + int previous = mEvent; + if(previous == START_DOCUMENT){ + mCurrentElement = mDocument.getResXmlElement(); + mCurrentText = null; + }else if(previous == END_TAG){ + int position = mCurrentElement.getIndex(); + ResXmlElement parent = mCurrentElement.getParentResXmlElement(); + position++; + mCurrentElement = (ResXmlElement) parent.getResXmlNode(position); + }else if(previous == TEXT){ + int position = mCurrentText.getIndex(); + ResXmlElement parent = mCurrentText.getResXmlText().getParentResXmlElement(); + position++; + mCurrentElement = (ResXmlElement) parent.getResXmlNode(position); + }else if(previous == START_TAG){ + mCurrentElement = (ResXmlElement) mCurrentElement.getResXmlNode(0); + }else { + throw new XmlPullParserException("Unknown state at onStartTag() prev="+previous); + } + mCurrentText = null; + + } + private void onStartDocument(){ + } + private void onEndDocument() throws XmlPullParserException { + mCurrentElement = null; + mCurrentText = null; + close(); + } + private void checkNotEnded() throws XmlPullParserException { + if(mEvent == END_DOCUMENT){ + throw new XmlPullParserException("Document reached to end"); + } + } + private int calculateNextEvent(int previous) throws XmlPullParserException { + if(previous < 0){ + if(mDocument == null){ + return previous; + } + return START_DOCUMENT; + } + if(previous == START_DOCUMENT){ + ResXmlElement element = mDocument.getResXmlElement(); + if(element==null){ + return END_DOCUMENT; + } + return START_TAG; + } + if(previous == END_DOCUMENT){ + return END_DOCUMENT; + } + if(previous == START_TAG){ + ResXmlElement element = mCurrentElement; + ResXmlNode firstChild = element.getResXmlNode(0); + if(firstChild == null){ + return END_TAG; + } + if(firstChild instanceof ResXmlTextNode){ + return TEXT; + } + return START_TAG; + } + if(previous == END_TAG || previous==TEXT){ + ResXmlElement element = mCurrentElement; + ResXmlElement parent = element.getParentResXmlElement(); + if(parent == null){ + return END_DOCUMENT; + } + int position = element.getIndex() + 1; + ResXmlNode nextNode = parent.getResXmlNode(position); + if(nextNode==null){ + return END_TAG; + } + if(nextNode instanceof ResXmlTextNode){ + return TEXT; + } + return START_TAG; + } + throw new XmlPullParserException("Unknown state at calculateNextEvent() prev="+previous); + } + @Override + public int nextToken() throws XmlPullParserException, IOException { + return next(); + } + @Override + public void require(int type, String namespace, String name) throws XmlPullParserException, IOException { + } + @Override + public String nextText() throws XmlPullParserException, IOException { + return null; + } + @Override + public int nextTag() throws XmlPullParserException, IOException { + return 0; + } + + private static InputStream getFromLock(Reader reader){ + try{ + Field field = Reader.class.getDeclaredField("lock"); + field.setAccessible(true); + Object obj = field.get(reader); + if(obj instanceof InputStream){ + return (InputStream) obj; + } + }catch (Throwable ignored){ + } + return null; + } + + /** + * This non-final re-declaration is to force compiler from using literal int value on this class + * */ + + +} diff --git a/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlStartElement.java b/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlStartElement.java index fc481ca1..eadf7709 100755 --- a/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlStartElement.java +++ b/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlStartElement.java @@ -52,6 +52,15 @@ public ResXmlStartElement() { addChild(mStyleAttributePosition); addChild(mAttributeArray); } + public ResXmlAttribute getIdAttribute(){ + return getResXmlAttributeArray().get(mIdAttributePosition.unsignedInt()-1); + } + public ResXmlAttribute getClassAttribute(){ + return getResXmlAttributeArray().get(mClassAttributePosition.unsignedInt()-1); + } + public ResXmlAttribute getStyleAttribute(){ + return getResXmlAttributeArray().get(mStyleAttributePosition.unsignedInt()-1); + } void setAttributesUnitSize(int size){ mAttributeArray.setAttributesUnitSize(size); } diff --git a/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlTextNode.java b/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlTextNode.java index a37227d2..4218846d 100644 --- a/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlTextNode.java +++ b/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlTextNode.java @@ -35,6 +35,22 @@ public ResXmlText getResXmlText() { public int getLineNumber(){ return getResXmlText().getLineNumber(); } + @Override + public String getComment() { + return getResXmlText().getComment(); + } + @Override + public int getDepth(){ + ResXmlElement parent = getParentResXmlElement(); + if(parent!=null){ + return parent.getDepth() + 1; + } + return 0; + } + public ResXmlElement getParentResXmlElement(){ + return getResXmlText().getParentResXmlElement(); + } + public void setLineNumber(int lineNumber){ getResXmlText().setLineNumber(lineNumber); } diff --git a/src/main/java/com/reandroid/arsc/container/BlockList.java b/src/main/java/com/reandroid/arsc/container/BlockList.java index 271b7ea9..daef4bf0 100755 --- a/src/main/java/com/reandroid/arsc/container/BlockList.java +++ b/src/main/java/com/reandroid/arsc/container/BlockList.java @@ -57,6 +57,9 @@ public void add(T item){ mItems.add(item); } public T get(int i){ + if(i>=mItems.size() || i<0){ + return null; + } return mItems.get(i); } public int size(){ diff --git a/src/main/java/com/reandroid/arsc/value/ValueItem.java b/src/main/java/com/reandroid/arsc/value/ValueItem.java index 6d042dd8..861c0847 100755 --- a/src/main/java/com/reandroid/arsc/value/ValueItem.java +++ b/src/main/java/com/reandroid/arsc/value/ValueItem.java @@ -118,6 +118,9 @@ public void setData(int data){ public StringItem getDataAsPoolString(){ + if(getValueType()!=ValueType.STRING){ + return null; + } StringPool stringPool = getStringPool(); if(stringPool == null){ return null; diff --git a/src/main/java/com/reandroid/xml/parser/XMLDocumentParser.java b/src/main/java/com/reandroid/xml/parser/XMLDocumentParser.java index 45634369..f199503e 100755 --- a/src/main/java/com/reandroid/xml/parser/XMLDocumentParser.java +++ b/src/main/java/com/reandroid/xml/parser/XMLDocumentParser.java @@ -15,7 +15,10 @@ */ package com.reandroid.xml.parser; +import com.android.org.kxml2.io.KXmlParser; import com.reandroid.xml.*; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; import java.io.*; import java.util.ArrayList; @@ -343,7 +346,7 @@ private static XmlPullParser createParser(File file) throws XMLParseException { } private static XmlPullParser createParser(InputStream in) throws XMLParseException { try { - XmlPullParser parser = new MXParserNonValidating(); + XmlPullParser parser = new KXmlParser(); parser.setInput(in, null); return parser; } catch (XmlPullParserException e) { diff --git a/src/main/java/com/reandroid/xml/parser/XmlPullParser.java b/src/main/java/com/reandroid/xml/parser/XmlPullParser.java index 19fc6e08..fe2c9ea8 100644 --- a/src/main/java/com/reandroid/xml/parser/XmlPullParser.java +++ b/src/main/java/com/reandroid/xml/parser/XmlPullParser.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.io.Reader; +@Deprecated public interface XmlPullParser { String NO_NAMESPACE = ""; diff --git a/src/main/java/com/reandroid/xml/parser/XmlPullParserException.java b/src/main/java/com/reandroid/xml/parser/XmlPullParserException.java index 1e9a48b7..50915cc7 100644 --- a/src/main/java/com/reandroid/xml/parser/XmlPullParserException.java +++ b/src/main/java/com/reandroid/xml/parser/XmlPullParserException.java @@ -8,6 +8,7 @@ /*This package is renamed from org.xmlpull.* to avoid conflicts*/ package com.reandroid.xml.parser; +@Deprecated public class XmlPullParserException extends Exception { protected Throwable detail; protected int row = -1; diff --git a/src/main/java/org/xmlpull/v1/XmlPullParser.java b/src/main/java/org/xmlpull/v1/XmlPullParser.java new file mode 100644 index 00000000..675e3576 --- /dev/null +++ b/src/main/java/org/xmlpull/v1/XmlPullParser.java @@ -0,0 +1,78 @@ +/* -*- c-basic-offset: 4; indent-tabs-mode: nil; -*- //------100-columns-wide------>|*/ +// for license please see accompanying LICENSE.txt file (available also at http://www.xmlpull.org/) +package org.xmlpull.v1; + +import java.io.InputStream; +import java.io.IOException; +import java.io.Reader; + +public interface XmlPullParser{ + String NO_NAMESPACE = ""; + int START_DOCUMENT = 0; + int END_DOCUMENT = 1; + int START_TAG = 2; + int END_TAG = 3; + int TEXT = 4; + int CDSECT = 5; + int ENTITY_REF = 6; + int IGNORABLE_WHITESPACE = 7; + int PROCESSING_INSTRUCTION = 8; + int COMMENT = 9; + int DOCDECL = 10; + String [] TYPES = { + "START_DOCUMENT", + "END_DOCUMENT", + "START_TAG", + "END_TAG", + "TEXT", + "CDSECT", + "ENTITY_REF", + "IGNORABLE_WHITESPACE", + "PROCESSING_INSTRUCTION", + "COMMENT", + "DOCDECL" + }; + String FEATURE_PROCESS_NAMESPACES = "http://xmlpull.org/v1/doc/features.html#process-namespaces"; + String FEATURE_REPORT_NAMESPACE_ATTRIBUTES = "http://xmlpull.org/v1/doc/features.html#report-namespace-prefixes"; + String FEATURE_PROCESS_DOCDECL = "http://xmlpull.org/v1/doc/features.html#process-docdecl"; + String FEATURE_VALIDATION = "http://xmlpull.org/v1/doc/features.html#validation"; + + void setFeature(String name, boolean state) throws XmlPullParserException; + boolean getFeature(String name); + void setProperty(String name, Object value) throws XmlPullParserException; + Object getProperty(String name); + void setInput(Reader in) throws XmlPullParserException; + void setInput(InputStream inputStream, String inputEncoding) throws XmlPullParserException; + String getInputEncoding(); + void defineEntityReplacementText( String entityName, String replacementText ) throws XmlPullParserException; + int getNamespaceCount(int depth) throws XmlPullParserException; + String getNamespacePrefix(int pos) throws XmlPullParserException; + String getNamespaceUri(int pos) throws XmlPullParserException; + String getNamespace (String prefix); + int getDepth(); + String getPositionDescription (); + int getLineNumber(); + int getColumnNumber(); + boolean isWhitespace() throws XmlPullParserException; + String getText (); + char[] getTextCharacters(int [] holderForStartAndLength); + String getNamespace (); + String getName(); + String getPrefix(); + boolean isEmptyElementTag() throws XmlPullParserException; + int getAttributeCount(); + String getAttributeNamespace (int index); + String getAttributeName (int index); + String getAttributePrefix(int index); + String getAttributeType(int index); + boolean isAttributeDefault(int index); + String getAttributeValue(int index); + String getAttributeValue(String namespace, String name); + int getEventType() throws XmlPullParserException; + int next() throws XmlPullParserException, IOException; + int nextToken() throws XmlPullParserException, IOException; + void require(int type, String namespace, String name) throws XmlPullParserException, IOException; + String nextText() throws XmlPullParserException, IOException; + int nextTag() throws XmlPullParserException, IOException; + +} diff --git a/src/main/java/org/xmlpull/v1/XmlPullParserException.java b/src/main/java/org/xmlpull/v1/XmlPullParserException.java new file mode 100644 index 00000000..c62a21b4 --- /dev/null +++ b/src/main/java/org/xmlpull/v1/XmlPullParserException.java @@ -0,0 +1,76 @@ +/* -*- c-basic-offset: 4; indent-tabs-mode: nil; -*- //------100-columns-wide------>|*/ +// for license please see accompanying LICENSE.txt file (available also at http://www.xmlpull.org/) + +package org.xmlpull.v1; + +/** + * This exception is thrown to signal XML Pull Parser related faults. + * + * @author Aleksander Slominski + */ +public class XmlPullParserException extends Exception{ + protected Throwable detail; + protected int row = -1; + protected int column = -1; + + /* public XmlPullParserException() { + }*/ + + public XmlPullParserException(String s) { + super(s); + } + + /* + public XmlPullParserException(String s, Throwable thrwble) { + super(s); + this.detail = thrwble; + } + + public XmlPullParserException(String s, int row, int column) { + super(s); + this.row = row; + this.column = column; + } + */ + + public XmlPullParserException(String msg, XmlPullParser parser, Throwable chain) { + super ((msg == null ? "" : msg+" ") + + (parser == null ? "" : "(position:"+parser.getPositionDescription()+") ") + + (chain == null ? "" : "caused by: "+chain)); + + if (parser != null) { + this.row = parser.getLineNumber(); + this.column = parser.getColumnNumber(); + } + this.detail = chain; + } + + public Throwable getDetail() { return detail; } + // public void setDetail(Throwable cause) { this.detail = cause; } + public int getLineNumber() { return row; } + public int getColumnNumber() { return column; } + + /* + public String getMessage() { + if(detail == null) + return super.getMessage(); + else + return super.getMessage() + "; nested exception is: \n\t" + + detail.getMessage(); + } + */ + + //NOTE: code that prints this and detail is difficult in J2ME + public void printStackTrace() { + if (detail == null) { + super.printStackTrace(); + } else { + synchronized(System.err) { + System.err.println(super.getMessage() + "; nested exception is:"); + detail.printStackTrace(); + } + } + } + +} + diff --git a/src/main/java/org/xmlpull/v1/XmlPullParserFactory.java b/src/main/java/org/xmlpull/v1/XmlPullParserFactory.java new file mode 100644 index 00000000..46bff9bd --- /dev/null +++ b/src/main/java/org/xmlpull/v1/XmlPullParserFactory.java @@ -0,0 +1,119 @@ +/* -*- c-basic-offset: 4; indent-tabs-mode: nil; -*- //------100-columns-wide------>|*/ +// for license please see accompanying LICENSE.txt file (available also at http://www.xmlpull.org/) +package org.xmlpull.v1; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class XmlPullParserFactory { + public static final String PROPERTY_NAME = "org.xmlpull.v1.XmlPullParserFactory"; + protected ArrayList parserClasses; + protected ArrayList serializerClasses; + protected String classNamesLocation = null; + protected HashMap features = new HashMap(); + protected XmlPullParserFactory() { + parserClasses = new ArrayList(); + serializerClasses = new ArrayList(); + try { + parserClasses.add(Class.forName("com.android.org.kxml2.io.KXmlParser")); + serializerClasses.add(Class.forName("com.android.org.kxml2.io.KXmlSerializer")); + } catch (ClassNotFoundException e) { + throw new AssertionError(); + } + } + public void setFeature(String name, boolean state) throws XmlPullParserException { + features.put(name, state); + } + public boolean getFeature(String name) { + Boolean value = features.get(name); + return value != null ? value.booleanValue() : false; + } + public void setNamespaceAware(boolean awareness) { + features.put (XmlPullParser.FEATURE_PROCESS_NAMESPACES, awareness); + } + public boolean isNamespaceAware() { + return getFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES); + } + public void setValidating(boolean validating) { + features.put(XmlPullParser.FEATURE_VALIDATION, validating); + } + + public boolean isValidating() { + return getFeature(XmlPullParser.FEATURE_VALIDATION); + } + public XmlPullParser newPullParser() throws XmlPullParserException { + final XmlPullParser pp = getParserInstance(); + for (Map.Entry entry : features.entrySet()) { + if (entry.getValue()) { + pp.setFeature(entry.getKey(), entry.getValue()); + } + } + return pp; + } + private XmlPullParser getParserInstance() throws XmlPullParserException { + ArrayList exceptions = null; + if (parserClasses != null && !parserClasses.isEmpty()) { + exceptions = new ArrayList(); + for (Object o : parserClasses) { + try { + if (o != null) { + Class parserClass = (Class) o; + return (XmlPullParser) parserClass.newInstance(); + } + } catch (InstantiationException e) { + exceptions.add(e); + } catch (IllegalAccessException e) { + exceptions.add(e); + } catch (ClassCastException e) { + exceptions.add(e); + } + } + } + throw newInstantiationException("Invalid parser class list", exceptions); + } + private XmlSerializer getSerializerInstance() throws XmlPullParserException { + ArrayList exceptions = null; + if (serializerClasses != null && !serializerClasses.isEmpty()) { + exceptions = new ArrayList(); + for (Object o : serializerClasses) { + try { + if (o != null) { + Class serializerClass = (Class) o; + return (XmlSerializer) serializerClass.newInstance(); + } + } catch (InstantiationException e) { + exceptions.add(e); + } catch (IllegalAccessException e) { + exceptions.add(e); + } catch (ClassCastException e) { + exceptions.add(e); + } + } + } + throw newInstantiationException("Invalid serializer class list", exceptions); + } + private static XmlPullParserException newInstantiationException(String message, + ArrayList exceptions) { + if (exceptions == null || exceptions.isEmpty()) { + return new XmlPullParserException(message); + } else { + XmlPullParserException exception = new XmlPullParserException(message); + for (Exception ex : exceptions) { + exception.addSuppressed(ex); + } + return exception; + } + } + + public XmlSerializer newSerializer() throws XmlPullParserException { + return getSerializerInstance(); + } + public static XmlPullParserFactory newInstance () throws XmlPullParserException { + return new XmlPullParserFactory(); + } + public static XmlPullParserFactory newInstance (String unused, Class unused2) + throws XmlPullParserException { + return newInstance(); + } +} diff --git a/src/main/java/org/xmlpull/v1/XmlSerializer.java b/src/main/java/org/xmlpull/v1/XmlSerializer.java new file mode 100644 index 00000000..2cfeec49 --- /dev/null +++ b/src/main/java/org/xmlpull/v1/XmlSerializer.java @@ -0,0 +1,52 @@ +package org.xmlpull.v1; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; + +public interface XmlSerializer { + void setFeature(String name, boolean state) + throws IllegalArgumentException, IllegalStateException; + boolean getFeature(String name); + void setProperty(String name, Object value) + throws IllegalArgumentException, IllegalStateException; + Object getProperty(String name); + void setOutput(OutputStream os, String encoding) + throws IOException, IllegalArgumentException, IllegalStateException; + void setOutput(Writer writer) + throws IOException, IllegalArgumentException, IllegalStateException; + void startDocument(String encoding, Boolean standalone) + throws IOException, IllegalArgumentException, IllegalStateException; + void endDocument() + throws IOException, IllegalArgumentException, IllegalStateException; + void setPrefix(String prefix, String namespace) + throws IOException, IllegalArgumentException, IllegalStateException; + String getPrefix(String namespace, boolean generatePrefix) + throws IllegalArgumentException; + int getDepth(); + String getNamespace(); + String getName(); + XmlSerializer startTag(String namespace, String name) + throws IOException, IllegalArgumentException, IllegalStateException; + XmlSerializer attribute(String namespace, String name, String value) + throws IOException, IllegalArgumentException, IllegalStateException; + XmlSerializer endTag(String namespace, String name) + throws IOException, IllegalArgumentException, IllegalStateException; + XmlSerializer text(String text) + throws IOException, IllegalArgumentException, IllegalStateException; + XmlSerializer text(char [] buf, int start, int len) + throws IOException, IllegalArgumentException, IllegalStateException; + void cdsect(String text) + throws IOException, IllegalArgumentException, IllegalStateException; + void entityRef(String text) throws IOException, + IllegalArgumentException, IllegalStateException; + void processingInstruction(String text) + throws IOException, IllegalArgumentException, IllegalStateException; + void comment(String text) + throws IOException, IllegalArgumentException, IllegalStateException; + void docdecl(String text) + throws IOException, IllegalArgumentException, IllegalStateException; + void ignorableWhitespace(String text) + throws IOException, IllegalArgumentException, IllegalStateException; + void flush() throws IOException; +}