| /* |
| * 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); |
| } |
| } |
| } |