DOM API

Martin Kompf

Das Document Object Model (DOM) dient zur Abbildung kompletter XML Dokumente als Baum von Java Objekten. Damit lässt sich ein dynamisches und programmgesteuertes Erstellen und Modifizieren von XML Daten im Speicher der Java Virtual Machine realisieren. Das Umwandeln von XML Dateien in DOM und zurück erfolgt mit den Komponenten Parser und Transformer aus der JAXP API.

Die Artikel XML Stream Reader und SAX Parser stellen eventbasierte XML Parser vor, die nach dem Push- oder Pull-Prinzip arbeiten. Daneben gibt es in JAXP eine weitere API, die das Darstellen einer kompletten XML Datei in Form von Java Objekten erlaubt: DOM. Diese API ist in drei Bestandteile gegliedert:

Lesen und Schreiben

Das Parsen einer XML Datei mittels DOM erfordert nur drei JAXP-Aufrufe, die hier in der Funktion parseFile gekapselt werden:

import java.io.*;

import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.*;
import org.xml.sax.SAXException;

public class GpxDOMEditor {

  private Document parseFile(File file)
      throws ParserConfigurationException, SAXException, IOException {
    DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
    DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
    return docBuilder.parse(file);
  }

parseFile liefert ein Objekt vom Typ org.w3c.dom.Document zurück, das die komplette XML Datei repräsentiert. Darauf wird gleich noch genauer eingegangen, zunächst soll jedoch das Zurückschreiben des Document in eine Datei vorgestellt werden:

  private void writeFile(Document doc, File file) throws TransformerException {
    TransformerFactory transFactory = TransformerFactory.newInstance();
    Transformer trans = transFactory.newTransformer();
    trans.setOutputProperty(OutputKeys.INDENT, "yes");
    trans.setOutputProperty(OutputKeys.METHOD, "xml");
    trans.transform(new DOMSource(doc), new StreamResult(file));
  }

Die Umwandlung von DOM in eine XML Datei übernimmt hier ein Transformer, der ebenfalls Bestandteil von JAXP ist. Der Code zeigt nur einen Bruchteil der Fähigkeiten des Transformers. So wäre es möglich, beim Erzeugen des Transformers ein XSLT Stylesheet anzugeben. Dieses enthält Regeln zur Umwandlung des Dokuments während der Transformation. Damit kann man aus ein und demselben DOM durch Verwendung verschiedener Stylesheets unterschiedliche Repräsentationen, wie XML, HTML oder Text, erzeugen.

Beispiel GPX Datei

Die Arbeit mit dem im Speicher befindlichen DOM soll anhand eines Beispiels aus der Welt der GPS Navigation demonstriert werden. Ein etabliertes Format zum Austausch von GPS Daten ist das GPS Exchange Format GPX. Es basiert - natürlich - auf XML und kann im wesentlichen Tracks, Routen und Wegpunkte transportieren. Eine GPX-Datei könnte folgendermaßen aussehen:

<gpx>
  <wpt lat="49.991739" lon="8.413982">
    <ele>90</ele>
    <name>Rüsselheim</name>
  </wpt>
  <wpt lat="49.988097" lon="8.400249">
    <ele>90</ele>
    <name>Opelwerk</name>
  </wpt>
  <rte>
    <name>Route 1</name>
    <rtept lat="50.00156" lon="8.2588"/>
    <rtept lat="49.990194" lon="8.289871"/>
    <rtept lat="49.994387" lon="8.31459"/>
    <rtept lat="49.991629" lon="8.413982">
      <name>Mainz</name>
    </rtept>
  </rte>
</gpx>

Diese Datei enthält zwei Wegpunkte und eine Route, die aus vier Punkten besteht. Weg- und Routenpunkte haben mindestens die Attribute lat und lon, welche die geografischen Koordinaten (Breite und Länge, engl. latitude and longitude) bezeichnen. Darüber hinaus ist die Angabe weiterer Kindelemente, wie Name (name), Zeitstempel (time) oder Höhe (ele) möglich.

Manipulieren des DOM

Als Beispiel dient eine GPX-Datei mit einigen Wegpunkten, die man sich hier herunterladen kann. Die Datei lässt sich in jedem Texteditor betrachten oder mit dem GPX Trackviewer Trekka auf einer Landkarte visualisieren. Die Wegpunkte werden dort als einzelne Pins dargestellt. Ziel der Beispielanwendung soll sein, aus den Wegpunkten eine zusammenhängende Route zu erzeugen, die in Trekka als durchgehende Linie dargestellt wird.

Wichtig für das Verständnis der DOM-API ist, das man XML und DOM als Baum von Knoten betrachtet: Jede XML Datei hat genau einen Wurzelknoten, der auch als Root-Node oder Root-Element bezeichnet wird. In einer GPX-Datei heißt das Root-Element gpx. Ein Knoten (engl. node) kann Kinder (children) oder Geschwister (siblings) haben: wpt und rte sind direkte Kinder von gpx. Beide können wiederum Kinder haben, zum Beispiel hat wpt die Kinder ele und name. Die beiden letzteren sind Siblings. Auch name hat ein Kind in Form eines Textknotens - Knoten müssen nicht immer Elemente sein.

Der Code beginnt mit der Bestimmung des Root-Elements und einem Test, ob es sich hierbei wirklich im ein gpx Element handelt. Ausgangspunkt dabei ist das von parseFile zurückgelieferte Document:

  private void createRouteFromWaypoints(Document gpxDoc) throws SAXException {
    // get the GPX element
    Element gpxElement = gpxDoc.getDocumentElement();
    if (! "gpx".equals(gpxElement.getNodeName())) {
      throw new SAXException("This is not a GPX file: " + gpxElement.getNodeName());
    }

Eine sehr mächtige Methode von DOM ist die Möglichkeit, eine Liste aller Elemente mit einem bestimmten Namen zu erstellen:

    // get the waypoints
    NodeList wptList = gpxDoc.getElementsByTagName("wpt");

In wptList stehen jetzt alle Wegpunkte. Wenn die Liste nicht leer ist, dann erzeugt man die Route (Element rte) und hängt sie mittels appendChild als Kind unter dem Root-Element ein. Außerdem bekommt sie noch einen Namen:

    if (wptList.getLength() > 0) {
      // create the route with a name
      Element rteElement = gpxDoc.createElement("rte");
      gpxElement.appendChild(rteElement);
      rteElement.appendChild(createElementWithText(gpxDoc, "name", "Created from waypoints"));

Nun iteriert man durch die Liste der Wegpunkte und erzeugt für jeden ein rtept Element, welches als Kind unter die gerade erzeugte Route gehängt wird. Die Attribute lat und lon werden direkt vom Wegpunkt kopiert:

      // for each waypoint: create a routepoint with the same lat/lon attributes
      for (int i = 0; i < wptList.getLength(); ++i) {
        Element wpt = (Element) wptList.item(i);
        Element rtept = gpxDoc.createElement("rtept");
        copyAttributes(wpt, rtept);
        rteElement.appendChild(rtept);
      }
    }
  }

Die beiden verwendeten Hilfsfunktionen createElementWithText und copyAttributes zeigen, wie man mit Text- und Attributknoten umgeht:

  /**
   * Copy all attributes of the source element to the destination element.
   * @param src The source element.
   * @param dst The destination element.
   */
  private void copyAttributes(Element src, Element dst) {
    NamedNodeMap attrs = src.getAttributes();
    for (int i = 0; i < attrs.getLength(); ++i) {
      dst.setAttributeNode((Attr) attrs.item(i).cloneNode(false));
    }
  }

  /**
   * Create an element that contains a text node.
   * @param doc The document.
   * @param tagName The name of the element.
   * @param text The text.
   * @return The newly created element.
   */
  private Element createElementWithText(Document doc, String tagName, String text) {
    Element el = doc.createElement(tagName);
    el.appendChild(doc.createTextNode(text));
    return el;
  }

Damit ist alles notwendige beisammen, die main Methode gestaltet sich demzufolge sehr einfach:

  public static void main(String[] args) throws Exception {
    if (args.length == 0) {
      args = new String[] {
          "Rue-Mz.gpx", "Rue-Mz-Rte.gpx"
      };
    }
    File inFile = new File(args[0]);
    File outFile = new File(args[1]);
    
    GpxDOMEditor gpxDOMEditor = new GpxDOMEditor();
    Document gpxDoc = gpxDOMEditor.parseFile(inFile);
    gpxDOMEditor.createRouteFromWaypoints(gpxDoc);
    gpxDOMEditor.writeFile(gpxDoc, outFile);
    
    System.out.println("GPX file created: " + outFile.getAbsolutePath());
  }

Das Resultat kann wiederum mit Trekka betrachtet werden, die einzelnen Wegpunkte sollten nun durch eine farbige Linie, die Route, verbunden sein.

Fazit

Das Beispiel hat einige wichtige Funktionen zum Navigieren auf dem DOM und zum Modifieren seines Inhalts vorgestellt. Eine vollständigere Übersicht über die DOM-API bietet die Javadoc des Package org.w3c.dom und dort insbesondere Document, Node und Element. Im Gegensatz zu SAX und StAX wird bei DOM immer das komplette XML Dokument im Hauptspeicher gehalten. Das erlaubt einerseits das einfache Navigieren und Modifizieren. Andererseits kann der Hauptspeicherbedarf bei sehr großen oder sehr vielen Dokumenten zu einem Engpass führen. Auch dauert das Parsen eines kompletten Dokuments per DOM sicher länger als das Extrahieren einiger weniger Elemente per SAX.

Die Entscheidung zwischen SAX/StaX und DOM hängt also vom Anwendungsfall und dem zu erwartenden Datenvolumen ab. Benötigt man nur einen Subset der Input-Elemente und erfolgt die Weiterverarbeitung der Daten nicht in der XML-Domäne, dann sind die eventbasierten StaX/SAX-Parser die bessere Wahl. DOM sollte verwendet werden, wenn wirklich der komplette Inhalt des XMLs im Speicher benötigt wird. Zum Beispiel um komplizierte Navigationsoperationen oder eine Modifikation des Inhalts auszuführen, mit dem Ziel, wiederum XML auszugeben.