XML Stream Writer und Namespaces

Martin Kompf

Die Java Streaming API für XML (StAX) eignet sich nicht nur zum Parsen, sondern auch zum Schreiben von XML Dokumenten. So lässt sich zum Beispiel ganz einfach ein Atom Feed mit den wichtigsten News zur eigenen Website erstellen.

Der Artikel XML Stream Reader stellt eine Methode zum Lesen eines Atom Feeds mittels der Klasse XMLStreamReader aus der Java Streaming API (StAX) vor. Aber auch für den umgekehrten Weg, das Schreiben von XML, bietet StAX eine Lösung an, den XMLStreamWriter.

Ganz easy

Den XMLStreamWriter erzeugt man mittels einer XMLOutputFactory. Dabei ist das Encoding der XML Datei mit anzugeben. Der dazu notwendige Code wird hier in der Methode openWriter gekapselt. Das notwendige Schließen des Writers nach getaner Arbeit übernimmt dann closeWriter():

import java.io.*;

import javax.xml.stream.*;

public class AtomFeedStreamWriter {

  private static final String URI_NS_ATOM = "http://www.w3.org/2005/Atom";
  private static final String URI_NS_XHTML = "http://www.w3.org/1999/xhtml";

  private XMLStreamWriter writer;

  public void openWriter(File file) throws FileNotFoundException,
      XMLStreamException, FactoryConfigurationError {
    FileOutputStream fos = new FileOutputStream(file);
    writer = XMLOutputFactory.newInstance().createXMLStreamWriter(fos, "UTF-8");
  }
  
  public void closeWriter() throws XMLStreamException {
    writer.close();
  }

Der neue XMLStreamWriter stellt unter anderem die Methoden writeStartElement(name) und writeEndElement() zur Verfügung, mit denen sich Start- und Endtag eines Elements in den XML Ausgabestrom schreiben lassen. Soll das Element Attribute haben, können diese per writeAttribute(name, value) angehängt werden. Für Elemente, die keine weiteren Kinder besitzen, können Start- und Endtag auch mittels writeEmptyElement in einem Rutsch geschrieben werden. Zur Ausgabe von Text dient dagegen writeCharacters(text).

Mit diesen wenigen Methoden kann man bereits die Ausgabe eines <entry> Elements für einen Atom Feed inklusive Titel und Linkadresse programmieren. Man muss allerdings genau darauf achten, dass ein writeStartElement stets ein korrespondierendes writeEndElement hat - der Stream Writer stellt nicht automatisch die Wohlgeformtheit des XML Dokuments sicher! Allerdings übernimmt er selbsttätig die Umkodierung und Maskierung bestimmter reservierter Zeichen wie <, & und >. Um das Vorkommen dieser Zeichen in Text und Attributwerten muss sich der Programmierer also keine Gedanken mehr machen:

  public void writeEntry(String title, String link)
      throws XMLStreamException {
    writer.writeStartElement(URI_NS_ATOM, "entry");
    writeElementWithText("title", title);
    writer.writeEmptyElement(URI_NS_ATOM, "link");
    writer.writeAttribute("href", link);
    writeElementWithText("id", link);
    writer.writeEndElement(); // entry
    writer.writeCharacters("\n");
  }
  
  private void writeElementWithText(String elName, String text)
      throws XMLStreamException {
    writer.writeStartElement(URI_NS_ATOM, elName);
    writer.writeCharacters(text);
    writer.writeEndElement();
  }

Das hiermit ausgegebene XML Fragment könnte dann beispielsweise folgendermaßen aussehen:

<entry>
<title>Neue Version des GPX Track Viewers</title>
<link href="http://www.kompf.de/trekka/"/>
<id>http://www.kompf.de/trekka/</id>
</entry>

Achtung Namespaces!

Beim Schreiben der Elemente per writeStartElement wurde nicht nur der Name des Elements, sondern auch sein Namespace URI_NS_ATOM mit angegeben. XML Namespaces dienen zur Unterscheidung von XML Elementen, die den gleichen Namen haben, aber aus unterschiedlichen Anwendungsdomänen stammen. Beispielsweise kennen sowohl das Atom Feed Format als auch XHTML die Elemente title und link. Da es nun möglich ist, XHTML in einen Atom Feed einzubetten, sind zur Vermeidung von Namenskollisionen Namespaces eingeführt worden. Namespaces werden durch eine URI (Uniform Resource Identifier) weltweit eindeutig identifiziert: So sind alle Elemente aus der Atom Domäne im Namespace http://www.w3.org/2005/Atom definiert, während XHTML-Elemente aus dem Namespace mit der URI http://www.w3.org/1999/xhtml kommen.

Eine XML-Datei, die sowohl Atom als auch XHTML Elemente enthält, könnte zum Beispiel so aussehen:

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

  <entry>
    <title>Neuer Artikel AtomFeedStreamWriter</title>
    <content type="xhtml">
      <xh:div xmlns:xh="http://www.w3.org/1999/xhtml">
        <xh:h1>AtomFeed Stream Writer</xh:h1>
        Die Java Streaming API für XML ...
      </xh:div>
    </content>
  </entry>

</feed>

Um nicht bei jedem Element die komplette Namespace-URI hinschreiben zu müssen, wurde hier von mehreren Vereinfachungsmöglichkeiten Gebrauch gemacht: Über das Spezialattribut xmlns erfolgt die Abbildung einer Namespace-URI auf ein frei wählbares Prefix. Mittels xmlns:xh="http://www.w3.org/1999/xhtml" werden so im Beispiel alle Elemente mit dem Prefix xh als zum XHTML-Namespace zugehörig gekennzeichnet. Weiterhin gibt es die Möglichkeit, einen Default-Namespace für alle Elemente festzulegen, die kein explizites Namespace-Prefix haben. Das erfolgt durch Verwendung des xmlns Attributs ohne Angabe eines Prefixes: xmlns="http://www.w3.org/2005/Atom".

Den Default-Namespace muss man dem XMLStreamWriter mitteilen - natürlich bevor das erste Element geschrieben wird. Auch die Ausgabe des xmlns Attributs muss explizit programmiert werden:

  public void writeFeedStart(String title, String link, String author, String updated)
      throws XMLStreamException {
    writer.setDefaultNamespace(URI_NS_ATOM); // setze Default-Namespace
    writer.writeStartDocument("UTF-8", "1.0");
    writer.writeCharacters("\n");
    writer.writeStartElement(URI_NS_ATOM, "feed");
    writer.writeDefaultNamespace(URI_NS_ATOM); // schreibe xmlns Attribut
    writer.writeCharacters("\n");
    
    // Code gekürzt
  }

Will man jetzt XHTML in den Feed einbetten, dann reicht es nicht, einfach die Namespace-URI von XHTML in writeStartElement zu übergeben, sondern man muss sowohl das zu verwendende Prefix xh registrieren als auch das xmlns:xh Attribut schreiben:

  private void writeXHTMLContent(String title, String content) 
      throws XMLStreamException {
    writer.writeStartElement(URI_NS_ATOM, "content");
    writer.writeAttribute("type", "xhtml");
    writer.setPrefix("xh", URI_NS_XHTML); // registriere Prefix
    writer.writeStartElement(URI_NS_XHTML, "div");
    writer.writeNamespace("xh", URI_NS_XHTML); // schreibe xmlns:xh Attribut
    writer.writeStartElement(URI_NS_XHTML, "h1");
    writer.writeCharacters(title);
    writer.writeEndElement(); // h1
    writer.writeCharacters(content);
    writer.writeEndElement(); // div
    writer.writeEndElement(); // content
    writer.writeCharacters("\n");
  }

Falls man beim Erzeugen des XMLStreamWriter aus der XMLOutputFactory die Property javax.xml.stream.isRepairingNamespaces auf true setzt, dann kann auf setPrefix und writeNamespace verzichtet werden. In diesem Fall generiert der Stream Writer dynamisch Prefixes, diese sehen dann aber sehr kryptisch aus (zum Beispiel zdef-379513775), sodass das XML für Menschen schnell unlesbar wird.

Fazit und Alternativen

Da es sich bei XML um Text handelt, kann man als einfachste Alternative zu XMLStreamWriter natürlich auch die verschiedenen Klassen aus java.io verwenden:

FileOutputStream fos = new FileOutputStream("text.xml");
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8"));
writer.append("<feed>");
writer.append("<title>kompf.de News</title>");
// und so weiter
writer.close();

Hierbei muss man natürlich darauf achten, dass bestimmte Zeichen in Textelementen und Attributen nicht erlaubt sind und diese umkodieren. Während zum Beispiel xmlWriter.writeCharacters von sich aus das Zeichen < in &lt; umwandelt, muss man dies bei Verwendung von BufferedWriter.write selber tun. So liegt die Verantwortung für die Wohlgeformtheit des XML Dokuments vollständig in den Händen des Programmieres.

Eine andere Möglichkeit zur Ausgabe von XML ist die Verwendung eines Transformers, wie im Artikel DOM API beschrieben. Dieses Verfahren garantiert die Erzeugung eines wohlgeformten XML Dokuments. Es verlangt aber als Ausgangspunkt einen DOM-Tree, der zusätzlichen Hauptspeicher belegt und programmatisch nur relativ umständlich aufzubauen ist.

Ein weiterer Artikel wird ein Verfahren vorstellen, mit dem Java-XML-Binding Framework JAXB aus Java Objekten direkt XML zu erzeugen.

Die hier gezeigte Methode der Benutzung von XMLStreamWriter stellt einen annehmbaren Kompromiss aus Komplexität, Bequemlichkeit, Speicherverbrauch und Performance dar. Zwar muss sich der Programmierer hier immer noch um Wohlgeformtheit und Formatierung der XML Ausgabe selber kümmern, andere Dinge wie die korrekte Zeichenkodierung und die grundsätzliche Behandlung von Namespaces übernimmt jedoch schon die Streaming API.