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

JDOM für Fortgeschrittene

Dieses Kapitel fährt mit dem Thema JDOM fort und führt einige fortgeschrittenere Konzepte ein. Im vorigen Kapitel haben Sie gesehen, wie Sie mit Hilfe von JDOM XML lesen und schreiben können, und haben auch einen guten Einblick erhalten, welche Klassen in der JDOM-Distribution zur Verfügung stehen. In diesem Kapitel steige ich noch ein wenig tiefer ein, um zu erläutern, wie alles funktioniert. Sie werden einige Klassen zu Gesicht bekommen, die JDOM verwendet, die aber nicht häufig in Operationen zum Einsatz kommen, und Sie werden anfangen zu verstehen, wie JDOM zusammengesetzt ist.

Nachdem Sie dieses grundsätzliche Verständnis erworben haben, werde ich damit fortfahren, Ihnen zu zeigen, wie JDOM Factories und Ihre eigenen, selbstdefinierten JDOM-Implementierungsklassen einsetzen kann, wenn auch auf ganz andere Art und Weise als DOM. Dies wird Sie mitten in ein ziemlich fortgeschrittenes Beispiel hineinführen, in dem Wrapper und Decorator verwendet werden, weitere Hilfsmittel, um Funktionalität zu den JDOM-Kernklassen hinzuzufügen, ohne eine Interface-basierte API zu benötigen.

Nützliche JDOM-Interna

Das erste Thema, das ich behandle, ist die Architektur von JDOM. Im Kapitel JDOM habe ich Ihnen ein einfaches UML-basiertes Modell der JDOM-Kernklassen gezeigt. Wenn Sie sich dies jedoch näher anschauen, gibt es wahrscheinlich einige unter diesen Klassen, mit denen Sie noch nicht gearbeitet oder die Sie nicht erwartet haben. Ich werde diese speziellen Bestandteile in diesem Abschnitt behandeln und Ihnen zeigen, wie Sie mit JDOM die feineren Details bearbeiten können.

JDOM Beta 7 wurde buchstäblich einige Tage vor dem Schreiben dieses Kapitels veröffentlicht. In dieser Version war die Klasse Text vorgesehen, wurde dann aber noch nicht in die JDOM-Interna integriert. Allerdings vollzieht sich dieser Prozeß sehr schnell, sehr wahrscheinlich noch, bevor Sie dieses Buch in den Händen halten. Selbst wenn das nicht der Fall sein sollte, wird die Klasse bald darauf integriert werden, und dann werden die hier besprochenen Fragen ein Thema sein. Wenn Sie Probleme mit den Codeschnipseln in diesem Abschnitt haben, überprüfen Sie die Version von JDOM, die Sie benutzen, und versuchen Sie immer, die neueste zu bekommen.

Es wird Sie vielleicht ein wenig überraschen, daß es in JDOM eine Klasse Text gibt. Wenn Sie das vorige Kapitel gelesen haben, dann haben Sie wahrscheinlich verstanden, daß ein großer Unterschied zwischen DOM und JDOM darin besteht, daß JDOM den Textinhalt eines Elements (zumindest scheinbar) direkt verfügbar macht, während Sie in DOM zunächst den Text-Kindknoten holen und dann dessen Wert extrahieren müssen. Tatsächlich jedoch baut JDOM zeichenbasierten Inhalt von der Architektur her ziemlich ähnlich auf wie DOM; jede Zeicheninhaltseinheit wird in einer JDOM-Text-Instanz abgelegt. Wenn Sie jedoch die Methode getText( ) (oder getTextTrim( ) oder getTextNormalize( )) einer JDOM-Element-Instanz aufrufen, gibt die Instanz automatisch den Wert (bzw. die Werte) aus ihren Text-Kindknoten zurück:

// Textinhalt erhalten
String textualContent = element.getText(  );

// Textinhalt erhalten, bei dem umgebender Whitespace entfernt wurde
String trimmedContent = element.getText().trim(  );
// oder...
String trimmedContent = element.getTextTrim(  );

// Textinhalt erhalten, normalisiert (sämtlicher innere Whitespace wird auf genau
// ein Leerzeichen reduziert. Zum Beispiel wird "  das   wäre  " zu "das wäre"
String normalizedContent = element.getTextNormalize(  );

Im Ergebnis sieht das üblicherweise so aus, als würde wirklich keine Text-Klasse verwendet. Die gleiche Methodik kommt zum Tragen, wenn setText( ) für ein Element aufgerufen wird; der Text wird als Inhalt einer neuen Text-Instanz erzeugt, und diese neue Instanz wird als Kind dieses Elements hinzugefügt. Das logische Prinzip ist wiederum, daß es ein so häufiger Vorgang ist, den Textinhalt eines XML-Elements zu lesen und zu schreiben, daß dies so einfach und so schnell wie möglich gehen sollte.

Gleichzeitig macht ein strenges Baummodell die Navigation durch die Inhalte sehr leicht, wie ich bereits in früheren Kapiteln ausgeführt habe; instanceof und die Rekursion werden zu einfachen Lösungen für das Durchqueren von Bäumen. Insofern erleichtert eine explizite Text-Klasse, die als Kind (bzw. als Kinder) von Element-Instanzen verfügbar ist, diese Aufgabe erheblich. Des weiteren ermöglicht die Klasse Text Erweiterungen, während reine java.lang.String-Klassen nicht erweiterbar sind.

Aus all diesen Gründen (und aus einigen anderen, in die Sie sich in den jdom-interest-Mailinglisten vertiefen können) wird die Klasse Text zur Zeit zu JDOM hinzugefügt. Wenn sie auch nicht so offensichtlich zu Tage tritt wie in anderen APIs, steht sie für solche Fälle wie die erwähnte Iteration zur Verfügung. Um dies bequemer zu gestalten, erhalten Sie sämtlichen Inhalt innerhalb einer Element-Instanz, wenn Sie deren Methode getContent() aufrufen. Dazu gehören Bereiche mit Objekten der Klassen Comment, ProcessingInstruction, EntityRef und CDATA, aber auch Textinhalte. In diesem Fall werden die Textinhalte als eine oder mehrere Text-Instanzen zurückgegeben statt unmittelbar als Strings, was eine Verarbeitung wie diese ermöglicht:

public void processElement(Element element) {
    List mixedContent = element.getContent(  );
    for (Iterator i = mixedContent.iterator(); i.hasNext(  ); ) {
        Object o = i.next(  );
        if (o instanceof Text) {
            processText((Text)o);
        } else if (o instanceof CDATA) {
            processCDATA((CDATA)o);
        } else if (o instanceof Comment) {
            processComment((Comment)o);
        } else if (o instanceof ProcessingInstruction) {
            processProcessingInstruction((ProcessingInstruction)o);
        } else if (o instanceof EntityRef) {
            processEntityRef((EntityRef)o);
        } else if (o instanceof Element) {
            processElement((Element)o);
        }
    }
}

public void processComment(Comment comment) {
    // Etwas mit Kommentaren anstellen
}

public void processProcessingInstruction(ProcessingInstruction pi) {
    // Etwas mit PIs anstellen
}

public void processEntityRef(EntityRef entityRef) {
    // Etwas mit Entity-Referenzen anstellen
}

public void processText(Text text) {
    // Etwas mit Text anstellen
}

public void processCDATA(CDATA cdata) {
    // Etwas mit CDATA anstellen
}


Dies setzt die ziemlich leichte rekursive Verarbeitung eines JDOM-Baums in Gang. Sie könnten diese einfach mit folgendem Code starten:

// Ein JDOM-Document durch einen Builder erhalten
Document doc = builder.build(input);

// Rekursion starten
processElement(doc.getRootElement(  ));

Sie würden Comment- und ProcessingInstruction-Instanzen wohl eher auf der Dokumentebene behandeln, aber grundsätzlich funktioniert es so. Sie können sich entscheiden, die Klasse Text zu verwenden, wenn es einen Sinn hat, und brauchen sich ansonsten keine Gedanken über sie zu machen.

Als nächstes haben wir die Klasse EntityRef auf der Liste der JDOM-Interna, eine weitere Klasse, die Sie normalerweise nicht zu benutzen brauchen, die Sie aber kennen sollten, weil sie bei speziellen Programmierbedürfnissen helfen kann. Diese Klasse stellt eine XML-Entity-Referenz in JDOM dar, wie etwa die Entity-Referenz OReillyCopyright aus dem Dokument contents.xml, das ich schon mehrfach in Beispielen verwendet habe:

<ora:copyright>&OReillyCopyright;</ora:copyright>

Diese Klasse ermöglicht das Einstellen und Zurückholen eines Namens, einer Public-ID und einer System-ID, genau wie beim Definieren der Referenz in einer XML-DTD oder einem Schema. Sie kann an einer beliebigen Stelle in einem JDOM-Inhaltsbaum auftreten, genau wie die Knoten Element und Text. Aber genau wie Text-Knoten ist auch die Klasse EntityRef im Normalfall ein wenig hinderlich. Wenn zum Beispiel das Dokument contents.xml in JDOM modelliert wird, werden Sie üblicherweise mehr Interesse am Textwert der Referenz (ihrem aufgelösten Inhalt) haben als an der Referenz selbst.

Mit anderen Worten: Wenn Sie getContent( ) auf das Element copyright in einem JDOM-Baum anwenden, würden Sie gern »Copyright O’Reilly, 2000« erhalten – oder einen anderen Textinhalt, auf den die Entity-Referenz eben verweist. Dies ist (wiederum in den meisten Fällen) erheblich nützlicher, als die Anzeige zu erhalten, daß es keinen Inhalt gibt (ein leerer String), und dann die Existenz einer EntityRef überprüfen zu müssen. Aus diesem Grund werden standardmäßig alle Entity-Referenzen aufgelöst, wenn die JDOM-Builder (SAXBuilder und DOMBuilder) zur Erzeugung von JDOM aus existierenden XML-Dokumenten verwendet werden. Sie werden in gewöhnlichen Fällen selten EntityRefs zu Gesicht bekommen, weil Sie sich gar nicht mit Ihnen beschäftigen müssen. Wenn Sie allerdings meinen, daß Sie die Entity-Referenzen unaufgelöst und durch EntityRefs dargestellt benötigen, dann können Sie die Methode setExpandEntities( ) der Builder-Klassen benutzen:

// Einen neuen Builder erzeugen
SAXBuilder builder = new SAXBuilder(  );

// Entity-Referenzen nicht auflösen (Standard ist, sie aufzulösen)
builder.setExpandEnitites(false);

// Den Baum mit EntityRef-Objekten bauen (natürlich nur, wenn nötig)
Document doc = builder.build(inputStream);

In diesem Fall könnten sich EntityRef-Instanzen in Ihrem Baum befinden (zum Beispiel, wenn Sie das Dokument contents.xml verwendet haben). Und Sie können EntityRefs immer direkt erzeugen und im JDOM-Baum plazieren:

// Eine neue Entity-Referenz erzeugen
EntityRef ref = new EntityRef("TrueNorthGuitarsTagline");
ref.setSystemID("tngTagline.xml");

// In den Baum einfügen
tagLineElement.addContent(ref);

Wenn dieser Baum serialisiert wird, erhalten Sie XML wie dieses hier:

<gitarre>
  <tagLine>&TrueNorthGuitarsTagline;</tagLine>
</gitarre>

Und wenn das Dokument mit Hilfe eines Builders neu eingelesen wird, hängt das resultierende JDOM-Document von dem Flag expandEntities ab. Wenn es den Wert false hat, erhalten Sie die ursprüngliche EntityRef wieder zurück, und zwar mit dem richtigen Namen und der korrekten System-ID. Ist sein Wert dagegen true (der Standardfall), dann erhalten Sie den aufgelösten Inhalt. Eine zweite Serialisierung könnte das folgende Ergebnis haben:

<gitarre>
  <tagLine>zwei Hände, ein Herz</tagLine>
</gitarre>

Auch wenn das aussieht wie viel Lärm um eine einfache Angelegenheit, ist es wichtig zu verstehen, daß das ein- und ausgebende XML, mit dem Sie arbeiten, sich durch das Auflösen oder Nicht-Auflösen von Entities ändern kann. Behalten Sie stets im Auge, auf welchen Wert die Builder-Flags eingestellt sind und welchen Wert Sie benötigen, um den gewünschten JDOM-Baum oder die passende XML-Ausgabe zu erhalten.

Ich möchte kurz auf eine weitere JDOM-Klasse eingehen, die Klasse Namespace. Diese Klasse fungiert in der JDOM-Architektur sowohl als Instanzvariable als auch als Factory. Wenn Sie einen neuen Namensraum erzeugen müssen, sei es für ein Element oder für die Suche, sollten Sie die statischen getNamespace( )-Methoden dieser Klasse verwenden:

// Namensraum mit Präfix erzeugen
Namespace schemaNamespace = 
    Namespace.getNamespace("xsd", "http://www.w3.org/XMLSchema/2001");

// Namensraum ohne Präfix erzeugen
Namespace javaxml2Namespace =
    Namespace.getNamespace("http://www.oreilly.com/javaxml2");

Wie Sie sehen können, gibt es eine Version zum Erzeugen von Namensräumen mit Präfixen und eine zum Erzeugen von Namensräumen ohne Präfixe (Standard-Namensräume). Jede der beiden Versionen kann verwendet und anschließend an die verschiedenen JDOM-Methoden übergeben werden:

// Element mit Namensraum erzeugen
Element schema = new Element("schema", schemaNamespace);

// Kindobjekte im angegebenen Namensraum suchen
List chapterElements = contentElement.getChildren("kapitel", javaxml2Namespace);

// Einen neuen Namensraum für dieses Element festlegen
catalogElement.addNamespaceDeclaration(
    Namespace.getNamespace("tng", "http://www.truenorthguitars.com"));

Diese Methoden erklären sich beinahe von selbst. Auch wenn die XML-Serialisierung durch die unterschiedlichen Outputter (SAXOutputter, DOMOutputter und XMLOutputter) durchgeführt wird, werden die Namensraumdeklarationen automatisch beachtet und zum erzeugten XML-Code hinzugefügt.

Eine letzte Anmerkung: In JDOM basiert der Namensraumvergleich einzig und allein auf der URI. Mit anderen Worten: Zwei Namespace-Objekte sind identisch, wenn ihre URIs gleich sind, ohne Rücksicht auf das Präfix. Dies stimmt genau mit dem Sinn und mit den Buchstaben der XML-Namensraum-Spezifikation überein, die angibt, daß sich zwei Elemente im selben Namensraum befinden, wenn ihre URIs ungeachtet des Präfixes übereinstimmen. Schauen Sie sich das folgende XML-Dokumentfragment an:

<gitarre xmlns="http://www.truenorthguitars.com">
  <ni:eigentuemer xmlns:ni="http://www.newInstance.com">
    <ni:name>Brett McLaughlin</ni:name>
    <tng:modell xmlns:tng="http://www.truenorthguitars.com>Model 1</tng:modell>
    <rueckseitenHolz>Madagascar Rosewood</rueckseitenHolz>
  </ni:eigentuemer>
</gitarre>

Obwohl die Elemente gitarre, modell und rueckseitenHolz unterschiedliche Präfixe haben, befinden sie sich alle im selben Namensraum. Dies trifft genauso für das JDOM-Namespace-Modell zu. Tatsächlich wird die Methode equals( ) der Klasse Namespace die Gleichheit nur aufgrund der URIs und ohne Beachtung der Präfixe angeben.

Ich habe hier nur drei der Klassen von JDOM gestreift, aber das sind genau die Klassen, die tückisch sind und über die die meisten Fragen gestellt werden. Der Rest der API wurde im vorigen Kapitel behandelt und wird in den nächsten Abschnitten dieses Kapitels näher beleuchtet. Sie sollten nun in der Lage sein, in JDOM mit Textinhalten, Entity-Referenzen und Namensräumen umzugehen. Sie sollten die Hin-und-her-Konvertierung zwischen Strings und Text-Knoten sowie zwischen aufgelöstem Inhalt und EntityRefs mit Leichtigkeit beherrschen, ebenso die Arbeit mit Namensräumen mit unterschiedlichen Präfixen. Wenn Sie all das verstanden haben, sind Sie in der Lage, mit einigen komplexeren Beispielen und Fällen fortzufahren.

JDOM und Factories

Erinnern Sie sich an die Diskussion über JDOM und Factories aus dem vorigen Kapitel. Ich erwähnte bereits, daß Sie in JDOM-Anwendungen (zumindest mit der aktuellen Version) niemals Code wie diesen sehen würden:

// Dieser Code funktioniert nicht!!
JDOMFactory factory = new JDOMFactory(  );
factory.setDocumentClass("javaxml2.BrettsDocumentClass");
factory.setElementClass("javaxml2.BrettsElementClass");

Element rootElement = JDOMFactory.createElement("root");
Document document = JDOMFactory.createDocument(rootElement);

Nun, das bleibt auch weiterhin wahr. Allerdings habe ich bisher einige ziemlich wichtige Aspekte dieser Diskussion unterschlagen und möchte sie hier wieder aufnehmen. Wie ich im Kapitel JDOM erwähnte, ermöglicht die Existenz irgendeiner Art von Factories eine größere Flexibilität bezüglich der Art und Weise, wie Ihr XML in Java aufgebaut wird. Schauen Sie sich die einfache Unterklasse von JDOMs Element-Klasse an, die in Beispiel 8-1 gezeigt wird:

Beispiel 8-1: Eine Unterklasse der JDOM-Klasse Element bilden

package javaxml2;

import org.jdom.Element;
import org.jdom.Namespace;

public class ORAElement extends Element {

    private static final Namespace ORA_NAMESPACE =
        Namespace.getNamespace("ora", "http://www.oreilly.com");

    public ORAElement(String name) {
        super(name, ORA_NAMESPACE);
    }

    public ORAElement(String name, Namespace ns) {
        super(name, ORA_NAMESPACE);
    }

    public ORAElement(String name, String uri) {
        super(name, ORA_NAMESPACE);
    }

    public ORAElement(String name, String prefix, String uri) {
        super(name, ORA_NAMESPACE);
    }
}

Das ist eine so einfache Unterklasse, daß Sie damit zurechtkommen müßten; sie ähnelt in gewisser Hinsicht der Klasse NamespaceFilter aus dem Kapitel SAX für Fortgeschrittene. Sie ignoriert jegliche Namensraum-Information, die dem Element eigentlich übergeben wird (selbst dann, wenn kein Namensraum übergeben wird!), und stellt den Namensraum des Elements auf die Definition durch die URI http://www.oreilly.com und das Präfix ora ein.1 Dies ist ein einfacher Fall, aber er gibt Ihnen eine Vorstellung von den Möglichkeiten und dient als gutes Beispiel für diesen Abschnitt.

Nachdem Sie eine eigene Unterklasse definiert haben, besteht der nächste Schritt darin, sie tatsächlich zu nutzen. Wie ich bereits erwähnte, betrachtet JDOM die Notwendigkeit, alle Objekte mit Hilfe von Factories zu erzeugen, als ein wenig zu aufwendig. Die einfache Erzeugung von Elementen funktioniert in JDOM folgendermaßen:

// Ein neues Element erzeugen
Element element = new Element("gitarre");

Mit einer selbstdefinierten Unterklasse bleibt alles genauso einfach:

// Ein neues Element vom Typ ORAElement erzeugen
Element oraElement = new ORAElement("guitar");

Das Element wird wegen der selbstdefinierten Unterklasse in den O’Reilly-Namensraum eingefügt. Darüber hinaus ist diese Methode einleuchtender als die Verwendung einer Factory. Es ist zu jedem Zeitpunkt genau klar, welche Klassen verwendet werden, um Objekte zu erzeugen. Vergleichen Sie das mit dem folgenden Codefragment:

// Ein Element erzeugen: Welcher Typ wird erzeugt?
Element someElement = doc.createElement("gitarre");

Es ist unklar, ob das erzeugte Objekt eine Instanz von Element, von ORAElement oder von etwas völlig anderem ist. Aus diesen Gründen paßt der Ansatz einer selbstdefinierten Klasse gut zu JDOM. Zur Objekterzeugung können Sie Ihre selbstdefinierte Unterklasse einfach direkt instantiieren. Allerdings nimmt das Bedürfnis nach Factories zu, wenn Sie ein Dokument aufbauen:

// Aus einer Eingabequelle aufbauen
SAXBuilder builder = new SAXBuilder(  );
Document doc = builder.build(someInputStream);

Offensichtlich wären Sie hier nicht in der Lage, selbstdefinierte Klassen für den Aufbauprozeß anzugeben. Ich gehe davon aus, daß Sie waghalsig genug sein könnten, die Klasse SAXBuilder (und die zugehörige Klasse org.jdom.input.SAXHandler) zu modifizieren, aber das ist ein wenig lächerlich. Um dies also zu vereinfachen, wurde das Interface JDOMFactory im Package org.jdom.input eingeführt. Dieses Interface definiert Methoden für jeden Typ der Objekterzeugung (siehe Anhang A, in dem Sie den kompletten Satz von Methoden finden). Zum Beispiel existieren vier Methoden zur Elementerzeugung, die mit den vier Konstruktoren der Klasse Element übereinstimmen:

public Element element(String name);
public Element element(String name, Namespace ns);
public Element element(String name, String uri);
public Element element(String name, String prefix, String uri);

Sie werden ähnliche Methoden für Document, Attribute, CDATA und den ganzen Rest finden. Standardmäßig verwendet JDOM die org.jdom.input.DefaultJDOMFactory, die einfach alle JDOM-Kernklassen innerhalb dieser Methoden zurückgibt. Allerdings können Sie einfach Unterklassen dieser Implementierung bilden und Ihre eigenen Factory-Methoden zur Verfügung stellen. Schauen Sie sich Beispiel 8-2 an, das eine eigene Factory definiert:

Beispiel 8-2: Eine selbstdefinierte JDOMFactory-Implementierung

package javaxml2;

import org.jdom.Element;
import org.jdom.Namespace;
import org.jdom.input.DefaultJDOMFactory;

class CustomJDOMFactory extends DefaultJDOMFactory {

    public Element element(String name) {
        return new ORAElement(name);
    }

    public Element element(String name, Namespace ns) {
        return new ORAElement(name, ns);
    }

    public Element element(String name, String uri) {
        return new ORAElement(name, uri);
    }

    public Element element(String name, String prefix, String uri) {
        return new ORAElement(name, prefix, uri);
    }
}

Das ist eine einfache Implementierung; sie braucht nicht besonders komplex zu sein. Sie überschreibt alle element( )-Methoden und gibt eine Instanz der selbstdefinierten Unterklasse, ORAElement, zurück statt der JDOM-Standardklasse Element. Auf diese Weise wird jeder Builder, der diese Factory verwendet, in dem erzeugten JDOM-Document-Objekt letzten Endes ORAElement-Instanzen statt der üblichen Element-Instanzen enthalten, die Sie normalerweise vorfinden würden. Jetzt müssen Sie nur noch den Aufbauprozeß über diese selbstdefinierte Factory in Kenntnis setzen.

Nachdem Sie eine gültige Implementierung von JDOMFactory erzeugt haben, informieren Sie Ihre Builder darüber, daß sie diese benutzen sollen, indem Sie die Methode setFactory( ) aufrufen und ihr eine Factory-Instanz übergeben. Diese Methode steht für beide JDOM-Builder der aktuellen Version zur Verfügung, für SAXBuilder und DOMBuilder. Um sie in Aktion zu erleben, testen Sie Beispiel 8-3. Diese einfache Klasse nimmt ein XML-Dokument entgegen und baut es mit Hilfe der Klasse ORAElement und der CustomJDOMFactory aus den Beispielen 8-1 und 8-2 auf. Anschließend schreibt sie das Dokument wieder in eine angegebene Ausgabedatei zurück, so daß Sie die Wirkung der selbstdefinierten Klassen sehen können.

Beispiel 8-3: Aufbau mit selbstdefinierten Klassen und einer selbstdefinierten Factory

package javaxml2;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

import org.jdom.Document;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.input.JDOMFactory;
import org.jdom.output.XMLOutputter;

public class ElementChanger {

    public void change(String inputFilename, String outputFilename)
        throws IOException, JDOMException {

        // Den Builder erzeugen und die Factory einrichten
        SAXBuilder builder = new SAXBuilder(  );
        JDOMFactory factory = new CustomJDOMFactory(  );
        builder.setFactory(factory);
        
        // Das Dokument aufbauen 
        Document doc = builder.build(inputFilename);

        // Das Dokument ausgeben
        XMLOutputter outputter = new XMLOutputter(  ); 
        outputter.output(doc, new FileWriter(new File(outputFilename)));
    }

    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println("Verwendung: javaxml2.ElementChanger " +
                "[XML-Eingabedatei] [XML-Ausgabedatei]");
            return;
        }

        try {
            ElementChanger changer = new ElementChanger(  );
            changer.change(args[0], args[1]);
        } catch (Exception e) {
            e.printStackTrace(  );
        }
    }
}

Ich habe diese Klasse mit der Datei contents.xml gestartet, die ich in den ersten paar Kapiteln verwendet habe:

bmclaugh@GANDALF
$ java javaxml2.ElementChanger contents.xml neuerInhalt.xml

Das Programm hat eine Sekunde lang vor sich hin gearbeitet und hat mir dann ein neues Dokument (neuerInhalt.xml) geliefert. Ein Teil dieses neuen Dokuments wird in Beispiel 8-4 gezeigt.

Beispiel 8-4: Fragment der Ausgabe von contents.xml aus ElementChanger

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

  <!-- Kapitelliste -->
  <ora:contents>
    <ora:kapitel titel="Einleitung" nummer="1">
      <ora:thema name="XML spielt eine wichtige Rolle" />
      <ora:thema name="Was ist wichtig?" />
      <ora:thema name="Was Sie benötigen" />
      <ora:thema name="Und was kommt jetzt?" />
    </ora:kapitel>
    <ora:kapitel titel="Ans Eingemachte" nummer="2">
      <ora:thema name="Die Grundlagen" />
      <ora:thema name="Beschränkungen" />
      <ora:thema name="Transformationen" />
      <ora:thema name="Und mehr..." />
      <ora:thema name="Und was kommt jetzt?" />
    </ora:kapitel>
    <ora:kapitel titel="SAX" nummer="3">
      <ora:thema name="Vorbereitungen" />
      <ora:thema name="SAX-Reader" />
      <ora:thema name="Content-Handler" />
      <ora:thema name="Vorsicht Falle!" />
      <ora:thema name="Und was kommt jetzt?" />
    </ora:kapitel> 
    <ora:kapitel titel="Fortgeschrittenes SAX" nummer="4">
      <ora:thema name="Eigenschaften und Features" />
      <ora:thema name="Weitere Handler" />
      <ora:thema name="Filter und Writer" />
      <ora:thema name="Und noch mehr Handler" />
      <ora:thema name="Vorsicht Falle!" />
      <ora:thema name="Und was kommt jetzt?" />
    </ora:kapitel>
    <!-- Weitere Kapitel -->
</ora:buch>

Alle Elemente befinden sich nun im O’Reilly-Namensraum und sind mit dem Präfix und der URI-Referenz versehen, die in der Klasse ORAElement festgelegt wurden.

Natürlich können Sie dieses Erzeugen von Unterklassen auch auf einen erheblich höheren Komplexitätsgrad führen. Zu den gängigen Beispielen gehört das Hinzufügen bestimmter Attribute oder sogar Kindelemente zu jedem Element, das bearbeitet wird. Viele Entwickler verfügen über existierende Business-Interfaces und definieren eigene JDOM-Klassen, die die JDOM-Kernklassen erweitern und auch diese Business-spezifischen Interfaces implementieren. Andere Entwickler haben »leichtgewichtige« Unterklassen erzeugt, die die Namensraum-Information verwerfen und nur die reinen Grundlagen behalten, was die Dokumente klein hält (wenn auch in manchen Fällen vielleicht nicht XML-konform). Die einzige Begrenzung besteht in Ihren eigenen Ideen für Unterklassen. Denken Sie nur daran, daß Sie Ihre eigene Factory einrichten, bevor Sie Dokumente aufbauen, damit Ihre neue Funktionalität enthalten ist.

Wrapper- und Decorator-Klassen

Eine der am häufigsten formulierten Anfragen zu JDOM bezieht sich auf Interfaces. Viele, viele Benutzer haben Interfaces in JDOM gefordert, aber die Anfrage ist stets verneint worden. Die Gründe sind einfach: Es gibt keinen Satz von Methoden, die alle JDOM-Konstrukte gemeinsam haben. Es besteht eine gewisse Abneigung dagegen, den DOM-Ansatz zu benutzen, der einen Satz gemeinsamer Methoden für die meisten Konstrukte bietet. Zum Beispiel ist getChildren( ) Bestandteil des gängigen DOM-Interfaces org.w3c.dom.Node; es gibt allerdings null zurück, wenn es nicht anwendbar ist, etwa bei einem Text-Knoten.

Der JDOM-Ansatz besteht darin, nur solche Methoden in einem grundlegenden Interface anzubieten, die allen JDOM-Klassen gemeinsam sind; aber es wurden keine Methoden gefunden, die dieser Anforderung genügen. Darüber hinaus gibt es mindestens ebenso viele Bitten, die API zu lassen, wie sie ist, wie es Vorschläge gibt, Interfaces hinzuzufügen.

Allerdings gibt es Muster, die die Anwendung Interface-ähnlicher Funktionalität in JDOM ermöglichen, ohne die API drastisch zu verändern (tatsächlich sogar, ohne sie überhaupt zu ändern!). In diesem Abschnitt möchte ich die effektivsten dieser Muster ansprechen, zu denen die Verwendung von Wrapper- oder Decorator-Klassen gehört. Ich werde in diesem Buch nicht allzu tief auf den Bereich Designmuster eingehen, es genügt zu sagen, daß ein Wrapper oder ein Decorator (ich verwende die beiden Begriffe in diesem Kapitel abwechselnd) sich außerhalb existierender Klassen befindet und nicht innerhalb, wo ein JDOM-Kern-Interface zu finden wäre. Mit anderen Worten: Existierendes Verhalten wird umhüllt. In diesem Abschnitt zeige ich Ihnen, wie Sie mit diesem Muster JDOM (oder eine andere API) nach Belieben anpassen können.

Zu diesem Zeitpunkt sollten Sie in Java und XML schon recht fortgeschritten sein. Aus diesem Grund gehe ich den Beispielcode in diesem Abschnitt mit einer minimalen Menge an Kommentaren durch. Sie sollten ziemlich leicht in der Lage sein dahinterzukommen, was passiert, und ich werde lieber mehr Code als mehr Kommentar liefern.

Für den Anfang habe ich das Interface JDOMNode in Beispiel 8-5 definiert. Dieses Interface definiert ein sehr einfaches Verhalten, das ich allen JDOM-Knoten zur Verfügung stellen möchte, und zwar ohne die Notwendigkeit, Typecasting einzusetzen.

Beispiel 8-5: Ein Knoten-Decorator-Interface

package javaxml2;

import java.util.List;
import java.util.Iterator;

// JDOM-Importe
import org.jdom.Document;

public interface JDOMNode {

    public Object getNode(  );
	 
    public String getNodeName(  );
	
    public JDOMNode getParentNode(  );
	
    public String getQName(  );
	
    public Iterator iterator(  );
	
    public String toString(  );
}

Die einzige Methode, die ein wenig albern erscheint, ist iterator( ); sie wird einen Java-Iterator über die Kindobjekte eines Knotens zurückgeben oder einen Iterator über eine leere Liste, wenn keine Kinder da sind (etwa bei Attributen oder Textknoten). Natürlich hätte ich mich genauso leicht zur Verwendung des DOM-Interfaces org.w3c.dom.Node entschließen können (um Interoperabilität zwischen DOM und JDOM auf Klassenebene zu erreichen), oder ich hätte ein anderes Interface benutzen können, das speziell meinen Business-Bedürfnissen entspricht. Es gibt keine Grenze für dieses Kern-Interface.

Der nächste, interessantere Schritt besteht darin, Implementierungen dieses Interfaces bereitzustellen, die existierende JDOM-Konstrukte umhüllen. Diese bieten eine Umhüllung der konkreten Klassen, die bereits in JDOM vorhanden sind, und die meisten Methoden des Interfaces JDOMNode werden einfach an das darunterliegende (umhüllte) Objekt weitergereicht. Als erstes haben wir Beispiel 8-6, das ein JDOM-Element umhüllt.

Beispiel 8-6: Decorator für JDOM-Elemente

package javaxml2;

import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;

// JDOM-Importe
import org.jdom.Element;

public class ElementNode implements JDOMNode {
	
    /** das umhüllte Element */
    protected Element decorated;
	
    public ElementNode(Element element) {
        this.decorated = element;
    }
	
    public Object getNode(  ) {
        return decorated;
    }

    public String getNodeName(  ) {
        if (decorated != null) {
            return decorated.getName(  );
        }
        return "";
    }

    public JDOMNode getParentNode(  ) {
        if (decorated.getParent(  ) != null) {
            return new ElementNode(decorated.getParent(  ));
        }
        return null;
    }
	
    public String getQName(  ) {
        if (decorated.getNamespacePrefix(  ).equals("")) { 
            return decorated.getName(  );
        } else {
            return new StringBuffer(decorated.getNamespacePrefix(  ))
                           .append(":")
                           .append(decorated.getName()).toString(  );
        }
    }
	
    public Iterator iterator(  ) {
        List list = decorated.getAttributes(  );
        ArrayList content = new ArrayList(list);

        // Den Inhalt des Elements in der Liste in die richtige Reihenfolge bringen
        Iterator i = decorated.getMixedContent().iterator(  );
        while (i.hasNext(  )) {
            content.add(i.next(  ));
        }
        return content.iterator(  );
    }

    public String toString(  ) {
        return decorated.toString(  );
    }
}

Hier gibt es nichts allzu Bemerkenswertes, deshalb wollen wir weitermachen. In Beispiel 8-7 habe ich eine ähnliche Klasse definiert, AttributeNode, die ein JDOM-Attribute umhüllt und meine JDOMNode-Kernklasse implementiert. Beachten Sie die verschiedenen No-op-Methoden (»no operation«, keine Operation) für Dinge wie das Erhalten der Kinder des Attributs; diese modellieren recht genau den DOM-Ansatz nach. Behalten Sie wieder im Hinterkopf, daß diese Klassen genauso einfach ein anderes Interface implementieren können (denken Sie in diesem Fall an org.w3c.dom.Attr), ohne Änderungen an der JDOM-Kern-API vornehmen zu müssen.

Beispiel 8-7: Decorator für JDOM-Attribute

package javaxml2;

import java.util.Iterator;
import java.util.Collections;

// JDOM-Importe
import org.jdom.Attribute;

public class AttributeNode implements JDOMNode {

    /** Das umhüllte Attribut */
    protected Attribute decorated;

    public AttributeNode(Attribute attribute) {
        this.decorated = attribute;
    }

    public Object getNode(  ) {
        return decorated;
    }

    public String getNodeName(  ) {
        if (decorated != null) {
            return decorated.getName(  );
        }
        return "";
    }

    public JDOMNode getParentNode(  ) {
        if (decorated.getParent(  ) != null) {
            return new ElementNode(decorated.getParent(  ));
        }
        return null;
    }

    public String getQName(  ) {
        if (decorated.getNamespacePrefix(  ).equals("")) { 
            return decorated.getName(  );
        } else {
            return new StringBuffer(decorated.getNamespacePrefix(  ))
                           .append(":")
                           .append(decorated.getName()).toString(  );
        }
    }

    public Iterator iterator(  ) {
        return Collections.EMPTY_LIST.iterator(  );
    }

    public String toString(  ) {
        return decorated.toString(  );
    }
}

Zu guter Letzt werde ich den Textinhalt von JDOM umhüllen (siehe Beispiel 8-8). Zu dem Zeitpunkt, zu dem ich dies schreibe, war die JDOM-Klasse Text, die ich im ersten Abschnitt dieses Kapitels besprochen habe, noch nicht in ihrer endgültigen Form in den JDOM-Quellcodebaum integriert. Im Ergebnis umhülle ich im Moment einen Java-String in der Klasse TextNode. Wenn der Text-Knoten endgültig verfügbar sein wird, muß dies dahingehend geändert werden, daß dieser Typ umhüllt wird, was eine einfache Operation ist.

Beispiel 8-8: Decorator für JDOM-Textinhalte

package javaxml2;

import java.util.Collections;
import java.util.Iterator;

// JDOM-Importe
import org.jdom.Element;

public class TextNode implements JDOMNode {
	
    /** Der umhüllte String */
    protected String decorated;
	
    /** Das manuell eingestellte Elternobjekt dieses Stringinhalts */
    private Element parent = null;
	
    public TextNode(String string) {
        decorated = string;
    }
	
    public Object getNode(  ) {
        return decorated;
    }
	
    public String getNodeName(  ) {
        return "";
    }
	
    public JDOMNode getParentNode(  ) {
        if (parent == null) {
            throw new RuntimeException(
                "Das Parent-Objekt dieses String-Inhalts wurde nicht eingestellt!");
        }	
        return new ElementNode(parent);
    }
	
    public String getQName(  ) {
        // Textknoten haben keinen Namen
        return "";
    }
	
    public Iterator iterator(  ) {
        return Collections.EMPTY_LIST.iterator(  );
    }

    public TextNode setParent(Element parent) {
        this.parent = parent;
        return this;
    }

    public String toString(  ) {
        return decorated;
    }
}

Ich werde keine Decorators für alle anderen JDOM-Typen anbieten, weil Sie allmählich im Bilde sein müßten. Beachten Sie, daß ich auch eine einzelne JDOMNode-Implementierung zur Verfügung gestellt haben könnte, ConcreteNode oder so etwas ähnliches, das sämtliche JDOM-Typen in einer einzigen Klasse umhüllen würde. Allerdings würde dies eine ziemliche Menge an speziellem Casting-Code erfordern, der uns hier nicht weiterbringen würde. Statt dessen gibt es eine Eins-zu-eins-Entsprechung zwischen den JDOM-Kernklassen und den JDOMNode-Implementierungen.

Nun, da Sie einige Interface-basierte JDOM-Knoten haben, werde ich das Ganze ein wenig weiterführen. Dies ist ein gängiges Business-Szenario, in dem Sie eine bestimmte Funktionalität auf der Grundlage einer existierenden API zur Verfügung stellen müssen. Für ein praktisches Beispiel schnappe ich mir XPath. Ich möchte in der Lage sein, für jede JDOMNode-Implementierung den XPath-Ausdruck zu ermitteln, der diesen Knoten darstellt. Um diese Funktionalität möglich zu machen, habe ich eine weitere Wrapper-Klasse geschrieben, die in Beispiel 8-9 gezeigt wird. Diese Klasse, XPathDisplayNode, umhüllt einen existierenden Knoten (eines beliebigen Typs, wegen der Interface-basierten Logik) und stellt eine einzige öffentliche XPath-Methode zur Verfügung, getXPath( ). Diese Methode gibt einen XPath-Ausdruck für den umhüllten Knoten als Java-String zurück.

Beispiel 8-9: Wrapper für die XPath-Unterstützung

package javaxml2;

import java.util.Vector;
import java.util.List;
import java.util.Iterator;
import java.util.Stack;

// JDOM-Importe
import org.jdom.Attribute;
import org.jdom.Element;
import org.jdom.Namespace;

public class XPathDisplayNode {

    /** Der JDOMNode, auf dem dieser XPath basiert */
    JDOMNode node;
	
    public XPathDisplayNode(JDOMNode node) {
        this.node = node;
    }

    private String getElementXPath(JDOMNode currentNode) {
        StringBuffer buf = new StringBuffer("/")
            .append(currentNode.getQName(  ));
        Element current = (Element)currentNode.getNode(  );
        Element parent = current.getParent(  );

        // Überprüfen, ob wir uns am Wurzelelement befinden
        if (parent == null ) {
            return buf.toString(  );
        }

        // Auf Geschwisterelemente mit demselben Namen und Namensraum überprüfen
        Namespace ns = current.getNamespace(  );
        List siblings = parent.getChildren(current.getName(  ), ns);
		
        int total = 0;
        Iterator i = siblings.iterator(  );
        while (i.hasNext(  )) {
            total++;
            if (current == i.next(  )) {
                break;
            }
        }

        // Kein Selektor erforderlich, wenn dies das einzige Element ist
        if ((total == 1) && (!i.hasNext(  ))) {
            return buf.toString(  );
        }

        return buf.append("[")
                  .append(String.valueOf(total))
                  .append("]").toString(  );
    }

    public String getXPath(  ) {
        // Elemente bearbeiten
        if (node.getNode(  ) instanceof Element) {
            JDOMNode parent = node.getParentNode(  );

            // Wenn dies null ist, sind wir an der Wurzel
            if (parent == null) {
                return "/" + node.getQName(  );
            }

            // Andernfalls einen Pfad zurück zur Wurzel einrichten
            Stack stack = new Stack(  );
            stack.add(node);
            do {
                stack.add(parent);
                parent = parent.getParentNode(  );
            } while (parent != null);

            // Den Pfad aufbauen
            StringBuffer xpath = new StringBuffer(  );
            while (!stack.isEmpty(  )) {
                xpath.append(getElementXPath((JDOMNode)stack.pop(  )));
            }
            return xpath.toString(  );
        }	
		
        // Attribute bearbeiten
        if (node.getNode(  ) instanceof Attribute) {
            Attribute attribute = (Attribute)node.getNode(  );
            JDOMNode parent = node.getParentNode(  );
            StringBuffer xpath = new StringBuffer("//")
                .append(parent.getQName(  ))
                .append("[@")
                .append(node.getQName(  ))
                .append("='")
                .append(attribute.getValue(  ))
                .append("']");

            return xpath.toString(  );
        }

        // Text bearbeiten
        if (node.getNode(  ) instanceof String) {
            StringBuffer xpath = new StringBuffer(
                new XPathDisplayNode(node.getParentNode()).getXPath(  ))
                    .append("[child::text(  )]");
            return xpath.toString(  );
        }
					
        // Hier könnten noch andere Knotentypen folgen
        return "Knotentyp wird noch nicht unterstützt.";
    }
}

In dieser Klasse habe ich jeden Knotentyp als Spezialfall behandelt; mit anderen Worten: Ich habe nicht etwa XPathElementNode, XPathAttributeNode und so weiter implementiert. Das kommt daher, daß die Gemeinsamkeiten bei der Erzeugung dieses XPath-Ausdrucks viel größer sind als der Vorteil, den das Aufteilen des Codes auf die einzelnen Typen bringen würde. Selbstverständlich ist dies genau das Gegenteil von der Bereitstellung eines typspezifischen Knoten-Decorators für jeden JDOM-Typ. Sie sollten in Ihren Anwendungen stets versuchen, diesen Unterschied zu beachten, was zu erheblich klarerem (und oft auch kürzerem) Code führt.

Ich werde die Details beim Durcharbeiten des Prozesses, der in diesem Code abläuft, Ihnen überlassen. Für jeden Knoten wird der XPath-Ausdruck zu Fuß berechnet und zusammengefügt, und es sollte Ihnen ein leichtes sein, der Logik zu folgen. Dieser Ausdruck wird dann an das aufrufende Programm zurückgegeben, das ich als nächstes behandeln werde.

Nachdem Sie alle Ihre verschiedenen Knotentypen sowie den XPath-Wrapper fertiggestellt haben, ist es Zeit, etwas Nützliches zu tun. In diesem Fall möchte ich einen Dokumentenbetrachter für einen JDOM-Baum anbieten, ähnlich der Klasse SAXTreeViewer aus dem Kapitel SAX. Allerdings möchte ich gleichzeitig den XPath-Ausdruck für jedes Objekt unten in der Statusleiste anzeigen. Beispiel 8-10 zeigt Ihnen, wie das vonstatten geht, und zwar unter Verwendung der Knoten und Wrapper, die in diesem Abschnitt besprochen wurden.

Beispiel 8-10: Die Klasse SimpleXPathViewer

package javaxml2;

import java.awt.*;
import java.io.File;
import javax.swing.*;
import javax.swing.tree.*;
import javax.swing.event.*;
import java.util.Iterator;

// JDOM-Importe
import org.jdom.*;
import org.jdom.input.SAXBuilder;

public class SimpleXPathViewer extends JFrame {

    /** Die innere EventHandler-Klasse */
    EventHandler eventHandler = new EventHandler(  );

    /** Ein Textfeld, das den XPath für den ausgewählten Knoten anzeigt */
    private JTextField statusText;

    /** Der JTree, der verwendet wird, um die Knoten des XML-Dokuments anzuzeigen*/
    private JTree jdomTree;

    /** Das Auswahlmodell, um herauszufinden, welcher Knoten angeklickt wurde */
    private DefaultTreeSelectionModel selectionModel;

    /** Der Dateiname, der die anzuzeigende XML-Datei enthält */
    private String filename;

    /** Vorläufiges Helferlein, um das Fehlen eines Textknotens zu kompensieren */
    private static Element lastElement;

    class EventHandler implements TreeSelectionListener {

        public void valueChanged(TreeSelectionEvent e) {
            TreePath path= selectionModel.getLeadSelectionPath(  );

            // Wenn Sie den ganzen Baum zuklappen, erhalten Sie keinen neuen Pfad
            if (path != null) {
                JDOMNode selection=
                    (JDOMNode)((DefaultMutableTreeNode)path.getLastPathComponent(  ))
                        .getUserObject(  );
                buildXPath(selection);
            }
        };
    };

    public SimpleXPathViewer(String fileName) throws Exception {
        super(  );
        this.filename = fileName;
        setSize(600, 450);
        initialize(  );
    }

    private void initialize(  ) throws Exception {
        setTitle("Simple XPath Viewer");

        // Die Oberfläche einrichten
        initConnections(  );
		
        // Das JDOM-Dokument laden
        Document doc = loadDocument(filename);

        // Den ursprünglichen JDOMNode mit Hilfe der Factory-Methode erzeugen
        JDOMNode root = createNode(doc.getRootElement(  ));
			
        // Den Wurzelknoten des JTrees erzeugen und aus dem JDOM-Dokument aufbauen
        DefaultMutableTreeNode treeNode = 
            new DefaultMutableTreeNode("Dokument: " + filename);
        buildTree(root, treeNode);

        // Den Knoten zum Modell des Baums hinzufügen
        ((DefaultTreeModel)jdomTree.getModel(  )).setRoot(treeNode);
    }

    private void initConnections(  ) {
        setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);

        // Den JTree und einen Anzeigebereich für diesen einrichten
        jdomTree = new JTree(  );
        jdomTree.setName("JDOM Tree");
        jdomTree.addTreeSelectionListener(eventHandler);
        selectionModel = (DefaultTreeSelectionModel)jdomTree.getSelectionModel(  );
        getContentPane(  ).add(new JScrollPane(jdomTree), BorderLayout.CENTER);

        // Eine Textbox für die Verwendung in einer Statusleiste erzeugen
        statusText = new JTextField("Klick auf ein Element zeigt dessen XPath");
        JPanel statusBarPane= new JPanel(  );
        statusBarPane.setLayout(new BorderLayout(  ));
        statusBarPane.add(statusText, BorderLayout.CENTER );
        getContentPane(  ).add(statusBarPane, BorderLayout.SOUTH);
    }

    private Document loadDocument(String filename) throws JDOMException {
        SAXBuilder builder = new SAXBuilder(  );
        builder.setIgnoringElementContentWhitespace(true);
        return builder.build(new File(filename));
    }

    private JDOMNode createNode(Object node) {
        if (node instanceof Element) {
            lastElement = (Element)node;
            return new ElementNode((Element)node);
        }

        if (node instanceof Attribute) {
            return new AttributeNode((Attribute)node);
        }

        if (node instanceof String) {
            return new TextNode((String)node).setParent(lastElement);
        }

        // Alle anderen Knoten sind nicht implementiert
        return null;
    }
	
    private void buildTree(JDOMNode node, DefaultMutableTreeNode treeNode) {
        // Wenn dies ein Whitespace- oder unbekannter Knoten ist, ignorieren
        if ((node == null) || (node.toString().trim(  ).equals(""))) {
            return;
        }

        DefaultMutableTreeNode newTreeNode = new DefaultMutableTreeNode(node);
		
        // Die Kinder des Knotens durchgehen 
        Iterator i = node.iterator(  );
        while (i.hasNext(  )) {
            // JDOMNodes für die Kinder erzeugen und zum Baum hinzufügen
            JDOMNode newNode = createNode(i.next(  ));
            buildTree(newNode, newTreeNode);
        }

        // Mit dem Baum verbinden, nachdem alle Kinder hinzugefügt wurden
        treeNode.add(newTreeNode);
    }
	
    private void buildXPath(JDOMNode node) {
        statusText.setText(new XPathDisplayNode(node).getXPath(  ));
    }
	
    public static void main(java.lang.String[] args) {
        try {
            if (args.length != 1) {
                System.out.println("Verwendung: java javaxml2.SimpleXPathViewer " + 
                    "[XML-Dokument-Dateiname]");
                return;
            }

            /* Das Fenster erzeugen */
            SimpleXPathViewer viewer= new SimpleXPathViewer(args[0]);

            /* Einen windowListener für das Ereignis windowClosedEvent hinzufügen */
            viewer.addWindowListener(new java.awt.event.WindowAdapter(  ) {
                    public void windowClosed(java.awt.event.WindowEvent e) {
                        System.exit(0);
                    };
                });
            viewer.setVisible(true);
        } catch (Exception e) {
            e.printStackTrace(  );
        }
    }
}

Wie üblich lasse ich die Swing-Details außen vor. Wie Sie aber sehen können, wird das Wurzelelement des Dokuments geholt (in der Methode initialize( )), nachdem das Dokument mit Hilfe von SAXBuilder geladen wurde. Dieses Element wird verwendet, um mit der Hilfsfunktion createNode( ) eine Instanz von JDOMNode zu erzeugen. Die Funktion konvertiert einfach JDOM-Typen in JDOMNode-Implementierungen und war in ungefähr 15 Sekunden programmiert. Verwenden Sie in Ihren eigenen Programmen eine ähnliche Methode, wenn Sie mit Decorator- und Wrapper-Klassen arbeiten.

Nachdem ich JDOMNode-Implementierungen erstellt habe, ist es leicht, den Baum zu durchwandern und visuelle Objekte für jeden gefundenen Knoten zu erzeugen. Zusätzlich habe ich für jeden Knoten den Statustext des Fensters auf den XPath-Ausdruck für diesen Knoten eingestellt. Sie können alle diese Beispiele kompilieren und mit dem folgenden Befehl starten:

C:\javaxml2\build>java javaxml2.SimpleXPathViewer 
                       c:\javaxml2\ch08\xml\contents.xml

Stellen Sie sicher, daß sich JDOM und Ihr XML-Parser in Ihrem Klassenpfad befinden. Das Ergebnis ist die Swing-Oberfläche, die in Abbildung 8-1 gezeigt wird. Beachten Sie, wie die Statusleiste den XPath-Ausdruck für den aktuell ausgewählten Knoten widerspiegelt. Spielen Sie ein wenig damit herum – vier oder fünf Screenshots in einem Buch zu sehen ist nicht annähernd so nützlich wie Ihre eigene Erforschung des Werkzeugs.

Abbildung 8-1: contents.xml und XPaths betrachten
contents.xml und XPaths betrachten

Und das ist alles! Ich weiß, ich bin schnell vorangeschritten, aber die zugrundeliegenden Konzepte sind einfach. Sie sollten darüber nachdenken, wie Decorator- und Wrapper-Klassen Ihnen helfen könnten, wenn Sie eine Interface-ähnliche Funktionalität in Ihren Anwendungen benötigen. Halten Sie auch auf der JDOM-Website unter http://www.jdom.org Ausschau nach Beiträgen, zu denen Wrapper gehören könnten (wie dieser hier, oder etwa ein Satz von DOM-Decorator-Klassen).

Zu guter Letzt möchte ich Philip Nelson danken, der den Löwenanteil der Arbeit am hier gezeigten Decorator-Code erledigt hat. Philip hat die Verwendung von Decorator-Klassen mit JDOM wirklich gründlich untersucht und war mir bei diesem Abschnitt eine große Hilfe.

Vorsicht Falle!

Genau wie in den anderen Kapiteln über APIs möchte ich auch hier einige tückischere Punkte ansprechen, die mit dem Thema dieses Kapitels zu tun haben. Dies sind häufige Probleme, die dazu führen können, daß Sie vor Verzweiflung den Kopf gegen die Wand schlagen möchten; versuchen Sie deshalb, sie zu vermeiden.

Auch wenn ich in diesem Kapitel über Factories und selbstdefinierte Klassen gesprochen habe, lohnt es sich, einige wichtige Dinge über das Bilden von Unterklassen noch einmal hervorzuheben, da es Probleme aufwerfen kann. Wenn Sie eine Klasse erweitern, insbesondere die JDOM-Klassen, müssen Sie sicherstellen, daß Ihr selbstdefiniertes Verhalten aktiviert wird, wenn Sie es möchten.

Mit anderen Worten: Sorgen Sie dafür, daß es keinen Pfad von einer Anwendung durch Ihre Unterklasse bis in die übergeordnete Klasse gibt, mit dem Sie nicht leben möchten. Beinahe in jedem Fall gehört dazu, daß Sie alle Konstruktoren der übergeordneten Klasse überschreiben. Sie werden dies in Beispiel 8-1, in der Klasse ORAElement, bemerken, in der ich alle vier Konstruktoren der Klasse Element überschrieben habe. Dies sorgt dafür, daß jede Anwendung, die ORAElement verwendet, das Objekt mit Hilfe eines dieser Konstruktoren erzeugen muß. Auch wenn dies ein triviales Detail sein mag: Stellen Sie sich vor, ich hätte den Konstruktor ausgelassen, der einen Namen und eine URI für das Element entgegennimmt. Dieser Schritt reduziert tatsächlich die Anzahl der Möglichkeiten, das Objekt als solches zu erzeugen. Das mag trivial scheinen, ist es aber nicht!

Unter Beibehaltung dieser Hypothese implementieren Sie eine CustomJDOMFactory-Klasse wie die, die in Beispiel 8-2 gezeigt wird, und überschreiben die verschiedenen element( )-Methoden. Allerdings würden Sie wahrscheinlich vergessen, element(String name,String uri) zu überschreiben, wie Sie schon vergessen haben, diesen Konstruktor in Ihrer Unterklasse zu überschreiben.

Plötzlich haben Sie ein Problem: Jedesmal wenn nun ein Element mit Name und URI angefordert wird (was übrigens im SAXBuilder-Prozeß recht häufig der Fall ist), erhalten Sie eine einfache, bloße Element-Instanz. Die anderen Elementerzeugungsmethoden geben jedoch allesamt Instanzen von ORAElement zurück. Und bloß wegen dieses einen lausigen Konstruktors besitzt Ihr Dokument zwei verschiedene Element-Implementierungen – ziemlich sicher nicht das, was Sie erreichen wollten. Es ist unerläßlich, jedes Mittel zur Objekterzeugung in Ihren Unterklassen zu untersuchen, und Sie müssen grundsätzlich sicherstellen, daß Sie jeden Konstruktor überschreiben, der in der übergeordneten Klasse öffentlich ist.

Ein weiterer tückischer Fall, auf den Sie bei der Bildung von Unterklassen achten müssen, ist die versehentliche Erzeugung von ungültigem XML. Wenn Sie JDOM verwenden, ist es so gut wie unmöglich, nicht-wohlgeformtes XML zu erzeugen, aber betrachten Sie erneut die Unterklasse ORAElement. Diese Unterklasse hat das Präfix ora zu jedem Element hinzugefügt, was für sich genommen schon einmal dazu führen kann, daß die Validierung fehlschlägt. Dies ist wahrscheinlich kein größeres Problem, aber Sie müssen die DOCTYPE-Deklaration auskommentieren oder entfernen, um Probleme zu vermeiden, wenn das Dokument erneut eingelesen wird.

Was noch wichtiger ist, Sie können einige unerwartete Ergebnisse erhalten, wenn Sie nicht vorsichtig sind. Schauen Sie sich dieses Fragment des XML-Codes an, der mit Hilfe der Unterklasse ORAElement erzeugt wurde und nur die letzten paar Zeilen des serialisierten Dokuments zeigt:

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

  <!-- Weiterer Inhalt -->

  <ora:copyright><ora:copyright>  <ora:jahr zahl="2001" />  <ora:inhalt>Alle Rechte vorbehalten, O'Reilly &amp; Associates</ora:inhalt></ora:copyright></ora:copyright>
</ora:buch>

Beachten Sie, daß es nun zwei ora:copyright-Elemente gibt! Folgendes ist passiert: Es war bereits ein existierendes Element im O’Reilly-Namensraum vorhanden (das ursprüngliche ora:copyright-Element). Allerdings wurden dem verschachtelten copyright-Element (ohne Namensraum) durch die Klasse ORAElement ebenfalls das Präfix ora und der O’Reilly-Namensraum zugewiesen. Das Ergebnis sind zwei Elemente mit demselben Namen und Namensraum, aber unterschiedlichen Content-Modellen. Dies macht die Validierung sehr problematisch und ist wahrscheinlich nicht das, was Sie vorhatten. Dies sind einfache Beispiele, aber in komplexeren Dokumenten mit komplexeren Unterklassen müssen Sie sorgfältig darauf achten, welche Ergebnisse Sie erzeugen, besonders im Hinblick auf eine DTD, ein XML Schema oder eine andere Art der Dokumentbeschränkung.

Und was kommt jetzt?

Ich bin dabei, die Betrachtung von APIs der unteren Ebenen abzuschließen, und werde ihre Behandlung im nächsten Kapitel mit der Betrachtung von JAXP abrunden, der Java API for XML Processing von Sun. JAXP erfordert die Verwendung von SAX und DOM und befindet sich deshalb logisch gesehen oberhalb von ihnen. Es dient auch als Halbzeit für dieses Buch, und es ist wahrscheinlich ein guter Zeitpunkt für eine kurze Pause, wenn Sie mit diesem Kapitel fertig sind! Ich werde sowohl JAXP 1.0 als auch 1.1 behandeln, die beide weit verbreitet sind, erkläre Ihnen, wie sie mit den APIs zusammenarbeiten, die Sie bereits kennen, und warum JAXP Ihnen wirklich bei der Anwendungsprogrammierung weiterhelfen kann. Machen Sie sich also bereit für einen weiteren Hammer in Ihrem Werkzeugkasten, und blättern Sie um.

1)
Die Klasse unterscheidet sich darin ein wenig von NamespaceFilter, daß sie die Namensraum-Information aller Elemente ändert, statt nur von Elementen aus einem bestimmten Namensraum.