• +49-(0)721-402485-12
Ihre Experten für XML, XQuery und XML-Datenbanken

DOM

In den vorangehenden Kapiteln habe ich über Java und XML im allgemeinen gesprochen, aber nur SAX im einzelnen beschrieben. Es ist Ihnen sicher bereits bewußt, daß SAX nur eine von mehreren APIs ist, die es ermöglichen, XML-Arbeit mit Java zu erledigen. Dieses Kapitel und das folgende werden Ihre API-Kenntnisse ausweiten, denn ich führe das Document Object Model ein, üblicherweise DOM genannt. Diese API unterscheidet sich ziemlich von SAX und ergänzt sich auf vielfältige Art und Weise mit der Simple API for XML. Sie benötigen beide, und auch die anderen APIs und Werkzeuge im Rest dieses Buches, um ein kompetenter XML-Entwickler zu sein.

Da DOM sich grundlegend von SAX unterscheidet, werde ich eine längere Zeit damit verbringen, die mit DOM verbundenen Konzepte zu behandeln, und erklären, warum es für manche Anwendungen an Stelle von SAX verwendet werden sollte. Die Auswahl irgendeiner XML-API erfordert Kompromisse, und die Wahl zwischen DOM und SAX macht dabei sicherlich keine Ausnahme. Ich werde dann zu dem wohl wichtigsten Thema übergehen: Code. Ich werde Ihnen eine Utility-Klasse vorstellen, die DOM-Bäume serialisiert, etwas, das in der DOM-API selbst derzeit nicht enthalten ist. Dies wird einen ziemlich guten Überblick über die Struktur von DOM und die zugehörigen Klassen bieten und Sie auf fortgeschrittenere Arbeit mit DOM vorbereiten. Schließlich werde ich Ihnen im »Vorsicht Falle!«-Abschnitt einige Problembereiche und wichtige Aspekte von DOM aufzeigen.

Das Document Object Model

Im Gegensatz zu SAX hat das Document Object Model seinen Ursprung beim World Wide Web Consortium (W3C). Während SAX Public Domain-Software ist, die durch lange Diskussionen in der XML-dev-Mailingliste weiterentwickelt wird, ist DOM ein Standard, genau wie die eigentliche XML-Spezifikation. DOM wurde nicht speziell für Java entworfen, sondern um den Inhalt und das Modell von Dokumenten über alle Programmiersprachen und Werkzeuge hinweg darzustellen. Es gibt Bindungen für JavaScript, Java, CORBA und andere Sprachen, was DOM zu einer plattform- und sprachen-übergreifenden Spezifikation macht.

Abgesehen davon, daß es sich hinsichtlich der Standardisierung und der Sprachenbindungen von SAX unterscheidet, ist DOM in »Levels« statt in Versionen angeordnet. DOM Level One ist eine anerkannte Empfehlung, und Sie können die vollständige Spezifikation unter http://www.w3.org/TR/REC-DOM-Level-1/ nachschlagen. Level 1 enthält die Details der Funktionalität und der Navigation durch Inhalte innerhalb eines Dokuments. Ein Dokument in DOM ist nicht nur auf XML beschränkt, sondern kann auch aus HTML und anderen Inhaltsmodellen bestehen!

Level 2, das im November 2000 fertiggestellt wurde, baut auf Level 1 auf, indem es Module und Optionen bereitstellt, die für bestimmte Inhaltsmodelle wie XML, HTML und Cascading Style Sheets (CSS) entworfen wurden. Diese weniger allgemeinen Module fangen an, die »Lücken zu schließen«, die von den allgemeineren Werkzeugen in DOM Level 1 zur Verfügung gestellt werden. Sie können die aktuelle Level-2-Empfehlung unter http://www.w3.org/TR/DOM-Level-2/ nachlesen. An Level 3 wird bereits gearbeitet, und es sollte sogar noch mehr Möglichkeiten für bestimmte Arten von Dokumenten hinzufügen, wie etwa Validierungs-Handler für XML und weitere Bestandteile, die ich im Kapitel DOM für Fortgeschrittene behandeln werde.

Sprachenbindungen

Die Verwendung von DOM für eine bestimmte Programmiersprache erfordert einen Satz von Interfaces und Klassen, die DOM als solches definieren und implementieren. Da die beteiligten Methoden nicht speziell in der DOM-Spezifikation dargestellt werden und sich statt dessen auf das Modell eines Dokuments konzentrieren, müssen Sprachenbindungen entwickelt werden, um die konzeptionelle Struktur von DOM für dessen Verwendung in Java oder jeder anderen Sprache darzustellen. Diese Sprachenbindungen dienen dann als API, mit der Sie Dokumente auf die Art und Weise manipulieren können, die in der DOM-Spezifikation beschrieben wird.

Ich beschäftige mich in diesem Buch verständlicherweise mit der Java-Sprachenbindung. Die neuesten Java-Bindungen, die DOM-Level-2-Java-Bindungen, können von http://www.w3.org/TR/DOM-Level-2/java-binding.html heruntergeladen werden. Die Klassen, die Sie zu Ihrem Klassenpfad hinzufügen können sollten, befinden sich alle im Package org.w3c.dom (und dessen Unter-Packages). Allerdings sollten Sie, bevor Sie sie selbst herunterladen, den XML-Parser und den XSLT-Prozessor überprüfen, die Sie gekauft oder heruntergeladen haben; wie die SAX-Packages sind auch die DOM-Packages oft in diesen Produkten enthalten. Dies garantiert auch eine Übereinstimmung zwischen Ihrem Parser, Ihrem Prozessor und der unterstützten Version von DOM.

Die meisten XSLT-Prozessoren erzeugen eine DOM-Eingabe nicht selbst, sondern verlassen sich statt dessen auf einen XML-Parser, der in der Lage ist, einen DOM-Baum zu erzeugen. Dies dient zur Aufrechterhaltung der sehr lockeren Verbindung zwischen Parser und Prozessor, die auf diese Weise beide durch vergleichbare Produkte ersetzt werden können. Da Apache Xalan standardmäßig Apache Xerces für das XML-Parsing und die DOM-Erzeugung verwendet, ist es der Grad der Unterstützung von DOM durch Xerces, der von Interesse ist. Dasselbe wäre der Fall, wenn Sie Oracles XSLT- und XML-Prozessor und -Parser verwenden würden.1

Die Grundlagen

Zusätzlich zu den Grundlagen der DOM-Spezifikation möchte ich Ihnen einige Informationen über die DOM-Programmierstruktur selbst geben. Den Kern von DOM bildet ein Baummodell. Denken Sie daran, daß SAX Ihnen eine stückweise Ansicht eines XML-Dokuments bietet und jedes Ereignis im Parsing-Verlauf meldet, sobald es stattfindet. DOM ist auf vielerlei Art und Weise das Gegenteil davon, es liefert eine komplett im Speicher befindliche Darstellung des Dokuments. Das Dokument wird Ihnen im Baumformat zur Verfügung gestellt und baut vollständig auf dem DOM-Interface org.w3c.dom.Node auf. DOM bietet auch noch einige spezielle, von diesem Interface abgeleitete Interfaces wie Element, Document, Attr und Text. Auf diese Weise könnten Sie in einem typischen XML-Dokument eine Struktur erhalten, die so aussieht wie Abbildung 5-1.

Abbildung 5-1: DOM-Struktur, die XML darstellt
DOM-Struktur, die XML darstellt

Das Baummodell wird in jeder Hinsicht beibehalten. Besonders bemerkenswert ist dies im Falle der Element-Knoten mit Textwerten (wie dem Element titel). Statt daß der Textwert des Knotens durch den Element-Knoten verfügbar gemacht würde (zum Beispiel durch eine Methode getText( )), gibt es einen Kindknoten vom Typ Text. Also würden Sie das Kindobjekt (oder die Kindobjekte) und den Wert des Elements aus dem Text-Knoten selbst erhalten. Auch wenn dies ein bißchen merkwürdig erscheinen könnte, bewahrt es ein sehr strenges Baummodell in DOM und macht es möglich, daß Aufgaben wie das Durchwandern des Baumes sehr einfache Algorithmen ohne Unmengen besonderer Klassen sein können.

Wegen seines Modells können alle DOM-Strukturen entweder als ihr allgemeiner Typ, Node oder als ihr jeweiliger spezieller Typ (Element, Attr usw.) behandelt werden. Viele Navigationsmethoden wie getParent( ) und getChildren( ) sind Teil dieses grundlegenden Node-Interfaces. Auf diese Weise können Sie im Baum hinauf- und herunterwandern, ohne sich Gedanken über den richtigen Strukturtyp machen zu müssen.

Ein weiterer Aspekt von DOM, dessen Sie sich bewußt sein sollten, ist, daß es wie SAX seine eigenen Listenstrukturen definiert. Sie werden die Klassen NodeList und NamedNodeMap verwenden müssen, wenn Sie mit DOM arbeiten, anstatt einfach Java-Collections zu nutzen. Je nach Ihrem Standpunkt ist dies weder positiv noch negativ, sondern einfach etwas, das zum Leben gehört. Abbildung 5-2 zeigt ein einfaches Modell der wichtigsten Interfaces und Klassen von DOM im UML-Style, auf das Sie sich bei der Bearbeitung des gesamten restlichen Kapitels beziehen können.

Abbildung 5-2: UML-Modell der DOM-Hauptklassen und -Interfaces
UML-Modell der DOM-Hauptklassen und -Interfaces

Warum nicht SAX?

Eine letzte konzeptionelle Bemerkung, bevor wir mit dem Code loslegen, denn Anfänger könnten sich fragen, warum sie nicht einfach SAX für die Arbeit mit XML verwenden können. Aber manchmal ist die Verwendung von SAX so effektiv wie die Benutzung eines Hammers, um an einer Wand zu kratzen, es ist einfach nicht das richtige Werkzeug für die entsprechende Aufgabe. Ich nenne hier einige Bereiche, in denen SAX weniger gut geeignet ist.

SAX ist sequentiell

Das sequentielle Modell, das SAX anbietet, erlaubt keinen beliebigen Zugriff auf ein XML-Dokument. Mit anderen Worten: In SAX erhalten Sie Informationen über das XML-Dokument, wenn der Parser sie erhält, und verlieren diese Informationen, wenn der Parser sie verliert. Wenn das zweite Element in einem Dokument erreicht wird, kann es nicht auf Informationen im vierten Element zugreifen, da das Parsing des vierten Elements noch nicht stattgefunden hat. Wenn das vierte Element schließlich doch an der Reihe ist, kann es nicht auf das zweite Element »zurückschauen«. Natürlich steht es Ihnen frei, die vorgefundenen Informationen zu speichern, wenn die Verarbeitung weitergeht; allerdings kann die Programmierung all dieser speziellen Klassen ziemlich tückisch sein. Die andere, extremere Option wäre der Aufbau einer im Speicher befindlichen Darstellung des XML-Dokuments. Wir werden gleich feststellen, daß ein DOM-Parser genau das tut, deshalb wäre die Erledigung derselben Aufgabe in SAX sinnlos – und wahrscheinlich auch langsamer und schwieriger.

SAX-»Geschwister«

Auch der Wechsel von einem Element zu einem nebengeordneten ist mit dem SAX-Modell schwierig. Der durch SAX angebotene Zugriff ist zu einem Großteil hierarchisch, aber auch sequentiell. Sie erreichen die Blattknoten des ersten Elements und wandern dann den Baum wieder aufwärts, dann wieder hinunter zu den Blattknoten des zweiten Elements und so weiter. Zu keinem Zeitpunkt gibt es eine klare Angabe darüber, auf welcher »Ebene« der Hierarchie Sie sich befinden. Obwohl dies durch ein paar clevere Counter nachgerüstet werden kann, ist es nicht das, wofür SAX entworfen wurde. Es gibt kein Konzept eines Geschwisterelements, des nächsten Elements auf derselben Ebene oder für die Bestimmung, welche Elemente in welche anderen Elemente hineinverschachtelt sind.

Das Problem an diesem Informationsmangel ist, daß ein XSLT-Prozessor (näher erläutert im Kapitel Einstieg in XML) in der Lage sein muß, die Geschwister eines Elements zu bestimmen und, noch wichtiger, die Kinder eines Elements. Betrachten Sie den folgenden Codeschnipsel aus einem XSL-Template:

<xsl:template match="elternElement">
  <!-- Inhalt zum Ausgabebaum hinzufügen -->
  <xsl:apply-templates select="kindElementEins|kindElementZwei" />
</xsl:template>

Hier werden Templates durch das Konstrukt xsl:apply-templates angewendet, aber sie werden auf eine bestimmte Knotenmenge angewendet, die dem angegebenen XPath-Ausdruck entspricht. In diesem Beispiel sollte das Template nur auf die Elemente kindElementEins oder kindElementZwei (getrennt durch den XPath-Oder-Operator, die Pipe) angewandt werden. Außerdem müssen diese direkte Kindobjekte des Elements eltern-Element sein, da ein relativer Pfad verwendet wird. Die Ermittlung und Ortsbestimmung dieser Knoten durch die SAX-Darstellung eines XML-Dokuments wäre äußerst schwierig. Mit Hilfe einer im Speicher befindlichen, hierarchischen Darstellung des XML-Dokuments ist die Ortsbestimmung dieser Knoten trivial – das ist einer der Hauptgründe, warum der DOM-Ansatz sehr häufig für die Eingabe in XSLT-Prozessoren genutzt wird.

Warum SAX überhaupt benutzen?

Die gesamte Diskussion über die »Mängel« von SAX könnte dazu führen, daß Sie sich fragen, warum irgend jemand überhaupt jemals SAX wählen sollte. Aber alle diese Mängel betreffen bestimmte Verwendungszwecke von XML-Daten, in diesem Fall ihre Verarbeitung durch XSL oder die Verwendung des beliebigen Zugriffs zu anderen Zwecken. In der Tat sind alle diese »Probleme« bei der Verwendung von SAX genau der Grund dafür, warum Sie SAX verwenden wollen würden.

Stellen Sie sich das Parsing eines in XML dargestellten Inhaltsverzeichnisses einer Ausgabe von National Geographic vor. Dieses Dokument könnte leicht 500 Zeilen lang sein, sogar noch mehr, wenn diese Ausgabe besonders viel Inhalt hätte. Stellen Sie sich den XML-Index eines O’Reilly-Buches vor: Hunderte von Wörtern mit Seitenzahlen, Querverweisen und anderes. Und das sind alles noch ziemlich kleine, überschaubare Anwendungen von XML. Mit dem XML-Dokument wächst auch dessen im Speicher befindliche Darstellung durch einen DOM-Baum.

Stellen Sie sich als nächstes ein XML-Dokument vor, das so groß ist und so viele Verschachtelungen besitzt, daß seine Darstellung unter Verwendung von DOM anfängt, die Performance Ihrer Anwendung zu beeinträchtigen. Und nun stellen Sie sich vor, daß die gleichen Ergebnisse durch sequentielles Parsing des Eingabedokuments mit SAX erzielt werden könnten, wobei es lediglich ein Zehntel oder sogar ein Hundertstel Ihrer Systemressourcen benötigen würde, diese Aufgabe zu erfüllen.

Genau wie es in Java selbst viele verschiedene Möglichkeiten gibt, um die gleiche Aufgabe zu erledigen, gibt es auch viele Möglichkeiten, um die Daten in einem XML-Dokument zu erhalten. In manchen Szenarien ist SAX einfach die bessere Wahl für schnelle, weniger intensive Parsing- und Verarbeitungsabläufe. In anderen Situationen stellt DOM eine einfach zu nutzende, saubere Schnittstelle zu den Daten im gewünschten Format zur Verfügung. Als Entwickler müssen Sie stets Ihre Anwendung und deren Bedürfnisse analysieren, um die richtige Entscheidung darüber zu treffen, welche Methode verwendet werden soll oder wie beide gemeinsam verwendet werden können. Wie immer hängt die Fähigkeit, gute oder schlechte Entscheidungen zu treffen, von Ihrer Kenntnis der Alternativen ab. Während wir das im Hinterkopf behalten, ist es Zeit, daß wir uns DOM in Aktion ansehen.

Serialisierung

Eine der am häufigsten gestellten Fragen zur Anwendung von DOM lautet: »Ich habe einen DOM-Baum, wie kann ich ihn in eine Datei ausgeben?« Diese Frage wird so häufig gestellt, weil die DOM-Levels 1 und 2 kein Standardmittel zur Serialisierung von DOM besitzen. Auch wenn dies ein gewisser Mangel der API ist, stellt es ein großartiges Beispiel für die Verwendung von DOM dar (und wie Sie im nächsten Kapitel sehen werden, bemüht sich DOM Level 3, dieses Problem in den Griff zu bekommen). Um Sie mit DOM vertraut zu machen, werde ich in diesem Abschnitt mit Ihnen eine Klasse durchgehen, die einen DOM-Baum als Eingabe annimmt und diesen Baum in die angegebene Ausgabedatei serialisiert.

Einen DOM-Parser besorgen

Bevor ich über die Ausgabe eines DOM-Baums spreche, gebe ich Ihnen zuerst Informationen darüber, wie Sie einen DOM-Baum erhalten. Um Ihnen ein Beispiel zu liefern, tut der Code in diesem Kapitel nicht mehr, als eine Datei einzulesen, einen DOM-Baum zu erzeugen und diesen DOM-Baum dann wiederum in eine andere Datei zu schreiben. Trotzdem bietet Ihnen dies einen guten Einstieg in DOM und bereitet Sie auf einige fortgeschrittenere Themen im nächsten Kapitel vor.

Daraus ergibt sich, daß es in diesem Kapitel zwei Java-Quelldateien gibt, die von Interesse sind. Die erste ist der Serialisierer selbst, der (wenig überraschend) DOMSerializer.java heißt. Die zweite Quelldatei, mit der ich nun beginne, ist SerializerTest.java. Diese Klasse nimmt einen Dateinamen für das zu lesende XML-Dokument entgegen und einen weiteren für das Dokument, das serialisiert ausgegeben werden soll. Zusätzlich demonstriert sie, wie das Einlesen und das Parsing einer Datei funktioniert und wie Sie das entstehende DOM-Baumobjekt erhalten, das durch die Klasse org.w3c.dom.Document dargestellt wird. Legen Sie los, und laden Sie diese Klasse von der Website zum Buch herunter, oder geben Sie den Code für die Klasse SerializerTest.java ein, der in Beispiel 5-1 gezeigt wird.

Beispiel 5-1: Die Klasse SerializerTest

package javaxml2;

import java.io.File;
import org.w3c.dom.Document;

// Parser-Import
import org.apache.xerces.parsers.DOMParser;

public class SerializerTest {

    public void test(String xmlDocument, String outputFilename) 
        throws Exception {

        File outputFile = new File(outputFilename);
        DOMParser parser = new DOMParser(  );

        // Den DOM-Baum als Dokument-Objekt erhalten

        // Serialisieren
    }

    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println(
                "Verwendung: java javaxml2.SerializerTest " +
                "[Zu lesendes XML-Dokument] " +
                "[Name der Ausgabedatei]");
            System.exit(0);
        }

        try {
            SerializerTest tester = new SerializerTest(  );
            tester.test(args[0], args[1]);
        } catch (Exception e) {
            e.printStackTrace(  );
        }
    }
}

In dieser Klasse fehlen offensichtlich noch einige Teile, die hier durch die beiden Kommentare in der Methode test( ) dargestellt werden. Ich werde diese in den nächsten beiden Abschnitten nachliefern, wobei ich als erstes das Erzeugen des DOM-Baumobjekts erläutere und anschließend auf die Details der Klasse DOMSerializer selbst eingehe.

DOM-Parser-Ausgabe

Wie Sie sich sicher erinnern, liegt der Interessenschwerpunkt bei SAX auf dem Ablauf des Parsing-Prozesses, denn alle Callback-Methoden versorgen uns mit »Einstiegspunkten« in die Daten, während deren Parsing stattfindet. In DOM liegt der Interessenschwerpunkt dagegen auf dem Ergebnis des Parsing-Prozesses. Bis zu dem Zeitpunkt, zu dem das Dokument vollständig eingelesen, geparst und dem Ausgabebaum hinzugefügt wurde, befinden sich die Daten nicht in einem verwendbaren Zustand. Die Ausgabe eines Parsings, das für die Verwendung mit dem DOM-Interface durchgeführt wird, ist ein org.w3c.dom.Document-Objekt. Dieses Objekt dient als »Handle« auf den Baum, in dem sich Ihre XML-Daten befinden, und in der Elementhierarchie, die ich besprochen habe, ist es äquivalent zu der Ebene direkt oberhalb des Wurzelelements im XML-Eingabedokument.

Da der DOM-Standard sich auf die Manipulation von Daten konzentriert, gibt es eine Vielzahl von Mechanismen, um das Document-Objekt nach einem Parsing zu erhalten. In vielen Implementierungen, etwa bei älteren Versionen des IBM-XML4J-Parsers, gibt die Methode parse( ) das Document-Objekt zurück. Der Code, um eine solche Implementierung eines DOM-Parsers zu verwenden, würde so aussehen:

File outputFile = new File(outputFilename);
DOMParser parser = new DOMParser(  );
Document doc = parser.parse(xmlDocument);

Die meisten neueren Parser, etwa Apache Xerces, folgen dieser Methodik nicht. Um ein Standard-Interface sowohl über SAX- als auch über DOM-Parser aufrechtzuerhalten, gibt die Methode parse( ) in diesen Parsern void zurück, genau wie bei der Anwendung der Methode parse( ) in dem SAX-Beispiel. Diese Änderung ermöglicht es einer Anwendung, zwischen einer DOM-Parser-Klasse und einer SAX-Parser-Klasse hin- und herzuwechseln; allerdings erfordert sie eine zusätzliche Methode, um das Document-Objekt zu erhalten, das beim Parsing herauskommt. In Apache Xerces heißt diese Methode getDocument( ). Wenn Sie diesen Parsertyp verwenden (wie ich es in dem Beispiel tue), können Sie den folgenden Beispielcode zu Ihrer Methode test( ) hinzufügen, um den beim Parsing der übergebenen Eingabedatei entstandenen DOM-Baum zu erhalten:

    public void test(String xmlDocument, String outputFilename) 
        throws Exception {

        File outputFile = new File(outputFilename);
        DOMParser parser = new DOMParser(  );

        // Den DOM-Baum als Dokument-Objekt erhalten
        parser.parse(xmlDocument);
        Document doc = parser.getDocument(  );

        // Serialisieren
    }

Dies setzt natürlich voraus, daß Sie Xerces verwenden, wie auch die import-Anweisung zu Beginn der Quelldatei anzeigt:

import org.apache.xerces.parsers.DOMParser;

Wenn Sie einen anderen Parser verwenden, müssen Sie diese Import-Anweisung auf die DOM-Parser-Klasse Ihres Herstellers setzen. Sehen Sie anschließend in der Dokumentation Ihres Herstellers nach, um herauszufinden, welche parse( )-Mechanismen Sie anwenden müssen, um das DOM-Ergebnis Ihres Parsings zu erhalten. Im Kapitel JDOM werde ich die JAXP-API von Sun und andere Möglichkeiten betrachten, den Zugriff auf einen DOM-Baum aus jeder Parser-Implementierung heraus zu standardisieren. Obwohl es in der genauen Vorgehensweise zum Erhalten dieses Ergebnisses gewisse Variationen gibt, sind alle Verwendungen dieses Ergebnisses, die wir uns anschauen, Standards in der DOM-Spezifikation, so daß Sie sich keine Sorgen über weitere Implementierungshindernisse im Rest dieses Kapitels machen müssen.

DOMSerializer

Ich habe nun schon eine Weile mit dem Begriff Serialisierung um mich geworfen und sollte wahrscheinlich sicherstellen, daß Sie wissen, was ich meine. Wenn ich Serialisierung sage, meine ich einfach die XML-Ausgabe. Dies könnte eine Datei sein (durch die Verwendung des Java-Objekts File), ein OutputStream oder ein Writer. Es sind in Java sicherlich noch mehr Ausgabearten verfügbar, aber diese drei decken die meisten Fälle ab (tatsächlich tun das nur die beiden letzteren, da ein File sich leicht in einen Writer konvertieren läßt, aber daß ein File akzeptiert wird, ist eine nette Annehmlichkeit).

In diesem Fall findet die Serialisierung in ein XML-Format statt; der DOM-Baum wird in ein wohlgeformtes XML-Dokument im Textformat zurückkonvertiert. Es ist wichtig anzumerken, daß das XML-Format verwendet wird, denn Sie könnten Code-Serialisierer leicht benutzen, um HTML, WML, XHTML oder irgendein anderes Format zu schreiben. In der Tat bietet Apache Xerces diese verschiedenen Klassen an, und ich werde sie am Ende dieses Kapitels kurz ansprechen.

Loslegen

Damit Sie über die Einleitung hinauskommen, finden Sie in Beispiel 5-2 das Grundgerüst für die Klasse DOMSerializer. Es importiert alle benötigten Klassen, um den Code in Gang zu setzen, und definiert die verschiedenen Einstiegspunkte (für File, OutputStream und Writer) in die Klasse. Zwei dieser drei Methoden verweisen einfach auf die dritte (mit ein wenig I/O-Magie). Das Beispiel stellt auch die Werte einiger Instanzvariablen ein, die für die Art der Einrückung und des Zeilenumbruchs verwendet werden, sowie Methoden, um diese Eigenschaften zu modifizieren.

Beispiel 5-2: Das DOMSerializer-Grundgerüst

package javaxml2;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class DOMSerializer {

    /** Zu verwendende Einrückung */
    private String indent;

    /** Zu verwendender Zeilentrenner */
    private String lineSeparator;

    public DOMSerializer(  ) {
        indent = "";
        lineSeparator = "\n";
    }

    public void setLineSeparator(String lineSeparator) {
        this.lineSeparator = lineSeparator;
    }

    public void serialize(Document doc, OutputStream out)
        throws IOException {
        
        Writer writer = new OutputStreamWriter(out);
        serialize(doc, writer);
    }

    public void serialize(Document doc, File file)
        throws IOException {

        Writer writer = new FileWriter(file);
        serialize(doc, writer);
    }

    public void serialize(Document doc, Writer writer)
        throws IOException {

        // Dokument serialisieren
    }
}

Nachdem dieser Code in der Quelldatei DOMSerializer.java gespeichert ist, wird das Ganze mit derjenigen Version der Methode serialize( ) abgeschlossen, die einen Writer als Argument annimmt. Angenehm und sauber.

Die Serialisierung in Gang setzen

Nachdem die Serialisierung in Gang gesetzt wurde, ist es an der Zeit, den Prozeß zu definieren, der sich um das Durcharbeiten des DOM-Baums kümmert. Ein bereits erwähnter angenehmer Aspekt von DOM ist, daß all diese speziellen DOM-Strukturen, die XML repräsentieren (inklusive des Document-Objekts), das DOM-Interface Node erweitern. Dies ermöglicht die Programmierung einer einzelnen Methode, die die Serialisierung sämtlicher DOM-Knotentypen übernimmt.

Innerhalb dieser Methode können Sie zwischen Knotentypen unterscheiden; aber indem sie ein Node-Objekt als Eingabe annimmt, bietet sie eine sehr einfache Möglichkeit, mit allen DOM-Typen umzugehen. Zusätzlich wird eine Methodik aufgebaut, die die Rekursion ermöglicht, den besten Freund aller Programmierer. Fügen Sie die hier gezeigte Methode serializeNode( ) hinzu, genau wie den initialen Aufruf dieser Methode in der Methode serialize( ) – den gemeinsamen Startpunkt des soeben besprochenen Codes:

    public void serialize(Document doc, Writer writer)
        throws IOException {

        // Serialisierungsrekursion ohne Einrückung starten
        serializeNode(doc, writer, "");
        writer.flush(  );
    }
 
    public void serializeNode(Node node, Writer writer, 
                              String indentLevel)
        throws IOException {
    }

Darüber hinaus wird die Variable indentLevel eingerichtet; diese stellt gewissermaßen die Rekursionstiefe dar. Mit anderen Worten: Die Methode serializeNode( ) kann anzeigen, wie weit der gerade bearbeitete Knoten eingerückt werden sollte, und wenn eine Rekursion stattfindet, kann sie eine weitere Einrückungsstufe hinzufügen (durch die Verwendung der Instanzvariable indent). Zu Beginn – mit der Methode serialize( ) – ist ein leerer String für die Einrückung eingestellt; auf der nächsten Stufe sind es zwei Leerzeichen für die Einrückung, dann vier Leerzeichen auf der nächsten Stufe und so weiter. Natürlich wird am Schluß jeder Rekursionsstufe auch die Einrückung Schritt für Schritt zurückgenommen. Alles, was nun übrig bleibt, ist die Behandlung der unterschiedlichen Knotentypen.

Mit Knoten arbeiten

Nach dem Start der Methode serializeNode( ) besteht die erste Aufgabe darin herauszufinden, welcher Typ von Knoten ihr übergeben wurde. Auch wenn Sie dies mit Hilfe einer typischen Java-Lösung erreichen könnten, indem Sie das Schlüsselwort instanceof und Java-Reflection verwenden, vereinfachen die DOM-Sprachenbindungen für Java diese Aufgabe erheblich. Das Interface Node definiert die Hilfsmethode getNodeType( ), die einen ganzzahligen Wert zurückgibt. Dieser Wert kann mit einem Satz von Konstanten verglichen werden (die ebenfalls im Interface Node definiert sind), so daß der Typ des untersuchten Knotens schnell und einfach ermittelt werden kann.

Dies paßt auch auf sehr natürliche Weise zu dem Java-Konstrukt switch, das verwendet werden kann, um die Serialisierung in einzelne logische Abschnitte zu unterteilen. Der hier dargestellte Code umfaßt so gut wie alle DOM-Knotentypen; auch wenn noch einige weitere Knotentypen definiert wurden (siehe Abbildung 5-2), sind dies die häufigsten, und die hier gezeigten Konzepte können auch auf die weniger häufig genutzten Knotentypen angewendet werden:

    public void serializeNode(Node node, Writer writer, 
                              String indentLevel)
        throws IOException {

        // Je nach Knotentyp die Aktion bestimmen
        switch (node.getNodeType(  )) {
            case Node.DOCUMENT_NODE:
                break;
            
            case Node.ELEMENT_NODE:
                break;
            
            case Node.TEXT_NODE:
                break;

            case Node.CDATA_SECTION_NODE:
                break;

            case Node.COMMENT_NODE:
                break;
            
            case Node.PROCESSING_INSTRUCTION_NODE:
                break;
            
            case Node.ENTITY_REFERENCE_NODE:
                break;
                
            case Node.DOCUMENT_TYPE_NODE: 
                break;                
        }        
    }

Dieser Code ist zwar recht nutzlos, aber er stellt alle DOM-Knotentypen übersichtlich und der Reihe nach dar, anstatt sie mit einer Masse Code zu vermischen, der für die eigentliche Serialisierung benötigt wird. Mit letzterem möchte ich aber nun beginnen, angefangen beim ersten Knoten, der an diese Methode übergeben wird: einer Instanz des Interfaces Document.

Da das Interface Document eine Erweiterung des Interfaces Node ist, kann es genau wie alle anderen Knotentypen behandelt werden. Allerdings ist es ein Spezialfall, da es das Wurzelelement enthält, außerdem die DTD des XML-Dokuments und einige weitere Spezialinformationen, die nicht Teil der XML-Elementhierarchie sind. Daraus folgt, daß Sie das Wurzelelement extrahieren und an die Serialisierungsmethode zurückgeben müssen (und so die Rekursion starten). Darüber hinaus wird die XML-Deklaration als solche ausgegeben:

            case Node.DOCUMENT_NODE:	
                writer.write("<?xml version=\"1.0\"?>");
                writer.write(lineSeparator);

                Document doc = (Document)node;
                serializeNode(doc.getDocumentElement(  ), writer, "");
                break;

DOM Level 2 (genau wie SAX 2.0) stellt die XML-Deklaration nicht dar. Das sieht nicht nach einem großen Problem aus, solange Sie berücksichtigen, daß die Codierung des Dokuments in dieser Deklaration enthalten ist. Es ist zu erwarten, daß sich DOM Level 3 um diesen Mangel kümmert; ich werde das im nächsten Kapitel behandeln. Seien Sie vorsichtig, und schreiben Sie keine DOM-Anwendungen, die von dieser Information abhängen, bis diese Funktion verfügbar ist.

Da dieser Code auf eine Document-spezifische Methode zugreifen muß (im Gegensatz zu einer im allgemeinen Node-Interface definierten), muß die Node-Implementierung per Typecasting in den Datentyp Document-Interface umgewandelt werden. Rufen Sie anschließend die Methode getDocumentElement( ) des Objekts auf, um das Wurzelelement des eingegebenen XML-Dokuments zu erhalten, und geben Sie dieses wiederum an die Methode serializeNode( ) weiter, wodurch die Rekursion und die Durchschreitung des DOM-Baums beginnt.

Selbstverständlich besteht die häufigste Aufgabe bei der Serialisierung darin, ein DOM-Element entgegenzunehmen und seinen Namen, seine Attribute und seinen Wert auszugeben und dann seine Kind-Elemente auszugeben. Sie vermuten richtig, daß all diese Tätigkeiten leicht durch den Aufruf von DOM-Methoden erledigt werden können. Als erstes müssen Sie den Namen des XML-Elements ermitteln, der durch die Methode getNodeName( ) des Interfaces Node zur Verfügung steht. Der Code muß sich anschließend um die Kindobjekte des aktuellen Elements kümmern und diese ebenfalls serialisieren. Auf die Kindobjekte eines Nodes kann mit Hilfe der Methode getChildNodes( ) zugegriffen werden, die eine Instanz einer DOM-NodeList zurückgibt.

Es ist trivial, die Länge dieser Liste zu ermitteln und dann iterativ die Kindobjekte durchzugehen, die Serialisierungsmethode für jedes davon aufzurufen und dadurch die Rekursion fortzusetzen. Es gibt auch eine gewisse Logik, die für korrekte Einrückung und Zeilenumbrüche sorgt; dies sind wirklich nur Formatierungsprobleme, mit denen ich hier keine Zeit verbringen werde. Zum Schluß kann die schließende Klammer des Elements ausgegeben werden:

            case Node.ELEMENT_NODE:
                String name = node.getNodeName(  );
                writer.write(indentLevel + "<" + name);
                writer.write(">");
                
                // Rekursion über jedes Kindobjekt
                NodeList children = node.getChildNodes(  );
                if (children != null) {
                    if ((children.item(0) != null) &&
                        (children.item(0).getNodeType(  ) == 
                        Node.ELEMENT_NODE)) {
                            
                        writer.write(lineSeparator);
                    }
                           for (int i=0; i<children.getLength(  ); i++) {                        
                        serializeNode(children.item(i), writer,
                            indentLevel + indent);
                    }
                    if ((children.item(0) != null) &&
                        (children.item(children.getLength(  )-1)
                                .getNodeType(  ) ==
                        Node.ELEMENT_NODE)) {
                     
                        writer.write(indentLevel);       
                    }
                }
                
                writer.write("</" + name + ">");
                writer.write(lineSeparator);
                break;

Natürlich werden besonders aufmerksame Leser (oder DOM-Experten) bemerken, daß ich etwas Wichtiges ausgelassen habe: die Attribute des Elements! Diese bilden die einzige vermeintliche Ausnahme von dem strengen Baum, den DOM aufbaut. Sie sollten aber auch eine Ausnahme sein, da ein Attribut kein richtiges Kind eines Elements ist; es ist ihm gewissermaßen nebengeordnet. Im Grunde genommen ist die Beziehung ein wenig verworren.
Auf jeden Fall sind die Attribute eines Elements durch die Methode getAttributes( ) des Node-Interfaces verfügbar. Diese Methode gibt eine NamedNodeMap zurück, und auch diese kann iterativ durchwandert werden. Von jedem Node innerhalb dieser Liste kann der Name und der Wert ermittelt werden, und plötzlich werden die Attribute verarbeitet! Geben Sie den Code wie hier dargestellt ein, um sich darum zu kümmern:

            case Node.ELEMENT_NODE:
                String name = node.getNodeName(  );
                writer.write(indentLevel + "<" + name);
                NamedNodeMap attributes = node.getAttributes(  );
                for (int i=0; i<attributes.getLength(  ); i++) {
                    Node current = attributes.item(i);
                    writer.write(" " + current.getNodeName(  ) +
                                 "=\"" + current.getNodeValue(  ) +
                                 "\"");
                }
                writer.write(">");
                
                // Rekursion über jedes Kindobjekt
                NodeList children = node.getChildNodes(  );
                if (children != null) {
                    if ((children.item(0) != null) &&
                        (children.item(0).getNodeType(  ) == 
                        Node.ELEMENT_NODE)) {
                            
                        writer.write(lineSeparator);
                    }
                           for (int i=0; i<children.getLength(  ); i++) {                        
                        serializeNode(children.item(i), writer,
                            indentLevel + indent);
                    }
                    if ((children.item(0) != null) &&
                        (children.item(children.getLength(  )-1)
                                .getNodeType(  ) ==
                        Node.ELEMENT_NODE)) {
                     
                        writer.write(indentLevel);       
                    }
                }
                
                writer.write("</" + name + ">");
                writer.write(lineSeparator);
                break;

Als nächstes in der Liste der Knotentypen haben wir die Text-Knoten. Die Ausgabe ist recht einfach, da Sie lediglich die bereits bekannte Methode getNodeValue( ) des DOM-Interfaces Node verwenden müssen, um die Textdaten zu erhalten und auszugeben; das gleiche gilt für CDATA-Knoten, außer daß die Daten aus einem CDATA-Bereich in die XML-CDATA-Schreibweise eingeschlossen werden müssen (von <![CDATA[ und ]]> umgeben). Sie können diese Programmschritte jetzt an den beiden passenden case-Positionen einfügen:

            case Node.TEXT_NODE:
                writer.write(node.getNodeValue(  ));
                break;

            case Node.CDATA_SECTION_NODE:
                writer.write("<![CDATA[" +
                             node.getNodeValue(  ) + "]]>");
                break;

Der Umgang mit Kommentaren in DOM ist fast so leicht, wie er nur sein könnte. Die Methode getNodeValue( ) gibt den Text zwischen den XML-Konstrukten <!-- und --> zurück. Das ist wirklich alles, was zu tun ist; betrachten Sie den hinzugefügten Code:

            case Node.COMMENT_NODE:
                writer.write(indentLevel + "<!-- " +
                             node.getNodeValue(  ) + " -->");
                writer.write(lineSeparator);
                break;

Machen wir mit dem nächsten DOM-Knotentyp weiter: Die DOM-Bindungen für Java definieren ein Interface für den Umgang mit Processing Instructions (PIs; Verarbeitungsanweisungen), die sich im eingegebenen XML-Dokument befinden, das recht einleuchtend als ProcessingInstruction bezeichnet wird. Dies ist nützlich, da diese Anweisungen nicht dem gleichen Auszeichnungsmodell folgen wie XML-Elemente und -Attribute, die Anwendungen sie aber dennoch kennen müssen. In dem Inhaltsverzeichnis-XML-Dokument befinden sich keine PIs (auch wenn es einfach wäre, zum Testen welche hinzuzufügen).

Der PI-Knoten in DOM stellt eine gewisse Ausnahme von allem dar, was Sie bisher gesehen haben: Damit die Syntax in das Modell des Node-Interfaces paßt, gibt die Methode getNodeValue( ) alle in einer PI enthaltenen Daten in einem String zurück. Dies ermöglicht die schnelle Ausgabe der PI; allerdings müssen Sie noch getNodeName( ) verwenden, um den Namen der PI zu erhalten. Würden Sie eine Anwendung schreiben, die PIs von einem XML-Dokument empfängt, könnte es Ihnen lieber sein, das eigentliche Interface ProcessingInstruction zu verwenden; auch wenn es die gleichen Daten liefert, sind die Methodennamen (getTarget( ) und getData( )) eher im Einklang mit dem Format einer PI. Wenn Sie das verstanden haben, können Sie Code hinzufügen, um sämtliche PIs in den eingegebenen XML-Dokumenten auszugeben:

            case Node.PROCESSING_INSTRUCTION_NODE:
                writer.write("<?" + node.getNodeName(  ) +
                             " " + node.getNodeValue(  ) +
                             "?>");                
                writer.write(lineSeparator);
                break;

Obwohl der Code für den Umgang mit PIs perfekt funktioniert, gibt es ein Problem. In dem Fall, in dem Dokumentknoten verarbeitet wurden, bestand die Aufgabe des Serialisierers darin, beim Dokument-Element zu beginnen und alles rekursiv durchzugehen. Das Problem ist, daß bei diesem Ansatz alle anderen Kindknoten des Document-Objekts ignoriert werden, etwa PIs der obersten Ebene und alle DOCTYPE-Deklarationen. Diese Knotentypen sind dem Dokument-Element (Wurzelelement) nebengeordnet und werden ignoriert. Anstatt nur das Dokument-Element durchzuarbeiten, serialisiert der folgende Code nun alle Kindknoten des übergebenen Document-Objekts:

            case Node.DOCUMENT_NODE:
                writer.write("<?xml version=\"1.0\"?>");
                writer.write(lineSeparator);

                // Rekursion über alle Kindobjekte
                NodeList nodes = node.getChildNodes(  );
                if (nodes != null) {
                    for (int i=0; i<nodes.getLength(  ); i++) {
                        serializeNode(nodes.item(i), writer, "");
                    }
                }
                /*
                Document doc = (Document)node;
                serializeNode(doc.getDocumentElement(  ), writer, "");
                */
                break;

Nachdem dies eingerichtet ist, kann der Code mit DocumentType-Knoten umgehen, die eine DOCTYPE-Deklaration darstellen. Wie PIs kann auch eine DTD-Deklaration bei der Darlegung externer Informationen behilflich sein, die bei der Verarbeitung eines XML-Dokuments benötigt werden könnten. Allerdings – da es sowohl Public- und System-IDs als auch andere DTD-spezifische Daten gibt – muß der Code die Node-Instanz durch Typecasting in den Typ des DocumentType-Interfaces umwandeln, um auf diese zusätzlichen Daten zugreifen zu können. Anschließend können Sie Hilfsmethoden verwenden, um den Namen des Node-Objekts zu erhalten, das den Namen des beschränkten Elements im Dokument zurückgibt, sowie die Public-ID (falls vorhanden) und die System-ID der DTD, auf die Bezug genommen wird. Unter Verwendung dieser Informationen kann die ursprüngliche DTD serialisiert werden:

            case Node.DOCUMENT_TYPE_NODE: 
                DocumentType docType = (DocumentType)node;
                writer.write("<!DOCTYPE " + docType.getName(  ));
                if (docType.getPublicId(  ) != null)  {
                    System.out.print(" PUBLIC \"" + 
                        docType.getPublicId(  ) + "\" ");                    
                } else {
                    writer.write(" SYSTEM ");
                }
                writer.write("\"" + docType.getSystemId(  ) + "\">");                                
                writer.write(lineSeparator);
                break;

Was an dieser Stelle noch übrigbleibt, ist die Behandlung von Entities und Entity-Referenzen. In diesem Kapitel werde ich die Entities im Schnelldurchlauf behandeln und mich auf Entity-Referenzen konzentrieren; weitere Details über Entities und ihre Notation folgen im nächsten Kapitel DOM für Fortgeschrittene. Um es hier kurz zu machen, kann eine Referenz einfach umgeben von den Zeichen & und ; ausgegeben werden:

            case Node.ENTITY_REFERENCE_NODE:
                writer.write("&" + node.getNodeName(  ) + ";");    
                break;

Es gibt ein paar Überraschungen, die Sie in die Falle locken können, wenn es zur Ausgabe eines Knotens wie diesem kommt. Die Definition, wie Entity-Referenzen innerhalb von DOM verarbeitet werden sollten, erlaubt eine Menge Spielraum und hängt sehr stark vom Verhalten des darunterliegenden Parsers ab. In der Tat haben die meisten Parser Entity-Referenzen längst aufgelöst und verarbeitet, bevor die Daten des XML-Dokuments in den DOM-Baum gelangen.

Wenn Sie erwarten, innerhalb Ihrer DOM-Struktur eine Entity-Referenz zu sehen, werden Sie oft den Text oder die Werte vorfinden, auf die zugegriffen wird, statt die Entity-Referenz selbst. Um dies mit Ihrem Parser zu testen, sollten Sie die Klasse SerializerTest mit dem Dokument contents.xml starten (was ich im nächsten Abschnitt behandeln werde) und prüfen, was mit der Entity-Referenz OReillyCopyright geschieht. In Apache-Xerces wird diese übrigens als Entity-Referenz weitergegeben.

Und das war’s! Wie ich bereits erwähnte, gibt es noch ein paar andere Knotentypen, aber deren Behandlung ist im Moment die Mühe nicht wert; Sie haben eine Idee davon erhalten, wie DOM arbeitet. Im nächsten Kapitel führe ich Sie dann tiefer hinein, als Sie wahrscheinlich je gehen wollten. Für den Augenblick wollen wir die Stücke zusammensetzen und ein paar Ergebnisse sehen.

Die Ergebnisse

Nach der Fertigstellung der Klasse DOMSerializer fehlt nur noch der Aufruf der Methode serialize( ) des Serialisierers aus der Testklasse heraus. Um dies zu tun, fügen Sie die folgenden Zeilen zur Klasse SerializerTest hinzu:

    public void test(String xmlDocument, String outputFilename) 
        throws Exception {

        File outputFile = new File(outputFilename);
        DOMParser parser = new DOMParser(  );

        // Den DOM-Baum als Document-Objekt entgegennehmen
        parser.parse(xmlDocument);
        Document doc = parser.getDocument(  );

        // Serialisieren
        DOMSerializer serializer = new DOMSerializer(  );
        serializer.serialize(doc, new File(outputFilename));
    }

Diese ziemlich einfache Erweiterung vervollständigt die Klassen, und Sie können das Beispiel mit der Datei contents.xml aus dem Kapitel Einstieg in XML wie folgt starten:

C:\javaxml2\build>java javaxml2.SerializerTest 
    c:\javaxml2\ch05\xml\contents.xml
    output.xml

Auch wenn Sie hier keine besonders aufregende Ausgabe erhalten, können Sie die neu erzeugte Datei output.xml öffnen und auf ihre Korrektheit überprüfen. Sie sollte sämtliche Informationen des ursprünglichen XML-Dokuments enthalten, mit den Unterschieden, die wir bereits in den vorangegangenen Abschnitten besprochen haben. Ein Teil meiner eigenen output.xml-Datei wird in Beispiel 5-3 gezeigt.

Beispiel 5-3: Ein Teil des serialisierten DOM-Baums output.xml

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE buch SYSTEM "DTD/JavaXML.dtd">
<!--  Java und XML Inhalt  -->
<book xmlns="http://www.oreilly.com/javaxml2" 
      xmlns:ora="http://www.oreilly.com">
  <titel ora:serie="Java">Java und XML</titel>


  <!--  Kapitelliste  -->

  <inhalt>
    <kapitel nummer="2" titel="Ans Eingemachte">
      <thema name="Die Grundlagen"></thema>

      <thema name="Beschränkungen"></thema>

      <thema name="Transformationen"></thema>

      <thema name="Und mehr..."></thema>

      <thema name="Und was kommt jetzt?"></thema>

    </kapitel>

Ihnen ist wahrscheinlich aufgefallen, daß es in der Ausgabe eine ziemliche Menge an zusätzlichem Whitespace gibt; das kommt daher, daß der Serialisierer bei jedem Vorkommen von writer.write(lineSeparator) im Code einige Zeilenumbrüche hinzufügt. Natürlich enthält der darunterliegende DOM-Baum auch schon einige Zeilenumbrüche, die als Text-Knoten gemeldet werden. Das Endergebnis sind in vielen Fällen doppelte Zeilenumbrüche, wie Sie in der Ausgabe sehen.

Ich möchte noch einmal deutlich betonen, daß die in diesem Beispiel gezeigte Klasse DOMSerializer Beispielzwecken dient und keine gute Produktionslösung darstellt. Auch wenn es Ihnen freisteht, die Klasse in Ihren eigenen Anwendungen zu verwenden, sollten Sie beachten, daß einige wichtige Optionen außen vor bleiben, wie etwa die Codierung und das Einstellen genauerer Optionen für Einrückung, Zeilenumbrüche und automatische Zeilenwechsel. Darüber hinaus werden Entity-Referenzen nur durch ihre Weitergabe behandelt (ihre vollständige Bearbeitung wäre doppelt so lang wie das gesamte bisherige Kapitel!). Ihr Parser besitzt wahrscheinlich seine eigene Serialisierer-Klasse, die diese Aufgabe mindestens genausogut erfüllt wie das Beispiel in diesem Kapitel, wenn nicht sogar besser.

Allerdings sollten Sie jetzt verstehen, was in diesen Klassen unter der Kühlerhaube passiert. Wenn Sie – als Referenzbeispiel – Apache Xerces verwenden, befinden sich die Klassen, nach denen Sie Ausschau halten sollten, in dem Paket org.apache.xml.serialize. Einige besonders nützliche sind XMLSerializer, XHTMLSerializer und HTMLSerializer. Probieren Sie sie aus – sie bieten eine gute Lösung, bis DOM Level 3 mit einer Standardlösung aufwartet.

Wandelbarkeit

Eine grobe Auslassung in diesem Kapitel ist das Thema der Modifikation eines DOM-Baums. Das geschah nicht versehentlich; die Arbeit mit DOM ist erheblich komplexer als mit SAX. Anstatt Sie in einer Fülle von Informationen untergehen zu lassen, wollte ich zunächst einen klaren Überblick über die verschiedenen in DOM verwendeten Knotentypen und Strukturen geben. Im nächsten Kapitel – neben der Betrachtung einiger speziellerer Aspekte der DOM-Levels 2 und 3 – werde ich die Wandelbarkeit der DOM-Bäume ansprechen, insbesondere, wie DOM-Bäume erzeugt werden. Also keine Panik – Hilfe ist in Sicht!

Vorsicht Falle!

Wie in früheren Kapiteln möchte ich auch hier einige der üblichen Stolperfallen für unerfahrene XML-Java-Entwickler näher betrachten. In diesem Kapitel habe ich mich auf das Document Object Model konzentriert, und dieser Abschnitt behält diesen Blickwinkel bei. Obwohl einige der hier vorgestellten Punkte eher informativ sind und nicht direkt Ihre Programmierung betreffen, können sie dabei helfen, Entwurfsentscheidungen über die Anwendung von DOM zu treffen, und tragen zum Verständnis dessen bei, was unter der Kühlerhaube Ihrer XML-Anwendungen passiert.

Speicher, Performance und »verzögertes DOM«

Vorhin habe ich die Gründe für die Verwendung von DOM oder SAX beschrieben. Obwohl ich bereits betont habe, daß die Verwendung von DOM es erfordert, daß das gesamte XML-Dokument in den Speicher geladen und in einer Baumstruktur abgelegt wird, kann dieses Thema nicht oft genug zur Sprache gebracht werden. Allzu häufig ist das Szenario, in dem ein Entwickler seine ausufernde Sammlung komplexer XML-Dokumente in einen XSLT-Prozessor lädt, mit einer Reihe von Offline-Transformationen beginnt und dann den Prozeß verläßt, um einen Happen zu essen. Bei seiner Rückkehr stellt er fest, daß sein Windows-Rechner den gefürchteten »Blue Screen of Death« zeigt und daß seine Linux-Box sich über Speicherprobleme beschwert. Machen Sie es nicht wie dieser Entwickler und wie Hunderte andere, sondern hüten Sie sich vor DOM bei übertrieben großen Datenmengen!

Die Anwendung von DOM erfordert eine Speichermenge, die proportional zu der Größe und Komplexität eines XML-Dokuments ist. Allerdings sollten Sie sich ein wenig mehr in die Dokumentation Ihres Parsers vertiefen. Oft enthalten moderne Parser eine Funktion, die auf etwas basiert, das üblicherweise »verzögertes DOM« (deferred DOM) genannt wird. Ein verzögertes DOM versucht, den Speicherverbrauch bei der Benutzung von DOM zu vermindern, indem es nicht sämtliche von einem DOM-Knoten benötigten Informationen liest und zuweist, bis dieser Knoten selbst angefordert wird.

Bis zu diesem Zeitpunkt werden die existierenden, aber nicht verwendeten Knoten einfach auf null gesetzt. Dies vermindert die Speicherbelastung für große Dokumente, wenn nur ein bestimmter Teil des Dokuments verarbeitet werden muß. Beachten Sie jedoch, daß mit dieser Einsparung von Speicher ein zusätzlicher Bedarf an Arbeit einhergeht. Da die Knoten sich nicht im Speicher befinden und mit Daten gefüllt werden müssen, wenn sie angefordert werden, wird grundsätzlich mehr Wartezeit fällig, wenn ein zuvor nicht angesprochener Knoten angefordert wird. Es ist ein Kompromiß. Allerdings kann ein verzögertes DOM Ihnen oft das Leben retten, wenn Sie mit großen Dokumenten hantieren müssen.

Polymorphie und das Interface Node

An früherer Stelle in diesem Kapitel habe ich das Baummodell hervorgehoben, auf dem DOM aufbaut. Ich habe Ihnen auch erzählt, daß der Schlüssel dazu ein gemeinsames Interface ist, org.w3c.dom.Node. Diese Klasse stellt eine gemeinsame Funktionalität für alle DOM-Klassen zur Verfügung, aber manchmal bietet sie noch mehr. Zum Beispiel definiert diese Klasse eine Methode namens getNodeValue( ), die einen String zurückgibt. Klingt nach einer guten Idee, oder? Ohne den Node durch Casting in einen bestimmten Typ umwandeln zu müssen, können Sie schnell seinen Wert erhalten. Allerdings wird das Ganze etwas kritisch, wenn Sie Typen wie Element betrachten.

Sie erinnern sich vielleicht, daß ein Element keinen Textinhalt besitzt, sondern statt dessen Kindobjekte vom Typ Text. Also hat ein Element in DOM keinen Wert, der von Bedeutung wäre; das Ergebnis ist, daß Sie so etwas wie #ELEMENT# erhalten. Der genaue Wert hängt vom jeweiligen Parser ab, aber Sie verstehen bestimmt, was ich meine.

Das gleiche trifft auch auf andere Methoden des Interfaces Node zu, wie etwa getNode-Name( ). Für Text-Knoten erhalten Sie #TEXT#, was Ihnen nicht besonders weiterhilft. Worin besteht denn hier genau die Falle? Sie müssen einfach vorsichtig sein, wenn Sie das Node-Interface für unterschiedliche DOM-Typen verwenden. Sie könnten sonst bei aller Bequemlichkeit des gemeinsamen Interfaces einige unerwartete Ergebnisse erhalten.

DOM-Parser, die SAX-Exceptions auslösen

In dem Beispiel über die Verwendung von DOM in diesem Kapitel habe ich nicht ausdrücklich die Exceptions aufgelistet, die beim Parsing eines Dokuments auftreten könnten; statt dessen wurde eine allgemeinere Exception höherer Ebene abgefangen. Das ist so, weil der Prozeß der DOM-Baum-Erzeugung, wie ich bereits erwähnte, der Parser-Implementierung überlassen bleibt und deshalb nicht immer gleich ist. Allerdings ist es üblicherweise guter Stil, spezifische Exceptions abzufangen, die auftreten können, und unterschiedlich auf sie zu reagieren, da die Art der Exception Informationen über das aufgetretene Problem liefert. Wenn wir den Parser-Aufruf in der Klasse SerializerTest auf diese Weise neu schreiben, könnte ein überraschender Aspekt dieses Prozesses zu Tage treten. Für Apache Xerces könnte dies folgendermaßen erledigt werden:

    public void test(String xmlDocument, String outputFilename) 
        throws Exception {
        Document doc = null;

        try {
            File outputFile = new File(outputFilename);
            DOMParser parser = new DOMParser(  );
            parser.parse(xmlDocument);
            doc = parser.getDocument(  );
        } catch (IOException e) {
            System.out.println("Fehler beim Lesen von URI: " + e.getMessage(  ));
        } catch (SAXException e) {
            System.out.println("Fehler beim Parsing: " + e.getMessage(  ));
        }

        // Serialisieren
        DOMSerializer serializer = new DOMSerializer(  );
        serializer.serialize(doc, new File(outputFilename));
    }

Die IOException hier zu finden, sollte keine Überraschung sein, da sie einen Fehler beim Auffinden der angegebenen Datei anzeigt, genau wie in den früheren SAX-Beispielen. Etwas anderes aus dem SAX-Bereich könnte Sie zu dem Gedanken verleiten, daß etwas nicht stimmt; haben Sie die SAXException bemerkt, die ausgelöst werden kann? Der DOM-Parser löst eine SAX-Exception aus? Sicher habe ich den falschen Satz von Klassen importiert! Nein, doch nicht; es sind die richtigen Klassen.

Wie Sie sich bestimmt erinnern, ist es möglich, eine Baumstruktur aus den Daten in einem XML-Dokument unter Verwendung von SAX selbst aufzubauen, aber DOM stellt eine Alternative zur Verfügung. Allerdings schließt das nicht aus, daß SAX in dieser Alternative verwendet wird! Tatsächlich bietet SAX eine leichtgewichtige und schnelle Möglichkeit für das Parsing eines Dokuments; in diesem Fall wird das Dokument einfach während des Parsings in einen DOM-Baum eingefügt. Da kein Standard für die DOM-Erzeugung existiert, ist dies akzeptabel und nicht einmal ungebräuchlich. Also seien Sie nicht überrascht oder gar verblüfft, wenn Sie in Ihren DOM-Anwendungen die org.xml.sax.SAXException importieren und abfangen.

Und was kommt jetzt?

Im Kapitel DOM für Fortgeschrittene werde ich auch weiterhin Ihr Reiseführer durch die Welt von DOM sein, da wir uns einige der fortgeschritteneren (und unbekannteren) Bestandteile von DOM anschauen werden. Um warm zu werden, zeige ich Ihnen, wie DOM-Bäume modifiziert werden, und auch, wie sie erzeugt werden. Dann geht es weiter mit weniger gebräuchlicher Funktionalität von DOM.

Für Einsteiger werden die in DOM Level 2 eingeführten Zusätze erläutert (manche davon haben Sie bereits verwendet, andere noch nicht). Als nächstes werde ich die Verwendung der DOM-HTML-Bindungen besprechen, die Ihnen helfen werden, wenn Sie mit DOM und Webseiten zu tun haben. Zu guter Letzt gebe ich Ihnen einige Informationen über Änderungen, die in der Spezifikation des neu erscheinenden DOM Level 3 erwartet weden. Auf diese Weise sollten Sie eine Menge Munition erhalten, um die Welt mit DOM zu erobern!

1)
Ich möchte nicht ausschließen, daß Sie den Parser eines Herstellers mit dem Prozessor eines anderen Herstellers anwenden können. In den meisten Fällen ist es möglich, einen anderen Parser zur Verwendung zu bestimmen. Allerdings ist es grundsätzlich üblich, die Software eines Herstellers für alles zu verwenden.