XML Stream Reader

Martin Kompf

Eine einfache und performante Methode zum Parsen eines XML Streams mittels Java ist der XML Stream Reader. Der Artikel zeigt eine beispielhafte Implementierung zum Lesen und Auswerten eines Atom Feeds.

Java und XML ergänzen sich hervorragend. Während Java eine Sprache zum Programmieren von Algorithmen ist, dient XML zur Beschreibung von Dokumenten und Daten. Diese Daten können von einem Java-Programm gelesen, verarbeitet und geschrieben werden. Sun hat schon frühzeitig die Bedeutung von XML erkannt und eine API mit dem Namen JAXP zum Parsen von XML-Daten definiert.

JAXP stellt verschiedene Methoden zum Parsen von XML zur Verfügung: Einen dokumentenorientierten DOM-Parser und die zwei eventbasierten Parser SAX und StAX. StAX, die Streaming API for XML, ist erst seit Java 6 in der Standard-API vertreten. Sie stellt sehr effektive Methoden zum Lesen und Schreiben von XML-Streams bereit. Ein Bestandteil von StAX, der XML Stream Reader, soll Thema dieses Artikels sein.

XML Atom Feed

Die Beispielanwendung soll einen Atom News Feed lesen und die Titel der einzelnen Nachrichten als Text ausgeben. Schauen wir uns zunächst das von einem Atom Feed gelieferte XML einmal an:

<feed xmlns="http://www.w3.org/2005/Atom">
  <title>heise online News</title>

  <entry>
    <title>Windows Server und vereinfachte Tarife für Amazons Webservices</title>
    <link href="http://..." />
  </entry>

  <entry>
    <title>Forscher entwickeln Diesel-Katalysatoren ohne Platin</title>
    <link href="..." />
  </entry>
</feed>

Ohne sich groß mit der Theorie und den Spezifikationen von XML oder Atom zu befassen, erkennt man hier leicht die generelle Struktur eines XML-Dokuments: XML besteht aus Elementen, die durch Start- und Endtags begrenzt sind. So gibt es im Beispiel mehrere Elemente entry, welche durch die Tags <entry> und </entry> begrenzt werden. Ein Element kann wiederum andere Elemente (zum Beispiel title) oder Text enthalten. Hat ein Element keinen Inhalt, so kann man Start- und Endtag zusammenfassen, wie bei <link /> zu sehen. Außerdem kann ein Element mehrere Attribute haben, die innerhalb des Starttags notiert werden. Ein Attribut besitzt einen Namen und einen Wert. Im Beispiel haben feed und link jeweils ein Attribut.

Die Namen der Elemente und ihrer Attribute sind völlig frei wählbar, sofern man sich an gewisse Regeln zur verwendbaren Zeichenmenge hält. Die Tatsache, dass unser XML die Elemente feed, entry und so weiter enthält, ist nicht Bestandteil der XML-Spezifikation, sondern wird durch das Atom Syndication Format vorgegeben. Eine XML-Datei, die nicht von einem Atom Feed stammt, sondern vielleicht geografische Daten enthält, hat zwar prinzipiell die gleiche Struktur aus Elementen, Attributen und Text. Jedoch tragen die Elemente wahrscheinlich völlig andere Namen und haben eine andere Bedeutung und Anordnung.

Pullen mit dem Stream Reader

Um die gestellte Aufgabe zu erfüllen, muss also der Textinhalt aller title Elemente bestimmt werden, die sich innerhalb eines entry Elements befinden.

Dazu bauen wir die Java-Klasse AtomFeedStreamReader:

import java.io.*;
import java.net.URL;
import java.util.*;

import javax.xml.stream.*;
import javax.xml.stream.events.XMLEvent;

/**
 * Read and parse an AtomFeed XML stream and print the titles of the entries.
 */
public class AtomFeedStreamReader {

  public List<String> readNewsTitles(URL url) throws IOException,
      XMLStreamException, FactoryConfigurationError {

Die Methode readNewsTitles bekommt die URL des Atom Feeds übergeben und liefert eine Liste aller Nachrichtentitel zurück. Zunächst wird die Liste erzeugt und die URL zum Lesen geöffnet:

    List<String> titleList = new LinkedList<String>();
    InputStream in = url.openStream();

Mit dem InputStream lässt sich sofort ein XML Stream Reader erzeugen, der die eigentliche XML-Verarbeitung durchführt. Der Code dafür wird in einem try-finally Block gekapselt, um das Schließen des InputStream unter allen Bedingungen - also auch im Fehlerfall - sicherzustellen.

Der erzeugte XMLStreamReader ist die zentrale Instanz zur Verarbeitung der XML-Daten. Die Verarbeitung erfolgt dabei auf Basis von Events, die man mittels der Methoden hasNext() und next() aus dem Reader herausziehen kann - man spricht daher auch von einem Pull-Parser. Typischerweise kombiniert man diese beiden Methoden in einer while-Schleife:

    try {
      XMLStreamReader reader = XMLInputFactory.newInstance()
          .createXMLStreamReader(in);
          
      // while-Schleife zum Verarbeiten der Events vom reader
      while (reader.hasNext()) {
        int event = reader.next();
        if (event == XMLEvent.START_ELEMENT
            && "entry".equals(reader.getLocalName())) {
          titleList.add(readEntry(reader));
        }
      }
    } finally {
      in.close();
    }

    return titleList;
  }

Die Methode hasNext() liefert zurück, ob noch weitere Events vorliegen. Falls ja, lässt sich mittels next() der Eventtyp bestimmen. Eine vollständige Liste aller Eventtypen liefert die Javadoc von XMLStreamConstants. Das wohl am meisten verwendete Event ist START_ELEMENT, das beim Parsen des Starttags auftritt.

In Abhängigkeit vom aktuellen Event kann man weitere Informationen aus dem Reader abfragen. Im Falle von START_ELEMENT zum Beispiel den Namen des Elements, seine Attribute oder den enthaltenen Text. Wäre das aktuelle Event dagegen zum Beispiel CHARACTERS, dann ließe sich nur der Text abfragen, Attribute oder Tags gibt es für Characters nicht.

Der Beispielcode liest solange Events aus dem Stream, bis keine mehr da sind. Tritt dabei ein Starttag mit dem Namen entry auf, dann wird die Methode readEntry aufgerufen:

  private String readEntry(XMLStreamReader reader) throws XMLStreamException {
    String title = "";
    while (reader.hasNext()) {
      int event = reader.next();
      if (XMLEvent.START_ELEMENT == event
          && "title".equals(reader.getLocalName())) {
        title = reader.getElementText();
        break;
      }
    }
    return title;
  }

Sie holt weiter Events aus dem Stream, bis ein title Tag zum Vorschein kommt. Mittels getElementText() wird der Textinhalt bestimmt und an die Titelliste angehängt. Die ineinander verschachtelten while-Schleifen sind notwendig, damit man nur die title unterhalb von entry erfasst und nicht etwa auch title direkt unterhalb von feed!

Fertig!

Der Rest des Programms befasst sich mit dem Konstruieren der URL und der Ausgabe des Ergebnis:

  private void printTitles(List<String> titleList, PrintStream out) {
    for (String name : titleList) {
      out.println(name);
    }
  }
  
  public static void main(String[] args) throws Exception {
    // Heise News
    URL url = new URL("http://www.heise.de/newsticker/heise-atom.xml");
    // Twitter public time line
    URL url2 = new URL("http://api.twitter.com/1/statuses/public_timeline.atom");
    
    AtomFeedStreamReader atomFeedStreamReader = new AtomFeedStreamReader();
    List<String> titles = atomFeedStreamReader.readNewsTitles(url);
    atomFeedStreamReader.printTitles(titles, System.out);
  }

Fazit

Dank der standardisierten Formate für XML und Atom lässt sich das Programm ohne weitere Änderungen zum Lesen der verschiedensten Feeds von Heise bis Twitter verwenden. Das Grundprinzip des XML Stream Readers ist, dass sich immer nur ein kleiner Teil des zu verarbeitenden XMLs im Zugriff des Programmieres befindet. Man muss sich sequentiell von vorn nach hinten durch das XML durcharbeiten. Ist einmal ein Element überlesen worden, gibt es keine Chance mehr auf ein Wiedersehen. Dokumentenorientierte Methoden, wie getParent() oder getChildren() gibt es beim Stream Reader nicht. Dafür arbeitet er sehr performant und benötigt relativ wenig Hauptspeicher.