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