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

JAXP

Als Sun die Java API for XML Parsing veröffentlichte, die üblicherweise als JAXP bezeichnet wird, ist es Sun gelungen, eine Reihe von Widersprüchen in die Java-Welt zu setzen. Mit einem Schlag hatte Sun die wichtigste API veröffentlicht, die keine API für Java-Entwickler war, und große Verwirrung über die einfachste API gestiftet. Die Leute wechselten den Parser, ohne zu wissen, daß sie den Parser gewechselt hatten. Es herrscht eine Menge Verwirrung um JAXP, nicht nur über seine Verwendung, sondern auch darüber, was es eigentlich ist.

In diesem Kapitel werde ich mich als erstes mit der Frage befassen, was JAXP ist und was nicht.1 Anschließend werden Sie einen Überblick über JAXP 1.0 erhalten, das noch vielerorts im Einsatz ist. Nachdem Sie mit den Grundlagen vertraut sind, werden wir mit JAXP 1.1 fortfahren, der neuesten Version. (Sie war noch nicht veröffentlicht, während ich dieses Kapitel schrieb, ist aber fast sicher verfügbar, wenn dieses Buch veröffentlicht wird.) Das wird Ihnen bei einigen neuen Features in der neuesten Version auf die Sprünge helfen, insbesondere bei der in JAXP 1.1 enthaltenen TrAX-API. Schnallen Sie sich an und machen Sie sich bereit, endlich die Geheimnisse von JAXP zu verstehen.

API oder Abstraktion

Bevor wir mit dem Code loslegen, ist es wichtig, einige grundlegende Konzepte zu behandeln. Strenggenommen ist JAXP eine API, läßt sich aber genauer als Abstraktionsschicht beschreiben. Es stellt keine neuen Mittel für das Parsing von XML zur Verfügung, fügt nichts zu SAX, DOM oder JDOM hinzu und stellt auch keine neue Funktionalität für die Verarbeitung von Java und XML zur Verfügung. Statt dessen erleichtert es die Lösung einiger schwieriger Aufgaben in DOM und SAX. Es bietet auch die Möglichkeit, einige herstellerspezifische Aufgaben zu erledigen, die bei der Verwendung der APIs DOM und SAX vorkommen können, was es wiederum ermöglicht, diese APIs auf herstellerneutrale Art und Weise zu benutzen.

Während ich diese Features einzeln durchgehen werde, ist das, was Sie wirklich begreifen müssen, die Tatsache, daß JAXP keine Parsing-Funktionalität bietet! Ohne SAX, DOM oder eine andere XML-Parsing-API gibt es kein XML-Parsing. Ich habe schon viele Anfragen nach einem Vergleich zwischen DOM, SAX oder JDOM mit JAXP erhalten. Diese Vergleiche lassen sich unmöglich ziehen, weil die ersten drei APIs einen völlig anderen Zweck erfüllen als JAXP. SAX, DOM und JDOM führen alle XML-Parsing durch. JAXP stellt ein Werkzeug dar, um mit diesen APIs und mit dem Ergebnis des Parsings eines Dokuments umzugehen. Es stellt keine neue Möglichkeit für das Parsing des Dokuments als solches zur Verfügung. Diesen wichtigen Unterschied müssen Sie machen, um JAXP korrekt zu nutzen. Mit diesem Wissen werden Sie Ihren XML-Entwickler-Kollegen wahrscheinlich um Meilen voraus sein.

Wenn Sie noch immer Zweifel haben, laden Sie die JAXP 1.0-Distribution von der Sun-Website unter http://java.sun.com/xml herunter, und Sie bekommen eine Vorstellung davon, wie einfach JAXP ist. In der entsprechenden jar-Datei (jaxp.jar) werden Sie nur sechs Klassen finden! Wie schwierig kann diese API sein? All diese Klassen (die zum Package javax.xml.parsers gehören) setzen auf einen existierenden Parser auf. Und zwei von diesen Klassen dienen zur Fehlerbehandlung. JAXP ist einfacher, als die Leute glauben.

Suns JAXP und Parser

Ein Teil der Probleme rührt von der Tatsache her, daß Suns eigener Parser im JAXP-Download enthalten ist. Die Parser-Klassen befinden sich alle im Archiv parser.jar und sind Bestandteil des Packages com.sun.xml.parser und damit verbundener Unterpackages. Dieser Parser (der nun den Codenamen Crimson trägt) ist kein Bestandteil von JAXP. Er ist Teil der JAXP-Distribution, aber nicht der JAXP-API. Verwirrend? Ein wenig vielleicht.

Stellen Sie es sich so vor: JDOM-Downloads beinhalten den Parser Apache Xerces. Dieser Parser ist kein Bestandteil von JDOM, wird aber von JDOM benutzt, deshalb wird er mitgeliefert, um sicherzustellen, daß JDOM sofort ausgepackt und benutzt werden kann. Dasselbe Prinzip gilt für JAXP, aber es wird nicht so klar publik gemacht: JAXP wird mit dem Sun-Parser geliefert, damit es unmittelbar benutzt werden kann. Allerdings halten viele Leute die Klassen, die zu dem Sun-Parser gehören, für Teile der JAXP-API selbst. Zum Beispiel lautet eine häufige Frage in Newsgroups: »Wie kann ich die Klasse XMLDocument verwenden, die in JAXP enthalten ist? Welche Aufgabe hat sie?« Die Antwort ist in gewisser Hinsicht kompliziert.

Erstens ist die Klasse com.sun.xml.tree.XMLDocument kein Bestandteil von JAXP. Sie ist ein Teil von Suns Parser. Insofern ist die Frage von Anfang an irreführend. Zweitens ist es bei JAXP gerade der Punkt, Herstellerunabhängigkeit im Umgang mit Parsern zu bieten. Unter Verwendung von JAXP könnte derselbe Code mit Suns XML-Parser, dem Apache Xerces XML-Parser und mit Oracles XML-Parser benutzt werden. Der Einsatz einer Sun-spezifischen Klasse ist in diesem Zusammenhang keine gute Idee. Er widerstrebt vollkommen dem eigentlichen Sinn der Anwendung von JAXP.

Beginnen Sie zu begreifen, warum dieses Thema immer undurchsichtiger geworden ist? Der Parser und die API in der JAXP-Distribution (zumindest in derjenigen von Sun) wurden zusammengewürfelt, und Entwickler halten Klassen und Features des einen irrtümlich für Bestandteile des anderen und umgekehrt.

Alte und neue Version

Es gibt im Zusammenhang mit JAXP ein weiteres verwirrendes Problem. JAXP 1.0 unterstützt lediglich SAX 1.0 und DOM Level 1. Es gehört grundsätzlich zur Geschäftspolitik von Sun, keine API und kein Produkt auszuliefern, die bzw. das auf einer vorläufigen Arbeitsversion, einer Beta oder einer anderen unvollendeten Version darunter gelegener APIs basiert. Als JAXP 1.0 finalisiert wurde, setzte Sun es auf SAX 1.0 auf, da SAX 2.0 noch in der Beta-Phase war, und auf DOM Level 1, da Level 2 noch Candidate-Status hatte. Eine Menge Anwender setzten JAXP auf existierende Parser auf (wie etwa Apache Xerces), die eigentlich eine Unterstützung für SAX 2.0 und DOM Level 2 enthielten, und auf diese Weise verloren sie plötzlich Funktionalität. Das Ergebnis waren eine Menge Fragen, wie man Features benutzen könnte, die einfach nicht von JAXP unterstützt wurden.

Es trifft außerdem zu, daß SAX 2.0 sich um diese Zeit herum von der Beta-Version zur finalen Fassung entwickelte und damit wirklich ein gewaltiges Durcheinander anrichtete. Dies hat jedoch viele, die diese späteren Versionen von DOM und SAX nicht benötigten, nicht davon abgehalten, JAXP 1.0 in der realen Produktion einzusetzen, so daß ich etwas Wichtiges vernachlässigen würde, wenn ich nicht sowohl die alte Version (1.0) als auch die neue (1.1), die SAX 2.0 und DOM Level 2 unterstützt, behandeln würde. Der Rest dieses Kapitels ist in zwei Teile unterteilt: Der erste behandelt JAXP 1.0 und der zweite 1.1. Da 1.1 hinsichtlich seiner Funktionalität auf das aufbaut, was 1.0 bietet, sollten Sie beide Abschnitte lesen, ungeachtet der Version der API, die Sie tatsächlich benutzen.

JAXP 1.0

Das Ganze beginnt (und hat begonnen) mit JAXP 1.0. Diese erste Version von Suns API stellte grundsätzlich eine dünne Funktionsschicht über existierenden APIs dar, die herstellerunabhängiges Parsing von Code ermöglichte. Bezüglich SAX ist dies keine große Sache; da Sie nun ein SAX-Experte sind, sind Sie klug genug, die Klasse XMLReaderFactory zu benutzen, anstatt direkt die Parser-Klasse eines Herstellers zu instantiieren. Da Sie natürlich auch ein DOM-Experte sind, wissen Sie, daß es ein Krampf ist, auf herstellerunabhängige Art und Weise mit DOM umzugehen, so daß JAXP in diesem Punkt ein wenig weiterhilft. Darüber hinaus stellte JAXP einige Methoden für den Umgang mit der Validierung und Namensräumen bereit, eine weitere herstellerspezifische Aufgabe, die nun (in den meisten Fällen) auf viel bessere Art erledigt werden kann.

Mit SAX beginnen

Bevor ich darauf eingehe, wie JAXP mit SAX arbeitet, werde ich Sie mit einigen Details von SAX 1.0 vertraut machen. Erinnern Sie sich noch an die Klasse org.xml.sax.helpers.DefaultHandler, die ich Ihnen im Kapitel SAX für Fortgeschrittene gezeigt habe und die die Kern-Handler von SAX 2.0 implementiert hat? Es gab in SAX 1.0 eine ähnliche Klasse namens org.xml.sax.HandlerBase; diese Klasse hat die SAX 1.0-Handler implementiert (die in dieser Version leicht unterschiedlich waren). Sobald Sie dies verstehen, werden Sie in der Lage sein, mit JAXP 1.0 umzugehen.

Wenn Sie JAXP mit einem SAX-konformen Parser einsetzen wollen, besteht Ihre einzige Aufgabe darin, die Klasse HandlerBase zu erweitern und die Callbacks zu implementieren, die Sie für Ihre Anwendung haben möchten. Das war’s, nichts weiter, als das gleiche mit DefaultHandler in SAX 2.0 zu machen. Eine Instanz Ihrer Erweiterungsklasse wird dann zum Kernargument für die meisten JAXP-Methoden, die mit SAX arbeiten.

Dies ist der typische SAX-Arbeitsablauf:

  • eine SAXParser-Instanz unter Verwendung der Parser-Implementierung eines bestimmten Herstellers erzeugen
  • die Callback-Implementierungen registrieren (unter Verwendung einer Klasse, die HandlerBase erweitert)
  • mit dem Parsing beginnen und sich zurücklehnen, während Ihre Callback-Implementierungen aufgerufen werden

Die SAX-Komponente von JAXP bietet ein einfaches Mittel, um all dies zu tun. Ohne JAXP muß entweder eine SAX-Parser-Instanz direkt von einer Herstellerklasse (wie etwa org.apache.xerces.parsers.SAXParser) abgeleitet werden, oder es muß eine SAX-Helferklasse namens ParserFactory (die SAX 1.0-Version der XMLReaderFactory von SAX 2.0) verwendet werden.

JAXP bietet eine bessere Alternative. Es ermöglicht Ihnen die Verwendung der Herstellerklasse als Parser mit Hilfe einer Java-Systemeigenschaft. Wenn Sie natürlich eine Distribution von Sun herunterladen, erhalten Sie eine JAXP-Implementierung, die standardmäßig den Sun-Parser verwendet. Dieselben Interfaces, aber mit einer Implementierung, die auf Apache Xerces aufbaut, können von der Apache-XML-Website unter http://xml.apache.org heruntergeladen werden, und diese verwenden standardmäßig Apache Xerces. Deshalb erfordert die Änderung des Parsers, den Sie verwenden (in jedem Fall) die Änderung einer Klassenpfad-Einstellung oder einer Systemeigenschaft, aber sie erfordert keine Neukompilierung des Codes. Und genau das ist die Magie, die Abstraktion, um die es bei JAXP geht.

Es spielt eine Rolle, von wo Sie die JAXP-Klassen heruntergeladen haben. Auch wenn Sie natürlich selbst die Systemeigenschaften einstellen können, um die Parser-Klasse zu wechseln, hängt der Standard-Parser (wenn keine Systemeigenschaften zur Verfügung stehen) von der Implementierung ab – die wiederum von der Adresse abhängt, von der Sie JAXP haben. Die Version von Apache XML verwendet standardmäßig Apache Xerces, während Suns Version als Standard Crimson benutzt.

Ein Blick auf die Klasse SAXParserFactory

Die JAXP-Klasse SAXParserFactory (wie alle JAXP-Klassen im Package javax.xml.parsers zu finden) ist der Schlüssel zum einfachen Wechsel der Parser-Implementierung. Sie müssen eine neue Instanz dieser Klasse erzeugen (wie das geht, werde ich gleich beschreiben). Nachdem die Factory erzeugt wurde, stellt sie eine Methode zur Verfügung, mit der Sie einen SAX-fähigen Parser erhalten. Hinter den Kulissen kümmert sich die JAXP-Implementierung um den herstellerabhängigen Code und hält Ihren Code frei von Verunreinigung. Diese Factory bietet auch noch einige nette Features.

Zusätzlich zu der grundlegenden Aufgabe, SAX-Parser zu erzeugen, ermöglicht die Factory das Einstellen von Konfigurationsoptionen. Diese Optionen betreffen sämtliche Parser-Instanzen, die Sie durch die Factory erhalten haben. Die beiden in JAXP 1.0 verfügbaren Optionen sind das Einstellen der Namensraumfähigkeit (setNamespaceAware (boolean awareness)) und das Einschalten der Validierung (setValidating (boolean validating)). Denken Sie daran, daß das Setzen dieser Optionen alle Instanzen betrifft, die Sie nach dem Methodenaufruf aus der Factory erhalten.

Nachdem Sie die Factory eingerichtet haben, gibt der Aufruf der Methode newSAXParser( ) eine einsatzbereite Version der JAXP-Klasse SAXParser zurück. Diese Klasse umhüllt einen zugrundeliegenden SAX-Parser (eine Instanz der SAX-Klasse org.xml.sax.Parser). Sie schützt Sie auch davor, herstellerspezifische Ergänzungen der Parser-Klasse zu verwenden. (Erinnern Sie sich an unsere frühere Besprechung der Klasse xmlDocument?) Diese Klasse macht es möglich, daß das eigentliche Parsing-Verhalten in Gang gesetzt wird. Beispiel 9-1 zeigt, wie eine SAX-Factory erzeugt, konfiguriert und angewendet werden kann.

Beispiel 9-1: Die Verwendung der Klasse SAXParserFactory

package javaxml2;

import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;

// JAXP
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.parsers.SAXParser;

// SAX
import org.xml.sax.AttributeList;
import org.xml.sax.HandlerBase;
import org.xml.sax.SAXException;

public class TestSAXParsing {

    public static void main(String[] args) {
        try {
            if (args.length != 1) {
                System.err.println(
                    "Verwendung: java TestSAXParsing [XML-Dokument-Dateiname]");
                System.exit(1);
            }

            // Eine SAX-Parser-Factory erhalten
            SAXParserFactory factory = SAXParserFactory.newInstance(  );

            // Validierung ein- und Namensräume ausschalten
            factory.setValidating(true);
            factory.setNamespaceAware(false);

            SAXParser parser = factory.newSAXParser(  );
            parser.parse(new File(args[0]), new MyHandler(  ));

        } catch (ParserConfigurationException e) {
            System.out.println("Der zugrundeliegende Parser " +
                               "unterstützt die geforderten Features nicht.");
        } catch (FactoryConfigurationError e) {
            System.out.println(
                "Fehler beim Erhalt der SAX-Parser-Factory.");
        } catch (Exception e) {
            e.printStackTrace(  );
        }
    }
}

class MyHandler extends HandlerBase {
    // SAX-Callback-Implementierungen von DocumentHandler, ErrorHandler, 
    //   DTDHandler und EntityResolver
}

Beachten Sie in diesem Code, daß bei der Verwendung der Factory zwei JAXP-spezifische Probleme auftreten können: die Unfähigkeit, eine SAX-Factory zu erhalten oder zu konfigurieren, sowie die Unfähigkeit, einen SAX-Parser zu konfigurieren. Das erste dieser Probleme, dargestellt durch einen FactoryConfigurationError, tritt gewöhnlich auf, wenn der in einer JAXP-Implementierung oder Systemeigenschaft angegebene Parser nicht geladen werden kann. Das zweite Problem, ParserConfigurationException, tritt auf, wenn ein gefordertes Feature im verwendeten Parser nicht zur Verfügung steht. Beide sind leicht zu beheben und sollten nicht zu irgendwelchen Schwierigkeiten führen.

Ein SAXParser wird geholt, nachdem Sie die Factory erhalten haben. Namensräume werden aus- und die Validierung wird eingeschaltet; dann beginnt das Parsing. Beachten Sie, daß die Methode parse( ) des SAX-Parsers eine Instanz der SAX-Klasse HandlerBase erwartet, die ich vorhin erwähnt habe (ich habe die Implementierung dieser Klasse im Code-Listing weggelassen, aber Sie können die komplette Quelldatei TestSAXParsing.java von der Website zum Buch herunterladen). Natürlich übergeben Sie auch die Datei, deren Parsing erfolgen soll (als Java-File-Objekt). Allerdings enthält die Klasse SAXParser noch viel mehr als nur diese eine Methode.

Mit der Klasse SAXParser arbeiten

Nachdem Sie eine Instanz der Klasse SAXParser haben, können Sie mehr damit tun, als ihr bloß eine File-Instanz für das Parsing zu übergeben. Wegen der Art und Weise, wie die Komponenten innerhalb von großen Anwendungen heutzutage miteinander kommunizieren, ist es nicht immer sicher anzunehmen, daß die Komponente, die eine Objektinstanz erzeugt hat, auch diejenige ist, die sie benutzt. Mit anderen Worten: Es könnte sein, daß eine Komponente die SAXParser-Instanz erzeugt hat, während eine weitere Komponente (möglicherweise von einem anderen Entwickler programmiert) diese gleiche Instanz benutzen muß.

Aus diesem Grund werden Methoden zur Verfügung gestellt, um die Einstellungen einer Parser-Instanz ausfindig zu machen. Die beiden Methoden, die diese Funktionalität bieten, sind isValidating( ) zur Information der aufrufenden Stelle darüber, ob der Parser eine Validierung durchführen wird, sowie isNamespace Aware( ) für eine Angabe darüber, ob der Parser Namensräume in einem XML-Dokument verarbeiten kann. Auch wenn diese Methoden Ihnen Informationen darüber geben können, was der Parser zu leisten vermag, haben Sie keine Möglichkeit, diese Features zu ändern. Sie müssen dies auf der Ebene der Parser-Factory erledigen.

Darüber hinaus gibt es eine Vielzahl von Möglichkeiten, um das Parsing eines Dokuments anzufordern. Anstatt nur eine File- und eine SAX HandlerBase-Instanz zuzulassen, ist die SAXParser-Methode parse( ) auch in der Lage, eine SAX-InputSource, einen Java-InputStream oder eine URL in Form eines Strings zu akzeptieren, und zwar alle mit einer HandlerBase-Instanz als zweitem Argument. Unterschiedliche Arten von Eingabedokumenten können durch unterschiedliche Parsing-Mittel behandelt werden.

Zu guter Letzt kann der zugrundeliegende SAX-Parser (eine Instanz von org.xml.sax.Parser) direkt durch die SAXParser-Methode getParser( ) entgegengenommen und verwendet werden. Nachdem diese grundlegende Instanz erzeugt wurde, stehen die üblichen SAX-Methoden zur Verfügung. Beispiel 9-2 zeigt Beispiele für die unterschiedlichen Anwendungsmöglichkeiten der Klasse SAXParser, der JAXP-Kernklasse für das SAX-Parsing.

Beispiel 9-2: Verwendung der JAXP-Klasse SAXParser

    // Eine SAX-Parser-Instanz erhalten
    SAXParser saxParser = saxFactory.newSAXParser(  );

    // Herausfinden, ob Validierung unterstützt wird
    boolean isValidating = saxParser.isValidating(  );

    // Herausfinden, ob Namensräume unterstützt werden
    boolean isNamespaceAware = saxParser.isNamespaceAware(  );

    // ------- Parsing, auf verschiedene Arten ----------------- //

    // Eine Datei und eine SAX-HandlerBase-Instanz verwenden
    saxParser.parse(new File(args[0]), myHandlerBaseInstance);

    // Eine SAX-InputSource und eine SAX-HandlerBase-Instanz verwenden
    saxParser.parse(mySaxInputSource, myHandlerBaseInstance);

    // Einen InputStream und eine SAX-HandlerBase-Instanz verwenden
    saxParser.parse(myInputStream, myHandlerBaseInstance);

    // Eine URI und eine SAX-HandlerBase-Instanz verwenden
    saxParser.parse("http://www.newInstance.com/xml/doc.xml", 
                    myHandlerBaseInstance);

    // Den zugrundeliegenden (umhüllten) SAX-Parser erhalten
    org.xml.sax.Parser parser = saxParser.getParser(  );    

    // Den zugrundeliegenden Parser verwenden
    parser.setContentHandler(myContentHandlerInstance);
    parser.setErrorHandler(myErrorHandlerInstance);
    parser.parse(new org.xml.sax.InputSource(args[0]));

Bis jetzt habe ich eine Menge über SAX gesagt, aber ich habe noch nichts Bemerkenswertes oder auch nur Überraschendes enthüllt. Fakt ist, daß die Funktionalität von JAXP nicht besonders umfangreich ist, insbesondere dann, wenn SAX ins Spiel kommt. Ich persönlich bin damit einverstanden (und Sie sollten es auch sein), denn minimale Funktionalität bedeutet, daß Ihr Code leichter portierbar ist und von anderen Entwicklern, egal ob frei (als Open Source) oder kommerziell, mit einem beliebigen SAX-konformen XML-Parser verwendet werden kann. Das ist alles. Es gibt nichts weiter über die Anwendung von SAX mit JAXP zu sagen.

Wenn Sie SAX bereits kennen, haben Sie 98 Prozent des Weges hinter sich gebracht. Sie brauchen lediglich zwei neue Klassen und einige Java-Exceptions zu erlernen, und schon können Sie loslegen. Wenn Sie SAX noch nie benutzt haben, ist es dennoch ein leichtes, jetzt damit anzufangen.

Umgang mit DOM

Die Anwendung von JAXP mit DOM ist beinahe identisch mit der Verwendung von JAXP mit SAX; es ist lediglich die Änderung zweier Klassennamen und des Rückgabetyps einer Methode erforderlich, und Sie haben fast das Ziel erreicht. Wenn Sie nicht verstehen, wie SAX funktioniert, aber begreifen, was DOM ist, werden Sie kein Problem haben. Natürlich besteht die Möglichkeit, in den Kapiteln DOM und DOM für Fortgeschrittene nachzuschlagen; insofern sind Sie gut ausgerüstet. Da JAXP keine SAX-Callbacks auslösen muß, wenn es mit DOM arbeitet, ist es lediglich für die Rückgabe eines DOM-Document-Objekts aus dem Parsing zuständig.

Ein Blick auf die DOM-Parser-Factory

Wenn Sie ein grundlegendes Verständnis für DOM und die Unterschiede zwischen DOM und SAX erworben haben, bleibt nur noch wenig zu sagen. Der Code in Beispiel 9-3 sieht dem SAX-Code in Beispiel 9-1 auffallend ähnlich. Als erstes wird eine Instanz der Klasse DocumentBuilderFactory (auf die gleiche Art und Weise wie die SAXParserFactory-Instanz in SAX) entgegengenommen. Anschließend wird (wiederum auf dieselbe Art und Weise wie in SAX) die Art und Weise konfiguriert, wie die Factory mit der Validierung und mit Namensräumen umgehen soll. Als nächstes wird aus der Factory ein DocumentBuilder entnommen, das DOM-Äquivalent zu einem SAXParser. Dann kann das Parsing stattfinden, und das resultierende DOM-Document-Objekt wird an eine Instanz der Klasse DOMSerializer (aus dem Kapitel DOM) weitergereicht.

Beispiel 9-3: Die Klasse DocumentBuilderFactory verwenden

package javaxml2;

import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;

// JAXP
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;

// DOM
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 TestDOMParsing {

    public static void main(String[] args) {
        try {
            if (args.length != 1) {
                System.err.println (
                    "Verwendung: java TestDOMParsing [Dateiname]");
                System.exit(1);
            }

            // Die Document Builder Factory erhalten
            DocumentBuilderFactory factory = 
                DocumentBuilderFactory.newInstance(  );

            // Die Validierung ein- und die Namensräume ausschalten
            factory.setValidating(true);
            factory.setNamespaceAware(false);

            DocumentBuilder builder = factory.newDocumentBuilder(  );
            Document doc = builder.parse(new File(args[0]));

            // Den DOM-Baum serialisieren
            DOMSerializer serializer = new DOMSerializer(  );
            serializer.serialize(doc, System.out);

        } catch (ParserConfigurationException e) {
            System.out.println("Der zugrundeliegende Parser " +
                "unterstützt die geforderten Features nicht.");
        } catch (FactoryConfigurationError e) {
            System.out.println("Fehler beim Erhalt der Document " +
                "Builder Factory.");
        } catch (Exception e) {
            e.printStackTrace(  );
        }
    }
}

Aus diesem Code können sich zwei Probleme ergeben: ein FactoryConfigurationError und eine ParserConfigurationException. Der Grund für die beiden ist der gleiche wie in SAX. Entweder besteht ein Problem in den Implementierungsklassen (FactoryConfigurationError), oder der angegebene Parser unterstützt die geforderten Features nicht (ParserConfigurationException). Der einzige Unterschied zwischen DOM und SAX ist, daß Sie in DOM die SAXParserFactory durch die DocumentBuilderFactory und den SAXParser durch den DocumentBuilder ersetzen.

Mit dem DOM-Parser arbeiten

Nachdem Sie eine DOM-Factory haben, können Sie aus dieser eine DocumentBuilder-Instanz erhalten. Die Methoden, die einer DocumentBuilder-Instanz zur Verfügung stehen, ähneln den Methoden ihres SAX-Gegenstücks stark. Der Hauptunterschied besteht darin, daß die Variationen der Methode parse( ) keine Instanz der SAX-Klasse HandlerBase entgegennehmen. Statt dessen geben sie eine DOM-Document-Instanz zurück, die das XML-Dokument darstellt, dessen Parsing durchgeführt wurde.

Der einzige weitere Unterschied ist, daß zwei Methoden für eine SAX-ähnliche Funktionalität angeboten werden: setErrorHandler( ), die eine SAX-ErrorHandler-Implementierung zur Behandlung von Problemen während des Parsings entgegennimmt, und setEntityResolver( ), die eine SAX-EntityResolver-Implementierung zur Bearbeitung von Entity-Auflösungen annimmt. Beispiel 9-4 zeigt Beispiele dieser Methoden in Aktion.

Beispiel 9-4: Verwendung von JAXP DocumentBuilder

    // Eine DocumentBuilder-Instanz erhalten
    DocumentBuilder builder = builderFactory.newDocumentBuilder(  );

    // Herausfinden, ob Validierung unterstützt wird
    boolean isValidating = builder.isValidating(  );

    // Herausfinden, ob Namensräume unterstützt werden
    boolean isNamespaceAware = builder.isNamespaceAware(  );

    // Einen SAX-ErrorHandler einstellen
    builder.setErrorHandler(myErrorHandlerImpl);

    // Einen SAX-EntityResolver einstellen
    builder.setEntityResolver(myEntityResolverImpl);

    // ------------ Parsing, auf verschiedene Arten ------------------- //

    // Eine Datei verwenden
    Document doc = builder.parse(new File(args[0]));

    // Eine SAX-InputSource verwenden
    Document doc = builder.parse(mySaxInputSource);

    // Einen InputStream verwenden
    Document doc = builder.parse(myInputStream, myHandlerBaseInstance);

    // Eine URI verwenden
    Document doc = builder.parse("http://www.newInstance.com/xml/doc.xml");

Es ist tatsächlich so unproblematisch, das über SAX Gelernte auf DOM zu übertragen. Also wetten Sie ruhig mit Ihren Freunden und Kollegen, daß die Verwendung von JAXP ein Kinderspiel sei; Sie werden jedesmal gewinnen.

Den Parser wechseln

Das letzte Thema, das ich im Zusammenhang mit JAXP ansprechen muß, ist die einfache Austauschbarkeit des Parsers, der von den Factory-Klassen verwendet wird. Der Wechsel des von JAXP benutzten Parsers bedeutet den Wechsel der Parser-Factory, da alle SAXParser- und DocumentBuilder-Instanzen aus diesen Factories kamen. Da die Factories bestimmen, welcher Parser geladen wird, sind es die Factories, die gewechselt werden müssen. Die Implementierung der zu verwendenden SAXParserFactory kann durch das Einstellen der Java-Systemeigenschaft Javax.xml.parsers.SAXParserFactory geändert werden. Wenn diese Eigenschaft nicht definiert ist, wird die Standardimplementierung (eben der Parser, den Ihr Hersteller angegeben hat) angewandt.

Das gleiche Prinzip gilt für die DocumentBuilderFactory-Implementierung, die Sie verwenden. In diesem Fall wird die Systemeigenschaft javax.xml.parsers.DocumentBuilderFactory befragt. Und genauso einfach sind wir alles durchgegangen! Das ist der ganze Umfang von JAXP 1.0: Ansatzpunkte für SAX zu bieten, Ansatzpunkte für DOM zu bieten und den einfachen Wechsel des Parsers zu ermöglichen.

JAXP 1.1

Gegen Ende des Jahres 2000 wurde die Expertengruppe für JAXP 1.1 gebildet und begannen die Arbeiten, um JAXP 1.0 zu einer besseren und effektiveren Lösung für das Parsing und die Verarbeitung von XML-Dokumenten zu machen. Während ich dieses Kapitel schreibe, ist die endgültige Fassung von JAXP 1.1 gerade erst auf der Website von Sun unter http://java.sun.com/xml zum Download angeboten worden. Viele Änderungen an der API drehen sich um das Parsing, was sinnvoll ist, wenn wir uns erinnern, daß das »P« in JAXP für »Parsing« steht. Aber die bedeutendsten Änderungen in JAXP 1.1 haben mit XML-Transformationen zu tun, die ich im letzten Teil dieses Kapitels behandeln werde. Betrachtet man die Erweiterungen gegenüber der Funktionalität der Version 1.0, sind die Änderungen eher geringfügig.

Die größte Ergänzung ist die Unterstützung für SAX 2.0, die im Mai 2000 vollendet wurde, sowie für DOM Level 2, fertiggestellt im November 2000. Erinnern Sie sich, daß JAXP 1.0 nur SAX 1.0 und DOM Level 1 unterstützt. Dieser Mangel an aktualisierten Standards war einer der Hauptkritikpunkte an JAXP 1.0 und ist wahrscheinlich der Grund, warum die Version 1.1 so schnell erschienen ist.

Zusätzlich zur Aktualisierung von JAXP auf die neuesten Versionen von SAX und DOM wurden verschiedene kleine Änderungen an der Feature-Liste der API vorgenommen. Fast alle diese Änderungen sind das Ergebnis von Feedback der verschiedenen Unternehmen und Einzelpersonen in der JAXP-Expertengruppe. Diese Änderungen betreffen auch die Konfiguration der Parser, die von den beiden JAXP-Factories, SAXParserFactory und DocumentBuilderFactory, zurückgegeben werden. Ich behandle diese nun, genau wie die Aktualisierung der Unterstützung für die SAX- und DOM-Standards, und dann schauen wir uns die neue TrAX-API an, die Bestandteil von JAXP 1.1 ist.

Die Aktualisierung der Standards

Die am sehnlichsten erwartete Änderung in JAXP 1.1 gegenüber 1.0 ist die akualisierte Unterstützung der SAX- und DOM-Standards. Es ist dabei wichtig, darauf hinzuweisen, daß SAX 2.0 Namensräume unterstützt, was SAX 1.0 nicht getan hat.2 Diese Namensraum-Unterstützung ermöglicht die Benutzung zahlreicher anderer XML-Vokabularien wie etwa XML Schema, XLink und XPointer. Auch wenn es möglich war, diese Vokabularien in SAX 1.0 anzuwenden, lag die Last, den lokalen (oder qualifizierten) Namen eines Elements von dessen Namensraum abzutrennen und die Namensräume durch das Dokument zu verfolgen, allein beim Entwickler.

SAX 2.0 liefert dem Entwickler diese Informationen und vereinfacht damit drastisch diese Programmieraufgaben. Das gleiche gilt für DOM Level 2: Namensraum-Unterstützung ist verfügbar, genau wie eine Menge anderer Methoden in den DOM-Klassen.

Die gute Nachricht ist, daß diese Änderungen durch die Verwendung von JAXP grundsätzlich für den Benutzer transparent geschehen. Mit anderen Worten: Standard-Updates finden in gewisser Hinsicht »automatisch« und ohne Benutzereingriff statt. Die einfache Festlegung eines SAX 2.0-konformen Parsers für die SAXParserFactory und eines DOM Level 2-konformen Parsers zur Klasse DocumentBuilderFactory kümmert sich um die Aktualisierung der Funktionalität.

Der Weg zu SAX 2.0

Es gibt einige relevante Änderungen im Zusammenhang mit der Aktualisierung dieser Standards, insbesondere im Hinblick auf SAX. In SAX 1.0 war das von Herstellern und XML-Parser-Projekten implementierte Parser-Interface org.xml.sax.Parser. Die JAXP-Klasse SAXParser stellte demnach eine Methode zur Verfügung, um diese zugrundeliegende Implementierungsklasse mit Hilfe der Methode getParser( ) zu erhalten. Die Signatur dieser Methode sieht folgendermaßen aus:

public interface SAXParser {

    public org.xml.sax.Parser getParser(  );

    // Weitere Methoden
}

Allerdings ist das Interface Parser mit dem Wechsel von SAX 1.0 zu 2.0 veraltet und wurde durch das neue Interface org.xml.sax.XMLReader ersetzt (mit dem Sie aus früheren Kapiteln vertraut sind). Dadurch ist die Methode getParser( ) für den Erhalt einer Instanz der SAX 2.0-Klasse XMLReader nutzlos geworden. Um dieses neue Interface zu unterstützen, wurde eine neue Methode zur JAXP-Klasse SAXParser hinzugefügt. Diese Methode heißt getXMLReader( ), was keine Überraschung ist, und sieht so aus:

public interface SAXParser {

    public org.xml.sax.XMLReader getXMLReader(  );

    public org.xml.sax.Parser getParser(  );

    // Weitere Methoden
}

Auf dieselbe Weise hat JAXP 1.0 die Methode parse( ) verwendet, indem es eine Instanz der Klasse HandlerBase (oder genaugenommen einer Unterklasse davon) übergab. Natürlich wurde die Klasse HandlerBase in SAX 2.0 durch DefaultHandler ersetzt. Um diese Änderung bequemer zu machen, wurden alle parse( )-Methoden der SAXParser-Klasse durch Versionen ergänzt, die zur Unterstützung von SAX 2.0 eine Instanz der Klasse DefaultHandler entgegennehmen. Wenn Sie Hilfe dabei benötigen, diesen Unterschied zu sehen, schauen Sie sich Beispiel 9-5 an, das einen größeren Ausschnitt des Interfaces SAXParser zeigt.

Beispiel 9-5: Die parse( )-Methoden des Interfaces SAXParser

public interface SAXParser {

    // Die SAX 1.0-parse-Methoden
    public void parse(File file, HandlerBase handlerBase);
    public void parse(InputSource inputSource, HandlerBase handlerBase);
    public void parse(InputStream inputStream, HandlerBase handlerBase);
    public void parse(InputStream inputStream, HandlerBase handlerBase, 
                      String systemID);
    public void parse(String uri, HandlerBase handlerBase);

    // Die SAX 2.0-parse-Methoden
    public void parse(File file, DefaultHandler defaultHandler);
    public void parse(InputSource inputSource, 
                      DefaultHandler defaultHandler);
    public void parse(InputStream inputStream, 
                      DefaultHandler defaultHandler);
    public void parse(InputStream inputStream, 
                      DefaultHandler defaultHandler, 
                      String systemID);
    public void parse(String uri, DefaultHandler defaultHandler);

    // Weitere Methoden

}

All diese Methoden für das Parsing könnten ein wenig verwirrend erscheinen, aber es ist nur dann tückisch, wenn Sie mit beiden Versionen von SAX arbeiten. Wenn Sie SAX 1.0 verwenden, werden Sie mit dem Interface Parser und der Klasse HandlerBase arbeiten, und es ist offensichtlich, welche Methoden dann benutzt werden. Ähnlich sieht es beim Einsatz von SAX 2.0 aus, wo es offensichtlich ist, daß die Methoden verwendet werden sollten, die DefaultHandler-Instanzen erwarten und XMLReader-Instanzen zurückgeben. Also betrachten Sie das Ganze einfach als Referenz, und machen Sie sich nicht zu viele Sorgen darüber! Es gibt auch noch einige weitere Änderungen am SAX-Teil der API.

Änderungen an den SAX-Klassen

Um die Besprechung der Änderungen an der existierenden JAXP-Funktionalität abzuschließen, muß ich noch einige neue Methoden durchgehen, die den SAX-Anwendern von JAXP zur Verfügung stehen. Als erstes hat die Klasse SAXParserFactory eine neue Methode, setFeature( ). Wie Sie vielleicht noch aus JAXP 1.0 wissen, ermöglicht die Klasse SAXParserFactory die Konfiguration von SAXParser-Instanzen, die von der Factory zurückgegeben wurden.

Zusätzlich zu den bereits in 1.0 verfügbaren Methoden (setValidating( ) und setNamespaceAware( )) macht diese neue Methode die Anforderung von SAX 2.0-Features für neue Parser-Instanzen möglich. Ein Benutzer kann zum Beispiel das Feature http://apache.org/xml/features/validation/schema anfordern, das die Validierung von XML Schema ein- oder ausschaltet. Dies kann nun unmittelbar mit einer SAXParserFactory durchgeführt werden, wie hier gezeigt wird:

    SAXParserFactory myFactory = SAXParserFactory.newInstance(  );

    // XML Schema-Validierung einschalten
    myFactory.setFeature(
        "http://apache.org/xml/features/validation/schema", true);

    // Eine Instanz des Parsers mit eingeschalteter Schema-Validierung erhalten
    SAXParser parser = myFactory.newSAXParser(  );

Es wird eine Methode namens getFeature( ) angeboten, um die Methode setFeature( ) zu ergänzen und die Erkundung bestimmter Features zu ermöglichen. Diese Methode gibt einen einfachen boolean-Wert zurück.

Abgesehen davon, daß JAXP 1.1 ein Mittel zum Einstellen von SAX-Features (mit den Werten true oder false) bietet, unterstützt es auch das Einstellen von SAX-Eigenschaften (mit Objekten als Werten). Zum Beispiel könnten Sie eine Instanz eines SAX-Parsers verwenden, um die Eigenschaft http://xml.org/sax/properties/lexical-handler einzustellen und ihr eine Implementierung des SAX-Interfaces LexicalHandler zuzuweisen. Da diese lexikalische Eigenschaft und andere Eigenschaften Parser-spezifisch und nicht Factory-spezifisch (wie Features) sind, ist setProperty( ) eine Methode der JAXP-Klasse SAXParser und nicht der Klasse SAXParserFactory. Aber genau wie bei den Features steht ein getProperty( )-Gegenstück zur Verfügung, das den mit einer bestimmten Eigenschaft verknüpften Wert zurückgibt und ebenfalls eine Methode der Klasse SAXParser ist.

Aktualisierungen für DOM

Es sind auch einige neue Methoden für den DOM-Teil von JAXP verfügbar. Diese Methoden wurden zu existierenden JAXP-Klassen hinzugefügt, um sowohl DOM-Level-2-Optionen als auch gängige Konfigurationssituationen zu unterstützen, die innerhalb des letzten Jahres aufgetreten sind. Ich werde all diese Optionen und die zugehörigen Methoden hier nicht behandeln, da viele von ihnen nur in sehr außergewöhnlichen Situationen zum Einsatz kommen und in den meisten Ihrer Anwendungen nicht benötigt werden. Ich ermutige Sie aber, diese in der neuesten JAXP-Spezifikation nachzuschlagen. Nachdem wir die Aktualisierungen der Standards, die SAX-Änderungen und die zusätzlichen DOM-Methoden behandelt haben, sind Sie bereit, etwas über die umwälzendste Änderung in JAXP 1.1 zu lesen: die TrAX-API.

Die TrAX-API

Bis jetzt habe ich die Änderungen beim XML-Parsing in JAXP behandelt. Nun kann ich mich den XML-Transformationen in JAXP 1.1 zuwenden. Die vielleicht aufregendste Entwicklung in der neuesten Version von Suns API ist, daß JAXP 1.1 herstellerneutrale XML-Dokumenttransformationen ermöglicht. Auch wenn diese Herstellerunabhängigkeit die Definition von JAXP als einfache Parsing-API verschleiern könnte, ist dies eine langerwartete Fähigkeit, da XSL-Prozessoren zur Zeit unterschiedliche Methoden und Mittel einsetzen, um die Interaktion mit Benutzern und Entwicklern zu ermöglichen. In der Tat gibt es bei XSL-Prozessoren noch größere Unterschiede zwischen den einzelnen Anbietern als bei den zugehörigen XML-Parsern.

Ursprünglich hatte die JAXP-Expertengruppe vorgehabt, eine einfache Transform-Klasse mit einigen wenigen Methoden anzubieten, um die Spezifikation eines Stylesheets und darauf aufbauender Dokumenttransformationen zu ermöglichen. Dieser erste Ansatz erwies sich als ziemlich wackelig, aber ich freue mich, berichten zu können, daß wir (die JAXP-Expertengruppe) in unseren fortgesetzten Bemühungen viel weiter gegangen sind.

Scott Boag und Michael Kay, zwei von den XSL-Prozessor-Gurus (die an Apache Xalan bzw. SAXON arbeiten), haben mit vielen anderen an der Entwicklung von TrAX gearbeitet, das ein erheblich größeres Feld von Optionen und Features unterstützt und eine vollständige Unterstützung für fast alle XML-Transformationen anbietet – alle unter dem gemeinsamen Dach von JAXP. Das Ergebnis ist die Ergänzung der JAXP-API um das Package javax.xml.transform und einige Unterpackages.

Wie der Parsing-Teil von JAXP erfordert auch die Durchführung von XML-Transformationen drei grundlegende Schritte:

  • eine Transformer-Factory erhalten
  • einen Transformer erzeugen
  • die Operationen (Transformationen) durchführen

Mit der Factory arbeiten

Im Transformationsteil von JAXP wird die Factory, mit der Sie arbeiten werden, durch die Klasse javax.xml.transform.TransformerFactory dargestellt. Diese Klasse entspricht den Klassen SAXParserFactory und DocumentBuilderFactory, die ich bereits in den Abschnitten über JAXP 1.0 und 1.1 behandelt habe. Natürlich ist es ein Kinderspiel, eine Factory-Instanz, mit der Sie arbeiten können, zu erhalten:

TransformerFactory factory = TransformerFactory.newInstance(  );

Dazu gibt es nichts Besonderes zu sagen, hier sind nur einfache Factory-Designprinzipien in Kombination mit einem Singleton-Muster am Werk.

Nachdem die Factory einsatzbereit ist, können verschiedene Optionen für diese eingestellt werden. Diese Optionen betreffen alle durch diese Factory erzeugten Instanzen der Klasse Transformer (die etwas später behandelt wird). Sie können mit Hilfe der TransformerFactory auch Instanzen von javax.xml.transform.Templates erhalten. Templates sind ein fortgeschrittenes JAXP/TrAX-Konzept und werden am Ende dieses Kapitels behandelt.

Die erste dieser Optionen, mit der Sie arbeiten können, ist attributes. Es geht nicht um XML-Attribute, sondern um etwas Ähnliches wie die Eigenschaften, die bei SAX benutzt werden. Attribute ermöglichen es, Optionen an den zugrundeliegenden XSL-Prozessor weiterzureichen, bei dem es sich um Apache Xalan, SAXON oder Oracles XSL-Prozessor (oder theoretisch um jeden beliebigen TrAX-konformen Prozessor) handeln kann. Diese sind allerdings größtenteils herstellerabhängig.

Wie im Parsing-Bereich von JAXP wird eine Methode namens setAttribute( ) und ein entsprechendes Gegenstück, getAttribute( ), angeboten. Genau wie setProperty( ) nimmt die Modifikationsmethode (setAttribute( )) einen Attributnamen und einen Wert vom Typ Object entgegen. Und genau wie getProperty( ) nimmt die Zugriffsmethode (getAttribute( )) einen Attributnamen entgegen und gibt den damit verbundenen Object-Wert zurück.

Die zweite verfügbare Option ist das Einstellen eines ErrorListeners. Der im Interface javax.xml.transform.ErrorListener definierte ErrorListener ermöglicht das Abfangen von Problemen bei der Transformation und deren programmtechnische Behandlung. Das hört sich nach org.xml.sax.ErrorHandler an und ist diesem auch sehr ähnlich. Beispiel 9-6 zeigt dieses Interface.

Beispiel 9-6: Das Interface ErrorListener

package javax.xml.transform;

public interface ErrorListener {
    public void warning(TransformerException exception)
        throws TransformerException;
    public void error(TransformerException exception)
        throws TransformerException;
    public void fatalError(TransformerException exception)
        throws TransformerException;
}

Durch das Erzeugen einer Implementierung dieses Interfaces, das Ausgestalten der drei Callback-Methoden und das Verwenden der Methode setErrorListener( ) der TransformerFactory-Instanz, mit der Sie arbeiten, sind Sie in der Lage, sämtliche Fehler zu behandeln, die während der Transformation auftreten können.

Zu guter Letzt wird eine Methode zur Verfügung gestellt, die den URI-Resolver für die von der Factory erzeugten Instanzen einstellt und zurückgibt. Das in javax.xml.transform. URIResolver definierte Interface verhält sich ebenfalls ähnlich wie ein SAX-Gegenstück, nämlich wie org.xml.sax.EntityResolver. Das Interface besitzt eine einzige Methode, die in Beispiel 9-7 gezeigt wird.

Beispiel 9-7: Das Interface URIResolver

package javax.xml.transform;

public interface URIResolver {
    public Source resolve(String href, String base)
        throws TransformerException;
}

Wird dieses Interface implementiert, ermöglicht es die Verarbeitung von URIs aus XSL-Konstrukten wie xsl:import und xsl:include. Da es eine Source zurückgibt (die ich gleich behandeln werde), können Sie Ihren Transformer anweisen, an unterschiedlichen Adressen nach dem angegebenen Dokument zu suchen, wenn eine bestimmte URI gefunden wird. Wenn zum Beispiel ein Include der URI http://www.oreilly.com/oreilly.xsl erkannt wird, könnten Sie statt dessen das lokale Dokument alternateOreilly.xsl zurückgeben und so verhindern, daß ein Netzwerkzugriff erforderlich wird. Implementierungen des Interfaces URIResolver können durch die Verwendung der Methode setURIResolver( ) der TransformerFactory eingestellt und durch die Methode getURIResolver( ) gelesen werden.

Zuletzt, nachdem Sie die Optionen Ihrer Wahl eingestellt haben, können Sie mit Hilfe der Methode newTransformer( ) der Factory eine oder mehrere Instanzen eines Transformers erhalten, wie hier gezeigt wird:

    // Die Factory erhalten
    TransformerFactory factory = TransformerFactory.newInstance(  );

    // Die Factory konfigurieren
    factory.setErrorResolver(myErrorResolver);
    factory.setURIResolver(myURIResolver);

    // Einen Transformer mit den angegebenen Optionen zum Arbeiten erhalten
    Transformer transformer = 
        factory.newTransformer(new StreamSource("foundation.xsl"));

Wie Sie sehen, nimmt diese Methode das Stylesheet als Eingabe für die Verwendung in allen Transformationen dieser Transformer-Instanz entgegen. Mit anderen Worten: Wenn Sie ein Dokument mit Hilfe der Stylesheets A und B transformieren wollten, dann bräuchten Sie zwei Transformer-Instanzen, eine für jedes Stylesheet. Wenn Sie jedoch mehrere Dokumente mit demselben Stylesheet (nennen wir es Stylesheet C) transformieren wollten, bräuchten Sie nur eine Transformer-Instanz, die mit Stylesheet C verknüpft wäre. Machen Sie sich keine Sorgen über die Klasse StreamSource; diese kommt als nächstes an die Reihe.

XML transformieren

Nachdem Sie eine Instanz eines Transformers haben, können Sie mit der eigentlichen Durchführung der XML-Transformationen beginnen. Diese besteht aus zwei grundlegenden Schritten:

  • Stellen Sie das zu verwendende XSL-Stylesheet ein.
  • Führen Sie die Transformation durch, wobei Sie das XML-Dokument und das Ziel für das Ergebnis festlegen.

Wie ich bereits gezeigt habe, ist der erste Schritt wirklich der einfachste. Ein Stylesheet kann angegeben werden, wenn Sie eine Transformer-Instanz aus der Factory erhalten. Die Adresse dieses Stylesheets muß durch die Übergabe einer javax.xml.transform.Source-Instanz (genauer gesagt einer Instanz einer Implementierung des Interfaces Source) für diese Adresse angegeben werden. Das Interface Source, das Sie in ein paar Codebeispielen gesehen haben, ist das Werkzeug, mit dem Sie die Adresse einer Eingabe ermitteln, sei es ein Stylesheet, ein Dokument oder eine andere Informationsmenge. TrAX bietet das Interface Source und drei konkrete Implementierungen an:

  • javax.xml.transform.stream.StreamSource
  • javax.xml.transform.dom.DOMSource
  • javax.xml.transform.sax.SAXSource

Die erste dieser Implementierungen, StreamSource, liest die Eingabe aus irgendeiner Art von Ein-/Ausgabegerät. Es werden Konstruktoren für die Annahme eines InputStreams, eines Readers oder einer System-ID in String-Form als Eingabe angeboten. Nachdem sie erzeugt wurde, kann die StreamSource dem Transformer zur Verwendung übergeben werden. Dies wird wahrscheinlich die Source-Implementierung sein, die Sie am häufigsten in Programmen einsetzen. Sie ist sehr gut geeignet, um ein Dokument aus einem Netzwerk, einem Eingabe-Stream, einer Benutzereingabe oder anderen statischen Darstellungen von XSL-Stylesheets zu lesen.

Die nächste Source-Implementierung, DOMSource, ermöglicht das Lesen aus einem existierenden DOM-Baum. Sie bietet einen Konstruktor, der einen DOM-Knoten vom Typ org.w3c.dom.Node entgegennimmt, und liest aus diesem Node, wenn sie verwendet wird. Dies ist ideal, um einer Transformation einen existierenden DOM-Baum zu übergeben, vielleicht, nachdem das Parsing bereits stattgefunden hat und das XML-Dokument sich bereits als DOM-Struktur im Speicher befindet, oder wenn Sie einen DOM-Baum per Programmierung aufgebaut haben.

SAXSource ermöglicht das Lesen von Eingaben, die aus SAX-produzierenden Strukturen stammen. Diese Source-Implementierung nimmt entweder eine SAX-org.xml.sax. Input-Source oder einen org.xml.sax.XMLReader als Eingabe entgegen und verwendet die Ereignisse aus diesen Quellen. Dies ist in solchen Situationen ideal, in denen ein SAX-Content-Handler bereits im Gebrauch ist und in denen Callbacks eingerichtet sind und vor den Transformationen ausgelöst werden müssen.

Nachdem Sie eine Instanz eines Transformers erhalten haben (indem Sie das zu verwendende Stylesheet durch eine passende Source angegeben haben), sind Sie in der Lage, eine Transformation durchzuführen. Die Methode transform( ) wird verwendet, wie hier gezeigt wird:

    // Die Factory erhalten
    TransformerFactory factory = TransformerFactory.newInstance(  );

    // Die Factory konfigurieren
    factory.setErrorResolver(myErrorResolver);
    factory.setURIResolver(myURIResolver);

    // Einen Transformer mit den angegebenen Optionen zum Arbeiten erhalten
    Transformer transformer = 
        factory.newTransformer(new StreamSource("foundation.xsl"));

    // Transformation mit einem Dokument durchführen und das Ergebnis ausgeben
    transfomer.transform(new StreamSource("asimov.xml"),
                         new StreamResult("results.xml"));

Die Methode transform( ) nimmt zwei Argumente entgegen: eine Source-Implementierung und eine javax.xml.transform.Result-Implementierung. Sie sollten bereits die Symmetrie erkennen, mit der dies funktioniert, und eine Vorstellung von der Funktionalität im Interface Result haben. Die Source stellt das zu transformierende XML-Dokument dar, und das Result bietet ein Ausgabeziel für die Transformation. Wie von Source gibt es auch von Result drei konkrete Implementierungen, die mit TrAX und JAXP geliefert werden:

  • javax.xml.transform.stream.StreamResult
  • javax.xml.transform.dom.DOMResult
  • javax.xml.transform.sax.SAXResult

Die Klasse StreamResult nimmt als Konstruktionsmechanismus entweder einen OutputStream (wie etwa System.out für einfaches Debugging!), ein Java-File, eine System-ID in String-Form oder einen Writer entgegen. DOMResult nimmt einen DOM-Node entgegen, in den die Ausgabe der Transformation erfolgen soll (wahrscheinlich als DOM-org.w3c.dom.Document), und SAXResult nimmt eine SAX-ContentHandler-Instanz entgegen, an die Callbacks gesendet werden können, die sich aus dem transformierten XML ergeben. Alle entsprechen ihren Source-Gegenstücken.

Auch wenn das vorige Ergebnis die Transformation von einem Stream in einen Stream zeigt, ist jede beliebige Kombination von Quellen und Ergebnissen möglich. Hier einige Beispiele:

    // Transformation an jordan.xml durchführen und das Ergebnis ausgeben
    transformer.transform(new StreamSource("jordan.xml"),
                         new StreamResult(System.out));

    // SAX-Eingabe transformieren und das Ergebnis in einen DOM-Knoten ausgeben
    transformer.transform(new SAXSource(
                              new InputSource(
                                  "http://www.oreilly.com/catalog.xml")),
                           new DOMResult(DocumentBuilder.newDocument(  )));

    // Aus DOM transformieren und in eine Datei ausgeben
    transformer.transform(new DOMSource(domTree),
                          new StreamResult(
                              new FileOutputStream("results.xml")));

    // selbstdefinierte Quelle und Ergebnis verwenden (JDOM)
    transformer.transform(new org.jdom.trax.JDOMSource(myJdomDocument),
                          new org.jdom.trax.JDOMResult(new org.jdom.Document()));

TrAX bietet eine überwältigende Flexibilität bezüglich der Wege von unterschiedlichen Eingabearten zu verschiedenen Ausgabearten und bezüglich der Verwendung von XSL-Stylesheets in einer Vielzahl von Formaten, wie Dateien, im Speicher befindlichen DOM-Bäumen, SAX-Readern und so weiter.

Dies und jenes

Bevor wir den JAXP-Laden dichtmachen, gibt es noch ein paar Einzelheiten zu TrAX, über die ich noch nicht gesprochen habe. Ich werde diese nicht vollständig behandeln, da sie weniger häufig genutzt werden, aber ich werde sie kurz ansprechen. Als erstes bietet TrAX ein Interface namens SourceLocator, das sich ebenfalls im Package javax.xml.transform befindet. Diese Klasse funktioniert bei Transformationen genau wie die Locator-Klasse beim SAX-Parsing: Sie liefert Informationen darüber, an welcher Stelle gerade Aktionen stattfinden. Das Interface, das am häufigsten für Fehlermeldungen verwendet wird, sieht so aus:

package javax.xml.transform;

public interface SourceLocator {
    public int getColumnNumber(  );
    public int getLineNumber(  );
    public String getPublicId(  );
    public String getSystemId(  );
}

Ich werde dieses Interface nicht großartig kommentieren, da es ziemlich selbsterklärend ist. Allerdings sollten Sie wissen, daß es im Package javax.xml.transform.dom ein Unter-Interface namens DOMLocator gibt. Dieses Interface fügt die Methode getOriginatingNode( ) hinzu, die den DOM-Knoten zurückgibt, der gerade bearbeitet wird. Dies macht die Fehlerbehandlung beim Arbeiten mit einer DOMSource recht einfach und ist nützlich für Anwendungen, die mit DOM-Bäumen arbeiten.

TrAX bietet außerdem eine konkrete Klasse, javax.xml.transform.OutputKeys, die verschiedene Konstanten für die Verwendung in den Ausgabeeigenschaften von Transformationen definiert. Diese Konstanten können dann für das Einstellen von Eigenschaften eines Transformer- oder Templates-Objekts verwendet werden. Dies führt mich zum letzten Thema, das mit TrAX zu tun hat.

Das Templates-Interface in TrAX wird verwendet, wenn eine bestimmte Einstellung von Ausgabeeigenschaften über mehrere Transformationen hinweg gewünscht wird oder wenn ein Satz von Transformationsanweisungen mehrfach genutzt werden kann. Indem Sie der Methode newTemplates( ) einer TransformerFactory eine Source übergeben, können Sie eine Instanz des Templates-Objekts erhalten:

// Eine Factory erhalten
TransformerFactory factory = TransformerFactory.newInstance(  );

// Ein Templates-Objekt erhalten
Templates template = factory.newTemplates(new StreamSource("html.xsl"));

Zu diesem Zeitpunkt wäre das Objekt template eine kompilierte Darstellung der Transformation, deren Details in html.xsl (in diesem Beispiel ein Stylesheet, das XML in HTML konvertiert) festgelegt sind. Durch die Verwendung eines Templates-Objekts können Transformationen von diesem Template über Threads hinweg durchgeführt werden, und Sie erhalten auch einiges an Optimierung, da die Anweisungen schon im voraus kompiliert werden. Nachdem Sie so weit gegangen sind, müssen Sie einen Transformer erzeugen, aber aus dem Templates-Objekt statt aus der Factory:

// Einen Transformer erhalten
Transformer transformer = template.newTransformer(  );

// Transformieren
transformer.transform(new DOMSource(orderForm), 
                      new StreamResult(res.getOutputStream(  )));

Es besteht hier keine Notwendigkeit, der Methode newTransformer( ) eine Source zu übergeben, da der Transformer lediglich ein Satz (schon vorher) kompilierter Anweisungen ist. Von da an geht alles weiter wie gehabt. In diesem Beispiel wird der Transformation ein DOM-Baum übergeben, der ein Bestellformular darstellt. Dieser wird mit Hilfe des Stylesheets html.xsl verarbeitet und dann an den Ausgabestream des Servlets gesendet, um angezeigt zu werden. Ganz schön raffiniert, was? Als generelle Regel gilt: Wenn Sie ein Stylesheet mehr als zweimal einsetzen möchten, verwenden Sie ein Templates-Objekt; dies wird sich als Performance-Gewinn auszahlen. Zusätzlich sind Templates immer dann der einzig mögliche Weg, wenn Sie mit Threads arbeiten.

Vorsicht Falle!

Die API-Kapitel wären nicht vollständig, wenn ich Sie nicht über einige Probleme informieren würde, die mir selbst regelmäßig widerfahren oder zu denen ich gefragt werde. Hoffentlich hilft Ihnen das, etwas Zeit zu sparen und Ihren Code möglicherweise fehlerresistenter zu machen. Lesen Sie weiter, und sehen Sie, wo JAXP heutzutage Ihre Mitmenschen hereinlegt.

Standard-Parser und JAXP-Implementierungen

Es lohnt sich, es noch einmal zu sagen: Die Implementierung von JAXP bestimmt den Standard-Parser. Wenn Sie die JAXP-Implementierung wechseln, läuft das oft darauf hinaus, daß auch der verwendete Parser gewechselt wird, wenn Sie keine Systemeigenschaften für JAXP eingestellt haben. Es könnte sein, daß Ihr Klassenpfad sich ändern muß.

Um dieses Problem vollständig zu vermeiden, können Sie einfach die zuständige JAXP-Systemeigenschaft auf die Parser-Factory einstellen, die Sie benutzen möchten, und ungeachtet der gewählten Implementierung werden Sie das erwartete Verhalten vorfinden. Oder legen Sie noch besser eine Datei namens jaxp.properties im lib-Verzeichnis Ihrer Java-Installation an.3 Diese Datei kann einfach so aussehen:

javax.xml.parsers.SAXParserFactory = org.apache.xerces.XercesFactory

Indem Sie die Factory-Implementierung ändern, ändern Sie den Parser-Wrapper, der von Aufrufen der Methode newSAXParser( ) zurückgegeben wird. Und damit Sie die angegebene Beispieldatei nicht ausprobieren, existiert die Klasse org.apache.xerces.XercesFactory nicht; sie dient nur zu Beispielzwecken. Sie paßte nur zufällig genau in die Begrenzung des Codeblocks!

Features bei Factories, Eigenschaften bei Parsern

Ein gängiger Fehler besteht darin, in der JAXP-Welt Factories und Eigenschaften miteinander zu verwechseln. Die beste Möglichkeit, sich die korrekte Anwendung zu merken, besteht darin, sich den Satz »Features bei Factories, Eigenschaften bei Parsern« einzuprägen. Sie würden erstaunt sein, wie viele Mails ich erhalte, die darauf bestehen, daß der Absender eine »defekte« Version von JAXP besitzt, weil sich der folgende Code nicht kompilieren läßt:

SAXParserFactory factory = SAXParserFactory.newInstance(  );
factory.setProperty(
    "http://apache.org/xml/properties/dom/document-class-name",
    "org.apache.xerces.dom.DocumentImpl");

Natürlich ist dies eine Eigenschaft und muß deshalb bei einer SAXParser-Instanz und nicht bei einer SAXParserFactory-Instanz eingestellt werden. Und natürlich trifft dies umgekehrt auch für das Einstellen von Features bei Parsern zu:

SAXParser parser = factory.newSAXParser(  );
parser.setFeature("http://xml.org/sax/features/namespaces", true);

In beiden Fällen handelt es sich um einen Anwendungsfehler und nicht um ein merkwürdiges Download-Problem, bei dem nur wenige Methoden korrekt übertragen wurden (ich verweise diese Leute grundsätzlich auf ein paar gute Bücher über I/O). Dies ist auch ein gutes Beispiel für einen Fall, in dem Javadoc nicht verwendet wurde, obwohl es hätte verwendet werden sollen. Ich persönlich glaube fest an den Wert von Javadoc.

Und was kommt jetzt?

Da JAXP eine Abstraktionsebene über den in früheren Kapiteln besprochenen APIs darstellt, besteht kein Bedarf, über »fortgeschrittenes JAXP« zu sprechen. Abgesehen davon sind die JAXP-Konzepte so einfach, daß sie kein zusätzliches Kapitel erforderlich machen. Nach dieser Tour durch die verschiedenen »Low-Level«-APIs für Java und XML sollten Sie mit allen Hämmern und Schraubenschlüsseln für Ihre XML-Programmierung ausgestattet sein.

Allerdings besteht XML heutzutage gewiß aus mehr als Low-Level-APIs. Zusätzlich zu den vertikalen Anwendungen von XML gibt es eine Anzahl von High-Level-APIs, die auf den Konzepten (und APIs) aus der ersten Hälfte dieses Buches aufbauen, um dem Entwickler mehr Komfort zu bieten. Diese spezielleren Konzepte und Programmierwerkzeuge bilden das Rückgrat der zweiten Hälfte dieses Buches. Ich beginne ihre Behandlung im Kapitel Web Publishing Frameworks, indem ich über Präsentations-Frameworks spreche, etwas, das einen wahren Augenschmaus auf der Basis von XML bietet. Lesen Sie weiter, und für etwa ein Kapitel werden wir alle zu Grafikdesignern.

1)
Wenn Ihnen dieses Kapitel ein wenig wie ein Déjà-vu vorkommt, haben Sie möglicherweise eine frühere Version dieses Textes bei IBM DeveloperWorks gelesen. Dort gab es ursprünglich zwei Artikel (veröffentlicht unter http://www.ibm.com/developer), die JAXP untersucht haben. Dieses Kapitel ist eine aktualisierte und leicht veränderte Fassung dieser Artikel.
2)
Aufmerksame Leser werden bemerkt haben, daß JAXP 1.0 durch die setNamespaceAware( )-Methoden eine Namensraum-Unterstützung für SAXParserFactory und DocumentBuilderFactory angeboten hat. Der JAXP-Code mußte diese Aufgabe »zu Fuß« erledigen, anstatt sich auf die SAX- oder DOM-APIs zu stützen. In SAX 2.0 und DOM Level 2 wurde dieser Prozeß standardisiert und ist damit erheblich verläßlicher und auch sauberer als die JAXP-Implementierung.
3)
Diese Option setzt voraus, daß Sie die Umgebungsvariable JAVA_HOME auf das Installationsverzeichnis Ihres JDK eingestellt haben. Sie nimmt dies an, weil es eine gute, um nicht zu sagen obligatorische Praxis ist und Ihnen langfristig weiterhelfen wird. JAXP hält in Wirklichkeit Ausschau nach %JAVA_HOME%/lib/jaxp.properties.