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

SAX

Wenn Sie als Programmierer mit XML arbeiten möchten, dann müssen Sie ein XML-Dokument als erstes durch einen Parser behandeln lassen. Wenn dies erledigt ist, stehen die Daten des Dokuments der Anwendung zur Verfügung, die den Parser verwendet, und schon haben Sie eine XML-fähige Anwendung! Auch wenn dies ein wenig zu einfach klingt, um wahr zu sein, ist es dennoch beinahe so. Dieses Kapitel beschreibt, wie ein XML-Dokument durch einen Parser bearbeitet wird, und konzentriert sich auf die Ereignisse, die während dieses Prozesses auftreten. Diese Ereignisse sind wichtig, da sie diejenigen Stellen sind, an denen anwendungsspezifischer Code eingefügt werden und an denen Datenmanipulation stattfinden kann.

Als Werkzeug werde ich in diesem Kapitel die Simple API for XML (SAX) einführen. SAX ermöglicht das Einfügen anwendungsspezifischen Codes in Ereignisse. Die Interfaces, die das SAX-Package zur Verfügung stellt, werden zu einem wichtigen Teil jeder Sammlung von Programmierwerkzeugen zur Bearbeitung von XML. Obwohl SAX nur über wenige und recht kleine Klassen verfügt, stellen sie ein ausgewogenes Framework zur Verfügung, um mit Java und XML arbeiten zu können. Ein gesundes Verständnis dafür, wie sie es bewerkstelligen, auf XML-Daten zuzugreifen, ist unabdingbar, um XML effektiv in Ihren Java-Programmen einzusetzen. In späteren Kapiteln fügen wir unserer Werkzeugsammlung andere Java-und-XML-APIs wie DOM, JDOM, JAXP und die Datenbindung hinzu. Aber jetzt genug der Vorrede; es ist Zeit, über SAX zu sprechen.

Vorbereitungen

Es gibt ein paar Dinge, die Sie benötigen, bevor Sie mit dem Programmieren loslegen können. Sie brauchen:

  • einen XML-Parser
  • die SAX-Klassen
  • ein XML-Dokument

Als erstes müssen Sie sich einen XML-Parser besorgen. Es ist eine schwierige Aufgabe, einen XML-Parser zu schreiben, und es finden einige Bemühungen statt, um hervorragende XML-Parser zur Verfügung zu stellen, besonders in der Open-Source-Szene. Ich werde hier nicht näher auf die Vorgehensweise eingehen, wie man tatsächlich einen XML-Parser schreibt; statt dessen werde ich die Anwendungen behandeln, die diese Parsing-Tätigkeit umgeben und mich darauf konzentrieren, existierende Werkzeuge zur Manipulation von XML-Daten zu verwenden. Dies führt zu besseren und schnelleren Programmen, weil weder Sie noch ich Zeit damit vertun, das Rad neu zu erfinden. Nach der Auswahl eines Parsers müssen Sie sicherstellen, daß Sie eine Kopie der SAX-Klassen zur Hand haben. Diese sind leicht zu finden, und sie sind für Java-Code der Schlüssel, um XML zu verarbeiten. Zu guter Letzt benötigen Sie ein XML-Dokument, das durch den Parser behandelt werden soll. Und dann auf in den Code!

Einen Parser beschaffen

Der erste Schritt zu Java-Programmen, die XML verwenden, besteht darin, den Parser, den Sie verwenden möchten, zu finden und zu besorgen. Ich habe diesen Vorgang kurz im Kapitel Einleitung angesprochen und mehrere XML-Parser aufgelistet, die Sie verwenden könnten. Um sicherzustellen, daß Ihr Parser mit allen Beispielen im Buch funktioniert, sollten Sie die Übereinstimmung Ihres Parsers mit der XML-Spezifikation überprüfen. Wegen des großen Variationsreichtums verfügbarer Parser und dem rapiden Wandel in der XML-Gemeinde gehen alle Details darüber, welche Parser wie stark mit dem Standard übereinstimmen, weit über den Rahmen dieses Buches hinaus. Konsultieren Sie den Anbieter des Parsers und besuchen Sie die Websites, die ich vorhin für diese Informationen angegeben habe.

Im Geiste der Open Source-Gemeinde verwenden alle Beispiele in diesem Buch den Xerces-Parser aus dem Apache-Projekt. Dieser C- und Java-basierte Parser ist in binärer Form und im Quellcode unter http://xml.apache.org frei verfügbar und gehört bereits zu den Parsern, die am meisten durch zahlreiche Beiträge weiterentwickelt werden (obwohl Hardcore-Java-Entwickler wie wir uns natürlich nicht um C kümmern, oder?). Abgesehen davon ermöglicht es Ihnen die Verwendung eines Open-Source-Parsers wie Xerces, Fragen oder Bug-Berichte an die Entwickler des Parsers zu senden, was zu einer Produktverbesserung führt und Ihnen auch helfen kann, die Software schnell und richtig einzusetzen.

Um der allgemeinen Mailingliste beizutreten und dort um Hilfe bei der Arbeit mit dem Xerces-Parser zu bitten, schicken Sie eine leere E-Mail an xerces-j-dev-subscribe@xml.apache.org. Die Mitglieder dieser Liste können Ihnen behilflich sein, wenn Sie Fragen zu oder Probleme mit einem Parser haben, der nicht speziell in diesem Buch behandelt wird. Natürlich funktionieren die Beispiele in diesem Buch normalerweise alle mit jedem Parser, der die hier beschriebene SAX-Implementierung verwendet.

Nachdem Sie einen Parser ausgewählt und heruntergeladen haben, müssen Sie dafür sorgen, daß der Klassenpfad Ihrer Java-Umgebung die Klassen des XML-Parsers enthält, egal, ob es sich um eine IDE (integrierte Entwicklungsumgebung) oder um eine Kommandozeilenanwendung handelt.

Wenn Sie nicht wissen, wie der CLASSPATH eingestellt wird, könnte dies ein wenig zu hoch für Sie sein. Falls Sie jedoch mit der Systemvariablen CLASSPATH vertraut sind, stellen Sie sie so ein, daß sie die jar-Datei Ihres Parsers enthält, wie hier gezeigt wird:

c: set CLASSPATH=.;c:\javaxml2\lib\xerces.jar;%CLASSPATH%

c: echo %CLASSPATH%

.;c:\javaxml2\lib\xerces.jar;c:\java\jdk1.3\lib\tools.jar

Selbstverständlich wird der Pfad bei Ihnen ein anderer sein als bei mir, aber Sie verstehen, was gemeint ist.

Die SAX-Klassen und -Interfaces besorgen

Nachdem Sie Ihren Parser haben, müssen Sie die SAX-Klassen finden. Diese Klassen werden fast immer mit einem Parser geliefert, wenn Sie ihn herunterladen, und Xerces ist da keine Ausnahme. Wenn dies bei Ihrem Parser der Fall ist, sollten Sie die SAX-Klassen nicht noch einmal separat herunterladen, da Ihr Parser wahrscheinlich die neueste Version von SAX enthält, die er überhaupt unterstützt. Im Moment ist SAX 2.0 schon seit längerem stabil, also können Sie erwarten, daß die hier gelieferten Beispiele (die alle SAX 2 verwenden) so funktionieren, wie sie gezeigt werden, und zwar ohne jede Änderung.

Wenn Sie sich nicht sicher sind, ob Sie bereits die SAX-Klassen besitzen, schauen Sie in der jar-Datei oder in der Klassenstruktur nach, die Ihr Parser verwendet. Die SAX-Klassen befinden sich in der Package-Struktur org.xml.sax. Vergewissern Sie sich zumindest, daß Sie die Klasse org.xml.sax.XMLReader sehen. Das bedeutet (fast) sicher, daß Sie einen Parser mit SAX-2-Unterstüzung verwenden, da die Klasse XMLReader ein Kernbestandteil von SAX 2 ist.

Als letztes sollten Sie die Javadoc-Dokumentation der SAX-API, die sich im Web befindet, entweder herunterladen oder bookmarken. Diese Dokumentation leistet enorme Hilfe bei der Verwendung der SAX-Klassen, und die Javadoc-Struktur ist eine standardisierte, einfache Art, zusätzliche Informationen über die Klassen und ihre Aufgaben zu erhalten. Diese Dokumentation finden Sie unter http://www.megginson.com/SAX. Wenn Sie möchten, können Sie die Javadoc-Dokumentation auch aus den SAX-Quellcodes erzeugen, indem Sie die Quellen verwenden, die mit Ihrem Parser geliefert werden, oder indem Sie die vollständigen Quellcodes von http://www.megginson.com/SAX herunterladen. Zu guter Letzt gibt es auch noch viele Parser, bei denen das Download-Paket eine Dokumentation enthält, in der die SAX-API-Dokumentation enthalten ist (Xerces ist ein Beispiel für einen solchen Fall).

Halten Sie ein XML-Dokument bereit

Sie sollten auch dafür sorgen, daß Sie ein XML-Dokument für den Parsing-Prozeß haben. Die Ausgabe, die in den Beispielen gezeigt wird, beruht auf dem Parsing des XML-Dokuments, das im Kapitel Einstieg in XML besprochen wird. Speichern Sie diese Datei als contents.xml auf Ihrer lokalen Festplatte. Ich empfehle Ihnen dringend, daß Sie meinen Ausführungen unter Verwendung dieses Dokuments folgen; es enthält diverse XML-Konstrukte zu Demonstrationszwecken. Sie können die XML-Datei einfach aus dem Buch abtippen oder sie von der Buch-Website, http://www.newInstance.com, herunterladen.

SAX-Reader

Anstatt noch mehr Zeit mit der Vorrede zu verbringen, ist es jetzt Zeit zu programmieren. Als Beispiel, um Sie mit SAX vertraut zu machen, erläutert dieses Kapitel die Klasse SAXTreeViewer genauer. Diese Klasse verwendet SAX, um das Parsing eines XML-Dokuments durchzuführen, das auf der Kommandozeile übergeben wurde, und stellt das Dokument visuell als Swing-JTree dar. Wenn Sie über Swing nicht Bescheid wissen, keine Sorge; ich konzentriere mich nicht darauf, sondern verwende es nur für visuelle Zwecke. Der Schwerpunkt bleibt SAX und die Frage, wie Ereignisse beim Parsing-Prozeß verwendet werden können, um selbstdefinierte Aktionen auszuführen. Was hier hauptsächlich passiert, ist, daß ein JTree verwendet wird, der ein nettes, einfaches Baummodell anbietet, um das eingebene XML-Dokument darzustellen. Der Schlüssel zu diesem Baum ist die Klasse DefaultMutableTreeNode, an deren Verwendung Sie sich in diesem Beispiel schnell gewöhnen werden, und außerdem die Klasse DefaultTreeModel, die sich um das Layout kümmert.

In einer SAX-basierten Anwendung müssen Sie als erstes eine Instanz einer Klasse erzeugen, die dem SAX-Interface org.xml.sax.XMLReader genügt. Dieses Interface definiert das Parsing-Verhalten und ermöglicht es uns, Features und Eigenschaften festzulegen (auf die ich weiter unten in diesem Kapitel eingehen werde). Für diejenigen unter Ihnen, die mit SAX 1.0 vertraut sind, sei gesagt, daß dieses Interface das Interface org.xml.sax.Parser ersetzt.

Jetzt ist eine gute Gelegenheit klarzustellen, daß SAX 1.0 in diesem Buch nicht behandelt wird. Es gibt zwar am Ende dieses Kapitels einen sehr kurzen Abschnitt, der erläutert, wie SAX 1.0-Code nach SAX 2.0 konvertiert wird, aber Sie sind nicht in einer angenehmen Lage, wenn Sie SAX 1.0 verwenden. Während die erste Auflage dieses Buches unmittelbar nach SAX 2.0 erschienen ist, ist es nun ein gutes Jahr her, seit die Version 2.0 der API in fertiger Form veröffentlicht wurde. Ich rate Ihnen dringend, auf die Version 2 umzusteigen, falls Sie das noch nicht getan haben.

Eine Reader-Instanz erzeugen

SAX bietet ein Interface, das alle SAX-konformen XML-Parser implementieren sollten. Es ermöglicht SAX, genau zu wissen, welche Methoden für das Callback und für die Verwendung innerhalb einer Anwendung verfügbar sind. Zum Beispiel implementiert die wichtigste SAX-Parserklasse von Xerces, org.apache.xerces.parsers.SAXParser, das Interface org.xml.sax.XMLReader. Wenn Sie Zugang zum Quellcode Ihres Parsers haben, sollten Sie auch in dessen Haupt-SAX-Parserklasse die Implementierung dieses Interfaces finden. Jeder XML-Parser benötigt eine Klasse (und hat oft sogar mehr als eine), die dieses Interface implementiert: Das ist die Klasse, von der Sie eine Instanz bilden müssen, um ein XML-Parsing durchführen zu können:

// Eine Reader-Instanz bilden
XMLReader reader = 
  new org.xml.sax.SAXParser(  );

// Etwas mit dem Parser machen
reader.parse(uri);

Mit diesen Vorgaben im Hinterkopf sollten wir uns ein realistischeres Beispiel anschauen. Beispiel 3-1 ist das Grundgerüst der Klasse SAXTreeViewer, auf die ich mich gerade bezogen habe und die es uns ermöglicht, ein XML-Dokument als grafischen Baum zu betrachten. Es gibt Ihnen auch die Möglichkeit, sich jedes SAX-Event und die damit verbundenen Callback-Methoden anzuschauen, die Sie verwenden können, um während des Parsings eines XML-Dokuments Aktionen auszuführen.

Beispiel 3-1: Das SAXTreeViewer-Grundgerüst

package javaxml2;

import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

// Dies ist ein XML-Buch - wir importieren nicht ausdrücklich bestimmte Swing-Klassen
import java.awt.*;
import javax.swing.*;
import javax.swing.tree.*;

public class SAXTreeViewer extends JFrame {

    /** Standardparser, der verwendet werden soll */
    private String vendorParserClass = 
        "org.apache.xerces.parsers.SAXParser";

    /** Der grundlegende Baum, der gerendert werden soll */
    private JTree jTree;

    /** Das Baummodell, das verwendet werden soll */
    DefaultTreeModel defaultTreeModel;

    public SAXTreeViewer(  ) {
        // Swing-Setup verarbeiten
        super("SAX Tree Viewer");
        setSize(600, 450);
    }

    public void init(String xmlURI) throws IOException, SAXException {
        DefaultMutableTreeNode base = 
            new DefaultMutableTreeNode("XML-Dokument: " + 
                xmlURI);
        
        // Das Baummodell bauen
        defaultTreeModel = new DefaultTreeModel(base);
        jTree = new JTree(defaultTreeModel);

        // Die Baumhierarchie konstruieren
        buildTree(defaultTreeModel, base, xmlURI);

        // Das Ergebnis darstellen
        getContentPane(  ).add(new JScrollPane(jTree), 
            BorderLayout.CENTER);
    }

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

        // Instanzen erzeugen, die für das Parsing benötigt werden
        XMLReader reader = 
            XMLReaderFactory.createXMLReader(vendorParserClass);

        // Den Content-Handler registrieren

        // Den Fehlerhandler registrieren

        // Parsing
    }

    public static void main(String[] args) {
        try {
            if (args.length != 1) {
                System.out.println(
                    "Verwendung: java javaxml2.SAXTreeViewer " +
                    "[XML-Dokument-URI]");
                System.exit(0);
            }
            SAXTreeViewer viewer = new SAXTreeViewer(  );
            viewer.init(args[0]);
            viewer.setVisible(true);
        } catch (Exception e) {
            e.printStackTrace(  );
        }
    }
}

Bis jetzt sollte das alles recht gut verständlich sein1. Der Code nimmt die visuellen Einstellungen für Swing vor und liest ansonsten die URI eines XML-Dokuments (unser contents.xml aus dem vorigen Kapitel Einstieg in XML). In der Methode init( ) wird ein JTree erzeugt, der den Inhalt der URI anzeigen soll. Diese Objekte (der Baum und die URI) werden anschließend an die Methode weitergegeben, auf die es sich zu konzentrieren lohnt, nämlich an die Methode buildTree( ). In dieser findet das Parsing statt, und die visuelle Darstellung des übergebenen XML-Dokuments wird erzeugt. Zusätzlich kümmert sich das Grundgerüst darum, einen Basisknoten für den grafischen Baum zu erzeugen mit dem Pfad des übergebenen XML-Dokuments als Text dieses Knotens.

Sie sollten in der Lage sein, dieses Programm zu laden und zu kompilieren, wenn Sie die zuvor besprochenen Vorbereitungen getroffen haben, um zu erreichen, daß ein XML-Parser und die SAX-Klassen sich in Ihrem Klassenpfad befinden. Wenn Sie einen anderen Parser als Apache Xerces verwenden, können Sie den Wert der Variablen vendorParserClass gegen den Klassennamen der XMLReader-Implementierung Ihres Parsers austauschen und den Rest des Codes stehenlassen. Dieses einfache Programm tut noch nicht viel; wenn Sie es starten und den Namen einer existierenden Datei als Argument angeben, wird es nichts weiter tun, als Ihnen einen leeren Baum mit dem Dateinamen des Dokuments als Wurzelknoten anzuzeigen. Das kommt daher, daß Sie nur einen Reader instantiiert, aber nicht das Parsing des Dokuments verlangt haben.

Wenn Sie Schwierigkeiten damit haben, den Quellcode zu kompilieren, haben Sie wahrscheinlich ein Problem mit dem Klassenpfad Ihrer IDE oder Ihres Systems. Als erstes sollten Sie sicherstellen, daß Sie den Apache Xerces-Parser (oder den Parser Ihrer Wahl) erhalten haben. Im Falle von Xerces bedeutet das, eine ZIP- oder GZIP-komprimierte Datei herunterzuladen. Dieses Archiv kann dann entpackt werden und enthält eine Datei namens xerces.jar; diese jar-Datei enthält die kompilierten Klassendateien des Programms. Fügen Sie dieses Archiv zu Ihrem Klassenpfad hinzu. Nun sollten Sie in der Lage sein, die Quelldatei zu kompilieren.

Parsing des Dokuments

Nachdem ein Reader geladen wurde und einsatzbereit ist, können Sie ihn anweisen, das Parsing eines XML-Dokuments durchzuführen. Dies wird auf bequeme Weise durch die Methode parse( ) der Klasse org.xml.sax.XMLReader ermöglicht, und diese Methode kann entweder eine org.xml.sax.InputSource oder eine einfache String-URI sein. Es ist viel empfehlenswerter, die SAX-Klasse InputSource zu verwenden, da sie mehr Informationen zur Verfügung stellen kann als eine einfache Adresse. Ich werde später näher darauf eingehen, für den Moment soll es erst einmal genügen, daß eine InputSource aus einem InputStream, einem Reader-Objekt oder einer String-URI gebildet werden kann.

Sie können nun sowohl die Erzeugung einer InputSource aus der übergebenen URI als auch den Aufruf der Methode parse( ) zu dem Beispiel hinzufügen. Da das Dokument geladen werden muß – entweder lokal oder über das Netzwerk –, könnte dabei eine java.io.IOException auftreten, die abgefangen werden muß. Abgesehen davon wird die org.xml.sax.SAXException ausgelöst, wenn Probleme beim Parsing des Dokuments auftreten. Beachten Sie, daß die Methode buildTree beide Exceptions auslösen kann:

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

        // Instanzen erzeugen, die für das Parsing benötigt werden
        XMLReader reader = 
            XMLReaderFactory.createXMLReader(vendorParserClass);

        // Den Content-Handler registrieren

        // Den Fehlerhandler registrieren

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

Kompilieren Sie die Datei mit diesen Änderungen, und Sie können das Parsing-Beispiel ausführen. Sie sollten dem Programm als erstes Argument den Pfad zu Ihrer Datei übergeben:

c:\javaxml2\build>java javaxml2.SAXTreeViewer ..\ch03\xml\contents.xml 

Die Angabe einer XML-URI kann ein ziemlich seltsames Unterfangen sein: In den Xerces-Versionen vor 1.1 durfte ein normaler Dateiname angegeben werden (unter Windows zum Beispiel ..\xml\contents.xml). Dieses Verhalten änderte sich jedoch in Xerces 1.1 und 1.2, und die URI mußte in der folgenden Form sein: file:///c:/javaxml2/ch03/xml/contents.xml. Allerdings hat das Verhalten sich in neueren Versionen von Xerces (ab 1.3 und auch in 2.0) abermals geändert und akzeptiert nun wieder normale Dateinamen. Beachten Sie diese Besonderheit, wenn Sie Xerces 1.1 bis 1.2 verwenden.

Abbildung 3-1: Ein uninteressanter JTree
Ein uninteressanter JTree

Die relativ langweilige Ausgabe, die in Abbildung 3-1 gezeigt wird, könnte Sie daran zweifeln lassen, daß sich irgend etwas getan hat. Wenn Sie sich allerdings schön ruhig verhalten, können Sie das leise Rotieren Ihrer Festplatte hören (oder einfach dem Bytecode vertrauen). Tatsächlich findet das Parsing Ihres Dokuments statt. Allerdings sind bisher noch keine Callbacks implementiert worden, um SAX zu veranlassen, während des Parsings in Aktion zu treten; ohne diese Callbacks findet das Parsing eines Dokuments im Hintergrund und ohne Eingriff einer Anwendung statt. Natürlich wollen wir in diesen Prozeß eingreifen, und deshalb müssen wir uns jetzt mit dem Erstellen von Parser-Callback-Methoden beschäftigen.

Eine Callback-Methode ist eine Methode, die nicht direkt von Ihnen oder vom Code Ihrer Anwendung aufgerufen wird. Statt dessen ruft der Parser diese Methoden während der Arbeit ohne Einwirkung von außen auf, wenn bestimmte Ereignisse auftreten. Mit anderen Worten: Nicht Ihr Code ruft Methoden im Parser auf, sondern der Parser ruft umgekehrt Methoden in Ihrem Code auf. Dies ermöglicht es Ihnen, Verhalten in den Parsing-Prozeß hineinzuprogrammieren. Dieser Eingriff ist der wichtigste Schritt bei der Verwendung von SAX. Durch Parser-Callbacks können Sie Aktionen in den Programmablauf einfügen und das ziemlich langweilige, stille Parsing eines XML-Dokuments zu einer Anwendung machen, die auf die Daten, Elemente, Attribute und auf die Struktur des im Parsing befindlichen Dokuments reagieren sowie währenddessen mit anderen Programmen und Clients interagieren kann.

Eine InputSource verwenden

Wie ich bereits erwähnte, gehe ich noch einmal auf die Anwendung einer SAX-InputSource ein, wenn auch nur kurz. Der Vorteil der Verwendung einer InputSource statt der direkten Angabe einer URI liegt auf der Hand: Sie kann dem Parser mehr Informationen zur Verfügung stellen. Eine InputSource kapselt Informationen über ein einzelnes Objekt, nämlich über das Dokument, das vom Parser bearbeitet werden soll. In Situationen, in denen ein System-Identifier, ein Public-Identifier oder ein Stream alle an eine URI gebunden sein können, kann die Verwendung einer InputSource für die Kapselung sehr praktisch sein.

Die Klasse besitzt Methoden, um auf ihre System-ID und Public-ID, eine Zeichencodierung, einen Byte-Stream (java.io.InputStream) und einen Zeichen-Stream (java.io.Reader) zuzugreifen und diese zu verändern. Wird die InputSource der Methode parse( ) als Argument übergeben, dann garantiert SAX auch, daß der Parser die InputSource niemals modifiziert. Was ursprünglich in den Parser hineingegeben wurde, steht nach seiner Verwendung durch den Parser oder durch eine XML-basierte Anwendung noch unverändert zur Verfügung. In unserem Beispiel ist das wichtig, weil das XML-Dokument einen relativen Pfad zur DTD enthält:

<!DOCTYPE Book SYSTEM "DTD/JavaXML.dtd">

Indem Sie eine InputSource verwenden und die übergebene XML-URI in sie einhüllen, haben Sie die System-ID des Dokuments festgelegt. Dies stellt für den Parser auf effektive Weise den Pfad zum Dokument ein und ermöglicht es ihm, alle relativen Pfade innerhalb des Dokuments aufzulösen, etwa den zu der Datei JavaXML.dtd. Wenn Sie, anstatt diese ID zu setzen, das Parsing eines I/O-Streams durchführen, würde die Adresse der DTD nicht bekanntgegeben (da kein Bezugspunkt vorhanden wäre); Sie könnten dies simulieren, indem Sie den Code in der Methode buildTree( ) wie folgt ändern:

        // Parsing
        InputSource inputSource = 
            new InputSource(new java.io.FileInputStream(
                new java.io.File(xmlURI)));
        reader.parse(inputSource);


Das Ergebnis wäre die Ausgabe der folgenden Exception, wenn Sie den Viewer starten:

C:\javaxml2\build>java javaxml2.SAXTreeViewer ..\ch03\xml\contents.xml
org.xml.sax.SAXParseException: File 
  "file:///C:/javaxml2/build/DTD/JavaXML.dtd" not found.

Obwohl dies ein wenig albern erscheinen mag (eine URI in ein File-Objekt und dieses wieder in einen I/O-Stream einzuhüllen), kann man in der Tat recht häufig sehen, daß Leute I/O-Streams als Eingabedaten für Parser verwenden. Sorgen Sie aber dafür, daß Sie sich im XML-Dokument nicht auf irgendwelche anderen Dateien beziehen und daß Sie eine System-ID für den XML-Stream einstellen (indem Sie die Methode setSystemId( ) von InputSource verwenden). Insofern könnte das obige Codebeispiel »repariert« werden, wenn es wie folgt geändert wird:

        // Parsing
        InputSource inputSource = 
            new InputSource(new java.io.FileInputStream(
                new java.io.File(xmlURI)));
        inputSource.setSystemId(xmlURI);
        reader.parse(inputSource);

Legen Sie immer eine System-ID fest. Entschuldigen Sie den übertriebenen Detailreichtum; aber dafür können Sie Ihre Kollegen jetzt mit Ihren Kenntnissen über die InputSources von SAX nerven.

Content-Handler

Damit eine Anwendung etwas Sinnvolles mit XML-Daten im Parsing-Prozeß anfangen kann, müssen Sie Handler beim SAX-Parser registrieren. Ein Handler ist nichts weiter als eine Menge von Callbacks, die SAX definiert, damit Programmierer Anwendungscode in das Parsing eines Dokuments einfügen können, der beim Auftreten wichtiger Ereignisse ausgeführt wird. Diese Ereignisse finden während des Parsings des Dokuments statt und nicht, nachdem es beendet wurde. Dies ist einer der Gründe, warum SAX so ein mächtiges Interface ist: Es ermöglicht die sequenzielle Abarbeitung eines Dokuments, ohne daß zunächst das gesamte Dokument in den Speicher eingelesen werden müßte. Später werden wir uns das Document Object Model (DOM) anschauen, das dieser Einschränkung unterworfen ist.2

Es gibt vier wichtige Handler-Interfaces, die von SAX 2.0 definiert werden: org.xml.sax. ContentHandler, org.xml.sax.ErrorHandler, org.xml.sax.DTDHandler und org.xml.sax. EntityResolver. In diesem Kapitel bespreche ich ContentHandler und ErrorHandler. Ich verschiebe die Besprechung von DTDHandler und EntityResolver ins nächste Kapitel SAX für Fortgeschrittene; es genügt im Moment zu verstehen, daß EntityResolver genauso funktioniert wie die anderen Handler und speziell dafür geschaffen wurde, externe Entities aufzulösen, die in einem XML-Dokument festgelegt wurden. Selbstdefinierte Anwendungsklassen, die während des Parsing-Prozesses bestimmte Aktionen ausführen, können jedes dieser Interfaces implementieren. Diese Implementierungsklassen können im Reader mit Hilfe der Methoden setContentHandler( ), setErrorHandler( ), setDTDHandler( ) und setEntityResolver( ) registriert werden. Dann ruft der Reader die Callback-Methoden der zugehörigen Handler während des Parsings auf.

Bei dem SAXTreeViewer-Beispiel besteht ein guter Start darin, das Interface ContentHandler zu implementieren. Dieses Interface definiert einige wichtige Methoden für den Ablauf des Parsings, auf die unsere Anwendung reagieren kann. Da alle erforderlichen Import-Anweisungen schon da sind (ich habe etwas geschummelt und sie bereits hingeschrieben), muß nur noch eine Implementierung des ContentHandler-Interfaces programmiert werden. Der Einfachheit halber schreibe ich diese als nicht-öffentliche Klasse innerhalb der Quelldatei SAXTreeViewer.java. Fügen Sie die Klasse JTreeContentHandler folgendermaßen hinzu:

class JTreeContentHandler implements ContentHandler {

    /** Baummodell, das Knoten erhalten soll */
    private DefaultTreeModel treeModel;

    /** Aktueller Knoten, der Unterknoten erhalten soll */
    private DefaultMutableTreeNode current;

    public JTreeContentHandler(DefaultTreeModel treeModel, 
                               DefaultMutableTreeNode base) {
        this.treeModel = treeModel;
        this.current = base;
    }

    // ContentHandler-Methodenimplementierungen
}

Versuchen Sie zu diesem Zeitpunkt nicht, die Quelldatei zu kompilieren; Sie erhalten bloß einen Haufen Fehlermeldungen bezüglich Methoden, die in ContentHandler definiert, aber nicht implementiert wurden. Der Rest dieses Abschnitts geht jede dieser Methoden durch und fügt sie dabei hinzu. In dieser grundlegenden Klasse genügt es, die TreeModel-Implementierung zu übergeben, die verwendet wird, um neue Knoten zu dem JTree hinzuzufügen, sowie den Basisknoten (der vorhin in der Methode buildTree( ) erzeugt wurde).

Der Basisknoten wird auf eine Objektvariable namens current gesetzt. Diese Variable zeigt jeweils auf den Knoten, der gerade bearbeitet wird, und der Code wird diesen Knoten in der Baumhierarchie abwärts bewegen (wenn verschachtelte Elemente gefunden werden) und auch wieder aufwärts (wenn Elemente beendet werden und deren Elternelement wieder zum aktuellen Element wird). Nachdem dies geregelt ist, ist es Zeit, daß wir uns die verschiedenen ContentHandler-Callbacks anschauen und jedes davon implementieren. Schauen Sie sich zunächst kurz das Interface ContentHandler an, das die Callbacks zeigt, die implementiert werden müssen:

public interface ContentHandler {
    public void setDocumentLocator(Locator locator);
    public void startDocument(  ) throws SAXException;
    public void endDocument(  ) throws SAXException;
    public void startPrefixMapping(String prefix, String uri)
		throws SAXException;
    public void endPrefixMapping(String prefix)
		throws SAXException;
    public void startElement(String namespaceURI, String localName,
	    		            String qName, Attributes atts)
		throws SAXException;
    public void endElement(String namespaceURI, String localName,
			          String qName)
		throws SAXException;
    public void characters(char ch[], int start, int length)
		throws SAXException;
    public void ignorableWhitespace(char ch[], int start, int length)
		throws SAXException;
    public void processingInstruction(String target, String data)
		throws SAXException;
    public void skippedEntity(String name)
		throws SAXException;
}

Der Document-Locator

Die erste Methode, die Sie definieren müssen, ist diejenige, die ein org.xml.sax.Locator-Objekt für die Verwendung mit allen anderen SAX-Ereignissen festlegt. Wenn ein Callback-Ereignis auftritt, benötigt die Klasse, die einen Handler implementiert, oft die Information, an welcher Stelle der SAX-Parser innerhalb einer XML-Datei gerade ist. Dies wird benötigt, um der Anwendung bei Entscheidungen darüber zu helfen, welches Ereignis gerade an welcher Stelle im XML-Dokument auftritt, um etwa die Zeile zu bestimmen, in der ein Fehler aufgetreten ist.

Die Klasse Locator besitzt einige nützliche Methoden wie getLineNumber( ) und getColumnNumber( ), die die aktuelle Position des Parsing-Prozesses innerhalb eines XML-Dokuments zurückgeben, wenn sie aufgerufen werden. Da diese Position nur während des aktuellen Parsing-Durchlaufs gültig ist, sollte der Locator nur innerhalb des Bereiches der ContentHandler-Implementierung verwendet werden. Da dies später praktisch sein könnte, speichert der hier gezeigte Code die übergebene Locator-Instanz in einer Objektvariable ab:

class JTreeContentHandler implements ContentHandler {

    /** Locator für Positionsinformation merken */
    private Locator locator;

    // Konstruktor

    public void setDocumentLocator(Locator locator) {
        // Für späteren Gebrauch speichern
        this.locator = locator;
    }
}

Anfang und Ende eines Dokuments

In jedem Ablauf gibt es immer einen Anfang und ein Ende. Diese wichtigen Ereignisse sollten beide nur einmal auftreten, ersteres vor allen anderen Ereignissen und letzteres nach allen anderen. Diese recht offensichtliche Tatsache ist ein kritischer Punkt für Anwendungen, da sie es ihnen ermöglicht, genau zu wissen, wann das Parsing beginnt und endet. SAX stellt Callback-Methoden für diese beiden Ereignisse zur Verfügung: startDocument( ) und endDocument( ).

Die erste Methode, startDocument( ), wird vor allen anderen Callbacks aufgerufen, auch vor den Callback-Methoden anderer SAX-Handler wie etwa DTDHandler. Mit anderen Worten: startDocument( ) ist nicht nur die erste Methode, die im ContentHandler aufgerufen wird, sondern auch die erste innerhalb des gesamten Parsing-Prozesses, abgesehen von der eben besprochenen Methode setDocumentLocator( ). Dies garantiert einen festgesetzten Anfang des Parsings, so daß die Anwendung in der Lage ist, alle Aufgaben zu erledigen, die erforderlich sind, bevor das Parsing stattfindet.

Die zweite Methode, endDocument( ), wird immer zuletzt aufgerufen, wiederum über alle Handler hinweg. Dies schließt auch Situationen ein, in denen Fehler auftreten, die das Parsing zum Anhalten bringen. Ich werde erst später über Fehler sprechen, aber es gibt sowohl reparable als auch irreparable Fehler. Wenn ein irreparabler Fehler auftritt, wird die Callback-Methode des ErrorHandlers aufgerufen, und ein abschließender Aufruf von endDocument( ) beendet das versuchte Parsing.

In dem Beispielcode soll in diesen Methoden kein sichtbares Ereignis auftreten; aber wie immer bei der Implementierung von Interfaces müssen die Methoden dennoch existieren:

    public void startDocument(  ) throws SAXException {
        // Hier treten keine sichtbaren Ereignisse auf
    }

    public void endDocument(  ) throws SAXException {
        // Hier treten keine sichtbaren Ereignisse auf
    }

Beide Callback-Methoden können SAXExceptions auslösen. Als einzige Art von Exceptions, die SAX-Ereignisse jemals auslösen, stellen sie einen weiteren Standardzugang zum Parsing-Verhalten zur Verfügung. Allerdings umhüllen diese Exceptions oftmals andere Exceptions, die anzeigen, welche Probleme aufgetreten sind. Wurde zum Beispiel das Parsing einer XML-Datei über das Netzwerk mit Hilfe einer URL durchgeführt und wurde die Verbindung plötzlich unterbrochen, könnte eine java.net.SocketException auftreten. Allerdings braucht eine Anwendung, die die SAX-Klassen verwendet, diese Exception nicht abzufangen, da sie nicht wissen muß, wo die XML-Ressource sich befindet (es könnte auch eine lokale Datei im Gegensatz zu einer Netzwerk-Ressource sein).

Statt dessen kann die Anwendung eine einzige SAXException abfangen. Innerhalb des SAX-Readers wird die ursprüngliche Exception abgefangen und als SAXException wieder ausgelöst, wobei die verursachende Exception in die neue hinein verpackt wird. Dies ermöglicht es, daß Anwendungen nur nach einer Standard-Exception Ausschau halten müssen, während nähere Details darüber, welche Fehler während des Parsing-Prozesses aufgetreten sind, verpackt und dem aufrufenden Programm durch diese Standard-Exception verfügbar gemacht werden. Die Klasse SAXException stellt die Methode getException( ) zur Verfügung, die die darunterliegende Exception zurückgibt (wenn es eine gibt).

Steueranweisungen

Ich sprach davon, daß Steueranweisungen (Processing Instructions, PIs) innerhalb von XML gewissermaßen einen Spezialfall darstellen. Sie werden nicht als XML-Elemente betrachtet und werden anders behandelt, indem sie für die aufrufende Anwendung verfügbar gemacht werden. Wegen dieser besonderen Eigenschaften definiert SAX ein besonderes Callback, um Steueranweisungen zu verarbeiten. Diese Methode nimmt das Ziel der Steueranweisung entgegen sowie alle Daten, die an die PI gesendet werden. In diesem Beispiel kann die PI in einen neuen Knoten umgewandelt und im Tree Viewer angezeigt werden:

    public void processingInstruction(String target, String data)
        throws SAXException {

        DefaultMutableTreeNode pi = 
            new DefaultMutableTreeNode("PI (target = '" + target +
                                       "', data = '" + data + "')");
        current.add(pi);
    }

In einer echten Anwendung, die XML-Daten verwendet, kann die Anwendung an dieser Stelle Anweisungen empfangen und Variablenwerte setzen oder Methoden ausführen, um anwendungsspezifische Verarbeitungsschritte vorzunehmen. Beispielsweise kann das Apache Cocoon Publishing-Framework Flags setzen, um Transformationen von Daten durchzuführen, nachdem deren Parsing erfolgt ist oder um das XML als Daten mit einem bestimmten Content-Type darzustellen. Diese Methode löst wie andere SAX-Callbacks eine SAXException aus, wenn Fehler auftreten.

Es ist wichtig, darauf hinzuweisen, daß diese Methode keine Benachrichtigung über die XML-Deklaration erhält:

<?xml version="1.0" standalone="yes"?>

In der Tat stellt SAX kein Mittel zur Verfügung, um an diese Information heranzukommen (und Sie werden herausfinden, daß dies im Moment auch nicht mit DOM oder JDOM möglich ist!). Dahinter steckt das grundlegende Prinzip, daß diese Information für den XML-Parser oder Reader und nicht für den Anwender der Dokumentdaten bestimmt ist. Aus diesem Grunde wird es dem Entwickler nicht zugänglich gemacht.

Namensraum-Callbacks

Seit der Besprechung von Namensräumen im Kapitel Einstieg in XML sollten Sie in der Lage sein, ihre Wichtigkeit und ihren Einfluß auf das Parsing und die Verarbeitung von XML nachvollziehen zu können. Neben XML Schema sind XML-Namensräume womöglich das wichtigste Konzept, das seit der ursprünglichen XML 1.0-Empfehlung zu XML hinzugefügt wurde. Durch SAX 2.0 wurde eine Unterstützung für Namensräume auf Elementebene eingeführt. Dies ermöglicht die Unterscheidung zwischen dem Namensraum eines Elements, der durch ein Element-Präfix und eine damit verbundene Namensraum-URI bezeichnet wird, und dem lokalen Namen eines Elements. In diesem Zusammenhang bedeutet der Ausdruck lokaler Name den Namen eines Elements ohne sein Präfix. Zum Beispiel ist der lokale Name des Elements ora:copyright einfach copyright. Das Namensraumpräfix ist ora und die Namensraum-URI wird als http://www.oreilly.com deklariert.

Es gibt zwei SAX-Callbacks, die sich speziell mit Namensräumen beschäftigen. Diese Callbacks werden aufgerufen, wenn der Parser den Anfang und das Ende eines Präfix-Mappings erreicht. Obwohl dies ein neuer Ausdruck ist, ist es kein neues Konzept; ein Präfix-Mapping ist einfach ein Element, das das Attribut xmlns verwendet, um einen Namensraum zu deklarieren, zum Beispiel:

<katalog>
  <buecher>
    <buch titel="XML in a Nutshell" 
          xmlns:xlink="http://www.w3.org/1999/xlink">
      <einband xlink:type="simple" xlink:show="onLoad" 
             xlink:href="xmlnutCover.jpg" ALT="XML in a Nutshell" 
             breite="125" hoehe="350" />
    </buch>
  </buecher>
</katalog>

In diesem Fall wird explizit ein Namensraum deklariert. Dieser Namensraum betrifft Elemente, die innerhalb anderer Elemente dieses Dokuments verschachtelt wurden. Dieses Präfix- und URI-Mapping (in diesem Beispiel xlink bzw. http://www.w3.org/1999/xlink) steht dann Elementen und Attributen innerhalb des deklarierenden Elements zur Verfügung.

Das Callback startPrefixMapping( ) erhält sowohl das Namensraum-Präfix als auch die URI, die mit diesem Präfix verbunden ist. Das Mapping wird als »geschlossen« oder »beendet« betrachtet, wenn das Element geschlossen wird, in dem das Mapping deklariert wurde. Dies setzt auch das Callback endPrefixMapping( ) in Gang.

Die einzige Besonderheit dieser Callbacks ist, daß sie sich nicht auf die sequenzielle Art und Weise verhalten, wie SAX normalerweise strukturiert ist; das Präfix-Mapping-Callback tritt unmittelbar vor dem Callback für das Element auf, das den Namensraum deklariert, und das Ende des Mappings führt zu einem Ereignis unmittelbar nach dem Schließen des deklarierenden Elements. Allerdings ist es in der Tat sehr sinnvoll, wenn das deklarierende Element selbst in der Lage ist, das deklarierte Namensraum-Mapping zu verwenden. Deshalb muß das Mapping schon vor dem Callback des Elements verfügbar sein. Es funktioniert beim Beenden eines Mappings genau umgekehrt: Das Element muß zuerst geschlossen werden (da es den Namensraum verwenden könnte), und anschließend kann das Mapping aus der Liste der verfügbaren Mappings entfernt werden.

Im JTreeContentHandler gibt es keine sichtbaren Ereignisse, die während dieser beiden Callbacks auftreten sollen. Allerdings ist es ein üblicher Vorgang, das Präfix und die URI-Mappings in einer Datenstruktur abzulegen. Sie werden gleich sehen, daß die Element-Callbacks die Namensraum-URI, aber nicht das Namensraum-Präfix liefern. Wenn Sie diese Präfixe nicht speichern (die durch startPrefixMapping( ) geliefert werden), stehen sie in Ihrem Callback-Code nicht zur Verfügung. Der einfachste Weg dazu besteht darin, eine Map zu verwenden, das ermittelte Präfix und die URI in startPrefixMapping( ) hinzuzufügen und sie in endPrefixMapping( ) wieder zu entfernen. Dies kann durch die folgenden Code-Erweiterungen erreicht werden:

class JTreeContentHandler implements ContentHandler {

    /** Locator für Positionsinformation merken */
    private Locator locator;

    /** URI-zu-Präfix-Mappings ablegen */
    private Map namespaceMappings;

    /** Baummodell, das Knoten erhalten soll */
    private DefaultTreeModel treeModel;

    /** Aktueller Knoten, der Unterknoten erhalten soll */
    private DefaultMutableTreeNode current;

    public JTreeContentHandler(DefaultTreeModel treeModel, 
                               DefaultMutableTreeNode base) {
        this.treeModel = treeModel;
        this.current = base;
        this.namespaceMappings = new HashMap(  );
    }

    // Existierende Methoden

    public void startPrefixMapping(String prefix, String uri) {
        // Hier treten keine sichtbaren Ereignisse auf.
        namespaceMappings.put(uri, prefix);
    }

    public void endPrefixMapping(String prefix) {
        // Hier treten keine sichtbaren Ereignisse auf.
        for (Iterator i = namespaceMappings.keySet().iterator(  ); 
             i.hasNext(  ); ) {

            String uri = (String)i.next(  );
            String thisPrefix = (String)namespaceMappings.get(uri);
            if (prefix.equals(thisPrefix)) {
                namespaceMappings.remove(uri);
                break;
            }
        }
    }
}

Noch ein Hinweis: Ich habe die URI statt dem Präfix als Schlüssel der Mappings verwendet. Wie ich kurz zuvor erwähnte, liefert das Callback startElement( ) die Namensraum-URI für das Element, nicht das Präfix. Insofern wird das Nachschlagen schneller, wenn die URIs als Schlüssel verwendet werden. Allerdings ist es, wie Sie in endPrefixMapping( ) sehen können, so ein wenig mehr Arbeit, das Mapping wieder zu entfernen, wenn es nicht mehr verfügbar ist. In jedem Fall ist das Ablegen von Namensraum-Mappings auf diese Weise ein recht typischer SAX-Trick, also legen Sie es sich in Ihrem XML-Programmierwerkzeugkasten zurecht.

Die hier gezeigte Lösung ist weit entfernt von einer vollständigen Lösung, die mit komplexeren Namensraumaufgaben zurechtkommt. Es ist absolut zulässig, im Bereich eines Elements Präfixe neuen URIs zuzuweisen oder mehrere Präfixe mit der gleichen URI zu verknüpfen. In diesem Beispiel würde dies dazu führen, daß Namensräume weiter außen liegender Bereiche durch weiter innen liegende überschrieben würden, wenn die gleiche URI verschiedenen Präfixen zugeordnet würde. In einer robusteren Anwendung sollten Sie Präfixe und URIs separat ablegen und eine Methode verwenden, die die beiden miteinander in Beziehung setzt, ohne daß bereits bestehende Beziehungen dabei verloren gehen. Aber immerhin vermittelt Ihnen das Beispiel einen Eindruck davon, wie Namensräume grundsätzlich gehandhabt werden.

Element-Callbacks

Nun sind Sie bestimmt in der Lage, sich mit den eigentlichen Daten im XML-Dokument zu beschäftigen. Es stimmt, daß über die Hälfte der SAX-Callbacks nichts mit XML-Elementen, Attributen und Daten zu tun haben. Der Grund ist, daß der Prozeß des Parsings von XML mehr bewirken soll, als einfach Ihre Anwendung mit den XML-Daten zu versorgen; er sollte der Anwendung Anweisungen aus XML-PIs heraus geben, damit Ihre Anwendung weiß, welche Aktionen sie durchführen soll, er sollte die Anwendung darüber informieren, wann das Parsing beginnt und wann es endet, und er sollte ihr sogar mitteilen, wann Whitespace auftritt, der ignoriert werden kann! Wenn einige dieser Callbacks noch keinen großen Sinn zu ergeben scheinen, lesen Sie weiter.

Selbstverständlich gibt es auch SAX-Callbacks mit der Aufgabe, Ihnen Zugriff auf die XML-Daten in Ihren Dokumenten zu geben. Die drei wichtigsten Ereignisse, die mit dem Holen von Daten zu tun haben, sind der Anfang und das Ende von Elementen sowie das Callback characters( ). Diese liefern Ihnen wärend des Parsings eines Elements die Daten in diesem Element und teilen Ihnen mit, wann das schließende Tag des Elements erreicht wird. Das erste von ihnen, startElement( ), liefert einer Anwendung Informationen über ein XML-Element und eventuell vorhandene Attribute. Die Parameter dieses Callbacks sind der Name des Elements (in unterschiedlicher Form) und eine Instanz von org.xml.sax.Attributes. Diese Hilfsklasse enthält Verweise auf alle Attribute in einem Element. Sie ermöglicht es, die Attribute eines Elements auf eine einfache Weise durchzugehen, die einem Vector ähnelt.

Zusätzlich zu der Möglichkeit, auf ein Attribut anhand seines Indexes zuzugreifen (die verwendet wird, wenn alle Attribute nacheinander angesprochen werden), ist es auch möglich, ein Attribut mit seinen Namen anzusprechen. Natürlich sollten Sie inzwischen ein wenig vorsichtig sein, wenn Sie das Wort »Name« im Zusammenhang mit einem XML-Element oder -Attribut sehen, da es verschiedenes bedeuten kann. In diesem Fall kann entweder der vollständige Name des Attributs (mit Namensraum-Präfix, wenn er eins hat), genannt Q-Name, verwendet werden oder die Kombination aus seinem lokalen Namen und seiner Namensraum-URI, wenn ein Namensraum verwendet wird. Es existieren auch Hilfsmethoden wie getURI(int index) und getLocalName(int index), die dabei helfen, zusätzliche Namensraum-Informationen zu einem Attribut zu geben. Insgesamt stellt das Interface Attributes eine umfassende Menge von Informationen über die Attribute eines Elements zur Verfügung.

Zusätzlich zu den Elementattributen erhalten Sie auch den Elementnamen in unterschiedlichen Formen. Dies geschieht wiederum unter Beachtung der XML-Namensräume. Die Namensraum-URI des Elements wird als erstes angegeben. Dies positioniert das Element in seinem korrekten Kontext innerhalb der gesamten Menge von Namensräumen des Dokuments. Anschließend wird der lokale Name des Elements angegeben, dies ist der Elementname ohne Präfix. Zusätzlich (und für die Abwärtskompatibilität) wird der Q-Name des Elements angegeben. Dies ist der unmodifizierte, unveränderte Name des Elements, der ein Namensraum-Präfix enthält, falls vorhanden; mit anderen Worten: genau das, was im XML-Dokument stand, nämlich ora:copyright für das Copyright-Element. Durch die Angabe dieser drei Arten von Namen sollten Sie in der Lage sein, ein Element mit oder ohne Berücksichtigung seines Namensraumes zu beschreiben.

In dem Beispiel gibt es mehrere Stellen, an denen diese Fähigkeit demonstriert wird. Als erstes wird ein neuer Knoten erzeugt und mit dem lokalen Namen des Elements zum Baum hinzugefügt. Anschließend wird dieser Knoten zum aktuellen Knoten; auf diese Weise werden alle verschachtelten Elemente und Attribute als Blätter hinzugefügt. Als nächstes wird der Namensraum bestimmt, indem die übergebene Namensraum-URI und das namespaceMappings-Objekt (zur Ermittlung des Präfixes) verwendet werden, die Sie gerade im vorigen Abschnitt zum Code hinzugefügt haben. Dieser wird ebenfalls als Knoten hinzugefügt. Als letztes geht der Code schrittweise das Interface Attributes durch und fügt jedes Attribut (mit Informationen über seinen lokalen Namen und Namensraum) als Kindknoten hinzu. Der Code, um all das zu veranlassen, wird hier gezeigt:

    public void startElement(String namespaceURI, String localName,
                             String qName, Attributes atts)
        throws SAXException {

        DefaultMutableTreeNode element = 
            new DefaultMutableTreeNode("Element: " + localName);
        current.add(element);
        current = element;

        // Namensraum ermitteln
        if (namespaceURI.length(  ) > 0) {
            String prefix = 
                (String)namespaceMappings.get(namespaceURI);
            if (prefix.equals("")) {
                prefix = "[None]";
            }
            DefaultMutableTreeNode namespace =
                new DefaultMutableTreeNode("Namensraum: Präfix = '" +
                    prefix + "', URI = '" + namespaceURI + "'");
            current.add(namespace);
        }

        // Attribute verarbeiten
        for (int i=0; i<atts.getLength(  ); i++) {
            DefaultMutableTreeNode attribute =
                new DefaultMutableTreeNode("Attribut (Name = '" +
                                           atts.getLocalName(i) + 
                                           "', Wert = '" +
                                           atts.getValue(i) + "')");
            String attURI = atts.getURI(i);
            if (attURI.length(  ) > 0) {
                String attPrefix = 
                    (String)namespaceMappings.get(attURI);
                if (attPrefix.equals("")) {
                    attPrefix = "[Keins]";
                }
                DefaultMutableTreeNode attNamespace =
                    new DefaultMutableTreeNode("Namensraum: Präfix = '" +
                        attPrefix + "', URI = '" + attURI + "'");
                attribute.add(attNamespace);            
            }
            current.add(attribute);
        }
    }

Das Ende eines Elements ist wesentlich einfacher zu programmieren. Da kein Bedarf besteht, irgendwelche sichtbaren Informationen auszugeben, müssen Sie nur im Baum um einen Knoten nach oben wandern und das Elternelement des Elements zum neuen aktuellen Knoten machen:

    public void endElement(String namespaceURI, String localName,
                           String qName)
        throws SAXException {

        // Im Baum wieder nach oben wandern
        current = (DefaultMutableTreeNode)current.getParent(  );
    }

Ein letzter Hinweis, bevor wir mit den Elementdaten weitermachen: Sie haben vielleicht bemerkt, daß es mit einer Namensraum-URI und dem Q-Namen eines Elements möglich wäre, das Präfix und die URI aus den Informationen zu bestimmen, die dem start-Element( )-Callback übergeben werden, ohne daß das Mapping von Namensraum-Assoziationen nötig wäre. Das stimmt absolut, und es würde für den Beispielcode vollkommen ausreichen. Allerdings befinden sich bei den meisten Anwendungen Hunderte oder sogar Tausende von Codezeilen in diesen Callbacks (oder – noch besser – in Methoden, die durch Code innerhalb dieser Callbacks aufgerufen werden). In solchen Fällen ist es eine wesentlich robustere Lösung, die Daten in einer selbstdefinierten Struktur abzulegen, anstatt sich auf den Q-Namen eines Elements zu verlassen. Mit anderen Worten: Das Zerlegen des Q-Namens am Doppelpunkt ist hervorragend für einfache Anwendungen geeignet, aber nicht so toll für komplexere (und deshalb realistischere).

Elementdaten

Nachdem der Anfang und das Ende eines Elementblocks identifiziert worden sind und die Attribute des Elements für eine Anwendung aufgelistet worden sind, besteht die nächste wichtige Information aus den eigentlichen Daten, die sich im Element als solchem befinden. Diese bestehen grundsätzlich aus weiteren Elementen, Textdaten oder einer Kombination aus beidem. Wenn andere Elemente auftauchen, werden die Callbacks für diese Elemente in Gang gesetzt, und eine Art Pseudorekursion findet statt: Elemente, die in Elemente hinein verschachtelt sind, haben Callbacks zur Folge, die in Callbacks »verschachtelt« sind. An einigen Stellen werden dagegen Textdaten gefunden. Diese Daten, die üblicherweise die wichtigste Information für einen XML-Client darstellen, werden normalerweise entweder dem Client sichtbar gemacht oder verarbeitet, um eine Client-Reaktion zu erzeugen.

Textdaten in XML-Elementen werden mit Hilfe des Callbacks characters( ) an eine umhüllende Anwendung übermittelt. Diese Methode versorgt die umhüllende Anwendung sowohl mit einem Array von Zeichen als auch mit einem Startindex und der Anzahl der zu lesenden Zeichen. Das Erzeugen eines Strings aus diesem Array und die Verwendung dieser Daten ist kinderleicht:

    public void characters(char[] ch, int start, int length)
        throws SAXException {

        String s = new String(ch, start, length);
        DefaultMutableTreeNode data =
            new DefaultMutableTreeNode("Zeichendaten: '" + s + "'");
        current.add(data);
    }

Obwohl sie ein einfaches Callback zu sein scheint, führt diese Methode oft zu großer Verwirrung, da das SAX-Interface und die Standards nicht genau definieren, wie dieses Callback im Zusammenhang mit besonders umfangreichen Zeichendaten verwendet werden muß. Mit anderen Worten kann ein Parser sich entweder entscheiden, alle enthaltenen Zeichendaten bei einem Aufruf zurückzugeben oder aber diese Daten auf mehrere Methodenaufrufe aufzuteilen. Für jedes einzelne Element wird diese Methode entweder gar nicht aufgerufen (wenn sich keine Zeichendaten im Element befinden) oder einmal oder auch mehrmals. Parser implementieren dieses Verhalten unterschiedlich, oftmals durch die Verwendung von Algorithmen, die zur Erhöhung der Parsing-Geschwindigkeit dienen. Verlassen Sie sich niemals darauf, daß Sie alle Textdaten eines Elements innerhalb eines Callback-Methodenaufrufs erhalten; aber gehen Sie umgekehrt auch nicht davon aus, daß die fortlaufenden Zeichendaten eines Elements grundsätzlich mehrere Callbacks erfordern.

Wenn Sie SAX-Event-Handler schreiben, sollten Sie darauf achten, daß Sie ständig an die hierarchische Struktur denken. Mit anderen Worten sollten Sie sich nicht angewöhnen, zu denken, daß ein Element seine Daten und seine Kindelemente besitzt, sondern Sie sollten sich vorstellen, daß es lediglich als Elternelement dient. Denken Sie auch daran, daß der Parser sich weiterbewegt und dabei Elemente, Attribute und Daten in der Reihenfolge verarbeitet, in der er sie antrifft. Dies kann zu einigen überraschenden Ergebnissen führen. Betrachten Sie das folgende Fragment eines XML-Dokuments:

<eltern>Dieses Element hat <kind>eingebetteten Text</kind> in seinem Inneren.</eltern>

Wenn Sie vergessen, daß das Parsing bei SAX sequenziell erfolgt und daß die Callbacks stattfinden, sobald ihm Elemente und Daten begegnen, und wenn Sie außerdem vergessen, daß XML als hierarchisch angesehen wird, könnten Sie vermuten, daß die Ausgabe hier so ählich aussieht wie in Abbildung 3-2.

Abbildung 3-2: Erwarteter, aber falscher grafischer Baum
Erwarteter, aber falscher grafischer Baum

Das erscheint logisch, da das Elternelement das Kindelement vollständig »besitzt«. Aber in Wirklichkeit findet an jedem SAX-Ereignispunkt ein Callback statt, was zu dem Baum führt, der in Abbildung 3-3 zu sehen ist.

Abbildung 3-3: Der tatsächlich erzeugte Baum
Der tatsächlich erzeugte Baum

SAX liest nicht voraus, deshalb ist das Ergebnis genau das, was Sie erwarten würden, wenn Sie das XML-Dokument ohne gesunden Menschenverstand als sequenzielle Daten betrachten würden. Dies ist ein wichtiger Punkt, den wir uns merken müssen.

Zur Zeit führen Apache Xerces und die meisten anderen verfügbaren Parser standardmäßig keine Validierung durch. In dem Beispielprogramm findet die Validierung nicht statt, weil sie nicht explizit eingeschaltet wurde. Allerdings wird in den meisten Fällen trotzdem eine DTD oder ein Schema verarbeitet. Beachten Sie, daß sogar ohne Validierung eine Exception auftritt, wenn keine System-ID gefunden wird und die DTD-Referenz nicht aufgelöst werden kann (nachzulesen im Abschnitt über InputSources). Merken Sie sich also genau den Unterschied zwischen der Durchführung der Validierung und der Durchführung der DTD- oder Schema-Verarbeitung. Die Auslösung von ignorableWhitespace( ) erfordert lediglich, daß DTD- oder Schema-Verarbeitung durchgeführt wird, und nicht, daß eine Validierung stattfindet.

Zu guter Letzt reagiert die Methode characters( ) auch oft auf Whitespace. Dies führt zu einer gewissen Verwirrung, da ein weiteres SAX-Callback, ignorableWhitespace( ), ebenfalls auf Whitespace reagiert. Unglücklicherweise werden in den meisten Büchern (darunter auch in meiner ersten Auflage von Java und XML, wie ich peinlicherweise zugeben muß) die Details von Whitespace teilweise oder auch völlig falsch dargestellt. Deshalb möchte ich nun die Gelegenheit ergreifen und das Thema ein für allemal klarstellen. Als erstes steht fest, daß die Methode ignorableWhitespace( ) niemals aufgerufen wird, wenn nicht auf eine DTD oder ein XML Schema Bezug genommen wird. Punkt.

Der Grund dafür ist, daß eine DTD (oder ein Schema) die Details des Content-Modells eines Elements regelt. Mit anderen Worten: In der Datei JavaXML.dtd kann das Element inhalt nichts weiter enthalten als kapitel-Elemente. Jeglicher Whitespace zwischen dem Anfang des Elements inhalt und dem Anfang eines kapitel-Elements kann logischerweise ignoriert werden. Er bedeutet nichts, da die DTD vorschreibt, keine Zeichendaten zu erwarten (weder Whitespace noch andere Zeichen). Das gleiche gilt für Whitespace zwischen dem Ende eines kapitel-Elements und dem Anfang eines weiteren kapitel-Elements oder zwischen ersterem und dem Ende des Elements inhalt.

Da die Beschränkung (in Form einer DTD oder eines Schemas) festlegt, daß keine Zeichendaten erlaubt sind, kann dieser Whitespace keine Bedeutung haben. Gibt es jedoch keine Beschränkung, die dem Parser diese Information übermittelt, ist es unmöglich, den Whitespace als bedeutungslos zu interpretieren. Insofern würde das Entfernen der Referenz auf eine DTD dazu führen, daß diese verschiedenen Whitespaces das Callback characters() auslösen, auch wenn sie im anderen Fall das Callback ignorableWhitespace( ) aufgerufen haben. Deshalb ist Whitespace niemals einfach ignorierbar oder auch nicht-ignorierbar; alles hängt davon ab, auf welche Beschränkungen (wenn überhaupt) Bezug genommen wird. Wenn Sie die Beschränkungen ändern, könnte sich auch die Bedeutung von Whitespace ändern.

Wir wollen sogar noch tiefer gehen: In dem Fall, wo ein Element nur andere Elemente enthalten darf, ist verständlicherweise alles klar. Whitespace zwischen Elementen kann ignoriert werden. Betrachten Sie aber einmal ein gemischtes Content-Modell:

<!ELEMENT p (#PCDATA | b* | i* | a*)>

Falls das ziemlich unverständlich aussieht, denken Sie an HTML; es stellt (teilweise) die Beschränkungen des Elements p, des Absatz-Tags, dar. Selbstverständlich kann innerhalb dieses Tags Text vorkommen und ebenso die Elemente fett (b), kursiv (i) und Links (a). In diesem Modell gibt es keinen Whitespace zwischen dem öffnenden und dem schließenden p-Tag, der jemals als ignorierbar gekennzeichnet wird (ob mit oder ohne DTD- oder Schema-Referenz). Das kommt daher, daß es nicht möglich ist, zwischen Whitespace zu unterscheiden, der für Lesbarkeit sorgen soll, und solchem, der zum Dokument gehört. Zum Beispiel:

<p>
  <i>Java und XML</i>, 2. Auflage, ist jetzt im Buchhandel erhältlich, aber
    auch direkt bei O'Reilly unter 
    <a href="http://www.oreilly.de">http://www.oreilly.de</a>.
</p>

In diesem XHTML-Fragment kann der Whitespace zwischen dem öffnenden p-Element und dem öffnenden i-Element nicht ignoriert werden, deshalb wird er durch das Callback characters( ) verarbeitet. Wenn Sie jetzt nicht vollkommen verwirrt sind (und davon gehe ich aus), machen Sie sich bereit, die beiden Callbacks, die mit Zeichen zu tun haben, genau zu untersuchen. Dadurch wird es ein Klacks, das letzte SAX-Callback zu erläutern, das mit diesem Thema zu tun hat.

Ignorierbarer Whitespace

Nachdem wir den Whitespace so ausführlich besprochen haben, ist es ein Kinderspiel, die Methode ignorableWhitespace( ) zu implementieren. Da der gelieferte Whitespace ignoriert werden darf, tut der Code genau das – ihn ignorieren:

    public void ignorableWhitespace(char[] ch, int start, int length)
        throws SAXException {
        
        // Dies kann ignoriert werden, also wird es nicht angezeigt
    }

Whitespace wird auf die gleiche Weise verarbeitet wie Zeichendaten; er kann durch ein einzelnes Callback abgehandelt werden, oder ein SAX-Parser kann den Whitespace auf mehrere Methodenaufrufe aufteilen. In jedem Fall sollten Sie immer genau darauf achten, sich nicht auf Whitespace als Textdaten zu verlassen, um problematische Fehler in Ihren Anwendungen zu vermeiden.

Entities

Wie Sie sich erinnern, gibt es nur eine Entity-Referenz in dem Dokument contents.xml, nämlich OReillyCopyright. Die Auflösung beim Parsing führt dazu, daß eine weitere Datei geladen wird, entweder aus dem lokalen Dateisystem oder aus irgendeiner anderen URI. Allerdings ist in der verwendeten Reader-Implementierung die Validierung nicht eingeschaltet.3 Ein oft unbeachteter Aspekt nicht-validierender Parser besteht darin, daß sie Entity-Referenzen nicht notwendigerweise auflösen, sondern sie statt dessen übergehen. Das hat schon zuvor ein wenig für Kopfschmerzen gesorgt, weil es vorkommen kann, daß die Parser-Ergebnisse Entity-Referenzen, die erwartet wurden, einfach nicht enthalten. SAX 2.0 kompensiert dies auf angenehme Weise durch ein Callback, das aktiv wird, wenn ein Entity durch einen nicht-validierenden Parser übergangen wird. Das Callback liefert den Namen des Entity, welches so in die Ausgabe des Viewers eingefügt werden kann:

    public void skippedEntity(String name) throws SAXException {
        DefaultMutableTreeNode skipped =
            new DefaultMutableTreeNode("Übergangenes Entity: '" + name + "'");
        current.add(skipped);
    }

Bevor Sie aber jetzt nach dem Knoten OReillyCopyright Ausschau halten, sollten Sie sich darüber im klaren sein, daß die meisten vorhandenen Parser Entities nicht übergehen, auch dann nicht, wenn sie nicht-validierend sind. Apache Xerces zum Beispiel ruft dieses Callback niemals auf; statt dessen wird die Entity-Referenz ausgewertet, und das Ergebnis ist in den Daten enthalten, die nach dem Parsing verfügbar sind. Mit anderen Worten: Das Callback steht für Parser bereit, aber Sie werden Schwierigkeiten haben, einen Fall zu finden, in dem es in Aktion tritt! Wenn Sie einen Parser haben, der dieses Verhalten an den Tag legt, dann beachten Sie, daß der Parameter, der an das Callback übergeben wird, nicht das führende Ampersand-Zeichen und das nachfolgende Semikolon der Entity-Referenz enthält. Für &OReillyCopyright; wird nur der Name der Entity-Referenz, OReillyCopyright, an skippedEntity( ) übergeben.

Die Ergebnisse

Als letztes müssen Sie die Content-Handler-Implementierung in dem XMLReader registrieren, den Sie instantiiert haben. Dies geschieht durch setContentHandler( ). Fügen Sie die folgenden Zeilen zur Methode buildTree( ) hinzu:

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

        // Instanzen erzeugen, die für das Parsing benötigt werden
        XMLReader reader = 
            XMLReaderFactory.createXMLReader(vendorParserClass);
        ContentHandler jTreeContentHandler = 
            new JTreeContentHandler(treeModel, base);

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

        // Den Fehlerhandler registrieren

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

Wenn Sie alle diese Dokument-Callbacks eingefügt haben, sollten Sie in der Lage sein, die SAXTreeViewer-Quelldatei zu kompilieren. Wenn das geschehen ist, können Sie die SAX-Viewer-Demonstration mit der XML-Beispieldatei starten, die wir zuvor erzeugt haben. Stellen Sie außerdem sicher, daß Sie Ihr Arbeitsverzeichnis zum Klassenpfad hinzugefügt haben. Die vollständige Java-Anweisung sollte so aussehen:

C:\javaxml2\build>java javaxml2.SAXTreeViewer ..\ch03\xml\contents.xml

Dies sollte dazu führen, daß sich ein Swing-Fenster öffnet, in das der Inhalt des XML-Dokuments geladen wird. Wenn Sie beim Start eine kurze Pause bemerken, warten Sie wahrscheinlich darauf, daß Ihr Rechner eine Verbindung ins Internet herstellt, um die Entity-Referenz OReillyCopyright aufzulösen. Wenn Sie nicht online sind, schauen Sie im Kapitel Einstieg in XML nach. Dort finden Sie eine Anleitung, wie Sie die Referenz in der DTD gegen eine lokale Copyright-Datei austauschen können. In jedem Fall sollte Ihre Ausgabe so ähnlich aussehen wie Abbildung 3-4, je nachdem, welche Knoten Sie aufgeklappt haben.

Abbildung 3-4: SAXTreeViewer in Aktion
SAXTReeViewer in Aktion

Folgende Dinge sollten Sie bemerken: Erstens ist der Whitespace, der Elemente umgibt, nicht verfügbar, da das Vorhandensein einer DTD und eines strengen Content-Modells das Ignorieren von Whitespace erzwingen (wenn er dem Callback ignorableWhitespace( ) übergeben wird). Zweitens wird die Entity-Referenz aufgelöst, und Sie sehen, daß der Inhalt der Datei copyright.xml in die äußere Baumstruktur hinein verschachtelt ist. Da diese Datei im übrigen keine DTD hat, wird Whitespace, den man für ignorierbar halten könnte, als Zeichendaten durch das Callback characters( ) verarbeitet. Das führt zu den merkwürdigen kleinen Steuerzeichen im Textinhalt des Baums (es handelt sich im zugrundeliegenden Dokument zumeist um Zeilenumbrüche).

Beachten Sie zu guter Letzt, daß der Text »O’Reilly & Associates« aus copyright.xml in Wirklichkeit durch drei Aufrufe des Callbacks characters( ) behandelt wird. Dies ist ein hervorragendes Beispiel für Textdaten, die nicht als zusammenhängender Textblock ausgegeben werden. In diesem Fall hat der Parser den Text bei der Entity-Referenz (&amp;) geteilt, was ein häufiges Verhalten ist. Auf jeden Fall sollten Sie den Viewer mit verschiedenen XML-Dokumenten ausprobieren und beobachten, wie die Ausgabe sich ändert.

Sie haben nun gesehen, wie ein SAX-kompatibler Parser ein wohlgeformtes XML-Dokument behandelt. Sie sollten ebenfalls die Dokument-Callbacks verstehen, die während des Parsing-Prozesses stattfinden, und wissen, wie eine Anwendung diese Callbacks nutzen kann, um beim Parsing Informationen über ein XML-Dokument zu erhalten. Im nächsten Kapitel SAX für Fortgeschrittene betrachte ich die Validierung eines XML-Dokuments mit Hilfe der Anwendung weiterer SAX-Klassen, die zur Verarbeitung von DTDs dienen. Bevor wir aber weitermachen, möchte ich die Frage ansprechen, was passiert, wenn Ihr XML-Dokument nicht gültig ist, und welche Fehler sich aus diesem Umstand ergeben können.

Fehlerhandler

SAX stellt nicht nur das Interface ContentHandler für die Verarbeitung von Parsing-Ereignissen zur Verfügung, sondern auch das Interface ErrorHandler, das implementiert werden kann, um verschiedene Fehlerzustände zu behandeln, die während des Parsings auftreten können. Diese Klasse funktioniert auf dieselbe Weise wie der bereits konstruierte Dokument-Handler, definiert aber nur drei Callback-Methoden. Durch diese drei Methoden werden alle denkbaren Fehlerzustände behandelt und von SAX-Parsern gemeldet. Das ErrorHandler-Interface sieht wie folgt aus:

public interface ErrorHandler {
    public abstract void warning (SAXParseException exception)
		throws SAXException;
    public abstract void error (SAXParseException exception)
		throws SAXException;
    public abstract void fatalError (SAXParseException exception)
		throws SAXException;
}

Jede Methode erhält durch eine SAXParseException Informationen über den aufgetretenen Fehler oder die Warnung. Dieses Objekt enthält die Nummer der Zeile, in der das Problem angetroffen wurde, die URI des behandelten Dokuments (was das Dokument im Parser oder aber eine externe Referenz innerhalb dieses Dokuments sein könnte) und die üblichen Exception-Details wie eine Meldung und einen druckbaren Stacktrace. Zusätzlich kann jede Methode eine SAXException auslösen.

Dies mag zunächst etwas seltsam erscheinen; ein Exception-Handler, der eine Exception auslöst? Denken Sie daran, daß jeder Handler eine Parsing-Exception empfängt. Das kann eine Warnung sein, die den Parsing-Prozeß nicht zum Anhalten bringen sollte, oder ein Fehler, der aufgelöst werden muß, damit das Parsing fortgesetzt werden kann; allerdings kann es sein, daß das Callback System-I/O oder eine andere Operation durchführen muß, die eine Exception auslösen kann, und es muß in der Lage sein, sämtliche Probleme, die sich aus diesen Aktionen ergeben, die Anwendungskette hinauf zu senden. Dies kann mit Hilfe der SAXException geschehen, die das Fehlerhandler-Callback auslösen darf.

Betrachten Sie als Beispiel einen Fehlerhandler, der Fehlerbenachrichtigungen entgegennimmt und diese Fehler in eine Fehler-Logdatei schreibt. Diese Callback-Methode muß in der Lage sein, im lokalen Dateisystem entweder eine Fehler-Logdatei zu erweitern oder neu anzulegen. Wenn während des Parsings eines XML-Dokuments eine Warnung auftreten sollte, wird die Warnung an diese Methode weitergeleitet. Die Aufgabe der Warnung besteht darin, dem Callback Informationen zu übermitteln und dann mit dem Parsing des Dokuments fortzufahren. Falls jedoch der Fehlerhandler nicht in die Logdatei schreiben könnte, könnte es nötig werden, den Parser und die Anwendung darüber zu informieren, daß das gesamte Parsing angehalten werden sollte. Dies kann durch das Abfangen von I/O-Exceptions und ihre Weiterleitung an die anfordernde Anwendung erfolgen, so daß das Anhalten jedes weiteren Dokument-Parsings veranlaßt wird. Dieses gängige Szenario ist der Grund, warum Fehlerhandler in der Lage sein müssen, Exceptions auszulösen (siehe Beispiel 3-2).

Beispiel 3-2: Fehlerhandler, der eine SAXException auslösen kann

public void warning(SAXParseException exception)
    throws SAXException {

    try {
        FileWriter fw = new FileWriter("error.log");
        BufferedWriter bw = new BufferedWriter(fw);
        bw.write("Warnung: " + exception.getMessage(  ) + "\n");
        bw.flush(  );
        bw.close(  );
        fw.close(  );
    } catch (IOException e) {
        throw new SAXException("Konnte nicht in Logdatei schreiben", e);
    }
}

Mit diesem Wissen im Hinterkopf ist es möglich, das Grundgerüst einer ErrorHandler-Implementierung zu definieren und sie in der Reader-Implementierung auf dieselbe Weise zu registrieren wie den Content-Handler. Da dieses Buch keine Abhandlung über Swing werden soll, halten diese Methoden lediglich das Parsing an und geben Warnungen und Fehler auf der Kommandozeile aus. Als erstes fügen Sie am Ende der Quelldatei SAXTreeViewer.java eine weitere nicht-öffentliche Klasse hinzu:

class JTreeErrorHandler implements ErrorHandler {

    // Methodenimplementierungen

}

Als nächstes müssen Sie den selbstdefinierten Fehlerhandler in Ihrem SAX-Reader registrieren, um ihn tatsächlich zu nutzen. Dies geschieht mit Hilfe der Methode setErrorHandler( ) der XMLReader-Instanz und muß in der Methode buildTree( ) des Beispiels geschehen:

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

        // Instanzen erzeugen, die für das Parsing benötigt werden
        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);

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

Als letztes wollen wir uns die Programmierung der drei Methoden anschauen, die vom Interface ErrorHandler benötigt werden.

Warnungen

Jedesmal wenn eine Warnung auftritt (wie sie durch die XML 1.0-Spezifikation definiert wird), wird diese Methode des registrierten Fehlerhandlers aufgerufen. Es gibt mehrere Umstände, die eine Warnung hervorrufen können; allerdings haben sie alle mit der DTD und der Gültigkeit eines Dokuments zu tun, und ich werde sie im nächsten Kapitel SAX für Fortgeschrittene ansprechen. Im Moment müssen Sie nur eine einfache Methode definieren, die die Zeilennummer, die URI und die Warnmeldung ausgibt, wenn eine Warnung auftritt. Da ich (für Demonstrationszwecke) erreichen möchte, daß jede Warnung das Parsing anhält, löst dieser Code eine SAXException aus und beendet auf diese Weise elegant die umgebende Anwendung, so daß alle verwendeten Ressourcen freigegeben werden:

    public void warning(SAXParseException exception)
        throws SAXException {
            
        System.out.println("**Parsing-Warnung**\n" +
                           "  Zeile:   " + 
                              exception.getLineNumber(  ) + "\n" +
                           "  URI:     " + 
                              exception.getSystemId(  ) + "\n" +
                           "  Meldung: " + 
                              exception.getMessage(  ));        
        throw new SAXException("Warnung aufgetreten");
    }

Nicht-kritische Fehler

Während des Parsings auftretende Fehler, nach denen eine Wiederaufnahme des Parsings möglich ist, die aber eine Verletzung eines Teils der XML-Spezifikation darstellen, werden als nicht-kritische Fehler betrachtet. Ein Fehlerhandler sollte diese immer mindestens protokollieren, da sie üblicherweise so schwerwiegend sind, daß der Anwender oder der Administrator der Anwendung informiert werden sollte, wenn nicht sogar so kritisch, daß sie die Beendigung des Parsings verursachen. Wie Warnungen haben auch die meisten nicht-kritischen Fehler mit der Validierung zu tun und werden im nächsten Kapitel SAX für Fortgeschrittene detaillierter behandelt. Ebenfalls wie bei den Warnungen gibt der Fehlerhandle in diesem Beispiel einfach die Information aus, die der Callback-Methode übergeben wurde, und beendet den Parsing-Prozeß:

    public void error(SAXParseException exception)
        throws SAXException {
        
        System.out.println("**Parsing-Fehler**\n" +
                           "  Zeile:   " + 
                              exception.getLineNumber(  ) + "\n" +
                           "  URI:     " + 
                              exception.getSystemId(  ) + "\n" +
                           "  Meldung: " + 
                              exception.getMessage(  ));
        throw new SAXException("Fehler aufgetreten");
    }

Kritische Fehler

Kritische Fehler sind diejenigen, die es erforderlich machen, daß der Parser angehalten wird. Diese Fehler haben üblicherweise damit zu tun, daß ein Dokument nicht wohlgeformt ist, und machen ein weiteres Parsing entweder zu einer völligen Zeitverschwendung oder sogar technisch unmöglich. Ein Fehlerhandler sollte so gut wie immer den Anwender oder Anwendungsadministrator informieren, wenn ein kritischer Fehler auftritt; ohne Eingreifen können diese Fehler zum gräßlichen Ende einer Anwendung führen. Für das Beispiel ahme ich einfach das Verhalten der beiden anderen Callback-Methoden nach, beende also das Parsing und schreibe eine Fehlermeldung auf den Bildschirm, wenn ein kritischer Fehler auftritt:

    public void fatalError(SAXParseException exception)
        throws SAXException {
    
        System.out.println("**Kritischer Parsing-Fehler**\n" +
                           "  Zeile:   " + 
                              exception.getLineNumber(  ) + "\n" +
                           "  URI:     " + 
                              exception.getSystemId(  ) + "\n" +
                           "  Meldung: " + 
                              exception.getMessage(  ));        
        throw new SAXException("Kritischer Fehler aufgetreten");
    }

Nachdem nun der dritte Fehlerhandler programmiert ist, sollten Sie in der Lage sein, die Beispielquelldatei erfolgreich zu kompilieren und wiederum mit dem XML-Dokument zu starten. Ihre Ausgabe sollte sich von der vorherigen nicht unterscheiden, da sich in dem XML keine auszugebenden Fehler befinden. Als nächstes zeige ich Ihnen, wie Sie einige dieser Fehler provozieren können (zu Testzwecken natürlich!).

Die Daten beschädigen

Da wir nun einige Fehlerhandler haben, lohnt es sich, einige Probleme zu erzeugen, um diese Handler in Aktion zu erleben. Die meisten Warnungen und nicht-kritischen Fehler haben mit Fragen der Dokumentgültigkeit zu tun, die ich im nächsten Kapitel SAX für Fortgeschrittene ansprechen werde (wo das Einschalten der Validierung im Detail behandelt wird). Allerdings gibt es einen nicht-kritischen Fehler, der sich aus einem ungültigen XML-Dokument ergibt und der mit der XML-Version zu tun hat, die ein Dokument angibt. Um diesen Fehler zu sehen, führen Sie die folgende Änderung an der ersten Zeile des XML-Inhaltsverzeichnis-Beispiels durch:

<?xml version="1.2"?>

Starten Sie nun das Java-SAX-Viewer-Programm mit dem geänderten XML-Dokument. Ihre Ausgabe sollte so ähnlich aussehen wie die hier gezeigte:

C:\javaxml2\build>java javaxml2.SAXTreeViewer ..\ch03\xml\contents.xml
**Parsing-Fehler**
  Zeile:   1
  URI:     file:///C:/javaxml2/ch03/xml/contents.xml
  Meldung: XML version "1.2" is not supported.
org.xml.sax.SAXException: Fehler aufgetreten.

Wenn ein XML-Parser mit einem Dokument arbeitet, das eine höhere XML-Version angibt als diejenige, die der Parser unterstützt, wird in Übereinstimmung mit der XML 1.0-Spezifikation ein nicht-kritscher Fehler angezeigt. Dies teilt einer Anwendung mit, daß neuere Features, die womöglich vom Dokument verwendet werden, vielleicht für den Parser und die von ihm unterstützte Version nicht verfügbar sind. Da das Parsing danach weitergeht, ist dies ein nicht-kritischer Fehler. Da er jedoch größere Auswirkungen auf das Dokument kennzeichnet (wie die Verwendung neuer Syntax, die möglicherweise Folgefehler erzeugt), wird er als wichtiger als eine Warnung betrachtet. Deshalb wird die Methode error( ) aufgerufen, und in dem Beispielprogramm löst diese die Fehlermeldung und das Anhalten des Parsings aus.

Alle anderen relevanten Warnungen und nicht-kritischen Fehler werden im nächsten Kapitel SAX für Fortgeschrittene besprochen; es gibt aber noch immer eine Menge kritischer Fehler, die ein nicht-validiertes XML-Dokument aufweisen kann. Diese haben damit zu tun, daß ein XML-Dokument nicht wohlgeformt ist. XML-Parser enthalten keine eingebaute Logik, die versucht, Reparaturen an fehlerhaftem XML vorzunehmen, so daß ein Fehler in der Syntax zum Anhalten des Parsing-Prozesses führt. Der einfachste Weg, dies zu demonstrieren, besteht darin, Probleme in Ihr XML-Dokument einzubauen. Setzen Sie die XML-Deklaration auf die Angabe der Version 1.0 zurück, und führen Sie die folgende Änderung am XML-Dokument durch:

<?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"
>
  <!-- Beachten Sie den fehlenden End-Slash beim schließenden titel-Element -->
  <titel ora:series="Java">Java und XML<titel>

  <!-- Rest des Inhalts -->
</buch>

Das ist kein wohlgeformtes Dokument mehr. Um den kritischen Fehler zu sehen, den das Parsing dieses Dokuments erzeugt, starten Sie das Programm SAXVTreeViewer mit dieser modifizierten Datei, um die folgende Ausgabe zu erhalten:

C:\javaxml2\build>java javaxml2.SAXTreeViewer ..\ch03\xml\contents.xml
**Kritischer Parsing-Fehler**
  Zeile:   23
  URI:     file:///C:/javaxml2/ch03/xml/contents.xml
  Meldung: The element type "titel" must be terminated by the matching 
           end-tag "</titel>".
org.xml.sax.SAXException: Kritischer Fehler aufgetreten

Der Parser meldet ein falsches Ende des Elements titel. Dieser kritische Fehler verhält sich genau wie erwartet; das Parsing konnte nach diesem Fehler nicht fortgesetzt werden. Dieser Fehlerhandler zeigt Ihnen ein erstes Beispiel dafür, was beim Parsing-Prozeß schiefgehen kann und wie Sie mit diesen Ereignissen umgehen können. Im Kapitel SAX für Fortgeschrittene werde ich den Fehlerhandler und seine Methoden erneut ansprechen und die Probleme betrachten, die von einem validierenden Parser gemeldet werden können.

Vorsicht Falle!

Bevor wir diese Einführung über das Parsing von XML-Dokumenten mit SAX beenden, wollen wir Sie noch auf einige Stolperfallen aufmerksam machen, auf die Sie achten sollten. Das Wissen um diese Fallen wird Ihnen helfen, häufige Programmierfehler zu vermeiden, wenn Sie SAX verwenden, und ich werde noch einige weitere Fallen in den entsprechenden Abschnitten über andere APIs besprechen.

Mein Parser unterstützt kein SAX 2.0

Denjenigen unter Ihnen, die gezwungen sind, einen SAX 1.0-Parser (vielleicht in einer bereits existierenden Anwendung) zu verwenden, sei gesagt: Verzweifeln Sie nicht. Erstens haben Sie immer die Möglichkeit, den Parser zu wechseln; es ist ein wichtiger Teil der Verantwortung eines Parsers, mit der SAX-Version Schritt zu halten, und wenn der Hersteller Ihres Parsers das nicht tut, müssen Sie womöglich auch andere Schwierigkeiten mit ihm klären. Es gibt jedoch sicherlich Fälle, in denen Sie gezwungen sind, aufgrund von übernommenem Code oder Anwendungen einen bestimmten Parser zu verwenden; aber selbst in solchen Situationen werden Sie nicht im Regen stehengelassen.

SAX 2.0 enthält eine Helferklasse, org.xml.sax.helpers.ParserAdapter, die tatsächlich dafür sorgen kann, daß eine SAX 1.0-Parser-Implementierung sich wie eine SAX 2.0-XMLReader-Implementierung verhält. Diese praktische Klasse nimmt eine 1.0-Parser-Implementierung als Argument entgegen und kann dann statt dieser Implementierung verwendet werden. Sie ermöglicht das Setzen eines ContentHandlers (der ein SAX 2.0-Konstrukt ist) und behandelt alle Namensraum-Callbacks korrekt (ebenfalls ein Feature von SAX 2.0). Der einzige Funktionalitätsverlust, den Sie bemerken werden, besteht darin, daß übergangene Entities nicht angezeigt werden, da diese Fähigkeit in einer 1.0-Implementierung überhaupt nicht zur Verfügung stand und nicht von einer 2.0-Adapterklasse emuliert werden kann. Beispiel 3-3 zeigt dieses Verhalten in Aktion.

Beispiel 3-3: SAX 1.0 mit SAX 2.0-Codekonstrukten verwenden

try {
    // Einen Parser bei SAX registrieren
    Parser parser = 
        ParserFactory.makeParser(
            "org.apache.xerces.parsers.SAXParser");
            
    ParserAdapter myParser = new ParserAdapter(parser);
                                        
    // Den Dokument-Handler registrieren
    myParser.setContentHandler(contentHandler);
    
    // Den Fehlerhandler registrieren
    myParser.setErrorHandler(errHandler);            
        
    // Parsing des Dokuments      
    myParser.parse(uri);
    
} catch (ClassNotFoundException e) {
    System.out.println(
        "Die Parser-Klasse wurde nicht gefunden.");
} catch (IllegalAccessException e) {
    System.out.println(
        "Ungenügende Rechte, um die Parser-Klasse zu laden.");
} catch (InstantiationException e) {
    System.out.println(
        "Die Parser-Klasse konnte nicht instantiiert werden.");
} catch (ClassCastException e) {
    System.out.println(
        "Der Parser implementiert org.xml.sax.Parser nicht");
} catch (IOException e) {
    System.out.println("Fehler beim Lesen der URI: " + e.getMessage(  ));
} catch (SAXException e) {
    System.out.println("Fehler beim Parsing: " + e.getMessage(  ));
}

Wenn SAX für Sie neu ist und dieses Beispiel für Sie keinen großen Sinn ergibt, machen Sie sich keine Sorgen; Sie verwenden die neueste und beste Version von SAX (2.0) und werden wahrscheinlich niemals Code wie diesen schreiben müssen. Dieser Code ist nur in solchen Fällen nützlich, in denen ein 1.0-Parser benutzt werden muß.

Der SAX XMLReader: Mehrfach verwendbar, aber nicht reentrant

Eines der angenehmsten Merkmale von Java ist die einfache Wiederverwendbarkeit von Objekten und der Speicherplatzvorteil einer solchen Wiederverwendung. SAX-Parser machen da keine Ausnahme. Nachdem eine XMLReader-Instanz erzeugt wurde, kann sie immer wieder für das Parsing mehrerer oder sogar Hunderter von XML-Dokumenten verwendet werden. Unterschiedliche Dokumente oder InputSources können einem Reader nacheinander übergeben werden, wodurch es möglich wird, ihn für eine Reihe unterschiedlicher Aufgaben zu verwenden. Allerdings sind Reader nicht »reentrant«. Das bedeutet, daß ein Reader nach dem Start des Parsing-Prozesses nicht verwendet werden kann, bis das gewünschte Parsing des Dokuments oder der Eingabe beendet ist. Mit anderen Worten: Der Wiedereinstieg in diesen Prozeß ist nicht möglich.

Alle, die dazu neigen, rekursive Methoden zu programmieren, sollten hier aufpassen! Das erste Mal, wenn Sie versuchen, einen Reader zu verwenden, der gerade dabei ist, ein anderes Dokument zu verarbeiten, erhalten Sie eine ziemlich unangenehme SAXException, und sämtliches Parsing wird angehalten. Was lernen wir daraus? Führen Sie das Parsing von Dokumenten nacheinander durch, oder nehmen Sie in Kauf, daß mehrere Reader-Instanzen erzeugt werden.

Der Locator am falschen Ort

Ein weiteres gefährliches, aber harmlos erscheinendes Feature von SAX-Ereignissen ist die Locator-Instanz, die durch die Callback-Methode setDocumentLocator( ) verfügbar gemacht wird. Diese teilt der Anwendung den Ursprung eines SAX-Ereignisses mit und dient dazu, Entscheidungen über den Fortschritt des Parsings und über die Reaktion auf Ereignisse zu treffen. Allerdings gilt dieser Ursprungspunkt nur für die Lebensdauer der ContentHandler-Instanz; nachdem das Parsing beendet ist, ist der Locator nicht mehr gültig, auch nicht zu dem Zeitpunkt, wenn das nächste Parsing beginnt. Eine Falle, in die viele XML-Neulinge tappen, ist es, eine Referenz auf das Locator-Objekt außerhalb der Callback-Methode abzulegen:

public void setDocumentLocator(Locator locator) {
    // Den Locator in einer Klasse außerhalb des ContentHandlers ablegen
    myOtherClass.setLocator(locator);
}
...

public myOtherClassMethod(  ) {
    // Versuchen, dies außerhalb des ContentHandlers zu verwenden
    System.out.println(locator.getLineNumber(  ));
}

Das ist überhaupt keine gute Idee, da diese Locator-Instanz bedeutungslos wird, sobald der Geltungsbereich der ContentHandler-Implementierung verlassen wird. Oftmals führt die Verwendung der Objektvariable, die aus dieser Operation entsteht, nicht nur dazu, daß fehlerhafte Informationen an eine Anwendung übermittelt werden, sondern auch dazu, daß im laufenden Code Exceptions erzeugt werden. Mit anderen Worten: Verwenden Sie dieses Objekt lokal und nicht global. In der Implementierung der Klasse JTreeContentHandler wird die übergebene Locator-Instanz in einer Objektvariable abgelegt. Sie könnte dann (zum Beispiel) korrekt verwendet werden, um Ihnen die Zeilennummer jedes gefundenen Elements anzugeben:

public void startElement(String namespaceURI, String localName,
                         String rawName, Attributes atts)
    throws SAXException {
    
    DefaultMutableTreeNode element =
        new DefaultMutableTreeNode("Element: " + localName +
            " in Zeile " + locator.getLineNumber());
    current.add(element);
    // Rest des existierenden Codes...
}

Den Daten zuvorkommen

Das Callback characters( ) nimmt ein Array von Zeichen entgegen und außerdem Parameter für Start und Länge, um anzugeben, bei welchem Index gestartet und wie weit in das Array hineingelesen werden soll. Das kann zu einer gewissen Verwirrung führen; ein häufiger Fehler besteht darin, Code wie in diesem Beispiel zu verwenden, um aus dem Zeichen-Array zu lesen:

public void characters(char[] ch, int start, int length)
    throws SAXException {

    for (int i=0; i<ch.length; i++)
        System.out.print(ch[i]);
}

Der Fehler hier besteht darin, vom Anfang bis zum Ende des Zeichen-Arrays zu lesen. Dieser natürliche Zustand des »In-der-Falle-sitzen« entsteht, wenn man sich viele Jahre lang durch Arrays bewegt, egal ob in Java, C oder einer anderen Sprache. Allerdings kann dies im Falle von SAX leicht zu einem Fehler führen. SAX-Parser erfordern es, daß die Werte für den Start und die Länge auf das Zeichen-Array angewendet werden. Diese sollten von jedem Schleifenkonstrukt benutzt werden, um aus dem Array zu lesen. Dies ermöglicht es, daß eine Manipulation von Textdaten auf niedrigerer Ebene stattfinden kann, um die Parser-Performance zu optimieren, etwa, um Daten jenseits der aktuellen Position vorauszulesen oder um Arrays wiederzuverwenden. All das ist zulässiges Verhalten in SAX, da davon ausgegangen wird, daß eine umgebende Anwendung nicht versuchen wird, über den Längenparameter hinaus zu lesen, der dem Callback übergeben wurde.

Fehler wie der im Beispiel gezeigte können dazu führen, daß überflüssige Daten auf dem Bildschirm ausgegeben oder in der umgebenden Anwendung verwendet werden, und sie sind für Anwendungen fast immer problematisch. Das Schleifenkonstrukt sieht völlig normal aus und läßt sich reibungslos kompilieren, deshalb kann es ein ziemlich kniffliges Problem sein, diesen Zustand des »In-der-Falle-sitzen« in den Griff zu bekommen. Statt dessen können Sie diese Daten in einen String konvertieren, diesen verwenden und brauchen sich keine Sorgen mehr zu machen:

public void characters(char[] ch, int start, int length)
    throws SAXException {

    String data = new String(ch, start, length);
    // Den String verwenden
}

Und was kommt jetzt?

Nachdem Sie nun einen ersten Eindruck von SAX erhalten haben, werden Sie im folgenden Kapitel SAX für Fortgeschrittene einige fortgeschrittene Bestandteile der API kennenlernen. Dazu gehören das Einstellen von Eigenschaften und Features, die Verwendung von Validierung und Namensraum-Verarbeitung und die Interfaces EntityResolver und DTDHandler. Zusätzlich betrachten Sie einige viel seltener verwendete (und doch nützliche) Features der Simple API for XML, wie Filter und das Package org.xml.sax.ext. Dies sollte denjenigen unter Ihnen, die SAX in Anwendungen einsetzen, helfen, die Entwickler um Sie herum zu überholen, sogar zu überfliegen. Das ist immer gut. Lassen Sie den Editor laufen, und blättern Sie um.

1)
Machen Sie sich keine Sorgen, wenn Sie nicht mit den verwendeten Swing-Konzepten vertraut sind; um ehrlich zu sein, mußte ich selbst die meisten nachschlagen! Wenn Sie eine gute Referenz über Swing benötigen, besorgen Sie sich ein Exemplar von Java Swing von Robert Eckstein, Marc Loy und Dave Wood (O’Reilly & Associates).
2)
Natürlich ist diese Einschränkung gleichzeitig ein Vorteil; befindet sich das gesamte Dokument im Speicher, dann ist beliebiger Zugriff möglich. Mit anderen Worten: Es ist ein zweischneidiges Schwert, das ich im Kapitel DOM genauer besprechen werde.
3)
Ich gehe davon aus, daß in Ihrem Parser die Validierung standardmäßig abgeschaltet ist, selbst wenn Sie nicht Apache Xerces verwenden. Wenn Sie andere Ergebnisse erhalten als die in diesem Kapitel gezeigten, sollten Sie Ihre Dokumentation konsultieren und nachsehen, ob die Validierung eingeschaltet ist. Falls dem so ist, werfen Sie einen Blick ins Kapitel SAX für Fortgeschrittene, um zu sehen, wie sie abgeschaltet wird.