500 lines
17 KiB
Java
500 lines
17 KiB
Java
![]() |
/*
|
||
|
* Copyright (C) 2007 The Android Open Source Project
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
|
||
|
package android.util;
|
||
|
|
||
|
import android.annotation.NonNull;
|
||
|
import android.annotation.Nullable;
|
||
|
import android.os.SystemProperties;
|
||
|
import android.system.ErrnoException;
|
||
|
import android.system.Os;
|
||
|
|
||
|
import com.android.internal.util.ArtBinaryXmlPullParser;
|
||
|
import com.android.internal.util.ArtBinaryXmlSerializer;
|
||
|
import com.android.internal.util.FastXmlSerializer;
|
||
|
import com.android.internal.util.XmlUtils;
|
||
|
import com.android.modules.utils.BinaryXmlPullParser;
|
||
|
import com.android.modules.utils.BinaryXmlSerializer;
|
||
|
import com.android.modules.utils.TypedXmlPullParser;
|
||
|
import com.android.modules.utils.TypedXmlSerializer;
|
||
|
|
||
|
import libcore.util.XmlObjectFactory;
|
||
|
|
||
|
import org.xml.sax.ContentHandler;
|
||
|
import org.xml.sax.InputSource;
|
||
|
import org.xml.sax.SAXException;
|
||
|
import org.xml.sax.XMLReader;
|
||
|
import org.xmlpull.v1.XmlPullParser;
|
||
|
import org.xmlpull.v1.XmlPullParserException;
|
||
|
import org.xmlpull.v1.XmlPullParserFactory;
|
||
|
import org.xmlpull.v1.XmlSerializer;
|
||
|
|
||
|
import java.io.BufferedInputStream;
|
||
|
import java.io.FileInputStream;
|
||
|
import java.io.IOException;
|
||
|
import java.io.InputStream;
|
||
|
import java.io.OutputStream;
|
||
|
import java.io.Reader;
|
||
|
import java.io.StringReader;
|
||
|
import java.io.UnsupportedEncodingException;
|
||
|
import java.nio.charset.StandardCharsets;
|
||
|
import java.util.Arrays;
|
||
|
|
||
|
import javax.xml.parsers.SAXParserFactory;
|
||
|
|
||
|
/**
|
||
|
* XML utility methods.
|
||
|
*/
|
||
|
@android.ravenwood.annotation.RavenwoodKeepWholeClass
|
||
|
public class Xml {
|
||
|
private Xml() {}
|
||
|
|
||
|
/**
|
||
|
* {@link org.xmlpull.v1.XmlPullParser} "relaxed" feature name.
|
||
|
*
|
||
|
* @see <a href="http://xmlpull.org/v1/doc/features.html#relaxed">
|
||
|
* specification</a>
|
||
|
*/
|
||
|
public static String FEATURE_RELAXED = "http://xmlpull.org/v1/doc/features.html#relaxed";
|
||
|
|
||
|
/**
|
||
|
* Feature flag: when set, {@link #resolveSerializer(OutputStream)} will
|
||
|
* emit binary XML by default.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public static final boolean ENABLE_BINARY_DEFAULT = shouldEnableBinaryDefault();
|
||
|
|
||
|
@android.ravenwood.annotation.RavenwoodReplace
|
||
|
private static boolean shouldEnableBinaryDefault() {
|
||
|
return SystemProperties.getBoolean("persist.sys.binary_xml", true);
|
||
|
}
|
||
|
|
||
|
private static boolean shouldEnableBinaryDefault$ravenwood() {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Feature flag: when set, {@link #resolvePullParser(InputStream)}} will attempt to sniff
|
||
|
* using {@code pread} optimization.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public static final boolean ENABLE_RESOLVE_OPTIMIZATIONS = shouldEnableResolveOptimizations();
|
||
|
|
||
|
@android.ravenwood.annotation.RavenwoodReplace
|
||
|
private static boolean shouldEnableResolveOptimizations() {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
private static boolean shouldEnableResolveOptimizations$ravenwood() {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parses the given xml string and fires events on the given SAX handler.
|
||
|
*/
|
||
|
public static void parse(String xml, ContentHandler contentHandler)
|
||
|
throws SAXException {
|
||
|
try {
|
||
|
XMLReader reader = newXMLReader();
|
||
|
reader.setContentHandler(contentHandler);
|
||
|
reader.parse(new InputSource(new StringReader(xml)));
|
||
|
} catch (IOException e) {
|
||
|
throw new AssertionError(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parses xml from the given reader and fires events on the given SAX
|
||
|
* handler.
|
||
|
*/
|
||
|
public static void parse(Reader in, ContentHandler contentHandler)
|
||
|
throws IOException, SAXException {
|
||
|
XMLReader reader = newXMLReader();
|
||
|
reader.setContentHandler(contentHandler);
|
||
|
reader.parse(new InputSource(in));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parses xml from the given input stream and fires events on the given SAX
|
||
|
* handler.
|
||
|
*/
|
||
|
public static void parse(InputStream in, Encoding encoding,
|
||
|
ContentHandler contentHandler) throws IOException, SAXException {
|
||
|
XMLReader reader = newXMLReader();
|
||
|
reader.setContentHandler(contentHandler);
|
||
|
InputSource source = new InputSource(in);
|
||
|
source.setEncoding(encoding.expatName);
|
||
|
reader.parse(source);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a new pull parser with namespace support.
|
||
|
*/
|
||
|
@android.ravenwood.annotation.RavenwoodReplace
|
||
|
public static XmlPullParser newPullParser() {
|
||
|
try {
|
||
|
XmlPullParser parser = newXmlPullParser();
|
||
|
parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true);
|
||
|
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
|
||
|
return parser;
|
||
|
} catch (XmlPullParserException e) {
|
||
|
throw new AssertionError(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public static XmlPullParser newPullParser$ravenwood() {
|
||
|
try {
|
||
|
// Prebuilt kxml2-android does not support FEATURE_PROCESS_DOCDECL, so omit here;
|
||
|
// it's quite rare and all tests are passing
|
||
|
XmlPullParser parser = newXmlPullParser();
|
||
|
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
|
||
|
return parser;
|
||
|
} catch (XmlPullParserException e) {
|
||
|
throw new AssertionError(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a new {@link TypedXmlPullParser} which is optimized for use
|
||
|
* inside the system, typically by supporting only a basic set of features.
|
||
|
* <p>
|
||
|
* In particular, the returned parser does not support namespaces, prefixes,
|
||
|
* properties, or options.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@SuppressWarnings("AndroidFrameworkEfficientXml")
|
||
|
public static @NonNull TypedXmlPullParser newFastPullParser() {
|
||
|
return XmlUtils.makeTyped(newPullParser());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a new {@link XmlPullParser} that reads XML documents using a
|
||
|
* custom binary wire protocol which benchmarking has shown to be 8.5x
|
||
|
* faster than {@code Xml.newFastPullParser()} for a typical
|
||
|
* {@code packages.xml}.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@android.ravenwood.annotation.RavenwoodReplace
|
||
|
public static @NonNull TypedXmlPullParser newBinaryPullParser() {
|
||
|
return new ArtBinaryXmlPullParser();
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public static TypedXmlPullParser newBinaryPullParser$ravenwood() {
|
||
|
// TODO: remove once we're linking against libcore
|
||
|
return new BinaryXmlPullParser();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a new {@link XmlPullParser} which is optimized for use inside the
|
||
|
* system, typically by supporting only a basic set of features.
|
||
|
* <p>
|
||
|
* This returned instance may be configured to read using an efficient
|
||
|
* binary format instead of a human-readable text format, depending on
|
||
|
* device feature flags.
|
||
|
* <p>
|
||
|
* To ensure that both formats are detected and transparently handled
|
||
|
* correctly, you must shift to using both {@link #resolveSerializer} and
|
||
|
* {@link #resolvePullParser}.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public static @NonNull TypedXmlPullParser resolvePullParser(@NonNull InputStream in)
|
||
|
throws IOException {
|
||
|
final byte[] magic = new byte[4];
|
||
|
if (ENABLE_RESOLVE_OPTIMIZATIONS && in instanceof FileInputStream) {
|
||
|
try {
|
||
|
Os.pread(((FileInputStream) in).getFD(), magic, 0, magic.length, 0);
|
||
|
} catch (ErrnoException e) {
|
||
|
throw e.rethrowAsIOException();
|
||
|
}
|
||
|
} else {
|
||
|
if (!in.markSupported()) {
|
||
|
in = new BufferedInputStream(in);
|
||
|
}
|
||
|
in.mark(8);
|
||
|
in.read(magic);
|
||
|
in.reset();
|
||
|
}
|
||
|
|
||
|
final TypedXmlPullParser xml;
|
||
|
if (Arrays.equals(magic, BinaryXmlSerializer.PROTOCOL_MAGIC_VERSION_0)) {
|
||
|
xml = newBinaryPullParser();
|
||
|
} else {
|
||
|
xml = newFastPullParser();
|
||
|
}
|
||
|
try {
|
||
|
xml.setInput(in, StandardCharsets.UTF_8.name());
|
||
|
} catch (XmlPullParserException e) {
|
||
|
throw new IOException(e);
|
||
|
}
|
||
|
return xml;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a new xml serializer.
|
||
|
*/
|
||
|
public static XmlSerializer newSerializer() {
|
||
|
return newXmlSerializer();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a new {@link XmlSerializer} which is optimized for use inside the
|
||
|
* system, typically by supporting only a basic set of features.
|
||
|
* <p>
|
||
|
* In particular, the returned parser does not support namespaces, prefixes,
|
||
|
* properties, or options.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@SuppressWarnings("AndroidFrameworkEfficientXml")
|
||
|
public static @NonNull TypedXmlSerializer newFastSerializer() {
|
||
|
return XmlUtils.makeTyped(new FastXmlSerializer());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a new {@link XmlSerializer} that writes XML documents using a
|
||
|
* custom binary wire protocol which benchmarking has shown to be 4.4x
|
||
|
* faster and use 2.8x less disk space than {@code Xml.newFastSerializer()}
|
||
|
* for a typical {@code packages.xml}.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@android.ravenwood.annotation.RavenwoodReplace
|
||
|
public static @NonNull TypedXmlSerializer newBinarySerializer() {
|
||
|
return new ArtBinaryXmlSerializer();
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public static @NonNull TypedXmlSerializer newBinarySerializer$ravenwood() {
|
||
|
// TODO: remove once we're linking against libcore
|
||
|
return new BinaryXmlSerializer();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a new {@link XmlSerializer} which is optimized for use inside the
|
||
|
* system, typically by supporting only a basic set of features.
|
||
|
* <p>
|
||
|
* This returned instance may be configured to write using an efficient
|
||
|
* binary format instead of a human-readable text format, depending on
|
||
|
* device feature flags.
|
||
|
* <p>
|
||
|
* To ensure that both formats are detected and transparently handled
|
||
|
* correctly, you must shift to using both {@link #resolveSerializer} and
|
||
|
* {@link #resolvePullParser}.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@android.ravenwood.annotation.RavenwoodReplace
|
||
|
public static @NonNull TypedXmlSerializer resolveSerializer(@NonNull OutputStream out)
|
||
|
throws IOException {
|
||
|
final TypedXmlSerializer xml;
|
||
|
if (ENABLE_BINARY_DEFAULT) {
|
||
|
xml = newBinarySerializer();
|
||
|
} else {
|
||
|
xml = newFastSerializer();
|
||
|
}
|
||
|
xml.setOutput(out, StandardCharsets.UTF_8.name());
|
||
|
return xml;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public static @NonNull TypedXmlSerializer resolveSerializer$ravenwood(@NonNull OutputStream out)
|
||
|
throws IOException {
|
||
|
// TODO: remove once we're linking against libcore
|
||
|
final TypedXmlSerializer xml = new BinaryXmlSerializer();
|
||
|
xml.setOutput(out, StandardCharsets.UTF_8.name());
|
||
|
return xml;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Copy the first XML document into the second document.
|
||
|
* <p>
|
||
|
* Implemented by reading all events from the given {@link XmlPullParser}
|
||
|
* and writing them directly to the given {@link XmlSerializer}. This can be
|
||
|
* useful for transparently converting between underlying wire protocols.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public static void copy(@NonNull XmlPullParser in, @NonNull XmlSerializer out)
|
||
|
throws XmlPullParserException, IOException {
|
||
|
// Some parsers may have already consumed the event that starts the
|
||
|
// document, so we manually emit that event here for consistency
|
||
|
if (in.getEventType() == XmlPullParser.START_DOCUMENT) {
|
||
|
out.startDocument(in.getInputEncoding(), true);
|
||
|
}
|
||
|
|
||
|
while (true) {
|
||
|
final int token = in.nextToken();
|
||
|
switch (token) {
|
||
|
case XmlPullParser.START_DOCUMENT:
|
||
|
out.startDocument(in.getInputEncoding(), true);
|
||
|
break;
|
||
|
case XmlPullParser.END_DOCUMENT:
|
||
|
out.endDocument();
|
||
|
return;
|
||
|
case XmlPullParser.START_TAG:
|
||
|
out.startTag(normalizeNamespace(in.getNamespace()), in.getName());
|
||
|
for (int i = 0; i < in.getAttributeCount(); i++) {
|
||
|
out.attribute(normalizeNamespace(in.getAttributeNamespace(i)),
|
||
|
in.getAttributeName(i), in.getAttributeValue(i));
|
||
|
}
|
||
|
break;
|
||
|
case XmlPullParser.END_TAG:
|
||
|
out.endTag(normalizeNamespace(in.getNamespace()), in.getName());
|
||
|
break;
|
||
|
case XmlPullParser.TEXT:
|
||
|
out.text(in.getText());
|
||
|
break;
|
||
|
case XmlPullParser.CDSECT:
|
||
|
out.cdsect(in.getText());
|
||
|
break;
|
||
|
case XmlPullParser.ENTITY_REF:
|
||
|
out.entityRef(in.getName());
|
||
|
break;
|
||
|
case XmlPullParser.IGNORABLE_WHITESPACE:
|
||
|
out.ignorableWhitespace(in.getText());
|
||
|
break;
|
||
|
case XmlPullParser.PROCESSING_INSTRUCTION:
|
||
|
out.processingInstruction(in.getText());
|
||
|
break;
|
||
|
case XmlPullParser.COMMENT:
|
||
|
out.comment(in.getText());
|
||
|
break;
|
||
|
case XmlPullParser.DOCDECL:
|
||
|
out.docdecl(in.getText());
|
||
|
break;
|
||
|
default:
|
||
|
throw new IllegalStateException("Unknown token " + token);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Some parsers may return an empty string {@code ""} when a namespace in
|
||
|
* unsupported, which can confuse serializers. This method normalizes empty
|
||
|
* strings to be {@code null}.
|
||
|
*/
|
||
|
private static @Nullable String normalizeNamespace(@Nullable String namespace) {
|
||
|
if (namespace == null || namespace.isEmpty()) {
|
||
|
return null;
|
||
|
} else {
|
||
|
return namespace;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Supported character encodings.
|
||
|
*/
|
||
|
public enum Encoding {
|
||
|
|
||
|
US_ASCII("US-ASCII"),
|
||
|
UTF_8("UTF-8"),
|
||
|
UTF_16("UTF-16"),
|
||
|
ISO_8859_1("ISO-8859-1");
|
||
|
|
||
|
final String expatName;
|
||
|
|
||
|
Encoding(String expatName) {
|
||
|
this.expatName = expatName;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Finds an encoding by name. Returns UTF-8 if you pass {@code null}.
|
||
|
*/
|
||
|
public static Encoding findEncodingByName(String encodingName)
|
||
|
throws UnsupportedEncodingException {
|
||
|
if (encodingName == null) {
|
||
|
return Encoding.UTF_8;
|
||
|
}
|
||
|
|
||
|
for (Encoding encoding : Encoding.values()) {
|
||
|
if (encoding.expatName.equalsIgnoreCase(encodingName))
|
||
|
return encoding;
|
||
|
}
|
||
|
throw new UnsupportedEncodingException(encodingName);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return an AttributeSet interface for use with the given XmlPullParser.
|
||
|
* If the given parser itself implements AttributeSet, that implementation
|
||
|
* is simply returned. Otherwise a wrapper class is
|
||
|
* instantiated on top of the XmlPullParser, as a proxy for retrieving its
|
||
|
* attributes, and returned to you.
|
||
|
*
|
||
|
* @param parser The existing parser for which you would like an
|
||
|
* AttributeSet.
|
||
|
*
|
||
|
* @return An AttributeSet you can use to retrieve the
|
||
|
* attribute values at each of the tags as the parser moves
|
||
|
* through its XML document.
|
||
|
*
|
||
|
* @see AttributeSet
|
||
|
*/
|
||
|
public static AttributeSet asAttributeSet(XmlPullParser parser) {
|
||
|
return (parser instanceof AttributeSet)
|
||
|
? (AttributeSet) parser
|
||
|
: new XmlPullAttributes(parser);
|
||
|
}
|
||
|
|
||
|
@android.ravenwood.annotation.RavenwoodReplace
|
||
|
private static @NonNull XmlSerializer newXmlSerializer() {
|
||
|
return XmlObjectFactory.newXmlSerializer();
|
||
|
}
|
||
|
|
||
|
private static @NonNull XmlSerializer newXmlSerializer$ravenwood() {
|
||
|
try {
|
||
|
return XmlPullParserFactory.newInstance().newSerializer();
|
||
|
} catch (XmlPullParserException e) {
|
||
|
throw new UnsupportedOperationException(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@android.ravenwood.annotation.RavenwoodReplace
|
||
|
private static @NonNull XmlPullParser newXmlPullParser() {
|
||
|
return XmlObjectFactory.newXmlPullParser();
|
||
|
}
|
||
|
|
||
|
private static @NonNull XmlPullParser newXmlPullParser$ravenwood() {
|
||
|
try {
|
||
|
return XmlPullParserFactory.newInstance().newPullParser();
|
||
|
} catch (XmlPullParserException e) {
|
||
|
throw new UnsupportedOperationException(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@android.ravenwood.annotation.RavenwoodReplace
|
||
|
private static @NonNull XMLReader newXMLReader() {
|
||
|
return XmlObjectFactory.newXMLReader();
|
||
|
}
|
||
|
|
||
|
private static @NonNull XMLReader newXMLReader$ravenwood() {
|
||
|
try {
|
||
|
final SAXParserFactory factory = SAXParserFactory.newInstance();
|
||
|
factory.setNamespaceAware(true);
|
||
|
return factory.newSAXParser().getXMLReader();
|
||
|
} catch (Exception e) {
|
||
|
throw new UnsupportedOperationException(e);
|
||
|
}
|
||
|
}
|
||
|
}
|