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

Commit b8d46988 authored by Paul Westbrook's avatar Paul Westbrook
Browse files

Bug 2330278

Deleted old copy of SimplePullParser, since it has been moved to
gdata
parent 7f6f6310
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());
    }
}