Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e8bae73b authored by Android (Google) Code Review's avatar Android (Google) Code Review
Browse files

Merge change I3632fede

* changes:
  Bug 2330278
parents 41c56178 b8d46988
Loading
Loading
Loading
Loading
+0 −391
Original line number Diff line number Diff line
/*
 * Copyright (C) 2008 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.google.android.util;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.Reader;
import java.io.Closeable;

import android.util.Xml;
import android.util.Log;

/**
 * This is an abstraction of a pull parser that provides several benefits:<ul>
 *   <li>it is easier to use robustly because it makes it trivial to handle unexpected tags (which
 *   might have children)</li>
 *   <li>it makes the handling of text (cdata) blocks more convenient</li>
 *   <li>it provides convenient methods for getting a mandatory attribute (and throwing an exception
 *   if it is missing) or an optional attribute (and using a default value if it is missing)
 * </ul>
 */
public class SimplePullParser {
    public static final String TEXT_TAG = "![CDATA[";

    private String mLogTag = null;
    private final XmlPullParser mParser;
    private Closeable source;
    private String mCurrentStartTag;

    /**
     * Constructs a new SimplePullParser to parse the stream
     * @param stream stream to parse
     * @param encoding the encoding to use
     */
    public SimplePullParser(InputStream stream, String encoding)
            throws ParseException, IOException {
        try {
            XmlPullParser parser = Xml.newPullParser();
            parser.setInput(stream, encoding);
            moveToStartDocument(parser);
            mParser = parser;
            mCurrentStartTag = null;
            source = stream;
        } catch (XmlPullParserException e) {
            throw new ParseException(e);
        }
    }

    /**
     * Constructs a new SimplePullParser to parse the xml
     * @param parser the underlying parser to use
     */
    public SimplePullParser(XmlPullParser parser) {
        mParser = parser;
        mCurrentStartTag = null;
        source = null;
    }

    /**
     * Constructs a new SimplePullParser to parse the xml
     * @param xml the xml to parse
     */
    public SimplePullParser(String xml) throws IOException, ParseException {
        this(new StringReader(xml));
    }

    /**
     * Constructs a new SimplePullParser to parse the xml
     * @param reader a reader containing the xml
     */
    public SimplePullParser(Reader reader) throws IOException, ParseException {
        try {
            XmlPullParser parser = Xml.newPullParser();
            parser.setInput(reader);
            moveToStartDocument(parser);
            mParser = parser;
            mCurrentStartTag = null;
            source = reader;
        } catch (XmlPullParserException e) {
            throw new ParseException(e);
        }
    }

    private static void moveToStartDocument(XmlPullParser parser)
            throws XmlPullParserException, IOException {
        int eventType;
        eventType = parser.getEventType();
        if (eventType != XmlPullParser.START_DOCUMENT) {
            throw new XmlPullParserException("Not at start of response");
        }
    }

    /**
     * Enables logging to the provided log tag. A basic representation of the xml will be logged as
     * the xml is parsed. No logging is done unless this is called.
     *
     * @param logTag the log tag to use when logging
     */
    public void setLogTag(String logTag) {
        mLogTag = logTag;
    }

    /**
     * Returns the tag of the next element whose depth is parentDepth plus one
     * or null if there are no more such elements before the next start tag. When this returns,
     * getDepth() and all methods relating to attributes will refer to the element whose tag is
     * returned.
     *
     * @param parentDepth the depth of the parrent of the item to be returned
     * @param textBuilder if null then text blocks will be ignored. If
     *   non-null then text blocks will be added to the builder and TEXT_TAG
     *   will be returned when one is found
     * @return the next of the next child element's tag, TEXT_TAG if a text block is found, or null
     *   if there are no more child elements or DATA blocks
     * @throws IOException propogated from the underlying parser
     * @throws ParseException if there was an error parsing the xml.
     */
    public String nextTagOrText(int parentDepth, StringBuilder textBuilder)
            throws IOException, ParseException {
        while (true) {
            int eventType = 0;
            try {
                eventType = mParser.next();
            } catch (XmlPullParserException e) {
                throw new ParseException(e);
            }
            int depth = mParser.getDepth();
            mCurrentStartTag = null;

            if (eventType == XmlPullParser.START_TAG && depth == parentDepth + 1) {
                mCurrentStartTag = mParser.getName();
                if (mLogTag != null && Log.isLoggable(mLogTag, Log.DEBUG)) {
                    StringBuilder sb = new StringBuilder();
                    for (int i = 0; i < depth; i++) sb.append("  ");
                    sb.append("<").append(mParser.getName());
                    int count = mParser.getAttributeCount();
                    for (int i = 0; i < count; i++) {
                        sb.append(" ");
                        sb.append(mParser.getAttributeName(i));
                        sb.append("=\"");
                        sb.append(mParser.getAttributeValue(i));
                        sb.append("\"");
                    }
                    sb.append(">");
                    Log.d(mLogTag, sb.toString());
                }
                return mParser.getName();
            }

            if (eventType == XmlPullParser.END_TAG && depth == parentDepth) {
                if (mLogTag != null && Log.isLoggable(mLogTag, Log.DEBUG)) {
                    StringBuilder sb = new StringBuilder();
                    for (int i = 0; i < depth; i++) sb.append("  ");
                    sb.append("</>"); // Not quite valid xml but it gets the job done.
                    Log.d(mLogTag, sb.toString());
                }
                return null;
            }

            if (eventType == XmlPullParser.END_DOCUMENT && parentDepth == 0) {
                // we could just rely on the caller calling close(), which it should, but try
                // to auto-close for clients that might have missed doing so.
                if (source != null) {
                    source.close();
                    source = null;
                }
                return null;
            }

            if (eventType == XmlPullParser.TEXT && depth == parentDepth) {
                if (textBuilder == null) {
                    continue;
                }
                String text = mParser.getText();
                textBuilder.append(text);
                return TEXT_TAG;
            }
        }
    }

    /**
     * The same as nextTagOrTexxt(int, StringBuilder) but ignores text blocks.
     */
    public String nextTag(int parentDepth) throws IOException, ParseException {
        return nextTagOrText(parentDepth, null /* ignore text */);
    }

    /**
     * Returns the depth of the current element. The depth is 0 before the first
     * element has been returned, 1 after that, etc.
     *
     * @return the depth of the current element
     */
    public int getDepth() {
        return mParser.getDepth();
    }

    /**
     * Consumes the rest of the children, accumulating any text at this level into the builder.
     *
     * @param textBuilder the builder to contain any text
     * @throws IOException propogated from the XmlPullParser
     * @throws ParseException if there was an error parsing the xml.
     */
    public void readRemainingText(int parentDepth, StringBuilder textBuilder)
            throws IOException, ParseException {
        while (nextTagOrText(parentDepth, textBuilder) != null) {
        }
    }

    /**
     * Returns the number of attributes on the current element.
     *
     * @return the number of attributes on the current element
     */
    public int numAttributes() {
        return mParser.getAttributeCount();
    }

    /**
     * Returns the name of the nth attribute on the current element.
     *
     * @return the name of the nth attribute on the current element
     */
    public String getAttributeName(int i) {
        return mParser.getAttributeName(i);
    }

    /**
     * Returns the namespace of the nth attribute on the current element.
     *
     * @return the namespace of the nth attribute on the current element
     */
    public String getAttributeNamespace(int i) {
        return mParser.getAttributeNamespace(i);
    }

    /**
     * Returns the string value of the named attribute.
     *
     * @param namespace the namespace of the attribute
     * @param name the name of the attribute
     * @param defaultValue the value to return if the attribute is not specified
     * @return the value of the attribute
     */
    public String getStringAttribute(
            String namespace, String name, String defaultValue) {
        String value = mParser.getAttributeValue(namespace, name);
        if (null == value) return defaultValue;
        return value;
    }

    /**
     * Returns the string value of the named attribute. An exception will
     * be thrown if the attribute is not present.
     *
     * @param namespace the namespace of the attribute
     * @param name the name of the attribute @return the value of the attribute
     * @throws ParseException thrown if the attribute is missing
     */
    public String getStringAttribute(String namespace, String name) throws ParseException {
        String value = mParser.getAttributeValue(namespace, name);
        if (null == value) {
            throw new ParseException(
                    "missing '" + name + "' attribute on '" + mCurrentStartTag + "' element");
        }
        return value;
    }

    /**
     * Returns the string value of the named attribute. An exception will
     * be thrown if the attribute is not a valid integer.
     *
     * @param namespace the namespace of the attribute
     * @param name the name of the attribute
     * @param defaultValue the value to return if the attribute is not specified
     * @return the value of the attribute
     * @throws ParseException thrown if the attribute not a valid integer.
     */
    public int getIntAttribute(String namespace, String name, int defaultValue)
            throws ParseException {
        String value = mParser.getAttributeValue(namespace, name);
        if (null == value) return defaultValue;
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            throw new ParseException("Cannot parse '" + value + "' as an integer");
        }
    }

    /**
     * Returns the string value of the named attribute. An exception will
     * be thrown if the attribute is not present or is not a valid integer.
     *
     * @param namespace the namespace of the attribute
     * @param name the name of the attribute @return the value of the attribute
     * @throws ParseException thrown if the attribute is missing or not a valid integer.
     */
    public int getIntAttribute(String namespace, String name)
            throws ParseException {
        String value = getStringAttribute(namespace, name);
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            throw new ParseException("Cannot parse '" + value + "' as an integer");
        }
    }

    /**
     * Returns the string value of the named attribute. An exception will
     * be thrown if the attribute is not a valid long.
     *
     * @param namespace the namespace of the attribute
     * @param name the name of the attribute @return the value of the attribute
     * @throws ParseException thrown if the attribute is not a valid long.
     */
    public long getLongAttribute(String namespace, String name, long defaultValue)
            throws ParseException {
        String value = mParser.getAttributeValue(namespace, name);
        if (null == value) return defaultValue;
        try {
            return Long.parseLong(value);
        } catch (NumberFormatException e) {
            throw new ParseException("Cannot parse '" + value + "' as a long");
        }
    }

    /**
     * Close this SimplePullParser and any underlying resources (e.g., its InputStream or
     * Reader source) used by this SimplePullParser.
     */
    public void close() {
        if (source != null) {
            try {
                source.close();
            } catch (IOException ioe) {
                // ignore
            }
        }
    }

    /**
     * Returns the string value of the named attribute. An exception will
     * be thrown if the attribute is not present or is not a valid long.
     *
     * @param namespace the namespace of the attribute
     * @param name the name of the attribute @return the value of the attribute
     * @throws ParseException thrown if the attribute is missing or not a valid long.
     */
    public long getLongAttribute(String namespace, String name)
            throws ParseException {
        String value = getStringAttribute(namespace, name);
        try {
            return Long.parseLong(value);
        } catch (NumberFormatException e) {
            throw new ParseException("Cannot parse '" + value + "' as a long");
        }
    }

    public static final class ParseException extends Exception {
        public ParseException(String message) {
            super(message);
        }

        public ParseException(String message, Throwable cause) {
            super(message, cause);
        }

        public ParseException(Throwable cause) {
            super(cause);
        }
    }
}
+0 −132
Original line number Diff line number Diff line
/*
 * 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 com.android.unit_tests;

import com.google.android.util.SimplePullParser;
import junit.framework.TestCase;
import android.test.suitebuilder.annotation.SmallTest;

public class SimplePullParserTest extends TestCase {
    @SmallTest
    public void testTwoLevels() throws Exception {
        String xml = ""
                + "<top a='1' b='hello'>\n"
                + "  <next c='2' d='there'/>\n"
                + "  <next c='3' d='bye'/>\n"
                + "</top>";
        SimplePullParser parser = new SimplePullParser(xml);
        int depth0 = parser.getDepth();
        assertEquals(0, depth0);
        assertEquals("top", parser.nextTag(depth0));
        assertEquals(1, parser.getIntAttribute(null, "a"));
        assertEquals("hello", parser.getStringAttribute(null, "b"));

        int depth1 = parser.getDepth();
        assertEquals(1, depth1);
        assertEquals("next", parser.nextTag(depth1));
        assertEquals(2, parser.getIntAttribute(null, "c"));
        assertEquals("there", parser.getStringAttribute(null, "d"));
        assertEquals("next", parser.nextTag(depth1));
        assertEquals(3, parser.getIntAttribute(null, "c"));
        assertEquals("bye", parser.getStringAttribute(null, "d"));
        assertNull(parser.nextTag(depth1));

        assertNull(parser.nextTag(depth0));
    }

    @SmallTest
    public void testAttributes() throws Exception {
        String xml = "<top a='1' b='hello'/>";
        SimplePullParser parser = new SimplePullParser(xml);
        int depth = parser.getDepth();
        parser.nextTag(depth);

        assertEquals(2, parser.numAttributes());
        assertEquals("a", parser.getAttributeName(0));
        assertEquals("b", parser.getAttributeName(1));

        assertEquals(1, parser.getIntAttribute(null, "a"));
        assertEquals(5, parser.getIntAttribute(null, "c", 5));
        assertEquals("hello", parser.getStringAttribute(null, "b"));
        assertEquals("not", parser.getStringAttribute(null, "d", "not"));
    }

    @SmallTest
    public void testRecovery() throws Exception {
        String xml = ""
                + "<top a='1' b='hello'>\n"
                + "  <middle c='2' d='there'>\n"
                + "    <inner/>\n"
                + "    <inner2/>\n"
                + "    <inner3/>\n"
                + "  </middle>\n"
                + "  <middle2/>\n"
                + "</top>";
        SimplePullParser parser = new SimplePullParser(xml);
        assertEquals(0, parser.getDepth());
        assertEquals("top", parser.nextTag(0));
        assertEquals(1, parser.getDepth());
        assertEquals("middle", parser.nextTag(1));
        assertEquals(2, parser.getDepth());
        assertEquals("inner", parser.nextTag(2));
        // Now skip some elements.
        assertEquals("middle2", parser.nextTag(1));
    }

    @SmallTest
    public void testCdata() throws Exception {
        StringBuilder cdataBuilder;
        String xml = ""
                + "<top>"
                + "<![CDATA[data0]]>"
                + "<next0/>"
                + "<![CDATA[data1]]>"
                + "<next1/>"
                + "<![CDATA[data2]]>"
                + "<next2/>"
                + "<![CDATA[data3]]>"
                + "<next3/>"
                + "<![CDATA[data4]]>"
                + "<next4/>"
                + "<![CDATA[data5]]>"
                + "</top>";
        SimplePullParser parser = new SimplePullParser(xml);
        assertEquals("top", parser.nextTag(0));

        // We can ignore cdata by not passing a cdata builder.
        assertEquals("next0", parser.nextTag(1));

        // We can get the most recent cdata by passing an empty cdata builder.
        cdataBuilder = new StringBuilder();
        assertSame(SimplePullParser.TEXT_TAG, parser.nextTagOrText(1, cdataBuilder));
        assertEquals("data1", cdataBuilder.toString());
        assertEquals("next1", parser.nextTag(1));

        // We can join multiple cdatas by reusing a builder.
        cdataBuilder = new StringBuilder();
        assertSame(SimplePullParser.TEXT_TAG, parser.nextTagOrText(1, cdataBuilder));
        assertEquals("next2", parser.nextTag(1));
        assertSame(SimplePullParser.TEXT_TAG, parser.nextTagOrText(1, cdataBuilder));
        assertEquals("data2data3", cdataBuilder.toString());
        assertEquals("next3", parser.nextTag(1));

        // We can read all of the remaining cdata while ignoring any elements.
        cdataBuilder = new StringBuilder();
        parser.readRemainingText(1, cdataBuilder);
        assertEquals("data4data5", cdataBuilder.toString());
    }
}