FileDocCategorySizeDatePackage
XmlGDataParser.javaAPI DocAndroid 1.5 API17147Wed May 06 22:41:16 BST 2009com.google.wireless.gdata.parser.xml

XmlGDataParser.java

// Copyright 2008 The Android Open Source Project

package com.google.wireless.gdata.parser.xml;

import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.data.Feed;
import com.google.wireless.gdata.data.StringUtils;
import com.google.wireless.gdata.data.XmlUtils;
import com.google.wireless.gdata.parser.GDataParser;
import com.google.wireless.gdata.parser.ParseException;

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

import java.io.IOException;
import java.io.InputStream;

/**
 * {@link GDataParser} that uses an {@link XmlPullParser} to parse a GData feed.
 */
// NOTE: we do not perform any validity checks on the XML.
public class XmlGDataParser implements GDataParser {

  /** Namespace URI for Atom */
  public static final String NAMESPACE_ATOM_URI =
      "http://www.w3.org/2005/Atom";

  public static final String NAMESPACE_OPENSEARCH = "openSearch";

  public static final String NAMESPACE_OPENSEARCH_URI =
      "http://a9.com/-/spec/opensearchrss/1.0/";

  /** Namespace prefix for GData */
  public static final String NAMESPACE_GD = "gd";

  /** Namespace URI for GData */
  public static final String NAMESPACE_GD_URI =
      "http://schemas.google.com/g/2005";

  private final InputStream is;
  private final XmlPullParser parser;
  private boolean isInBadState;

  /**
   * Creates a new XmlGDataParser for a feed in the provided InputStream.
   * @param is The InputStream that should be parsed.
   * @throws ParseException Thrown if an XmlPullParser could not be created
   * or set around this InputStream.
   */
  public XmlGDataParser(InputStream is, XmlPullParser parser)
      throws ParseException {
    this.is = is;
    this.parser = parser;
    this.isInBadState = false;
    if (this.is != null) {
      try {
        this.parser.setInput(is, null /* encoding */);
      } catch (XmlPullParserException e) {
        throw new ParseException("Could not create XmlGDataParser", e);
      }
    }
  }

  /*
  * (non-Javadoc)
  * @see com.google.wireless.gdata.parser.GDataParser#init()
  */
  public final Feed init() throws ParseException {
    int eventType;
    try {
      eventType = parser.getEventType();
    } catch (XmlPullParserException e) {
      throw new ParseException("Could not parse GData feed.", e);
    }
    if (eventType != XmlPullParser.START_DOCUMENT) {
      throw new ParseException("Attempting to initialize parsing beyond "
          + "the start of the document.");
    }

    try {
      eventType = parser.next();
    } catch (XmlPullParserException xppe) {
      throw new ParseException("Could not read next event.", xppe);
    } catch (IOException ioe) {
      throw new ParseException("Could not read next event.", ioe);
    }
    while (eventType != XmlPullParser.END_DOCUMENT) {
      switch (eventType) {
        case XmlPullParser.START_TAG:
          String name = parser.getName();
          if ("feed".equals(name)) {
            try {
              return parseFeed();
            } catch (XmlPullParserException xppe) {
              throw new ParseException("Unable to parse <feed>.",
                  xppe);
            } catch (IOException ioe) {
              throw new ParseException("Unable to parse <feed>.",
                  ioe);
            }
          }
          break;
        default:
          // ignore
          break;
      }

      try {
        eventType = parser.next();
      } catch (XmlPullParserException xppe) {
        throw new ParseException("Could not read next event.", xppe);
      } catch (IOException ioe) {
        throw new ParseException("Could not read next event." , ioe);
      }
    }
    throw new ParseException("No <feed> found in document.");
  }

  /**
   * Returns the {@link XmlPullParser} being used to parse this feed.
   */
  protected final XmlPullParser getParser() {
    return parser;
  }

  /**
   * Creates a new {@link Feed} that should be filled with information about
   * the feed that will be parsed.
   * @return The {@link Feed} that should be filled.
   */
  protected Feed createFeed() {
    return new Feed();
  }

  /**
   * Creates a new {@link Entry} that should be filled with information about
   * the entry that will be parsed.
   * @return The {@link Entry} that should be filled.
   */
  protected Entry createEntry() {
    return new Entry();
  }

  /**
   * Parses the feed (but not any entries).
   *
   * @return A new {@link Feed} containing information about the feed.
   * @throws XmlPullParserException Thrown if the XML document cannot be
   * parsed.
   * @throws IOException Thrown if the {@link InputStream} behind the feed
   * cannot be read.
   */
  private final Feed parseFeed() throws XmlPullParserException, IOException {
    Feed feed = createFeed();
    // parsing <feed>
    // not interested in any attributes -- move onto the children.
    int eventType = parser.next();
    while (eventType != XmlPullParser.END_DOCUMENT) {
      switch (eventType) {
        case XmlPullParser.START_TAG:
          String name = parser.getName();
          if ("totalResults".equals(name)) {
            feed.setTotalResults(StringUtils.parseInt(
                XmlUtils.extractChildText(parser), 0));
          } else if ("startIndex".equals(name)) {
            feed.setStartIndex(StringUtils.parseInt(
                XmlUtils.extractChildText(parser), 0));
          } else if ("itemsPerPage".equals(name)) {
            feed.setItemsPerPage(StringUtils.parseInt(
                XmlUtils.extractChildText(parser), 0));
          } else if ("title".equals(name)) {
            feed.setTitle(XmlUtils.extractChildText(parser));
          } else if ("id".equals(name)) {
            feed.setId(XmlUtils.extractChildText(parser));
          } else if ("updated".equals(name)) {
            feed.setLastUpdated(XmlUtils.extractChildText(parser));
          } else if ("category".equals(name)) {
            String category =
                parser.getAttributeValue(null /* ns */, "term");
            if (!StringUtils.isEmpty(category)) {
              feed.setCategory(category);
            }
            String categoryScheme =
                parser.getAttributeValue(null /* ns */, "scheme");
            if (!StringUtils.isEmpty(categoryScheme)) {
              feed.setCategoryScheme(categoryScheme);
            }
          } else if ("entry".equals(name)) {
            // stop parsing here.
            // TODO: pay attention to depth?
            return feed;
          } else {
            handleExtraElementInFeed(feed);
          }
          break;
        default:
          break;
      }
      eventType = parser.next();
    }
    // if we get here, we have a feed with no entries.
    return feed;
  }

  /**
   * Hook that allows extra (service-specific) elements in a <feed> to
   * be parsed.
   * @param feed The {@link Feed} being filled.
   */
  protected void handleExtraElementInFeed(Feed feed)
      throws XmlPullParserException, IOException {
    // no-op in this class.
  }

  /*
  * (non-Javadoc)
  * @see com.google.wireless.gdata.parser.GDataParser#hasMoreData()
  */
  public boolean hasMoreData() {
    if (isInBadState) {
      return false;
    }
    try {
      int eventType = parser.getEventType();
      return (eventType != XmlPullParser.END_DOCUMENT);
    } catch (XmlPullParserException xppe) {
      return false;
    }
  }

  /*
  * (non-Javadoc)
  * @see com.google.wireless.gdata.parser.GDataParser#readNextEntry
  */
  public Entry readNextEntry(Entry entry) throws ParseException, IOException {
    if (!hasMoreData()) {
      throw new IllegalStateException("you shouldn't call this if hasMoreData() is false");
    }

    int eventType;
    try {
      eventType = parser.getEventType();
    } catch (XmlPullParserException e) {
      throw new ParseException("Could not parse entry.", e);
    }

    if (eventType != XmlPullParser.START_TAG) {
      throw new ParseException("Expected event START_TAG: Actual event: "
          + XmlPullParser.TYPES[eventType]);
    }

    String name = parser.getName();
    if (!"entry".equals(name)) {
      throw new ParseException("Expected <entry>: Actual element: "
          + "<" + name + ">");
    }

    if (entry == null) {
      entry = createEntry();
    } else {
      entry.clear();
    }

    try {
      parser.next();
      handleEntry(entry);
      entry.validate();
    } catch (ParseException xppe1) {
      try {
        if (hasMoreData()) skipToNextEntry();
      } catch (XmlPullParserException xppe2) {
        // squelch the error -- let the original one stand.
        // set isInBadState to ensure that the next call to hasMoreData() will return false.
        isInBadState = true;
      }
      throw new ParseException("Could not parse <entry>, " + entry, xppe1);
    } catch (XmlPullParserException xppe1) {
      try {
        if (hasMoreData()) skipToNextEntry();
      } catch (XmlPullParserException xppe2) {
        // squelch the error -- let the original one stand.
        // set isInBadState to ensure that the next call to hasMoreData() will return false.
        isInBadState = true;
      }
      throw new ParseException("Could not parse <entry>, " + entry, xppe1);
    }
    return entry;
  }

  /**
   * Parses a GData entry.  You can either call {@link #init()} or
   * {@link #parseStandaloneEntry()} for a given feed.
   *
   * @return The parsed entry.
   * @throws ParseException Thrown if the entry could not be parsed.
   */
  public Entry parseStandaloneEntry() throws ParseException, IOException {
    Entry entry = createEntry();

    int eventType;
    try {
      eventType = parser.getEventType();
    } catch (XmlPullParserException e) {
      throw new ParseException("Could not parse GData entry.", e);
    }
    if (eventType != XmlPullParser.START_DOCUMENT) {
      throw new ParseException("Attempting to initialize parsing beyond "
          + "the start of the document.");
    }

    try {
      eventType = parser.next();
    } catch (XmlPullParserException xppe) {
      throw new ParseException("Could not read next event.", xppe);
    } catch (IOException ioe) {
      throw new ParseException("Could not read next event.", ioe);
    }
    while (eventType != XmlPullParser.END_DOCUMENT) {
      switch (eventType) {
        case XmlPullParser.START_TAG:
          String name = parser.getName();
          if ("entry".equals(name)) {
            try {
              parser.next();
              handleEntry(entry);
              return entry;
            } catch (XmlPullParserException xppe) {
              throw new ParseException("Unable to parse <entry>.",
                  xppe);
            } catch (IOException ioe) {
              throw new ParseException("Unable to parse <entry>.",
                  ioe);
            }
          }
          break;
        default:
          // ignore
          break;
      }

      try {
        eventType = parser.next();
      } catch (XmlPullParserException xppe) {
        throw new ParseException("Could not read next event.", xppe);
      }
    }
    throw new ParseException("No <entry> found in document.");
  }

  /**
   * Skips the rest of the current entry until the parser reaches the next entry, if any.
   * Does nothing if the parser is already at the beginning of an entry.
   */
  protected void skipToNextEntry() throws IOException, XmlPullParserException {
    if (!hasMoreData()) {
      throw new IllegalStateException("you shouldn't call this if hasMoreData() is false");
    }

    int eventType = parser.getEventType();

    // skip ahead until we reach an <entry> tag.
    while (eventType != XmlPullParser.END_DOCUMENT) {
      switch (eventType) {
        case XmlPullParser.START_TAG:
          if ("entry".equals(parser.getName())) {
            return;
          }
          break;
      }
      eventType = parser.next();
    }
  }

  /**
   * Parses the current entry in the XML document.  Assumes that the parser
   * is currently pointing just after an <entry>.
   *
   * @param entry The entry that will be filled.
   * @throws XmlPullParserException Thrown if the XML cannot be parsed.
   * @throws IOException Thrown if the underlying inputstream cannot be read.
   */
  protected void handleEntry(Entry entry)
      throws XmlPullParserException, IOException, ParseException {
    int eventType = parser.getEventType();
    while (eventType != XmlPullParser.END_DOCUMENT) {
      switch (eventType) {
        case XmlPullParser.START_TAG:
          // TODO: make sure these elements are at the expected depth.
          String name = parser.getName();
          if ("entry".equals(name)) {
            // stop parsing here.
            return;
          } else if ("id".equals(name)) {
            entry.setId(XmlUtils.extractChildText(parser));
          } else if ("title".equals(name)) {
            entry.setTitle(XmlUtils.extractChildText(parser));
          } else if ("link".equals(name)) {
            String rel =
                parser.getAttributeValue(null /* ns */, "rel");
            String type =
                parser.getAttributeValue(null /* ns */, "type");
            String href =
                parser.getAttributeValue(null /* ns */, "href");
            if ("edit".equals(rel)) {
              entry.setEditUri(href);
            } else if (("alternate").equals(rel) && ("text/html".equals(type))) {
                entry.setHtmlUri(href);
            } else {
              handleExtraLinkInEntry(rel,
                  type,
                  href,
                  entry);
            }
          } else if ("summary".equals(name)) {
            entry.setSummary(XmlUtils.extractChildText(parser));
          } else if ("content".equals(name)) {
            // TODO: parse the type
            entry.setContent(XmlUtils.extractChildText(parser));
          } else if ("author".equals(name)) {
            handleAuthor(entry);
          } else if ("category".equals(name)) {
            String category =
                parser.getAttributeValue(null /* ns */, "term");
            if (category != null && category.length() > 0) {
              entry.setCategory(category);
            }
            String categoryScheme =
                parser.getAttributeValue(null /* ns */, "scheme");
            if (categoryScheme != null && category.length() > 0) {
              entry.setCategoryScheme(categoryScheme);
            }
          } else if ("published".equals(name)) {
            entry.setPublicationDate(
                XmlUtils.extractChildText(parser));
          } else if ("updated".equals(name)) {
            entry.setUpdateDate(XmlUtils.extractChildText(parser));
          } else if ("deleted".equals(name)) {
            entry.setDeleted(true);
          } else {
            handleExtraElementInEntry(entry);
          }
          break;
        default:
          break;
      }

      eventType = parser.next();
    }
  }

  private void handleAuthor(Entry entry)
      throws XmlPullParserException, IOException {

    int eventType = parser.getEventType();
    String name = parser.getName();

    if (eventType != XmlPullParser.START_TAG ||
        (!"author".equals(parser.getName()))) {
      // should not happen.
      throw new
          IllegalStateException("Expected <author>: Actual element: <"
          + parser.getName() + ">");
    }

    eventType = parser.next();
    while (eventType != XmlPullParser.END_DOCUMENT) {
      switch (eventType) {
        case XmlPullParser.START_TAG:
          name = parser.getName();
          if ("name".equals(name)) {
            String authorName = XmlUtils.extractChildText(parser);
            entry.setAuthor(authorName);
          } else if ("email".equals(name)) {
            String email = XmlUtils.extractChildText(parser);
            entry.setEmail(email);
          }
          break;
        case XmlPullParser.END_TAG:
          name = parser.getName();
          if ("author".equals(name)) {
            return;
          }
        default:
          // ignore
      }

      eventType = parser.next();
    }
  }

  /*
  * (non-Javadoc)
  * @see com.google.wireless.gdata.parser.GDataParser#close()
  */
  public void close() {
    if (is != null) {
      try {
        is.close();
      } catch (IOException ioe) {
        // ignore
      }
    }
  }

  /**
   * Hook that allows extra (service-specific) elements in an <entry>
   * to be parsed.
   * @param entry The {@link Entry} being filled.
   */
  protected void handleExtraElementInEntry(Entry entry)
      throws XmlPullParserException, IOException, ParseException {
    // no-op in this class.
  }

  /**
   * Hook that allows extra (service-specific) <link>s in an entry to be
   * parsed.
   * @param rel The rel attribute value.
   * @param type The type attribute value.
   * @param href The href attribute value.
   * @param entry The {@link Entry} being filled.
   */
  protected void handleExtraLinkInEntry(String rel,
      String type,
      String href,
      Entry entry)
      throws XmlPullParserException, IOException {
    // no-op in this class.
  }
}