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

SAX für Fortgeschrittene

Das vorige Kapitel war eine gute Einführung in SAX. Allerdings gibt es noch einige weitere Themen, die Ihr Wissen über SAX vervollständigen. Auch wenn ich dieses Kapitel »Fortgeschrittenes SAX« genannt habe, sollten Sie sich nicht abschrecken lassen. Es könnte mit der gleichen Berechtigung »Weniger häufig verwendete Teile von SAX, die dennoch wichtig sind« heißen. Beim Schreiben dieser beiden Kapitel verfolge ich das 80/20-Prinzip. 80% von Ihnen werden wahrscheinlich niemals das Material dieses Kapitels nutzen, und das Kapitel SAX wird Ihren Bedarf vollständig decken. Aber für die Poweruser da draußen, die den lieben langen Tag mit XML arbeiten, enthält dieses Kapitel einige der spezielleren Fähigkeiten von SAX, die sie benötigen werden.

Ich beginne mit einem Überblick über das Einstellen von Parser-Eigenschaften und -Features und bespreche die Konfiguration Ihres Parsers für genau die Aufgaben, für die Sie ihn brauchen. Anschließend fahre ich mit einigen weiteren Handlern fort: EntityResolver und DTDHandler, die vom vorigen Kapitel übriggeblieben sind. Zu diesem Zeitpunkt haben Sie ein umfassendes Verständnis der SAX 2.0-Distribution erhalten. Allerdings fahren wir dann mit der Betrachtung einiger SAX-Erweiterungen fort, angefangen bei den Writern, die mit SAX verbunden werden können, sowie einiger Filtermechanismen. Zuletzt stelle ich Ihnen einige neue Handler vor, LexicalHandler und DeclHandler, und zeige Ihnen, wie sie funktionieren.

Nachdem das alles erledigt ist (nicht ohne einen weiteren »Vorsicht Falle!«-Abschnitt), sollten Sie in der Lage sein, nur mit Ihrem Parser und den SAX-Klassen die Welt zu erobern. Also schlüpfen Sie in Ihren glänzenden Raumanzug, und greifen Sie zum Steuerknüppel – ähm... nun, meine Pläne zur Welteroberung sind ein wenig mit mir durchgegangen. Auf jeden Fall wollen wir jetzt loslegen.

Eigenschaften und Features

Bei der Menge an XML-bezogenen Spezifikationen und Technologien, die vom World Wide Web Consortium (W3C) veröffentlicht werden, ist es schwierig geworden, Unterstützung für irgendein neues Feature oder eine neue Eigenschaft eines XML-Parsers hinzuzufügen. Viele Parser-Implementierungen enthalten proprietäre Erweiterungen oder Methoden auf Kosten der Code-Portierbarkeit. Auch wenn diese Software-Packages das SAX-XMLReader-Interface implementieren können, sind die Methoden für das Einstellen der Dokument- und Schema-Validierung, für die Namensraumunterstützung und andere Kern-Features kein Standard über alle Parser-Implementierungen hinweg. Um dies zu regeln, definiert SAX 2.0 einen Standardmechanismus, um wichtige Eigenschaften und Features eines Parsers einzustellen. Dieser ermöglicht das Hinzufügen neuer Eigenschaften und Features, die vom W3C angenommen wurden, ohne die Verwendung proprietärer Erweiterungen oder Methoden.

Eigenschaften und Features einstellen

Zum Glück für Sie und für mich sind die Methoden, die für das Einstellen von Eigenschaften und Features benötigt werden, in SAX 2.0 Bestandteil des XMLReader-Interfaces. Das bedeutet, daß Sie nur wenig an Ihrem existierenden Code ändern müssen, um eine Validierung anzufordern, um den Namensraum-Separator einzustellen und um andere Feature- und Eigenschaftsanforderungen zu bearbeiten. Die dafür verwendeten Methoden werden in Tabelle 4-1 zusammengefaßt.

Tabelle 4-1: Eigenschafts- und Feature-Methoden
MethodeRückgabeParameterSyntax
setProperty( )voidString propertyID, Object valueparser.setProperty("[Property URI]", propertyValue);
setFeature( )voidString featureID, boolean stateparser.setFeature("[Feature URI]", featureState);
getProperty( )ObjectString propertyIDObject propertyValue = parser.getProperty("[Property URI]");
getFeature( )booleanString featureIDboolean featureState = parser.getFeature("[Feature URI]");

Die ID einer bestimmten Eigenschaft oder eines Features ist eine URI. Die Grundmenge der Features und Eigenschaften wird im Kapitel SAX 2.0-Features und -Eigenschaften aufgelistet. Zusätzliche Dokumentation über Features und Eigenschaften, die der XML-Parser Ihres Herstellers unterstützt, sollte ebenfalls verfügbar sein. Diese URIs ähneln Namensraum-URIs; sie werden nur als Bezugspunkte auf bestimmte Features verwendet. Gute Parser sorgen dafür, daß Sie keinen Netzwerkzugang benötigen, um diese Features aufzulösen; stellen Sie sie sich als einfache Konstanten vor, die zufällig die Form von URIs haben. Diese Methoden werden einfach aufgerufen und die URI wird lokal aufgelöst, oftmals, um konstant die Information zu enthalten, welche Aktion im Parser stattfinden muß.

Tippen Sie diese Eigenschafts- und Feature-URIs nicht in einen Browser ein, um »ihre Existenz zu testen«. Dies führt oft zu dem Fehler 404 Nicht gefunden. Viele Browser haben mir schon dieses Ergebnis geliefert und darauf bestanden, daß die URIs ungültig seien. Dies ist jedoch nicht der Fall; die URI ist nur ein Bezeichner und wird normalerweise, wie ich bereits ausgeführt habe, lokal ausgewertet. Vertrauen Sie mir: Verwenden Sie die URI einfach, und verlassen Sie sich darauf, daß der Parser das Richtige tut.

Im Kontext der Parser-Konfiguration benötigt eine Eigenschaft einen Objektwert, um verwendbar zu sein. Zum Beispiel würde für die lexikalische Bearbeitung der entsprechenden Eigenschaft eine DOM-Node-Implementierung als Wert übergeben. Im Gegensatz dazu ist ein Feature ein Flag, das der Parser verwendet, um anzugeben, ob eine bestimmte Art der Verarbeitung stattfinden soll. Häufig verwendete Features sind Validierung, Unterstützung von Namensräumen und das Einfügen externer Parameter-Entities.

Der angenehmste Aspekt dieser Methoden ist, daß sie ein einfaches Hinzufügen und Modifizieren von Features ermöglichen. Obwohl neue oder geänderte Features es erforderlich machen, daß eine Parser-Implementierung um unterstützenden Code ergänzt wird, bleibt die Methode, durch die auf Features und Eigenschaften zugegriffen wird, standardkonform und einfach; es muß lediglich eine neue URI definiert werden. Ungeachtet der Komplexität (oder Unklarheit) neuer XML-bezogener Ideen sollte dieser robuste Satz von vier Methoden genügen, um Parsern die Implementierung dieser neuen Ideen zu ermöglichen.

SAX-Eigenschaften und -Features

Sie arbeiten sicherlich häufiger mit denjenigen Features und Eigenschaften, die zum SAX-Standard gehören, als mit anderen. Dies sind Features und Eigenschaften, die in jeder SAX-Distribution zur Verfügung stehen sollten und die jeder SAX-konforme Parser unterstützen sollte. Darüber hinaus behalten Sie so die Herstellerunabhängigkeit in Ihrem Code bei, deshalb empfehle ich Ihnen, falls möglich nur SAX-definierte Eigenschaften und Features zu verwenden.

Validierung

Das häufigste Feature, das Sie verwenden werden, ist das Validierungsfeature. Die URI dafür ist http://xml.org/sax/features/validation, und es ist keine Überraschung, daß er die Validierung im Parser ein- und ausschaltet. Wenn Sie zum Beispiel die Validierung in dem Parsing-Beispiel aus dem vorigen Kapitel SAX einschalten wollen (den Swing-Viewer, Sie erinnern sich?), nehmen Sie zum Beispiel die folgende Änderung in der Quelldatei SAXTreeViewer.java vor:

    public void buildTree(DefaultTreeModel treeModel, 
                          DefaultMutableTreeNode base, String xmlURI) 
        throws IOException, SAXException {

        // Für das Parsing benötigte Instanzen erzeugen
        XMLReader reader = 
            XMLReaderFactory.createXMLReader(vendorParserClass);
        ContentHandler jTreeContentHandler = 
            new JTreeContentHandler(treeModel, base);
        ErrorHandler jTreeErrorHandler = new JTreeErrorHandler(  );

        // Den Content-Handler registrieren
        reader.setContentHandler(jTreeContentHandler);

        // Den Fehlerhandler registrieren
        reader.setErrorHandler(jTreeErrorHandler);
 
        // Validierung anfordern
        reader.setFeature("http://xml.org/sax/features/validation", true);

        // Parsing
        InputSource inputSource = 
            new InputSource(xmlURI);
        reader.parse(inputSource);
    }

Kompilieren Sie diese Änderungen und starten Sie das Beispielprogramm. Nichts passiert, oder? Das ist keine Überraschung, denn das XML, das wir bisher betrachtet haben, ist im Vergleich mit der angegebenen DTD vollständig gültig. Allerdings ist es ziemlich einfach, das zu ändern. Führen Sie die folgende Änderung an Ihrer XML-Datei durch (beachten Sie, daß das Element in der DOCTYPE-Deklaration nicht mehr dem eigentlichen Wurzelelement enstspricht, weil XML zwischen Groß- und Kleinschreibung unterscheidet):

<?xml version="1.0"?>
<!DOCTYPE Buch SYSTEM "DTD/JavaXML.dtd">

<!-- Java und XML Inhalt-->
<buch xmlns="http://www.oreilly.com/javaxml2"
      xmlns:ora="http://www.oreilly.com"
>

Starten Sie nun Ihr Programm mit dem geänderten Dokument. Da die Validierung eingeschaltet ist, sollten Sie einen gräßlichen Stacktrace erhalten, der den Fehler meldet. Natürlich – da das alles ist, was unsere Fehlerhandler-Methoden tun – ist das genau das, was wir wollen:

C:\javaxml2\build>java javaxml2.SAXTreeViewer 
    c:\javaxml2\ch04\xml\contents.xml
**Parsing-Fehler**
  Zeile:   7
  URI:     file:///c:/javaxml2/ch04/xml/contents.xml
  Meldung: Document root element "buch", must match DOCTYPE root "Buch".
org.xml.sax.SAXException: Error encountered
        at javaxml2.JTreeErrorHandler.error(SAXTreeViewer.java:445)
[Überflüssiger Stacktrace folgt...]

Denken Sie daran, daß das Ein- oder Ausschalten der Validierung die DTD-Verarbeitung nicht beeinflußt; ich habe dies im vorigen Kapitel angesprochen und wollte Sie an diese heikle Tatsache erinnern. Um dies besser zu verstehen, sollten Sie die Validierung einmal ausschalten (kommentieren Sie die Feature-Einstellung aus, oder geben Sie ihr den Wert »false«) und das Programm mit dem modifizierten XML starten. Obwohl die DTD verarbeitet wird, was sich an der aufgelösten Entity-Referenz OReillyCopyright zeigt, treten keine Fehler auf. Das ist der Unterschied zwischen der Verarbeitung einer DTD und der Validierung eines XML-Dokuments anhand dieser DTD. Lernen Sie das auswendig, verstehen Sie es, und sagen Sie es auswendig auf; es wird Ihnen auf lange Sicht viele Stunden der Verwirrung ersparen.

Namensräume

Neben der Validierung werden Sie am häufigsten mit Namensräumen arbeiten. Es gibt zwei Features, die mit Namensräumen verbunden sind: eines, das die Namensraum-Verarbeitung ein- oder ausschaltet, und eines, das anzeigt, ob Namensraum-Präfixe als Attribute gemeldet werden sollen. Die beiden hängen grundlegend zusammen, und Sie sollten immer beide »umschalten«, wie in Tabelle 4-2 gezeigt wird.

Tabelle 4-2: Umschaltwerte für Features, die mit Namensräumen verbunden sind
Wert für die Namensraum-VerarbeitungWert für Namensraum-Präfix-Meldungen
TrueFalse
FalseTrue

Das sollte verständlich sein: Wenn die Namensraum-Verarbeitung eingeschaltet ist, sollten die Deklarationen im xmlns-Style nicht als Attribute an Ihre Anwendung weitergegeben werden, da ihr einziger Nutzen im Umgang mit Namensräumen besteht. Wenn Sie jedoch möchten, daß keine Namensraum-Verarbeitung stattfindet (oder wenn Sie sie selbst durchführen möchten), werden Sie wohl wollen, daß diese xmlns-Deklarationen als Attribute angezeigt werden, so daß Sie sie genau wie andere Attribute verwenden können. Wenn diese beiden sich allerdings nicht mehr im Einklang befinden (beide sind true oder beide sind false), kann das zu einem ziemlichen Durcheinander führen!

Sie sollten eventuell eine kleine Utility-Methode schreiben, um sicherzustellen, daß diese beiden Features im Einklang miteinander bleiben. Ich verwende zu diesem Zweck oft die hier gezeigte Methode:

private void setNamespaceProcessing(XMLReader reader, boolean state) 
    throws SAXNotSupportedException, SAXNotRecognizedException {
    reader.setFeature(
        "http://xml.org/sax/features/namespaces", state);
    reader.setFeature(
        "http://xml.org/sax/features/namespace-prefixes", !state);
}

Dies erhält die korrekte Einstellung für beide Features aufrecht, und Sie können nun einfach diese Methode aufrufen, anstatt in Ihrem Code zwei setFeature( )-Aufrufe zu verwenden. Ich persönlich habe dieses Feature weniger als zehnmal in ungefähr zwei Jahren benutzt; die Standardwerte (die Verarbeitung von Namensräumen sowie die Nicht-Meldung von Präfixen als Attribute) sind fast immer das Richtige für mich. Wenn Sie nicht gerade Low-Level-Anwendungen schreiben, die entweder keine Namensräume benötigen oder vom Geschwindigkeitszuwachs durch die Nichtverarbeitung von Namensräumen profitieren, oder Namensräume selbst behandeln müssen, sollten Sie sich über keines dieser Features zu viele Gedanken machen.

Dieser Code beleuchtet dennoch einen ziemlich wichtigen Aspekt von Features und Eigenschaften: Der Aufruf der Feature- und Eigenschaftsmethoden kann SAXNotSupported Exceptions und SAXNotRecognizedExceptions zur Folge haben. Beide befinden sich im Package org.xml.sax und müssen in jeden SAX-Code importiert werden, der sie verwendet.

Die erste Exception gibt an, daß der Parser das Feature oder die Eigenschaft kennt, aber nicht unterstützt. Sie werden das nicht einmal bei Parsern durchschnittlicher Qualität häufig erleben, aber es wird üblicherweise verwendet, wenn eine Standardeigenschaft oder ein Standard-Feature noch nicht einprogrammiert ist. Deshalb könnte der Aufruf setFeature( ) mit dem Namensraum-Verarbeitungs-Feature in einem im Entwicklungsstadium befindlichen Parser zu einer SAXNotSupportedException führen.

Der Parser erkennt das Feature, aber er hat nicht die Fähigkeit, die angeforderte Verarbeitung durchzuführen. Die zweite Exception tritt am häufigsten auf, wenn herstellerspezifische Features und Eigenschaften benutzt werden (wird im nächsten Abschnitt behandelt) und wenn dann die Parser-Implementierung gewechselt wird. Die neue Implementierung weiß nichts über die Features oder Eigenschaften anderer Hersteller und löst eine SAXNotRecognizedException aus.

Sie sollten diese Exceptions stets explizit abfangen, um sie behandeln zu können. Andernfalls verlieren Sie am Ende wertvolle Informationen darüber, was in Ihrem Code passiert ist. Zum Beispiel möchte ich Ihnen eine modifizierte Version des Codes aus dem vorigen Kapitel SAX zeigen, die versucht, verschiedene Features einzurichten, und Sie darauf aufmerksam machen, wie das die Exception-Behandlungsarchitektur verändert:

    public void buildTree(DefaultTreeModel treeModel, 
                          DefaultMutableTreeNode base, String xmlURI) 
        throws IOException, SAXException {
            
        String featureURI = "";

        try {
            // Für das Parsing benötigte Instanzen erzeugen
            XMLReader reader = 
                XMLReaderFactory.createXMLReader(vendorParserClass);
            ContentHandler jTreeContentHandler = 
                new JTreeContentHandler(treeModel, base);
            ErrorHandler jTreeErrorHandler = new JTreeErrorHandler(  );

            // Den Content-Handler registrieren
            reader.setContentHandler(jTreeContentHandler);

            // Den Fehlerhandler registrieren
            reader.setErrorHandler(jTreeErrorHandler);
            
            /** Mit Features umgehen **/
            featureURI = "http://xml.org/sax/features/validation";
        
            // Validierung anfordern
            reader.setFeature(featureURI, true);
            
            // Namensraum-Verarbeitung einschalten
            featureURI = "http://xml.org/sax/features/namespaces";
            setNamespaceProcessing(reader, true);
            
            // String-Interning einschalten
            featureURI = "http://xml.org/sax/features/string-interning";
            reader.setFeature(featureURI, true);
            
            // Schema-Verarbeitung ausschalten
            featureURI = 
                "http://apache.org/xml/features/validation/schema";
            reader.setFeature(featureURI, false);

            // Parsing
            InputSource inputSource = 
                new InputSource(xmlURI);
            reader.parse(inputSource);
        } catch (SAXNotRecognizedException e) {
            System.out.println("Die Parserklasse " + vendorParserClass +
                " erkennt die Feature-URI " + featureURI + " nicht");
            System.exit(0);
        } catch (SAXNotSupportedException e) {
            System.out.println("Die Parserklasse " + vendorParserClass +
                " unterstützt die Feature-URI " + featureURI + " nicht");
            System.exit(0);                
        }        
    }

Indem Sie sich mit diesen Ausnahmen und auch mit anderen besonderen Fällen beschäftigen, liefern Sie dem Anwender bessere Informationen und verbessern die Qualität Ihres Codes.

Interning und Entities

Die drei übrigen SAX-definierten Features sind ziemlich merkwürdig. Das erste, http://xml.org/sax/features/string-interning, schaltet das String-Interning ein oder aus. Standardmäßig ist es in den meisten Parsern false (ausgeschaltet). Wenn es eingeschaltet wird, bedeutet das, daß für jeden Elementnamen, Attributnamen, jede Namensraum-URI und jedes Präfix java.lang.String.intern() aufgerufen wird. Ich erläutere das Interning hier nicht im Detail; wenn Sie nicht wissen, was es ist, lesen Sie Suns Javadoc zu dieser Methode unter http://java.sun.com/j2se/1.3/docs/api/index.html.

Kurz gesagt versucht Java jedesmal, wenn es einen String findet, eine Referenz auf diesen String im aktuellen Vorrat von Strings zurückzugeben, anstatt (möglicherweise) ein neues String-Objekt zu erzeugen. Hört sich gut an, oder? Nun, der Grund, warum es standardmäßig ausgeschaltet ist, besteht darin, daß die meisten Parser ihre eigenen Optimierungen verwenden, die besser abschneiden können als das String-Interning. Ich rate Ihnen, diese Einstellung in Ruhe zu lassen; viele Leute haben mehrere Wochen damit verbracht, die Dinge so einzurichten, deshalb brauchen Sie nicht mit ihnen zu wetteifern.

Die beiden anderen Features legen fest, ob Text-Entities erweitert und aufgelöst werden (http://xml.org/sax/features/external-general-entities) und ob Parameter-Entities enthalten sind (http://xml.org/sax/features/external-parameter-entities), wenn das Parsing stattfindet. Diese werden bei den meisten Parsern auf true gesetzt, da sie mit allen Entities arbeiten, die XML zu bieten hat. Wieder empfehle ich Ihnen, diese Einstellungen beizubehalten, es sei denn, Sie haben einen besonderen Grund dafür, die Entity-Bearbeitung auszuschalten.

DOM-Knoten und Stringliterale

Die Verwendung der beiden Standard-SAX-Eigenschaften ist ein wenig unklarer. In beiden Fällen sind die Eigenschaften nützlicher, um Werte auszulesen, während die Hauptverwendung von Features darin besteht, Werte einzustellen. Darüber hinaus sind beide Eigenschaften für die Fehlerbehandlung hilfreicher als für den allgemeinen Gebrauch. Und zuletzt stellen beide Eigenschaften den Zugriff auf das zur Verfügung, was sich zu einer bestimmten Zeit beim Parsing tut. Die erste Eigenschaft, die durch die URI http://xml.org/sax/properties/dom-node bezeichnet wird, gibt den DOM-Knoten zurück, der gerade verarbeitet wird, oder den DOM-Wurzelknoten, wenn das Parsing nicht aktiv ist. Natürlich habe ich noch nicht über DOM gesprochen, aber das wird in den nächsten beiden Kapiteln mehr Sinn ergeben.

Die zweite Eigenschaft, bezeichnet durch die URI http://xml.org/sax/properties/xml-string, gibt die verarbeiteten Zeichen als Stringliteral zurück. Sie werden in den verschiedenen Parsern unterschiedliche Unterstützung für diese Eigenschaften vorfinden, was zeigt, daß viele Parser-Implementierer ebenfalls an der Nützlichkeit dieser Eigenschaften zweifeln. Zum Beispiel unterstützt Xerces die Eigenschaft xml-string nicht, damit er das Eingabedokument nicht puffern muß (zumindest auf diese spezielle Art und Weise). Andererseits unterstützt er die Eigenschaft dom-node, so daß Sie aus einem SAX-Parser einen (grundlegenden) DOM-Baum-Iterator machen können.

Proprietäre Eigenschaften und Features

Zusätzlich zu den Standard-Features und -eigenschaften, die durch SAX definiert sind, definieren die meisten Parser einige eigene Features und Eigenschaften. Zum Beispiel gibt es für Apache Xerces unter http://xml.apache.org/xerces-j/features.html eine Seite mit Features, die dieser unterstützt, und unter http://xml.apache.org/xerces-j/properties.html eine Seite mit Eigenschaften, die er unterstützt. Ich werde dies hier nicht detailliert behandeln, und Sie sollten die Finger davon lassen, wann immer das möglich ist; es bindet Ihren Code fest an einen bestimmten Hersteller. Allerdings gibt es auch Fälle, in denen das Verwenden herstellerspezifischer Funktionalität Ihnen Arbeit erspart. Lassen Sie in diesen Fällen Vorsicht walten, aber seien Sie nicht töricht; benutzen Sie, was Ihr Parser Ihnen bietet!

Betrachten Sie als Beispiel das Xerces-Feature, das die XML Schema-Verarbeitung ein- und ausschaltet: http://apache.org/xml/features/validation/schema. Da es über die Parser hinweg oder in SAX keine standardmäßige Unterstützung für XML Schemas gibt, sollten Sie dieses spezielle Feature (das standardmäßig auf true gesetzt ist) verwenden, um zum Beispiel zu verhindern, daß Parsing-Zeit damit vergeudet wird, sich in Ihren Dokumenten um referenzierte XML Schemas zu kümmern. Sie sparen Produktionszeit, wenn Sie diese Verarbeitung nicht verwenden, und sie benötigt ein herstellerspezifisches Feature. Überprüfen Sie die Dokumentation Ihres Herstellers in bezug auf Optionen, die zusätzlich zu denen von SAX verfügbar sind.

Weitere Handler

Im vorigen Kapitel habe ich Ihnen die Interfaces ContentHandler und ErrorHandler gezeigt und auch die Interfaces EntityResolver und DTDHandler kurz erwähnt. Da Sie jetzt ein gutes Verständnis der SAX-Grundlagen besitzen, sind Sie in der Lage, sich diese beiden anderen Handler anzuschauen.1 Sie werden feststellen, daß Sie von Zeit zu Zeit den EntityResolver verwenden (eher, wenn Sie Anwendungen schreiben, die verkauft werden sollen) und daß der DTDHandler etwas ist, das Sie nur selten aus Ihrer Trickkiste hervorholen müssen.

Einen EntityResolver verwenden

Der erste dieser neuen Handler ist org.xml.sax.EntityResolver. Dieses Interface tut genau das, was sein Name sagt: Entities auflösen (oder es deklariert zumindest eine Methode, die Entities auflöst, aber Sie verstehen schon). Das Interface definiert nur eine einzige Methode und sieht so aus:

public InputSource resolveEntity(String publicID, String systemID)
    throws SAXException, IOException;

Sie können eine Implementierung dieses Interfaces erzeugen und sie in Ihrer XMLReader-Instanz registrieren (wenig überraschend durch setEntityResolver( )). Nachdem dies geschehen ist, liefert der Reader jedesmal, wenn er eine Entity-Referenz findet, die Public-ID und System-ID dieses Entity an die Methode resolveEntity( ) Ihrer Implementierung. Nun können Sie den normalen Prozeß der Entity-Auflösung verändern.

Typischerweise löst der XML-Reader das Entity durch die angegebene Public- oder System-ID auf, sei es eine Datei, eine URL oder eine andere Ressource. Und wenn der Rückgabewert der Methode resolveEntity( ) gleich null ist, wird dieser Prozeß unverändert ausgeführt. Daher sollten Sie stets dafür sorgen, daß Ihre resolveEntity( )-Implementierung, was auch immer sie sonst für Code enthält, im Standardfall null zurückgibt. Mit anderen Worten: Beginnen Sie mit einer Implementierungsklasse, die so aussieht wie Beispiel 4-1.

Beispiel 4-1: Einfache Implementierung von EntityResolver

package javaxml2;

import java.io.IOException;

import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

public class SimpleEntityResolver implements EntityResolver {
    
    public InputSource resolveEntity(String publicID, String systemID)
        throws IOException, SAXException {
        
        // Im Standardfall null zurückgeben
        return null;    
    }
}    

Sie können diese Klasse ohne Probleme kompilieren und in der Reader-Implementierung registrieren, die in der Klasse SAXTreeViewer in der Methode buildTree( ) verwendet wird:

        // Für das Parsing benötigte Instanzen registrieren
        XMLReader reader = 
            XMLReaderFactory.createXMLReader(vendorParserClass);
        ContentHandler jTreeContentHandler = 
            new JTreeContentHandler(treeModel, base, reader);
        ErrorHandler jTreeErrorHandler = new JTreeErrorHandler(  );

        // Den content-Handler registrieren
        reader.setContentHandler(jTreeContentHandler);

        // Den Fehlerhandler registrieren
        reader.setErrorHandler(jTreeErrorHandler);
            
        // Den Entity-Resolver registrieren
        reader.setEntityResolver(new SimpleEntityResolver(  ));

        // Andere Anweisungen und Parsing...

Ein erneutes Kompilieren und Starten der Beispielklasse bringt keine Änderung mit sich. Natürlich war das vorhersehbar, also seien Sie nicht zu überrascht. Indem stets der Wert null zurückgegeben wird, wird der Prozeß der Entity-Auflösung normal durchgeführt. Wenn Sie allerdings nicht glauben, daß etwas passiert, können Sie diese kleine Änderung vornehmen, um in der Systemausgabe wiederzugeben, was passiert:

    public InputSource resolveEntity(String publicID, String systemID)
        throws IOException, SAXException {
            
        System.out.println("Entity mit der Public-ID " + publicID +
            " und der System-ID " + systemID + " gefunden.");
        
        // Im Standardfall null zurückgeben
        return null;    
    }

Kompilieren Sie diese Klasse erneut, und starten Sie den Beispiel-Tree-Viewer. Wenn die Swing-GUI angezeigt wird, schieben Sie sie zur Seite und überprüfen die Shell- oder Kommandozeilenausgabe; sie sollte so ähnlich aussehen wie Beispiel 4-2.

Beispiel 4-2: Ausgabe von SAXTreeViewer mit ausführlicher Ausgabe

C:\javaxml2\build>java javaxml2.SAXTreeViewer 
    c:\javaxml2\ch04\xml\contents.xml
Entity mit der Public-ID null und der
    System-ID file:///c:/javaxml2/ch04/xml/DTD/JavaXML.dtd gefunden.
Entity mit der Public-ID null und der
    System-ID http://www.newInstance.com/javaxml2/copyright.xml gefunden.

Wie immer dienen die Zeilenumbrüche lediglich der übersichtlicheren Anzeige. Jedenfalls können Sie sehen, daß beide Referenzen im XML-Dokument, für die DTD und die Entity-Referenz OReillyCopyright an die Methode resolveEntity( ) übergeben werden.

An diesem Punkt könnte es sein, daß Sie sich etwas verwundert am Kopf kratzen; eine DTD ist ein Entity? Der Begriff »Entity« wird in EntityResolver etwas vage verwendet. Ein besserer Name wäre vielleicht ExternalReferenceResolver gewesen, aber es würde nicht viel Spaß machen, das einzutippen. Behalten Sie jedenfalls im Kopf, daß jede externe Referenz in Ihrem XML an diese Methode weitergegeben wird. Worum geht es also, könnten Sie sich fragen.

Erinnern Sie sich noch an die Referenz auf OReillyCopyright und daran, wie sie auf eine Internet-URL zugreift (http://www.newInstance.com/javaxml2/copyright.xml)? Was passiert, wenn Sie keinen Internet-Zugang haben? Was ist, wenn bereits eine lokale Kopie heruntergeladen wurde und Sie Zeit sparen möchten, indem Sie diese Kopie verwenden? Was geschieht, wenn Sie einfach Ihr eigenes Copyright an diese Stelle setzen möchten? All das sind praktische Fragen, Probleme aus dem wirklichen Leben, die Sie vielleicht in Ihren Anwendungen lösen müssen. Die Antwort ist natürlich die Methode resolveEntity( ), über die ich gesprochen habe.

Wenn Sie aus dieser Methode eine gültige InputSource (statt null) zurückgeben, wird statt der festgelegten Public- oder System-ID diese InputSource als Wert der Entity-Referenz verwendet. Mit anderen Worten: Sie können Ihre eigenen Daten spezifizieren, statt daß der Reader die Auflösung selbst durchführen muß. Erzeugen Sie zum Beispiel eine copyright.xml-Datei auf Ihrem lokalen Rechner, wie sie in Beispiel 4-3 gezeigt wird.

Beispiel 4-3: Lokale Kopie von copyright.xml

<copyright xmlns="http://www.oreilly.com">
  <jahr wert="2001" />
  <inhalt>Dies ist meine lokale Copyright-Version.</inhalt>
</copyright>

Speichern Sie diese in einer Datei, auf die Sie aus Ihrem Java-Code heraus zugreifen können (ich habe das gleiche Verzeichnis verwendet wie für meine contents.xml-Datei), und führen Sie die folgende Änderung an der Methode resolveEntity( ) durch:

    public InputSource resolveEntity(String publicID, String systemID)
        throws IOException, SAXException {
         
        // Referenzen auf die Online-Version von copyright.xml bearbeiten
        if (systemID.equals(
            "http://www.newInstance.com/javaxml2/copyright.xml")) {
            return new InputSource(
                "file:///c:/javaxml2/ch04/xml/copyright.xml");
        }            
        
        // Im Standardfall null zurückgeben
        return null;    
    }

Sie sehen, daß eine InputSource zurückgegeben wird, die Zugriff auf die lokale Version von copyright.xml bietet, anstatt die Auflösung aus der Online-Ressource zu erlauben. Wenn Sie Ihre Quelldatei erneut kompilieren und den Tree-Viewer starten, können Sie visuell überprüfen, daß diese lokale Kopie verwendet wird. Abbildung 4-1 zeigt das aufgeklappte ora:copyright-Element, das den Inhalt des lokalen Copyright-Dokuments enthält.

In Anwendungen aus der Praxis neigt diese Methode dazu, eine lange Liste von if/then/else-Blöcken zu werden, die jeweils eine bestimmte System- oder Public-ID behandeln. Und das bringt uns zu einem wichtigen Punkt: Versuchen Sie zu vermeiden, daß diese Klasse und diese Methode zum Komposthaufen für IDs wird.

Wenn Sie eine bestimmte Auflösung nicht mehr benötigen, entfernen Sie ihre if-Bedingung. Versuchen Sie außerdem, in unterschiedlichen Anwendungen unterschiedliche EntityResolver-Implementierungen zu verwenden statt einer allgemeingültigen Implementierung für alle Ihre Anwendungen. Das verhindert die Aufblähung von Code und, noch wichtiger, beschleunigt die Entity-Auflösung. Wenn Sie darauf warten müssen, daß Ihr Reader fünfzig oder hundert String.equals( )-Vergleiche durchläuft, kann das eine Anwendung wirklich ausbremsen. Stellen Sie sicher, daß Referenzen, auf die häufig zugegriffen wird, im if/else-Stapel ganz oben stehen, so daß sie als erste gefunden werden und zu schnellerer Entity-Auflösung führen.

Abbildung 4-1: SAXTreeViewer, der mit lokaler copyright.xml-Datei läuft
SAXTreeViewer, der mit lokaler copyright.xml-Datei läuft

Zuletzt möchte ich noch eine weitere Empfehlung bezüglich Ihrer EntityResolver-Implementierungen geben. Sie werden bemerkt haben, daß ich meine Implementierung in einer separaten Klassendatei vorgenommen habe, während die Implementierungen von ErrorHandler, ContentHandler und (im nächsten Abschnitt) DTDHandler alle in der gleichen Quelldatei stehen, in der das Parsing stattfindet. Das war kein Zufall! Sie werden noch herausfinden, daß die Art und Weise, wie mit Inhalten, Fehlern und DTDs umgegangen wird, relativ statisch ist. Sie schreiben Ihr Programm, und das war’s.

Wenn Sie Änderungen durchführen, dann schreiben Sie das Programm im größeren Stil neu und müssen ohnehin große Änderungen machen. Sie werden jedoch häufig Änderungen an der Art und Weise vornehmen, wie Ihre Anwendung Entities auflösen soll. Je nach Art des Rechners, mit dem Sie arbeiten, je nach Art des Clients, an den die Anwendung ausgeliefert wird, und je nachdem, welche Dokumente wo zur Verfügung stehen, benötigen Sie unterschiedliche Versionen einer EntityResolver-Implementierung. Um schnelle Änderungen an dieser Implementierung ohne Bearbeitung oder erneute Kompilierung des eigentlichen Parsing-Codes vornehmen zu können, verwende ich eine separate Quelldatei für EntityResolver-Implementierungen, und ich rate Ihnen, das genauso zu machen. Und damit wissen Sie alles über die Auflösung von Entities in Ihren Anwendungen mit SAX.

Einen DTDHandler verwenden

Nach dieser recht ausführlichen Betrachtung von EntityResolver gehe ich nun den DTDHandler recht schnell durch. In zwei Jahren ausgedehnter XML-Programmierung habe ich dieses Interface nur einmal benutzt, beim Schreiben von JDOM, und sogar dabei war es ein recht merkwürdiger Sonderfall. Sie werden in der Regel nicht viel damit arbeiten, es sei denn, Sie haben Unmengen nicht beim Parsing berücksichtigter Entities in Ihren XML-Dokumenten.

Das Interface DTDHandler ermöglicht es Ihnen, eine Benachrichtigung zu erhalten, wenn ein Reader eine vom Parser unbehandelte Entity- oder Notation-Deklaration findet. Natürlich treten diese beiden Ereignisse in DTDs und nicht in XML-Dokumenten auf, deshalb heißt es DTDHandler. Anstatt immer weiter mit der Beschreibung fortzufahren, möchte ich Ihnen einfach zeigen, wie das Interface aussieht. Sie können es in Beispiel 4-4 gleich ausprobieren.

Beispiel 4-4: Das InterfaceDTDHandler

package org.xml.sax;

public interface DTDHandler {

    public void notationDecl(String name, String publicID, 
                             String systemID)
        throws SAXException;

    public void unparsedEntityDecl(String name, String publicId,
                                   String systemId, String notationName)
         throws SAXException;
}

Diese beiden Methoden tun genau das, was Sie wahrscheinlich erwarten. Die erste meldet eine Notation-Deklaration mit ihrem Namen, ihrer Public-ID und ihrer System-ID. Erinnern Sie sich an die NOTATION-Struktur in DTDs?

<!NOTATION jpeg SYSTEM "images/jpeg">

Die zweite Methode stellt Informationen über nicht vom Parser behandelte Entity-Deklarationen zur Verfügung, die folgendermaßen aussehen:

<!ENTITY stars_logo SYSTEM "http://www.nhl.com/img/team/dal38.gif"
                    NDATA jpeg>

In beiden Fällen können Sie Aktionen bei diesen Vorkommnissen durchführen, wenn Sie eine Implementierung von DTDHandler erzeugen und sie durch die Methode setDTD-Handler( ) des XMLReaders in Ihrem Reader registrieren. Das ist im allgemeinen nützlich, wenn Sie Low-Level-Anwendungen schreiben, die entweder XML-Inhalte reproduzieren müssen (etwa einen XML-Editor), oder wenn Sie die Beschränkungen einer DTD in Java darstellen müssen (wie etwa für die Datenbindung, die im Kapitel Data-Binding behandelt wird). In den meisten anderen Situationen ist es nichts, was Sie besonders oft benötigen werden.

Die Klasse DefaultHandler

Bevor wir das Thema Handler abschließen (zumindest für den Moment), gibt es noch eine weitere wichtige Handler-Klasse, die Sie kennen sollten. Diese Klasse ist org.xml. sax.helpers.DefaultHandler, und sie kann für Sie als SAX-Entwickler eine große Hilfe sein. Erinnern Sie sich, daß die Implementierung der verschiedenen Handler-Interfaces bisher eine Klasse für ContentHandler, eine für ErrorHandler, eine für EntityResolver (das ist in Ordnung, wegen all der besprochenen Gründe, aus denen diese Implementierung in einer separaten Quelldatei erfolgen sollte) und eine für DTDHandler benötigt hat, wenn diese Implementierungen gebraucht wurden? Darüber hinaus mußten Sie sich noch den Spaß gönnen, die zahlreichen Methoden in ContentHandler zu implementieren, sogar dann, wenn die meisten davon gar nichts zu tun brauchten.

Und hier eilt der DefaultHandler zu Hilfe. Diese Klasse definiert selbst keinerlei Verhalten; sie implementiert aber dafür ContentHandler, ErrorHandler, EntityResolver und DTDHandler und stellt leere Implementierungen jeder Methode aus jedem Interface zur Verfügung. Auf diese Weise können Sie mit einer einzigen Klasse arbeiten (nennen Sie sie zum Beispiel MyHandlerClass), die DefaultHandler erweitert. Diese Klasse braucht nur diejenigen Methoden zu überschreiben, in denen sie tatsächlich Aktionen ausführen muß.

Sie könnten zum Beispiel nur startElement( ), characters( ), endElement( ) und fatalError( ) implementieren. In jeder Kombination implementierter Methoden sparen Sie jedenfalls tonnenweise Codezeilen für Methoden, für die Sie keine Aktionen bereitstellen wollen, und machen Ihren Code auch erheblich lesbarer. Denn dann wäre das Argument für setErrorHandler( ), set-ContentHandler( ) und setDTDHandler( ) dieselbe Instanz dieser Klasse MyHandlerClass. Theoretisch könnten Sie diese Instanz auch an setEntityResolver( ) übergeben, obwohl ich (ungefähr zum vierten Mal!) von der Vermischung der Methode resolveEntity( ) mit den anderen Interface-Methoden abrate.

Filter und Writer

An dieser Stelle möchte ich vom vorgezeichneten Weg abweichen. Bisher habe ich die Details von allem geliefert, was zu einer »Standard«-SAX-Anwendung gehört, vom Reader bis hin zu den Callbacks an die Handler. Allerdings gibt es eine Menge zusätzlicher Features in SAX, die wirklich einen Power-Entwickler aus Ihnen machen können und Sie über die Einschränkungen von »Standard«-SAX hinaus tragen. In diesem Abschnitt stelle ich Ihnen zwei davon vor: SAX-Filter und -Writer.

Durch die Verwendung von zwei Arten von Klassen, solchen aus der Standard-SAX-Distribution und denen, die separat auf der SAX-Website (http://www.megginson.com/SAX) verfügbar sind, können Sie einiges ziemlich fortgeschrittenes Verhalten zu Ihren SAX-Anwendungen hinzufügen. Dadurch gewöhnen Sie sich auch an die Denkweise, SAX als eine Pipeline von Ereignissen und nicht als einzelne Verarbeitungsebene zu verwenden. Ich werde dieses Konzept später detaillierter erklären; im Moment soll es genügen zu sagen, daß es tatsächlich der Schlüssel zum Schreiben von effizientem und modularem SAX-Code ist.

XMLFilter

Zuoberst auf der Liste steht eine Klasse, die mit dem grundlegenden SAX-Download von David Megginsons Site geliefert wird, und sie sollte in jeder Parser-Distribution enthalten sein, die SAX 2.0 unterstützt. Die fragliche Klasse ist org.xml.sax.XMLFilter. Diese Klasse erweitert das XMLReader-Interface und fügt zwei neue Methoden zu dieser Klasse hinzu:

public void setParent(XMLReader parent);

public XMLReader getParent(  );

Es sieht vielleicht nicht so aus, als ob viel dazu zu sagen wäre; was ist der großartige Sinn dahinter? Nun, indem Sie durch diesen Filtermechanismus eine Hierarchie von XMLReadern ermöglichen, können Sie eine Verarbeitungskette oder eine Pipeline von Ereignissen einrichten. Damit Sie verstehen, was ich mit dem Begriff Pipeline meine, sehen Sie hier den normalen Ablauf eines SAX-Parsings:

  • Ereignisse in einem XML-Dokument werden dem SAX-Reader übergeben.
  • Der SAX-Reader und registrierte Handler geben Ereignisse und Daten an eine Anwendung weiter.

Entwickler haben jedoch begonnen zu merken, daß es einfach ist, ein oder mehrere zusätzliche Glieder in diese Kette einzufügen:

  • Ereignisse in einem XML-Dokument werden dem SAX-Reader übergeben.
  • Der SAX-Reader führt einige Verarbeitung durch und übergibt Informationen an einen weiteren SAX-Reader.
  • Wiederholen, bis sämtliche SAX-Verarbeitung erledigt ist.
  • Zuletzt geben der SAX-Reader und die registrierten Handler Ereignisse und Daten an eine Anwendung weiter.

In den zwei mittleren Schritten wird eine Pipeline eingeführt, wenn ein Reader, der eine bestimmte Verarbeitung durchgeführt hat, seine Informationen an den nächsten Reader weitergibt (und so weiter), anstatt sämtlichen Code in einen einzigen Reader stopfen zu müssen. Wenn diese Pipeline mit mehreren Readern eingerichtet wird, entsteht eine modulare und effiziente Programmierung. Und das ist das, was die XMLFilter-Klasse ermöglicht: das Verketten von XMLReader-Implementierungen durch das Filtern. Sogar noch stärker erweitert wird dies durch die Klasse org.xml.sax.helpers.XMLFilterImpl, die eine hilfreiche Implementierung von XMLFilter zur Verfügung stellt.

Es ist die Zusammenfassung eines XMLFilters und der Klasse DefaultHandler, die ich Ihnen im vorigen Abschnitt gezeigt habe; die Klasse XMLFilterImpl implementiert XMLFilter, ContentHandler, ErrorHandler, EntityResolver und DTDHandler und liefert fertig implementierte Versionen jeder Methode jedes Handlers. Mit anderen Worten richtet sie eine Pipeline für alle SAX-Ereignisse ein und ermöglicht es Ihrem Code, alle Methoden zu überschreiben, die Verarbeitungsschritte in die Pipeline einfügen müssen.

Wir wollen einen dieser Filter verwenden. Beispiel 4-5 ist ein funktionierender, einsatzbereiter Filter. Sie haben die Grundlagen hinter sich, also werden wir dieses Beispiel schnell durchgehen.

Beispiel 4-5: NamespaceFilter-Klasse

package javaxml2;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLFilterImpl;

public class NamespaceFilter extends XMLFilterImpl {
    
    /** Die alte URI, die ersetzt wird */
    private String oldURI;
    
    /** Die neue URI, durch die die alte URI ersetzt wird */
    private String newURI;

    public NamespaceFilter(XMLReader reader, 
                           String oldURI, String newURI) {
        super(reader);
        this.oldURI = oldURI;
        this.newURI = newURI;
    }
    
    public void startPrefixMapping(String prefix, String uri)
        throws SAXException {
            
        // URI wechseln, wenn nötig
        if (uri.equals(oldURI)) {
            super.startPrefixMapping(prefix, newURI);
        } else {
            super.startPrefixMapping(prefix, uri);
        }        
    }
    
    public void startElement(String uri, String localName,
                             String qName, Attributes attributes)
        throws SAXException {
            
        // URI wechseln, wenn nötig
        if (uri.equals(oldURI)) {
            super.startElement(newURI, localName, qName, attributes);
        } else {            
            super.startElement(uri, localName, qName, attributes);
        }       
    }
    
    public void endElement(String uri, String localName, String qName)
        throws SAXException {
            
        // URI wechseln, wenn nötig
        if (uri.equals(oldURI)) {
            super.endElement(newURI, localName, qName);
        } else {            
            super.endElement(uri, localName, qName);
        }
    }
}

Ich beginne mit der Erweiterung von XMLFilterImpl, so daß ich mir keine Sorgen über irgendwelche Ereignisse zu machen brauche, die ich nicht explizit ändern muß; die Klasse XMLFilterImpl kümmert sich um sie, indem sie alle Ereignisse unverändert weiterleitet, solange eine Methode nicht überschrieben wird. Ich kann mich sofort mit dem beschäftigen, was der Filter für mich tun soll; in diesem Fall geht es darum, eine Namensraum-URI auszutauschen. Auch wenn diese Aufgabe trivial erscheint, sollten Sie nicht ihren Nutzen unterschätzen. In den letzten paar Jahren hat sich die URI eines Namensraums oder einer Spezifikation (wie XML Schema oder XSLT) mehrere Male geändert. Anstatt daß ich alle meine XML-Dokumente von Hand ändern oder Code für XML schreiben muß, das ich erhalte, kümmert sich dieser NamespaceFilter für mich um dieses Problem.

Das Übergeben einer XMLReader-Instanz an den Konstruktor stellt diesen Reader als dessen Elternobjekt ein, so daß der übergeordnete Reader sämtliche Ereignisse empfängt, die vom Filter weitergegeben werden (das sind dank der XMLFilterImpl-Klasse alle Ereignisse, es sei denn, die Klasse NamespaceFilter überschreibt dieses Verhalten). Durch die Angabe von zwei URIs – der ursprünglichen und derjenigen, durch die sie ersetzt werden soll – richten Sie diesen Filter ein. Die drei überschriebenen Methoden kümmern sich um jeden benötigten Austausch dieser URI. Nachdem Sie einen Filter wie diesen gesetzt haben, übergeben Sie ihm einen Reader und arbeiten dann mit dem Filter und nicht mehr mit dem Reader.

Gehen wir zurück zu contents.xml und SAXTreeViewer, und nehmen wir an, daß O’Reilly mich darüber informiert hat, daß die Online-URL meines Buches nicht mehr http://www.oreilly.com/javaxml2 ist, sondern jetzt http://www.oreilly.com/catalog/javaxml2 heißt. Anstatt XML-Beispiele zu ändern und wieder hochzuladen, kann ich einfach die Klasse NamespaceFilter verwenden:

    public void buildTree(DefaultTreeModel treeModel, 
                          DefaultMutableTreeNode base, String xmlURI) 
        throws IOException, SAXException {

        // Für das Parsing benötigte Instanzen erzeugen
        XMLReader reader = 
            XMLReaderFactory.createXMLReader(vendorParserClass);
        NamespaceFilter filter = 
            new NamespaceFilter(reader, 
                "http://www.oreilly.com/javaxml2",
                "http://www.oreilly.com/catalog/javaxml2");
        ContentHandler jTreeContentHandler = 
            new JTreeContentHandler(treeModel, base, reader);
        ErrorHandler jTreeErrorHandler = new JTreeErrorHandler(  );

        // Den Content-Handler registrieren
        filter.setContentHandler(jTreeContentHandler);

        // Den Fehlerhandler registrieren
        filter.setErrorHandler(jTreeErrorHandler);
            
        // Den Entity-Resolver registrieren
        filter.setEntityResolver(new SimpleEntityResolver(  ));

        // Parsing
        InputSource inputSource = 
            new InputSource(xmlURI);
        filter.parse(inputSource);        
    }

Beachten Sie, wie ich schon sagte, daß alle Operationen mit der Filter- und nicht mit der Reader-Instanz durchgeführt werden. Nachdem diese Filterung eingeschaltet ist, können Sie beide Quelldateien kompilieren (NamespaceFilter.java und SAXTreeViewer.java) und den Viewer mit der Datei contents.xml starten. Sie werden sehen, daß die O’Reilly-Namensraum-URI für mein Buch bei jedem Vorkommen geändert wird, was in Abbildung 4-2 gezeigt wird.

Natürlich können Sie diese Filter auch miteinander verketten und als Standardbibliotheken verwenden. Wenn ich mit älteren XML-Dokumenten arbeite, erzeuge ich oft mehrere davon mit alten XSL- und XML Schema-URIs und installiere sie, damit ich mir keine Sorgen über inkorrekte URIs machen muß:

        XMLReader reader = 
            XMLReaderFactory.createXMLReader(vendorParserClass);
        NamespaceFilter xslFilter = 
            new NamespaceFilter(reader, 
                "http://www.w3.org/TR/XSL",
                "http://www.w3.org/1999/XSL/Transform");
        NamespaceFilter xsdFilter = 
            new NamespaceFilter(xslFilter, 
                "http://www.w3.org/TR/XMLSchema",
                "http://www.w3.org/2001/XMLSchema");

Hier baue ich eine längere Pipeline, um sicherzustellen, daß keine alten Namensraum-URIs vorkommen und in meinen Anwendungen irgendwelchen Ärger verursachen. Achten Sie aber darauf, daß Sie keine zu langen Pipelines bauen; jedes neue Glied in der Kette erhöht die Verarbeitungszeit etwas.

Abbildung 4-2: SAXTreeViewer mit contents.xml und gesetztem NamespaceFilter
SAXTreeViewer mit contents.xml und gesetztem NamespaceFilter

XMLWriter

Nun, da Sie verstehen, wie Filter in SAX funktionieren, möchte ich Ihnen einen besonderen Filter vorstellen, XMLWriter. Diese Klasse, sowie eine Unterklasse von ihr, DataWriter, kann von David Megginsons SAX-Site unter http://www.megginson.com/SAX heruntergeladen werden. XMLWriter erweitert XMLFilterImpl, und DataWriter erweitert XMLWriter. Diese beiden Filterklassen werden verwendet, um XML auszugeben, was nicht ganz im Einklang mit dem stehen mag, was Sie bisher über SAX gelernt haben. Aber genau wie Sie Statements, die Ausgaben in Java-Writer vornehmen, in SAX-Callbacks einfügen könnten, kann das auch diese Klasse.

Ich werde nicht viel Zeit mit dieser Klasse verbringen, da sie nicht unbedingt die Art und Weise darstellt, wie Sie im allgemeinen XML ausgeben sollten; es ist viel besser, dafür DOM, JDOM oder eine andere XML-API zu verwenden, wenn Sie Flexibilität brauchen. Allerdings bietet die Klasse XMLWriter eine wertvolle Möglichkeit zu untersuchen, was in einer SAX-Pipeline geschieht. Indem Sie sie zwischen anderen Filtern und Readern in Ihrer Pipeline einfügen, kann sie verwendet werden, um an einem beliebigen Punkt in Ihrer Verarbeitungskette, an dem sie sich gerade befindet, einen Schnappschuß Ihrer Daten auszugeben.

Beispielsweise könnte es in dem Fall, in dem ich die Namensraum-URIs tausche, vorkommen, daß Sie das XML-Dokument mit der neuen Namensraum-URI für die spätere Nutzung abspeichern möchten (sei es eine geänderte O’Reilly-URI, eine aktualisierte XML-API oder die XML Schema-API). Dies wird zu einem Kinderspiel, wenn Sie die Klasse XMLWriter verwenden.

Da Sie den SAXTreeViewer bereits dazu gebracht haben, NamespaceFilter zu verwenden, werde ich dies als Beispiel nutzen. Fügen Sie als erstes Anweisungen für den Import von java.io.FileWriter (für die Ausgabe) und die Klasse com.megginson.sax.XMLWriter hinzu. Wenn dies geschehen ist, müssen Sie eine Instanz von XMLWriter zwischen den NamespaceFilter- und XMLReader-Instanzen einfügen. Das bedeutet, daß die Ausgabe erfolgen wird, nachdem die Namensräume geändert worden sind, aber bevor die sichtbaren Ereignisse auftreten. Ändern Sie Ihren Code, wie hier gezeigt wird.

    public void buildTree(DefaultTreeModel treeModel, 
                          DefaultMutableTreeNode base, String xmlURI) 
        throws IOException, SAXException {

        // Für das Parsing benötigte Instanzen erzeugen
        XMLReader reader = 
            XMLReaderFactory.createXMLReader(vendorParserClass);        
        XMLWriter writer =
            new XMLWriter(reader, new FileWriter("snapshot.xml"));            
        NamespaceFilter filter = 
            new NamespaceFilter(writer, 
                "http://www.oreilly.com/javaxml2",
                "http://www.oreilly.com/catalog/javaxml2");
        ContentHandler jTreeContentHandler = 
            new JTreeContentHandler(treeModel, base, reader);
        ErrorHandler jTreeErrorHandler = new JTreeErrorHandler(  );

        // Den Content-Handler registrieren
        filter.setContentHandler(jTreeContentHandler);

        // Den Fehlerhandler registrieren
        filter.setErrorHandler(jTreeErrorHandler);
            
        // Register entity resolver
        filter.setEntityResolver(new SimpleEntityResolver(  ));

        // Parsing
        InputSource inputSource = 
            new InputSource(xmlURI);
        filter.parse(inputSource);        
    }

Stellen Sie sicher, daß Sie das Elternobjekt der NamespaceFilter-Instanz so einstellen, daß es der XMLWriter und nicht der XMLReader ist. Andernfalls findet gar keine Ausgabe statt. Nachdem Sie diese Änderungen einkompiliert haben, starten Sie das Beispiel. Sie sollten eine Datei snapshot.xml erhalten, die in dem Verzeichnis erzeugt wurde, aus dem Sie das Beispiel starten; ein Ausschnitt wird hier gezeigt:

<?xml version="1.0" encoding="ISO-8859-1" standalone="yes"?>

<buch xmlns="http://www.oreilly.com/catalog/javaxml2">
  <titel ora:serie="Java" 
         xmlns:ora="http://www.oreilly.com">Java und XML</titel>
  

  <inhalt>
    <kapitel titel="Einleitung" nummer="1">
      <thema name="XML spielt eine wichtige Rolle"></thema>
      <thema name="Was ist wichtig?"></thema>
      <thema name="Was Sie benötigen"></thema>
      <thema name="Und was kommt jetzt?"></thema>
    </kapitel>
    <kapitel titel="Ans Eingemachte" nummer="2">
      <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>
    <!-- Weiterer Inhalt... -->

  </inhalt>
</buch>

Beachten Sie, daß der Namensraum hier modifiziert ist, da er durch NamespaceFilter geändert wurde. Schnappschüsse wie dieser, die durch XMLWriter-Instanzen erzeugt werden, können hervorragende Werkzeuge für das Debugging und die Protokollierung von SAX-Ereignissen sein.

Sowohl XMLWriter als auch DataWriter stellen noch weit mehr Mittel und Methoden zur Verfügung, um XML vollständig oder auch teilweise auszugeben, und Sie sollten die Javadoc-Dokumentation durchlesen, die im Download-Paket enthalten ist. Ich rate Ihnen nicht, diese Klassen für die allgemeine Ausgabe zu verwenden. Meiner Erfahrung nach sind sie in einem Fall wie dem hier gezeigten am nützlichsten.

Und noch mehr Handler

Nun möchte ich Ihnen noch zwei weitere Handler-Klassen zeigen, die SAX anbietet. Diese beiden Interfaces sind nicht mehr Bestandteil der SAX-Kerndistribution und befinden sich im Package org.xml.sax.ext, um anzuzeigen, daß sie Erweiterungen von SAX sind. Allerdings enthalten die meisten Parser (wie etwa Apache Xerces) diese beiden Klassen bereits. Überprüfen Sie die Dokumentation Ihres Herstellers. Falls Sie diese Klassen nicht haben, können Sie sie von der SAX-Website herunterladen. Ich muß Sie warnen, daß nicht alle SAX-Treiber diese Erweiterungen unterstützen. Wenn Ihr Hersteller sie also nicht mitliefert, sollten Sie herausfinden, warum, und prüfen, ob der Hersteller eine Version ankündigt, die die SAX-Erweiterungen unterstützen wird.

LexicalHandler

Der erste der beiden Handler ist der nützlichere: org.xml.sax.ext.LexicalHandler. Dieser Handler stellt Methoden zur Verfügung, die Benachrichtigungen über verschiedene lexikalische Ereignisse empfangen können, etwa Kommentare, Entity-Deklarationen, DTD-Deklarationen und CDATA-Bereiche. In ContentHandler werden diese lexikalischen Ereignisse grundsätzlich ignoriert, und Sie erhalten einfach die Daten und Deklarationen ohne Benachrichtigung darüber, wann oder wo sie zur Verfügung gestellt wurden.

Dies ist nicht unbedingt ein Handler für den allgemeinen Gebrauch, da die meisten Anwendungen nicht zu wissen brauchen, ob ein Text sich in einem CDATA-Bereich befunden hat oder nicht. Wenn Sie jedoch mit einem XML-Editor, Serialisierer oder mit einer anderen Komponente arbeiten, die das genaue Format des Eingabedokuments kennen müssen und nicht nur dessen Inhalte, kann der LexicalHandler Ihnen wirklich dabei helfen. Um diesen Burschen in Aktion zu erleben, müssen Sie als erstes eine Anweisung für den Import von org.xml.sax.ext.LexicalHandler in Ihre Quelldatei SAXTreeViewer.java einfügen. Nachdem das erledigt ist, können Sie LexicalHandler zu der implements-Klausel in der nicht-öffentlichen Klasse JTreeContentHandler in dieser Quelldatei hinzufügen:

class JTreeContentHandler implements ContentHandler, LexicalHandler {
    // Callback-Implementierungen
}

Durch die Wiederverwendung des bereits in dieser Klasse befindlichen Content-Handlers können unsere lexikalischen Callbacks auf dem JTree operieren, um diese Callbacks visuell darzustellen. Nun müssen Sie aber auch Implementierungen für alle Methoden hinzufügen, die in LexicalHandler definiert werden. Diese Methoden sind die folgenden:

public void startDTD(String name, String publicID, String systemID)
        throws SAXException;
public void endDTD(  ) throws SAXException;
public void startEntity(String name) throws SAXException;
public void endEntity(String name) throws SAXException;
public void startCDATA(  ) throws SAXException;
public void endCDATA(  ) throws SAXException;
public void comment(char[] ch, int start, int length)
        throws SAXException;

Für den Anfang wollen wir uns das erste lexikalische Ereignis anschauen, das bei der Verarbeitung eines XML-Dokuments auftreten könnte: den Beginn und das Ende einer DTD-Referenz oder -Deklaration. Dieses Ereignis löst die hier gezeigten Callbacks startDTD( ) und endDTD( ) aus:

    public void startDTD(String name, String publicID,
                         String systemID)
        throws SAXException {
            
        DefaultMutableTreeNode dtdReference =
            new DefaultMutableTreeNode("DTD fuer '" + name + "'");
        if (publicID != null) {
            DefaultMutableTreeNode publicIDNode =
                new DefaultMutableTreeNode("Public-ID: '" + 
                    publicID + "'");
            dtdReference.add(publicIDNode);
        }
        if (systemID != null) {
            DefaultMutableTreeNode systemIDNode =
                new DefaultMutableTreeNode("System-ID: '" + 
                    systemID + "'");
            dtdReference.add(systemIDNode);
        }            
        current.add(dtdReference);
    }
     
    public void endDTD(  ) throws SAXException {
        // Hier wird keine Aktion benötigt
    }

Dies fügt einen visuellen Hinweis ein, wenn eine DTD gefunden wird, und eine System-ID und Public-ID, falls vorhanden. Wir machen mit einem Paar von ähnlichen Methoden für Entity-Referenzen, startEntity( ) und endEntity( ), weiter. Diese werden vor bzw. nach der Verarbeitung von Entity-Referenzen in Gang gesetzt. Sie können auch für dieses Ereignis einen visuellen Hinweis hinzufügen, indem Sie den hier gezeigten Code verwenden:

    public void startEntity(String name) throws SAXException {
        DefaultMutableTreeNode entity = 
            new DefaultMutableTreeNode("Entity: '" + name + "'");
        current.add(entity);
        current = entity;
    }  
 
    public void endEntity(String name) throws SAXException {
        // Im Baum wieder nach oben wandern
        current = (DefaultMutableTreeNode)current.getParent(  );
    }

Dies stellt sicher, daß der Inhalt einer Entity-Referenz, zum Beispiel OReillyCopyright, in einen »Entity«-Baum-Knoten hineinverschachtelt wird.

Da das nächste lexikalische Ereignis ein CDATA-Bereich ist und es zur Zeit im Dokument contents.xml keine gibt, können Sie die folgende Änderung an diesem Dokument vornehmen (CDATA ermöglicht das Ampersand-Zeichen im Inhalt des Elements titel):

<?xml version="1.0"?>
<!DOCTYPE buch SYSTEM "DTD/JavaXML.dtd">

<!-- Java und XML Inhalt -->
<buch xmlns="http://www.oreilly.com/javaxml2"
      xmlns:ora="http://www.oreilly.com"
>
  <titel ora:serie="Java"><![CDATA[Java & XML]]></titel>

  <!-- Weiterer Inhalt -->
</buch>

Nach dieser Änderung können Sie Code für die CDATA-Callbacks zur Klasse JTreeContentHandler hinzufügen:

    public void startCDATA(  ) throws SAXException {
        DefaultMutableTreeNode cdata = 
            new DefaultMutableTreeNode("CDATA-Bereich");
        current.add(cdata);
        current = cdata;        
    }
 
    public void endCDATA(  ) throws SAXException {
        // Im Baum zurück nach oben wandern
        current = (DefaultMutableTreeNode)current.getParent(  );
    }

Das ist nun schon ein alter Hut; der Inhalt des Elements titel erscheint nun als Kindknoten eines CDATA-Knotens. Und damit bleibt nur noch eine Methode übrig, die Benachrichtigungen über Kommentare empfängt:

    public void comment(char[] ch, int start, int length)
        throws SAXException {
        
        String comment = new String(ch, start, length);
        DefaultMutableTreeNode commentNode = 
            new DefaultMutableTreeNode("Kommentar: '" + comment + "'");
        current.add(commentNode);
    }

Diese Methode verhält sich genau wie die Methoden characters( ) und ignorable-Whitespace( ). Denken Sie daran, daß nur der Text des Kommentars an diese Methode übergeben wird, nicht die umgebenden Begrenzungen <!-- und -->. Nach diesen Änderungen können Sie das Beispielprogramm kompilieren und starten. Sie sollten eine ähnliche Ausgabe wie die in Abbildung 4-3 gezeigte erhalten.

Abbildung 4-3: Ausgabe nach hinzugefügter LexicalHandler-Implementierung
Ausgabe nach hinzugefügter LexicalHandler-Implementierung

Sie werden jedoch eine eigenartige Sache bemerken: ein Entity namens [dtd]. Dies geschieht jedesmal, wenn eine DOCTYPE-Deklaration vorhanden ist, und kann mit einer einfachen Klausel in den Methoden startEntity( ) und endEntity( ) entfernt werden (wahrscheinlich wollen Sie nicht, daß es da ist):

    public void startEntity(String name) throws SAXException {
        if (!name.equals("[dtd]")) {
            DefaultMutableTreeNode entity = 
                new DefaultMutableTreeNode("Entity: '" + name + "'");
            current.add(entity);
            current = entity;
        }        
    }  
 
    public void endEntity(String name) throws SAXException {
        if (!name.equals("[dtd]")) {
            // Im Baum zurück nach oben wandern
            current = (DefaultMutableTreeNode)current.getParent(  );
        }        
    }

Diese Klausel entfernt das störende Entity. Das ist wirklich fast alles, was es über LexicalHandler zu sagen gibt. Obwohl ich dieses Thema in den Bereich fortgeschrittenes SAX eingeordnet habe, ist es ziemlich leicht verständlich.

DeclHandler

Der letzte Handler, mit dem wir uns beschäftigen, ist der DeclHandler. Dieses Interface definiert Methoden, die Benachrichtigungen über bestimmte Ereignisse innerhalb einer DTD empfangen, wie etwa Element- und Attribut-Deklarationen. Dies ist ein weiteres Hilfsmittel, das nur für sehr spezielle Fälle taugt; wieder kommen einem XML-Editoren und andere Komponenten in den Sinn, die die genaue lexikalische Struktur von Dokumenten und ihren DTDs kennen müssen. Ich zeige Ihnen kein Beispiel für die Verwendung des DeclHandlers; an dieser Stelle wissen Sie bereits mehr über die Anwendung von Callback-Methoden, als Sie wahrscheinlich je benötigen werden. Statt dessen gebe ich Ihnen einen Überblick über das Interface, das Sie in Beispiel 4-6 sehen.

Beispiel 4-6: Das DeclHandler-Interface

package org.xml.sax.ext;

import org.xml.sax.SAXException;

public interface DeclHandler {

    public void attributeDecl(String eltName, String attName,
                              String type, String defaultValue,
                              String value)
        throws SAXException;
    
    public void elementDecl(String name, String model)
        throws SAXException;
    
    public void externalEntityDecl(String name, String publicID,
                                   String systemID)
        throws SAXException;
        
    public void internalEntityDecl(String name, String value)
        throws SAXException;
}

Dieses Beispiel ist beinahe selbsterklärend. Die ersten beiden Methoden verarbeiten die Konstrukte <!ELEMENT> und <!ATTLIST>. Die dritte, externalEntityDecl( ), meldet Entity-Deklarationen (durch <!ENTITY>), die auf externe Ressourcen verweisen. Die letzte Methode, internalEntityDecl( ), meldet Inline-definierte Entities. Das ist alles, was es darüber zu sagen gibt.

Und damit habe ich Ihnen alles mitgeteilt, was es über SAX zu wissen gibt. Nun, das ist wahrscheinlich übertrieben, aber Sie haben jetzt sicherlich eine Menge Werkzeuge, um loszulegen. Nun müssen Sie nur noch mit dem Programmieren anfangen, um Ihren eigenen Satz von Werkzeugen und Tricks aufzubauen. Bevor ich aber das Buch über SAX endgültig schließe, möchte ich einige häufige Fehler bei der Arbeit mit SAX behandeln.

Vorsicht Falle!

Wenn Sie sich mit den fortgeschritteneren Bestandteilen von SAX beschäftigen, vermindern Sie sicherlich nicht die Anzahl von Problemen, in die Sie dabei geraten können. Allerdings werden die Probleme oft kniffliger, was es schwieriger macht, einige heikle Fehler zu beseitigen. Ich werde einige dieser häufigen Probleme aufzeigen.

Rückgabewerte von einem EntityResolver

Wie ich bereits im Abschnitt über EntityResolver erwähnte, sollten Sie stets sicherstellen, daß Sie als Ausgangspunkt einer Implementierung der Methode resolveEntity( ) null zurückgeben. Glücklicherweise garantiert Java, daß die Methode einen Wert zurückgibt, aber ich habe oft Code wie diesen gesehen:

    public InputSource resolveEntity(String publicID, String systemID)
        throws IOException, SAXException {

        InputSource inputSource = new InputSource(  );

        // Referenzen auf die Online-Version von copyright.xml behandeln
        if (systemID.equals(
            "http://www.newInstance.com/javaxml2/copyright.xml")) {
            inputSource.setSystemId(
                "file:///c:/javaxml2/ch04/xml/copyright.xml");
        }            
        
        // Standardmäßig null zurückgeben
        return inputSource;    
    }

Wie Sie sehen können, wird am Anfang eine InputSource erzeugt, und dann wird die System-ID dieser Quelle gesetzt. Das Problem ist hier, daß eine InputSource ohne System- oder Public-ID und ohne Angabe von Reader oder InputStream zurückgegeben wird, wenn keine der if-Bedingungen erfüllt wird. Dies kann zu unvorhersagbaren Ergebnissen führen. In manchen Parsern geht es ohne Probleme weiter. In anderen Parsern jedoch führt die Rückgabe einer leeren InputSource dazu, daß Entities ignoriert werden oder daß Exceptions ausgelöst werden. Mit anderen Worten: Geben Sie am Ende jeder Implementierung von resolveEntity( ) statt einer uninitialisierten InputSource immer null zurück, und Sie brauchen sich über diese Details keine Sorgen zu machen.

DTDHandler und Validierung

Ich habe in diesem Kapitel das Einstellen von Eigenschaften und Features beschrieben, ihren Effekt auf die Validierung und auch das Interface DTDHandler. In der ganzen Diskussion über DTDs und Validierung ist es möglich, daß Sie einige Dinge durcheinanderbringen. Ich möchte klarstellen, daß das Interface DTDHandler überhaupt nichts mit der Validierung zu tun hat. Ich habe viele Entwickler beobachtet, die einen DTDHandler registriert und sich dann gefragt haben, warum keine Validierung stattfindet. DTDHandler tut jedoch nichts weiter, als Benachrichtigungen über Notation- und Entity-Deklarationen zur Verfügung zu stellen, die nicht vom Parsing verarbeitet wurden! Wahrscheinlich ist das nicht das, was der Entwickler erwartet hat. Denken Sie daran, daß es eine Eigenschaft ist, die die Validierung einschaltet, und keine Handler-Instanz:

reader.setFeature("http://xml.org/sax/features/validation", true);

Das ist das Mindeste, was erforderlich ist, um Ihnen eine Validierung zu liefern und Sie glücklich zu machen (außer bei einem Parser, der standardmäßig validiert).

Parsing im Reader statt im Filter

Ich habe in diesem Kapitel über Pipelines in SAX gesprochen, und hoffentlich haben Sie eine Vorstellung davon bekommen, wie nützlich sie sein können. Allerdings gibt es einen Fehler, den ich bei Filter-Anfängern immer und immer wieder sehe, und er gehört zu der Sorte, deren Behebung frustrierend ist. Das Problem besteht in der fehlerhaften Einrichtung der Pipeline-Kette. Es entsteht, wenn nicht jeder Filter den vorhergehenden als übergeordneten Filter einstellt, mit einer XMLReader-Instanz als Ursprung. Schauen Sie sich dieses Codefragment an:

    public void buildTree(DefaultTreeModel treeModel, 
                          DefaultMutableTreeNode base, String xmlURI) 
        throws IOException, SAXException {

        // Für das Parsing benötigte Instanzen erzeugen
        XMLReader reader = 
            XMLReaderFactory.createXMLReader(vendorParserClass);        
        XMLWriter writer =
            new XMLWriter(reader, new FileWriter("snapshot.xml"));            
        NamespaceFilter filter = 
            new NamespaceFilter(reader, 
                "http://www.oreilly.com/javaxml2",
                "http://www.oreilly.com/catalog/javaxml2");
        ContentHandler jTreeContentHandler = 
            new JTreeContentHandler(treeModel, base, reader);
        ErrorHandler jTreeErrorHandler = new JTreeErrorHandler(  );

        // Den Content-Handler registrieren
        reader.setContentHandler(jTreeContentHandler);

        // Den Fehlerhandler registrieren
        reader.setErrorHandler(jTreeErrorHandler);
            
        // Den Entity-Resolver registrieren
        reader.setEntityResolver(new SimpleEntityResolver(  ));

        // Parsing
        InputSource inputSource = 
            new InputSource(xmlURI);
        reader.parse(inputSource);        
    }

Fällt Ihnen etwas Falsches auf? Das Parsing wird mit der XMLReader-Instanz und nicht mit dem Ende der Pipeline-Kette durchgeführt. Darüber hinaus stellt die NamespaceFilter-Instanz den XML-Reader als Elternobjekt ein und nicht den XMLWriter, der ihr in der Kette vorausgehen sollte. Diese Fehler sind nicht auf den ersten Blick erkennbar und bringen die Pipeline, die Sie sich vorgestellt hatten, völlig durcheinander.

In diesem Beispiel findet keine einzige Filterung statt, da das Parsing mit dem Reader und nicht mit den Filtern durchgeführt wird. Wenn Sie diesen Fehler korrigieren, erhalten Sie noch immer keine Ausgabe, da sich der Writer durch den falsch eingestellten übergeordneten Filter des NamespaceFilters außerhalb der Pipeline befindet. Die korrekte Festlegung des Elternobjekts bringt Sie weiter, und so erhalten Sie schließlich das Verhalten, das Sie von Anfang an erwartet haben. Seien Sie sehr vorsichtig mit den Elternobjekt-Angaben und dem Parsing, wenn Sie SAX-Pipelines bearbeiten.

Und was kommt jetzt?

Das war eine Menge an Informationen über die Simple API for XML. Obwohl es sicherlich noch einiges zu vertiefen gäbe, sollten die Informationen aus diesem und dem vorigen Kapitel Sie auf fast alles vorbereitet haben, das auf Sie zukommen kann. Natürlich ist SAX nicht die einzige API für die Arbeit mit XML; um ein wahrer XML-Experte zu werden, müssen Sie mit DOM, JDOM, JAXP und weiteren Themen zurechtkommen. Ich werde im nächsten Kapitel DOM beginnen, die nächste API auf dieser Einkaufsliste mit Ihnen zu behandeln, das Document Object Model (DOM).

Bei der Einführung von DOM beginne ich mit den Grundlagen, ähnlich wie im vorigen Kapitel, das Ihnen eine solide Grundlage von SAX vermittelt hat. Sie werden etwas über Baum-APIs erfahren und darüber, inwiefern sich DOM deutlich von SAX unterscheidet, und Sie werden die Hauptklassen von DOM kennenlernen. Ich werde Ihnen eine Beispielanwendung zeigen, die DOM-Bäume serialisiert, und schon bald werden Sie Ihren eigenen DOM-Code schreiben.

1)
Für besonders penible Leser: Ich weiß, daß der EntityResolver als solcher technisch gesehen kein »Handler« ist. Natürlich könnte ich leicht argumentieren, daß das Interface auch EntityHandler heißen könnte, so daß es für mich einem Handler nahe genug kommt.