DOM für Fortgeschrittene
Genau wie im Kapitel SAX für Fortgeschrittene wird auch in diesem Kapitel nichts Mystisches behandelt. Die Themen bauen auf dem Fundament auf, das ich mit den DOM-Grundlagen aus dem vorigen Kapitel gelegt habe. Allerdings werden viele dieser Features, außer dem ersten Abschnitt über Mutationen, selten genutzt. Während fast alles, was Sie über SAX erfahren haben (außer vielleicht DTDHandler und DeclHandler) praktisch sein kann, ist mir aufgefallen, daß viele der Randbestandteile von DOM nur in bestimmten Anwendungen nützlich sind. Wenn Sie zum Beispiel keinerlei Präsentationsprogrammierung durchführen, werden Sie wahrscheinlich nie mit den DOM-HTML-Bindungen konfrontiert werden. Das gleiche gilt für viele der Features von DOM Level 2; wenn Sie sie brauchen, dann brauchen Sie sie dringend, und wenn nicht, dann brauchen Sie sie überhaupt nicht.
In diesem Kapitel präsentiere ich einige besondere DOM-Themen, die Ihnen in Ihrer eigenen DOM-Programmierung von Nutzen sein werden. Ich habe versucht, das Kapitel eher wie eine Referenz zu organisieren als die vorherigen Kapitel; wenn Sie zum Beispiel mehr über das DOM Level 2 Traversal-Modul erfahren möchten, können Sie einfach bis zu diesem Abschnitt weiterblättern. Allerdings bauen die Codebeispiele in diesem Kapitel dennoch aufeinander auf, so daß Sie vielleicht trotzdem alle Abschnitte durcharbeiten möchten, um ein vollständiges Bild des aktuellen DOM-Modells zu erhalten. Das führt zu praktischeren Codebeispielen als den nutzlosen einzeln erfundenen, die nirgendwohin führen. Also schnallen Sie sich an, und lassen Sie uns noch ein wenig tiefer in die Welt von DOM eintauchen.
Änderungen
Als erstes und wichtigstes Thema möchte ich über die Veränderbarkeit eines DOM-Baums sprechen. Die größte Einschränkung bei der Verwendung von SAX für die Arbeit mit XML besteht darin, daß Sie nichts an der vorgefundenen XML-Struktur ändern können, zumindest nicht ohne Filter und Writer. Deren eigentliche Aufgabe besteht aber sowieso nicht in der Änderung ganzer Dokumente, darum müssen Sie eine andere API verwenden, wenn Sie XML modifizieren möchten. DOM erfüllt diese Anforderungen mit Bravour, da es Fähigkeiten zur XML-Erzeugung und Modifikation mitbringt.
Bei der Arbeit mit DOM unterscheidet sich der Prozeß der Erzeugung eines XML-Dokuments deutlich von der Änderung eines existierenden, darum werde ich diese Prozesse einzeln betrachten. Dieser Abschnitt bietet Ihnen ein ziemlich realistisches Beispiel zum Nachvollziehen. Wenn Sie jemals eine Online-Auktionsseite wie eBay besucht haben, wissen Sie, daß der wichtigste Aspekt der Auktion in der Fähigkeit besteht, Artikel zu finden sowie etwas über diese Artikel herauszufinden. Diese Funktionen basieren darauf, daß ein Benutzer eine Beschreibung eines Artikels eingibt und daß die Auktion diese Informationen verwendet.
Bessere Auktions-Sites ermöglichen es Benutzern, sowohl grundlegende Informationen als auch richtige HTML-Beschreibungen einzugeben, so daß HTML-kundige Anwender ihre Artikelbeschreibungen fett oder kursiv setzen, mit Links versehen oder andere Formatierungen darauf anwenden können. Dies ist ein gutes Fallbeispiel für die Anwendung von DOM.
Einen neuen DOM-Baum erzeugen
Zu Anfang ist ein wenig Vorarbeit nötig. Beispiel 6-1 zeigt ein einfaches HTML-Formular, das grundlegende Informationen über einen Artikel entgegennimmt, die auf einer Auktions-Site aufgelistet werden sollen. Dieses Formular würde für eine echte Site sicherlich ein wenig mehr ausgestaltet, aber Sie verstehen bestimmt die Grundidee.
<html> <head><title>Artikeleintrag eingeben/aktualisieren</title></head> <body> <h1 align="center">Artikeleintrag eingeben/aktualisieren</h1> <p align="center"> <form method="POST" action="/javaxml2/servlet/javaxml2.UpdateItemServlet"> Artikel-ID (eindeutiger Schlüssel): <br /> <input name="id" type="text" maxLength="10" /><br /><br /> Artikelname: <br /> <input name="name" type="text" maxLength="50" /><br /><br /> Artikelbeschreibung: <br /> <textarea name="description" rows="10" cols="30" wrap="wrap" ></textarea> <br /><br /> <input type="reset" value="Formular zurücksetzen" /> <input type="submit" value="Artikel hinzufügen/ändern" /> </form> </p> </body> </html>
Beachten Sie, daß das Ziel dieser Formularübermittlung ein Servlet ist. Dieses Servlet wird in Beispiel 6-2 gezeigt. Die Methode doPost( ) liest die eingegebenen Parameter und legt ihre Werte in temporären Variablen ab. An diesem Punkt überprüft das Servlet das Dateisystem auf eine bestimmte Datei, in der diese Informationen gespeichert sind.
Um eines klarzustellen: Ich interagiere in diesem Servlet direkt mit dem Dateisystem. Dies ist jedoch im allgemeinen keine gute Idee. Ziehen Sie die Verwendung von ServletContext in Betracht, um Zugriff auf lokale Ressourcen zu erhalten, so daß Ihr Servlet abhängig vom Server und der Servlet-Engine, die es bereitstellen, einfach verteilt und modifiziert werden kann. Diese Art von Details neigt aber dazu, Beispiele unübersichtlich zu machen, und deshalb halte ich dies hier einfach.
Falls die Datei nicht existiert (für einen neuen Eintrag würde sie das nicht), erzeugt das Servlet einen neuen DOM-Baum und baut die Baumstruktur unter Verwendung der übergebenen Werte auf. Nachdem das geschehen ist, verwendet das Servlet die Klasse DOMSerializer (aus dem Kapitel DOM), um den DOM-Baum in die Datei auszugeben und ihn verfügbar zu machen, wenn dieses Servlet das nächste Mal aufgerufen wird. Zusätzlich habe ich eine doGet( )-Methode programmiert; diese Methode gibt einfach das in Beispiel 6-1 gezeigte HTML aus. Ich werde dies später verwenden, um Modifikationen von Artikeleinträgen zuzulassen. Machen Sie sich im Moment keine zu großen Sorgen darüber.
package javaxml2; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; // DOM-Importe import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.DOMImplementation; import org.w3c.dom.Element; import org.w3c.dom.Text; // Parser-Import import org.apache.xerces.dom.DOMImplementationImpl; public class UpdateItemServlet extends HttpServlet { private static final String ITEMS_DIRECTORY = "/javaxml2/ch06/xml/"; public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // Ausgabe erhalten PrintWriter out = res.getWriter( ); res.setContentType("text/html"); // HTML ausgeben out.println("<html>"); out.println(" <head><title>Artikeleintrag eingeben/aktualisieren</title> </head>"); out.println(" <body>"); out.println(" <h1 align='center'>Artikeleintrag eingeben/aktualisieren</h1> "); out.println(" <p align='center'>"); out.println(" <form method='POST' " + "action='/javaxml2/servlet/javaxml2.UpdateItemServlet'>"); out.println(" Artikel-ID (eindeutiger Schlüssel):<br />"); out.println(" <input name='id' type='text' maxLength='10' />" + "<br /><br />"); out.println(" Artikelname: <br />"); out.println(" <input name='name' type='text' maxLength='50' />" + "<br /><br />"); out.println(" Artikelbeschreibung: <br />"); out.println(" <textarea name='description' rows='10' cols='30' " + "wrap='wrap' ></textarea><br /><br />"); out.println(" <input type='reset' value='Formular zurücksetzen' /> "); out.println(" <input type='submit' value='Artikel hinzufügen/ ändern' />"); out.println(" </form>"); out.println(" </p>"); out.println(" </body>"); out.println("</html>"); out.close( ); } public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // Parameterwerte erhalten String id = req.getParameterValues("id")[0]; String name = req.getParameterValues("name")[0]; String description = req.getParameterValues("description")[0]; // Neuen DOM-Baum erzeugen DOMImplementation domImpl = new DOMImplementationImpl( ); Document doc = domImpl.createDocument(null, "artikel", null); Element root = doc.getDocumentElement( ); // ID des Artikels (als Attribut) root.setAttribute("id", id); // Name des Artikels Element nameElement = doc.createElement("name"); Text nameText = doc.createTextNode(name); nameElement.appendChild(nameText); root.appendChild(nameElement); // Beschreibung des Artikels Element descriptionElement = doc.createElement("beschreibung"); Text descriptionText = doc.createTextNode(description); descriptionElement.appendChild(descriptionText); root.appendChild(descriptionElement); // DOM-Baum serialisieren DOMSerializer serializer = new DOMSerializer( ); serializer.serialize(doc, new File(ITEMS_DIRECTORY + "item-" + id + ".xml")); // Bestätigung ausgeben PrintWriter out = res.getWriter( ); res.setContentType("text/html"); out.println("<HTML><BODY>Danke für Ihre Übermittlung. " + "Ihr Artikel wurde verarbeitet.</BODY></HTML>"); out.close( ); } }
Legen Sie los, und kompilieren Sie diese Klasse. Ich werde Sie einen Moment später mit Ihnen durchgehen, aber stellen Sie sicher, daß Ihre Umgebung richtig eingerichtet ist, um die erforderlichen Klassen zu importieren.
Sorgen Sie dafür, daß sich die Klasse DOMSerializer aus dem vorigen Kapitel in Ihrem Klassenpfad befindet, wenn Sie die Klasse UpdateItemServlet importieren. Sie sollten diese Klasse außerdem zu den Klassen im Kontext Ihrer Servlet-Engine hinzufügen. In meiner Konfiguration, in der ich Tomcat verwende, heißt mein Kontext javaxml2 und befindet sich in einem Verzeichnis namens javaxml2 unterhalb des Verzeichnisses webapps. In meinem Verzeichnis WEB-INF/classes gibt es auch ein Verzeichnis javaxml2 (für das Package), und die Klassen DOMSerializer.class und UpdateItemServlet.class befinden sich in diesem Verzeichnis.
Sie sollten auch sicherstellen, daß sich eine Kopie der jar-Datei Ihres Parsers (in meinem Falle xerces.jar) im Klassenpfad Ihrer Engine befindet. Bei Tomcat können Sie einfach eine Kopie in dessen lib-Verzeichnis ablegen. Als letztes müssen Sie dafür sorgen, daß Xerces und dessen DOM-Level-2-Implementierung vor der DOM-Level-1-Implementierung in Tomcats parser.jar-Archiv gefunden wird. Erledigen Sie dies, indem Sie parser.jar in z_parser.jar umbenennen. Ich werde im Kapitel Web Publishing Framework mehr darüber sagen, vertrauen Sie mir im Moment einfach, und führen Sie die Änderung durch. Starten Sie anschließend Tomcat neu, dann sollte alles funktionieren.
Nachdem Sie Ihr Servlet an den Start gebracht und die Servlet-Engine gestartet haben, navigieren Sie mit Ihrem Browser zu dem Servlet und lassen die GET-Anfrage, die er erzeugt, das HTML-Eingabeformular laden. Füllen Sie dieses Formular aus, so wie ich es in Abbildung 6-1 getan habe.
Da ich später ausführlich über das Beschreibungsfeld sprechen werde, zeige ich Ihnen hier den kompletten Inhalt, den ich in dieses Feld eingetippt habe. Ich weiß, er besteht aus einer Menge an Auszeichnungen (ich bin fast verrückt geworden mit den ganzen Fett- und Kursiv-Tags!), aber das wird später wichtig sein:
Dies ist eine <i>wunderschöne</i> Gitarre mit <b>Sitka-Deckel</b> und -Rücken und Seiten aus <b>indischem Rosenholz</b>. Diese vom Gitarrenbaumeister <a href="http://www.bourgeoisguitars.com">Dana Bourgeois</a> gebaute OM hat einen sehr <b>vollen Klang</b>. Die Gitarre besitzt ein <i>großartiges Spielverhalten</i>, einen 1 3/4"-Kopf, mit Kopf und Sattel aus <i>fossilem Elfenbein</i> und Schrauben aus <i>Ebenholz</i>. Neuwertiger Zustand, dies ist eine <b>hervorragende Gitarre</b>!
Das Abschicken dieses Formulars leitet dessen Daten (über eine POST-Anfrage) an das Servlet weiter, und die Methode doPost( ) kommt zum Zuge. Die eigentliche DOM-Erzeugung erweist sich als ziemlich einfach. Als erstes müssen Sie eine Instanz der Klasse org.w3c.dom.DOMImplementation erzeugen. Diese wird zur Grundlage Ihrer gesamten Arbeit bei der DOM-Erzeugung. Auch wenn Sie sicherlich direkt eine DOM-Document-Implementierung instantiieren könnten, gäbe es dann keine Möglichkeit, eine DocType-Klasse aus ihr zu erzeugen wie aus einer DOMImplementation; die Verwendung von DOM Implementation ist die bessere Wahl. Darüber hinaus besitzt die Klasse DOMImplementation eine weitere nützliche Methode, hasFeature( ). Ich werde diese Methode später detailliert behandeln, also machen Sie sich im Moment keine Sorgen deswegen.
Im Beispielcode habe ich die Implementierung von Xerces verwendet, org.apache.xerces.dom.DOMImplementationImpl (irgendwie ein verwirrender Name, oder?). Es gibt momentan keine herstellerunabhängige Möglichkeit, diese Aufgabe zu lösen, obwohl DOM Level 3 (das am Ende dieses Kapitels behandelt wird) einige Möglichkeiten für die Zukunft bietet. JAXP, das im gleichnamigen Kapitel detailliert beschrieben wird, bietet einige Lösungen, aber die werden später behandelt.
Nachdem Sie eine Instanz von DOMImplementation erzeugt haben, ist aber alles ziemlich einfach.
// Neuen DOM-Baum erzeugen DOMImplementation domImpl = new DOMImplementationImpl( ); Document doc = domImpl.createDocument(null, "artikel", null); Element root = doc.getDocumentElement( ); // ID des Artikels (als Attribut) root.setAttribute("id", id); // Name des Artikels Element nameElement = doc.createElement("name"); Text nameText = doc.createTextNode(name); nameElement.appendChild(nameText); root.appendChild(nameElement); // Beschreibung des Artikels Element descriptionElement = doc.createElement("beschreibung"); Text descriptionText = doc.createTextNode(description); descriptionElement.appendChild(descriptionText); root.appendChild(descriptionElement); // DOM-Baum serialisieren DOMSerializer serializer = new DOMSerializer( ); serializer.serialize(doc, new File(ITEMS_DIRECTORY + "item-" + id + ".xml"));
Als erstes wird die Methode createDocument( ) verwendet, um eine neue Document-Instanz zu erhalten. Das erste Argument dieser Methode ist der Namensraum für das Wurzelelement des Dokuments. Bisher benötige ich noch keinen speziellen Namensraum, also übergehe ich ihn, indem ich den Wert null übergebe. Das zweite Argument ist der Name des Wurzelelements selbst, hier einfach "artikel". Das letzte Argument ist eine Instanz einer DocType-Klasse, und da ich für dieses Dokument keine habe, übergebe ich erneut den Wert null. Sollte ich doch einen DocType benötigen, könnte ich ihn mit Hilfe der Methode createDocType( ) aus derselben Klasse – DOMImplementation – erzeugen. Wenn Sie sich für diese Methode interessieren, schlagen Sie in der vollständigen Beschreibung der DOM-API in Anhang A nach.
Beim Aufbau des DOM-Baums kann ich das Wurzelelement erhalten, um damit zu arbeiten (unter Verwendung von getDocumentElement( ), das im vorigen Kapitel behandelt wurde). Nachdem ich dies erledigt habe, füge ich ein Attribut mit der ID des Artikels hinzu, indem ich setAttribute( ) verwende. Ich übergebe den Namen und den Wert des Attributs, und fertig ist das Wurzelelement. Von jetzt an wird es einfach; jede Art von DOM-Konstrukt kann erzeugt werden, indem das Document-Objekt als Factory verwendet wird.
Zum Erstellen der Elemente »name« und »beschreibung« benutze ich die Methode createElement( ), einfach indem ich den jeweiligen Elementnamen übergebe. Der gleiche Ansatz wird verfolgt, um Textinhalte für jedes Element zu erzeugen; hat ein Element keinen Inhalt, aber dafür Kindelemente, die Text-Knoten sind (erinnern Sie sich noch aus dem vorigen Kapitel daran?), ist die Methode createTextNode( ) die richtige Wahl. Diese Methode nimmt den Text für den Knoten entgegen, der zu den Namen der Beschreibung und des Artikels weiterverarbeitet wird.
Sie könnten in Versuchung geraten, die Methode createCDATASection( ) zu verwenden und diesen Text in CDATA-Tags einzuschließen. Es befindet sich HTML in diesem Element. Dieses Vorgehen würde jedoch verhindern, daß der Inhalt als Satz von Elementen eingelesen wird, und statt dessen den Inhalt als großen Textblock liefern. Später wollen wir diese Elemente als solche behandeln, belassen Sie sie deshalb als Text-Knoten, indem Sie erneut createTextNode( ) verwenden.
Nachdem Sie all diese Knoten erzeugt haben, müssen sie nur noch miteinander verknüpft werden. Ihre beste Chance ist die Verwendung von appendChild( ) mit jedem einzelnen Knoten, um die Elemente an die Wurzel anzuhängen. Das erklärt sich beinahe von selbst. Und zu guter Letzt wird das Dokument an die Klasse DOMSerializer aus dem vorigen Kapitel weitergegeben und in eine XML-Datei auf der Festplatte geschrieben.
Ich habe einfach vorausgesetzt, daß der Anwender wohlgeformtes HTML eingibt; mit anderen Worten also XHTML. In einer realen Anwendung würden Sie diese Eingabe wahrscheinlich durch JTidy (http://www.sourceforge.net/projects/jtidy) schicken, um dies sicherzustellen; für dieses Beispiel setze ich einfach voraus, daß die Eingabe XHTML ist.
Ich habe in dem Servlet eine Konstante zur Verfügung gestellt, ITEMS_DIRECTORY, in der Sie festlegen können, welches Verzeichnis verwendet werden soll. Der Beispielcode verwendet ein Windows-Verzeichnis, und beachten Sie, daß die Backslashes allesamt Escape-Sequenzen sind. Vergessen Sie das nicht! Ändern Sie dies einfach auf das Verzeichnis, das Sie in Ihrem System verwenden möchten. Sie können das XML, das durch das Servlet erzeugt wurde, anschauen, indem Sie in das Verzeichnis navigieren, das Sie in dieser Konstante festgelegt haben, und die XML-Datei öffnen, die sich dort befinden sollte. Meine sieht so aus wie in Beispiel 6-3.
<?xml version="1.0" encoding="ISO-8859-1"?> <artikel id="bourgOM"> <name>Bourgeois OM-Gitarre</name> <beschreibung>Dies ist eine <i>wunderschöne</i> Gitarre mit <b>Sitka-Deckel</b> und -Rücken und Seiten aus <b>indischem Rosenholz</b>. Diese vom Gitarrenbaumeister <a href="http://www.bourgeoisguitars.com">Dana Bourgeois</a> gebaute OM hat einen sehr <b>vollen Klang</b>. Die Gitarre besitzt ein <i>großartiges Spielverhalten</i>, einen 1 3/4"-Kopf, mit Kopf und Sattel aus <i>fossilem Elfenbein</i> und Schrauben aus <i>Ebenholz</i>. Neuwertiger Zustand, dies ist eine <b>hervorragende Gitarre</b>!</beschreibung> </artikel>
Ich habe das Ganze ziemlich schnell abgehandelt, aber allmählich sollten Sie anfangen, mit DOM zurechtzukommen. Als nächstes möchte ich erläutern, wie ein DOM-Baum modifiziert wird, der bereits existiert.
Einen DOM-Baum modifizieren
Das Ändern eines existierenden DOM-Baums funktioniert ein wenig anders als die Erzeugung eines neuen; im großen und ganzen gehört dazu, das DOM aus irgendeiner Quelle zu laden, den Baum durchzugehen und dann Änderungen durchzuführen. Diese Änderungen betreffen normalerweise entweder die Struktur oder den Inhalt. Wenn es die Struktur ist, die geändert wird, handelt es sich wieder um einen Fall von Erzeugung:
// Ein Copyright-Element zur Wurzel hinzufügen. Element root = doc.getDocumentElement( ); Element copyright = doc.createElement("copyright"); copyright.appendChild(doc.createTextNode("Copyright O'Reilly 2001")); root.appendChild(copyright);
Das ist genau das, was ich vorhin beschrieben habe. Das Austauschen existierender Inhalte funktioniert ein wenig anders, ist aber auch nicht allzu schwierig. Als Beispiel zeige ich Ihnen eine geänderte Version des UpdateItemServlet. Diese Version liest die übergebene ID und versucht, eine bereits existierende Datei zu laden – falls sie denn existiert. Falls dem so ist, erzeugt sie keinen neuen DOM-Baum, sondern modifiziert statt dessen den existierenden. Da ziemlich viel hinzugefügt wird, drucke ich die komplette Klasse hier noch einmal ab und hebe die Änderungen hervor:
package javaxml2; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.xml.sax.SAXException; // DOM-Imports import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.DOMImplementation; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.w3c.dom.Text; // Parser-Import import org.apache.xerces.dom.DOMImplementationImpl; import org.apache.xerces.parsers.DOMParser; public class UpdateItemServlet extends HttpServlet { private static final String ITEMS_DIRECTORY = "/javaxml2/ch06/xml/"; // die Methode doGet( ) ist unverändert public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // Parameterwerte erhalten String id = req.getParameterValues("id")[0]; String name = req.getParameterValues("name")[0]; String description = req.getParameterValues("description")[0]; // Prüfen, ob die Datei existiert Document doc = null; File xmlFile = new File(ITEMS_DIRECTORY + "item-" + id + ".xml"); if (!xmlFile.exists( )) { // Neuen DOM-Baum erzeugen DOMImplementation domImpl = new DOMImplementationImpl( ); doc = domImpl.createDocument(null, "artikel", null); Element root = doc.getDocumentElement( ); // ID des Artikels (als Attribut) root.setAttribute("id", id); // Name des Artikels Element nameElement = doc.createElement("name"); Text nameText = doc.createTextNode(name); nameElement.appendChild(nameText); root.appendChild(nameElement); // Beschreibung des Artikels Element descriptionElement = doc.createElement("beschreibung"); Text descriptionText = doc.createText(description); descriptionElement.appendChild(descriptionText); root.appendChild(descriptionElement); } else { // Dokument laden try { DOMParser parser = new DOMParser( ); parser.parse(xmlFile.toURL().toString( )); doc = parser.getDocument( ); Element root = doc.getDocumentElement( ); // Name des Artikels NodeList nameElements = root.getElementsByTagName("name"); Element nameElement = (Element)nameElements.item(0); Text nameText = (Text)nameElement.getFirstChild( ); nameText.setData(name); // Beschreibung des Artikels NodeList descriptionElements = root.getElementsByTagName("beschreibung"); Element descriptionElement = (Element)descriptionElements.item(0); // Beschreibung entfernen und neu erstellen root.removeChild(descriptionElement); descriptionElement = doc.createElement("beschreibung"); Text descriptionText = doc.createTextNode(description); descriptionElement.appendChild(descriptionText); root.appendChild(descriptionElement); } catch (SAXException e) { // Ausgabefehler PrintWriter out = res.getWriter( ); res.setContentType("text/html"); out.println("<HTML><BODY>Fehler beim Lesen von XML: " + e.getMessage( ) + ".</BODY></HTML>"); out.close( ); return; } } // DOM-Baum serialisieren DOMSerializer serializer = new DOMSerializer( ); serializer.serialize(doc, xmlFile); // Bestätigung ausgeben PrintWriter out = res.getWriter( ); res.setContentType("text/html"); out.println("<HTML><BODY>Danke für Ihre Übermittlung. " + "Ihr Artikel wurde verarbeitet.</BODY></HTML>"); out.close( ); } }
Die Änderungen sind ziemlich einfach, nichts, was Sie aus der Bahn werfen sollte. Ich erzeuge die File-Instanz für die benannte Datei (unter Verwendung der übergebenen ID) und überprüfe deren Existenz. Auf diese Weise entscheidet das Servlet, ob die XML-Datei, die den übergebenen Artikel repräsentiert, bereits existiert. Falls nicht, wird alles im vorigen Abschnitt Besprochene ohne Änderungen durchgeführt. Falls die Datei schon existiert (was anzeigt, daß der Artikel bereits zuvor übermittelt wurde), wird sie geladen und in einen DOM-Baum eingelesen. Dabei kommen Verfahren aus dem vorigen Kapitel zum Einsatz. An diesem Punkt wird mit dem grundlegenden Durcharbeiten des Baums begonnen.
Der Code greift auf das Wurzelelement zu und verwendet dann die Methode getElements-ByTagName( ), um alle Elemente namens »name« und dann alle namens »beschreibung« aufzufinden. In beiden Fällen weiß ich, daß sich in der zurückgegebenen NodeList nur je ein Element befindet. Ich kann durch die Anwendung der Methode item( ) auf die NodeList darauf zugreifen, indem ich »0« als Argument übergebe (die Indizes beginnen alle bei 0). Dies gibt mir tatsächlich das gewünschte Element.
Ich hätte mir auch einfach durch getChildren( ) die Kindelemente der Wurzel beschaffen und dann das erste und zweite herauslösen können. Allerdings ist die Verwendung der Elementnamen leichter zu dokumentieren und auch klarer. Ich erhalte den Textinhalt des Elements »name«, indem ich getFirstChild( ) aufrufe. Da ich weiß, daß das Element »name« nur einen einzelnen Text-Knoten enthält, kann ich ihn direkt in den entsprechenden Typ umwandeln. Als letztes ermöglicht es die Methode setData( ) dem Code, den existierenden Wert zu einem gegebenen Namen zu ändern, der neue Wert ist die Information, die der Benutzer in das Formular eingegeben hat.
Sie werden bemerken, daß ich für die Beschreibung des Artikels einen recht unterschiedlichen Ansatz verfolgt habe. Da anzunehmen ist, daß sich ein vollständiges Dokumentfragment in dem Element befindet (denken Sie daran, daß der Anwender HTML eingeben kann, was verschachtelte Elemente wie »b«, »a« und »img« ermöglicht), ist es leichter, einfach das existierende Element »beschreibung« zu entfernen und durch ein neues zu ersetzen. Dies verhindert die Notwendigkeit, rekursiv durch den Baum zu wandern und jeden einzelnen Kindknoten zu entfernen – eine zeitraubende Aufgabe. Nachdem ich den Knoten mit Hilfe der Methode removeChild( ) entfernt habe, ist es leicht, ihn neu zu erzeugen und wieder an das Wurzelelement des Dokuments anzuhängen.
Es ist kein Zufall, daß dieser Code fest mit dem Format verbunden ist, in dem das XML ausgegeben wurde. Tatsächlich verläßt sich DOM-Modifikationscode in der Regel zumindest ein wenig darauf, daß Sie über den Inhalt Bescheid wissen. In Fällen, in denen die Struktur oder das Format unbekannt ist, paßt das DOM Level 2 Traversal-Modell besser; ich werde etwas später in diesem Kapitel darauf eingehen. Akzeptieren Sie im Moment einfach, daß die Kenntnis, wie das XML strukturiert ist (da dieses Servlet es zuvor erstellt hat!), einen gewaltigen Vorteil darstellt. Methoden wie getFirstChild( ) können verwendet werden, und das Ergebnis kann durch Casting in einen bestimmten Typ umgewandelt werden. Ansonsten wären langwierige Typüberprüfungen und switch-Blöcke notwendig.
Nachdem die Erzeugung oder Modifikation abgeschlossen ist, wird der resultierende DOM-Baum wieder in XML zurückserialisiert, und der Vorgang kann wiederholt werden. Ich mußte auch etwas Fehlerbehandlung für SAX-Probleme hinzufügen, die sich aus dem DOM-Parsing ergeben können, aber auch das ist nach dem vorigen Kapitel nichts Neues. Als Übung können Sie die Methode doGet( ) so erweitern, daß sie einen Parameter aus der URL liest und die XML-Voreinstellungen lädt, so daß der Benutzer sie im Formular ändern kann. Zum Beispiel würde die URL http://localhost:8080/javaxml2/servlet/javaxml2.UpdateItemServlet?id=bourgOM anzeigen, daß der Artikel mit der ID »bourg OM« zum Bearbeiten geladen werden soll. Dies ist eine einfache Änderung, eine, die Sie mittlerweile selbst erledigen können sollten.
Namensräume
Eine wichtige Erweiterung in DOM Level 2, die noch nicht besprochen wurde, ist die DOM-Unterstützung für XML-Namensräume. Sie werden sich aus den Kapiteln "SAX" und "SAX für Fortgeschrittene" vielleicht erinnern, daß eine Namensraum-Unterstützung in SAX 2.0 eingeführt wurde, und dasselbe gilt für die zweite Veröffentlichung von DOM. Der Schlüssel dazu sind hier zwei neue Methoden im Interface Node: getPrefix( ) und getNamespaceURI( ). Darüber hinaus stehen Namensraum-fähige Versionen aller Erzeugungsmethoden zur Verfügung. Statt also createElement( ) aufzurufen, können Sie createElementNS( ) aufrufen.
In jeder dieser neuen Namensraum-fähigen Methoden ist das erste Argument die Namensraum-URI und das zweite der qualifizierte Name des Elements, Attributs usw. Beachten Sie, daß ich »qualifiziert« gesagt habe, das bedeutet: Wenn Sie die Namensraum-URI »http://www.oreilly.com« und das Präfix »ora« mit einem Element namens »copyright« verwenden wollen, rufen Sie createElementNS("http://www.oreilly.com", "ora:copyright") auf. Das ist sehr wichtig, und wenn Sie daran denken, dieses Präfix zu benutzen, sparen Sie im weiteren Verlauf eine Menge Zeit.
Der Aufruf getPrefix( ) mit diesem neuen Element wird dennoch korrekt »ora« zurückgeben, wie es sein sollte. Wenn Sie das Element im Standard-Namensraum haben möchten (ohne Präfix), übergeben Sie einfach nur den Elementnamen (in diesem Fall den lokalen Namen), und alles ist erledigt. Der Aufruf von getPrefix( ) bei einem Element im Standard-Namensraum gibt null zurück, was übrigens auch bei einem Element ganz ohne Namensraum geschieht.
Das Präfix verrät Ihnen sehr wenig darüber, ob sich ein Element in einem Namensraum befindet. Bei Elementen in einem Standard-Namensraum (und ohne Präfix) hat getPrefix( ) den gleichen Rückgabewert wie bei Elementen in keinem Namensraum. Ich hoffe, daß die nächste Version der Spezifikation dies ändert und einen leeren String (»«) zurückgibt, wenn sich das Element im Standard-Namensraum befindet.
Anstatt einfach alle neuen Namensraum-fähigen Methoden aufzulisten (diese Liste können Sie in Anhang A finden), möchte ich Ihnen lieber etwas richtigen Code zeigen. In der Tat ist die Umstellung der Methode doPost( ) aus dem UpdateItemServlet ein perfektes Beispiel:
public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // Parameterwerte erhalten String id = req.getParameterValues("id")[0]; String name = req.getParameterValues("name")[0]; String description = req.getParameterValues("description")[0]; // Prüfen, ob die Datei existiert Document doc = null; File xmlFile = new File(ITEMS_DIRECTORY + "item-" + id + ".xml"); String docNS = "http://www.oreilly.com/javaxml2"; if (!xmlFile.exists( )) { // Neuen DOM-Baum erzeugen DOMImplementation domImpl = new DOMImplementationImpl( ); doc = domImpl.createDocument(docNS, "artikel", null); Element root = doc.getDocumentElement( ); // ID des Artikels (als Attribut) root.setAttribute("id", id); // Name des Artikels Element nameElement = doc.createElementNS(docNS, "name"); Text nameText = doc.createTextNode(name); nameElement.appendChild(nameText); root.appendChild(nameElement); // Beschreibung des Artikels Element descriptionElement = doc.createElementNS(docNS, "beschreibung"); Text descriptionText = doc.createText(description); descriptionElement.appendChild(descriptionText); root.appendChild(descriptionElement); } else { // Dokument laden try { DOMParser parser = new DOMParser( ); parser.parse(xmlFile.toURL().toString( )); doc = parser.getDocument( ); Element root = doc.getDocumentElement( ); // Name des Artikels NodeList nameElements = root.getElementsByTagNameNS(docNS, "name"); Element nameElement = (Element)nameElements.item(0); Text nameText = (Text)nameElement.getFirstChild( ); nameText.setData(name); // Beschreibung des Artikels NodeList descriptionElements = root.getElementsByTagNameNS(docNS, "beschreibung"); Element descriptionElement = (Element)descriptionElements.item(0); // Beschreibung entfernen und neu erstellen root.removeChild(descriptionElement); descriptionElement = doc.createElementNS(docNS, "beschreibung"); Text descriptionText = doc.createTextNode(description); descriptionElement.appendChild(descriptionText); root.appendChild(descriptionElement); } catch (SAXException e) { // Fehler ausgeben PrintWriter out = res.getWriter( ); res.setContentType("text/html"); out.println("<HTML><BODY>Fehler beim Lesen von XML: " + e.getMessage( ) + ".</BODY></HTML>"); out.close( ); return; } } // DOM-Baum serialisieren DOMSerializer serializer = new DOMSerializer( ); serializer.serialize(doc, xmlFile); // Bestätigung ausgeben PrintWriter out = res.getWriter( ); res.setContentType("text/html"); out.println("<HTML><BODY>Danke für Ihre Übermittlung. " + "Ihr Artikel wurde verarbeitet.</BODY></HTML>"); out.close( ); }
Die Verwendung der Methode createElementNS( ), um Elemente mit Namensraum zu erzeugen, und die Suche nach ihnen mit getElementsByTagNameNS( ) scheint perfekt zu sein. In der Methode createDocument( ) ist sogar praktischerweise eine Stelle vorgesehen, um die Namensraum-URI für das Wurzelelement einzusetzen. Diese Elemente werden alle in den Standard-Namensraum eingefügt, und alles sieht gut aus. Allerdings gibt es hier ein großes Problem. Schauen Sie sich die Ausgabe dieses Servlets an, nachdem es ohne existierendes XML gestartet wurde (es geht also um neu erzeugtes und nicht um modifiziertes XML):
<?xml version="1.0" encoding="ISO-8859-1"?> <artikel id="bourgOM"> <name>Bourgeois OM-Gitarre</name> <beschreibung>Dies ist eine <i>wunderschöne</i> Gitarre mit <b>Sitka-Deckel </b> und -Rücken und Seiten aus <b>indischem Rosenholz</b>. Diese vom Gitarrenbaumeister <a href="http://www.bourgeoisguitars.com">Dana Bourgeois</a> gebaute OM hat einen sehr <b>vollen Klang</b>. Die Gitarre besitzt ein <i>großartiges Spielverhalten</i>, einen 1 3/4"-Kopf, mit Kopf und Sattel aus <i>fossilem Elfenbein</i> und Schrauben aus <i>Ebenholz</i>. Neuwertiger Zustand, dies ist eine <b>hervorragende Gitarre</b>!</beschreibung> </artikel>
Erscheint Ihnen das vertraut? Es ist das XML von vorhin, ohne Änderungen! Was DOM beim besten Willen nicht tut, ist Namensraum-Deklarationen hinzuzufügen. Statt dessen müssen Sie das Attribut xmlns von Hand in Ihren DOM-Baum einfügen; andernfalls werden die Elemente nicht in einem Namensraum plaziert, und Sie bekommen Probleme. Eine kleine Änderung kümmert sich aber darum:
// Neuen DOM-Baum erzeugen
DOMImplementation domImpl = new DOMImplementationImpl( );
doc = domImpl.createDocument(docNS, "artikel", null);
Element root = doc.getDocumentElement( );
root.setAttribute("xmlns", docNS);
Nun erhalten Sie die Namensraum-Deklaration, die Sie wahrscheinlich schon im vorigen Durchgang erwartet haben. Sie können diese Änderungen kompilieren und es ausprobieren. Sie werden keinen Unterschied bemerken; genau wie vorher werden Änderungen durchgeführt. Ihre Dokumente sollten jedoch jetzt Namensräume aufweisen, sowohl im lesenden als auch im schreibenden Teil der Servlet-Anwendung.
Ein letztes Wort zu diesem Namensraum-Detail: Merken Sie sich, daß Sie die Klasse DOMSerializer durchaus so modifizieren könnten, daß sie auf die Namensräume der Elemente achtet und die entsprechenden xmlns-Deklarationen ausgibt, während sie den Baum durchwandert. Das ist eine vollkommen zulässige Änderung und wäre in gewisser Hinsicht nützlich; in der Tat führen viele Lösungen, wie die in Xerces eingebauten, sie bereits durch. Jedenfalls sind Sie davor geschützt, diesem Verhalten zum Opfer zu fallen, solange Sie sich seiner bewußt sind.
DOM Level 2-Module
Nachdem Sie nun gesehen haben, was für Möglichkeiten das Kernangebot von DOM und Level 2 zur Verfügung stellt, werde ich über einige Zusätze zu DOM Level 2 sprechen. Es handelt sich um die verschiedenen Module, die Funktionalität zum Kern hinzufügen. Sie sind von Zeit zu Zeit in gewissen DOM-Anwendungen nützlich.
Zunächst jedoch müssen Sie einen DOM Level 2-Parser zur Verfügung haben. Wenn Sie einen Parser verwenden, den Sie selbst gekauft oder heruntergeladen haben, ist das recht einfach. Zum Beispiel können Sie die Apache-XML-Website unter http://xml.apache.org aufsuchen, die neueste Version von Xerces herunterladen, und schon haben Sie DOM Level 2.
Wenn Sie jedoch einen Parser verwenden, der zusammen mit anderer Technologie geliefert wurde, kann das Ganze ein wenig komplizierter sein. Wenn Sie zum Beispiel die Tomcat-Servlet-Engine von Jakarta haben, werden Sie xml.jar und parser.jar im Verzeichnis lib/ und im Tomcat-Klassenpfad finden. Das ist nicht besonders gut, da es sich um Implementierungen von DOM Level 1 handelt und viele Features, über die ich in diesem Abschnitt rede, nicht unterstützt werden; laden Sie in diesem Fall einen DOM Level 2-Parser von Hand herunter, und stellen Sie sicher, daß er vor irgendeinem DOM Level 1-Parser geladen wird.
Vorsicht vor neueren Versionen von Tomcat. Diese tun etwas vermeintlich Praktisches: Sie laden alle jar-Dateien aus dem Verzeichnis lib/ schon beim Start. Unglücklicherweise wird dies alphabetisch erledigt, so daß parser.jar, ein DOM Level 1-Parser, auch dann noch zuerst geladen wird, wenn Sie xerces.jar in das Verzeichnis lib/ verschieben – Sie erhalten noch immer keine DOM Level 2-Unterstützung. Ein gängiger Trick, um dieses Problem zu lösen, besteht darin, die Dateien umzubenennen: parser.jar wird z_parser.jar und xml.jar wird z_xml.jar. Das führt dazu, daß sie nach Xerces geladen werden, und so erhalten Sie Unterstützung für DOM Level 2. Dieses Problem habe ich schon früher im Servlet-Beispiel erwähnt.
Nachdem Sie einen geeigneten Parser haben, sind Sie startklar. Bevor wir uns jedoch in die einzelnen Module vertiefen, möchte ich Ihnen einen allgemeingültigeren Überblick darüber verschaffen, worum es bei diesen Modulen eigentlich geht.
Verzweigungen
Als die DOM Level 1-Spezifikation herauskam, handelte es sich um eine einzelne Spezifikation. Sie wurde im wesentlichen so definiert, wie Sie im Kapitel DOM gelesen haben, mit einigen kleineren Ausnahmen. Als jedoch die Arbeit an DOM Level 2 begann, resultierte daraus eine ganze Menge weiterer Spezifikationen, die alle als Module bezeichnet werden. Wenn Sie sich den vollständigen Satz der DOM Level 2-Spezifikationen anschauen, werden Sie sechs verschiedene Module in der Liste finden. Das scheint ziemlich viel zu sein, oder? Ich werde nicht alle diese Module behandeln; Sie würden sonst die nächsten vier oder fünf Kapitel nur über DOM lesen. Allerdings gebe ich Ihnen eine Grundvorstellung von der Aufgabe jedes Moduls, zusammengefaßt in Tabelle 6-1. Ich habe die Spezifikation, den Namen und die Aufgabe jedes Moduls eingetragen, das Sie in Kürze benutzen werden.
Spezifikation | Modulname | Zusammenfassung der Aufgabe |
---|---|---|
DOM Level 2 Core | XML | Erweitert die DOM Level 1-Spezifikation; behandelt grundlegende DOM-Strukturen wie Element, Attr, Document usw. |
DOM Level 2 Views | Views | Bietet ein Modell für Skripten zur dynamischen Aktualisierung einer DOM-Struktur |
DOM Level 2 Events | Events | Definiert ein Event-Modell für Programme und Skripten zur Verwendung mit DOM |
DOM Level 2 Style | CSS | Stellt ein Modell für CSS (Cascading Style Sheets) zur Verfügung, das auf den Spezifikationen DOM Core und DOM Views basiert |
DOM Level 2 Traversal and Range | Traversal/Range | Definiert Erweiterungen von DOM für das schrittweise Durchgehen eines Dokuments und die Identifikation des Inhaltsbereichs in diesem Dokument |
DOM Level 2 HTML | HTML | Erweitert DOM so, daß es Schnittstellen für den Umgang mit HTML-Strukturen in einem DOM-Format zur Verfügung stellt |
Würden sich Views, Events, CSS, HTML und Traversal alle in einer einzelnen Spezifikation befinden, dann würde beim W3C nie etwas zu Ende gebracht werden! Um die Weiterentwicklung all dieser Bereiche zu fördern und dabei DOM in seinem Gesamtprozeß nicht zu behindern, wurden die unterschiedlichen Konzepte in einzelne Spezifikationen unterteilt.
Nachdem Sie sich entschieden haben, welche Spezifikationen Sie nutzen möchten, können Sie schon fast loslegen. Es wird kein DOM Level 2-Parser benötigt, um jede dieser Spezifikationen zu unterstützen; daraus ergibt sich, daß Sie prüfen müssen, ob die Features, die Sie nutzen möchten, in Ihrem XML-Parser vorhanden sind. Glücklicherweise ist das ziemlich einfach möglich.
Erinnern Sie sich an die Methode hasFeature( ) der Klasse DOMImplementation, die ich Ihnen gezeigt habe? Nun, wenn Sie dieser Methode einen Modulnamen und eine Version übergeben, wird sie Sie wissen lassen, ob das entsprechende Modul und die gewünschte Methode unterstützt werden. Beispiel 6-4 ist ein kleines Programm, das die Unterstützung eines XML-Parsers für die DOM-Module abfragt, die in Tabelle 6-1 aufgelistet sind. Sie werden den Namen der DOMImplementation-Implementierungsklasse auf den von Ihrem Hersteller verwendeten Namen ändern müssen, aber bis auf diese Anpassung sollte es mit jedem Parser funktionieren.
package javaxml2; import org.w3c.dom.DOMImplementation; public class DOMModuleChecker { /** DOMImplementation Impl.-Klasse des Herstellers */ private String vendorImplementationClass = "org.apache.xerces.dom.DOMImplementationImpl"; /** Zu prüfende Module */ private String[] moduleNames = {"XML", "Views", "Events", "CSS", "Traversal", "Range", "HTML"}; public DOMModuleChecker( ) { } public DOMModuleChecker(String vendorImplementationClass) { this.vendorImplementationClass = vendorImplementationClass; } public void check( ) throws Exception { DOMImplementation impl = (DOMImplementation)Class.forName(vendorImplementationClass) .newInstance( ); for (int i=0; i<moduleNames.length; i++) { if (impl.hasFeature(moduleNames[i], "2.0")) { System.out.println("Unterstützung für " + moduleNames[i] + " ist in dieser DOM-Implementierung enthalten."); } else { System.out.println("Unterstützung für " + moduleNames[i] + " ist in dieser DOM-Implementierung nicht enthalten."); } } } public static void main(String[] args) { if ((args.length != 0) && (args.length != 1)) { System.out.println("Verwendung: java javaxml2.DOMModuleChecker " + "[Zu prüfende DOMImplementation-Implementierungsklasse]"); System.exit(-1); } try { DOMModuleChecker checker = null; if (args.length == 1) { checker = new DOMModuleChecker(args[1]); } else { checker = new DOMModuleChecker( ); } checker.check( ); } catch (Exception e) { e.printStackTrace( ); } } }
Ich habe dieses Programm mit xerces.jar in meinem Klassenpfad gestartet und erhielt die folgende Ausgabe:
C:\javaxml2\build>java javaxml2.DOMModuleChecker Unterstützung für XML ist in dieser DOM-Implementierung enthalten. Unterstützung für Views ist in dieser DOM-Implementierung nicht enthalten. Unterstützung für Events ist in dieser DOM-Implementierung enthalten. Unterstützung für CSS ist in dieser DOM-Implementierung nicht enthalten. Unterstützung für Traversal ist in dieser DOM-Implementierung enthalten. Unterstützung für Range ist in dieser DOM-Implementierung nicht enthalten. Unterstützung für HTML ist in dieser DOM-Implementierung nicht enthalten.
Indem Sie die DOMImplementation-Implementierungsklasse für Ihren Hersteller angeben, können Sie die unterstützten Module in Ihrem eigenen DOM-Parser überprüfen. In den folgenden Unterabschnitten werde ich einige der Module ansprechen, die mir nützlich erscheinen und über die Sie bestimmt auch etwas erfahren möchten.
Traversal
Als erstes haben wir das DOM Level 2-Modul Traversal auf der Liste. Seine Aufgabe besteht darin, die Fähigkeit zur freien Bewegung zur Verfügung zu stellen, aber auch darin, Ihnen die Möglichkeit zu bieten, sein Verhalten genauer einzustellen. In dem früheren Abschnitt über die DOM-Mutation habe ich erwähnt, daß der größte Teil Ihres DOM-Codes über die Struktur des DOM-Baums, an dem gerade gearbeitet wird, informiert ist; dies ermöglicht ein schnelles Durcharbeiten und Modifizieren sowohl der Struktur als auch des Inhalts. In den Fällen jedoch, in denen Sie die Struktur des Dokuments nicht kennen, kommt das Traversal-Modul ins Spiel.
Betrachten Sie noch einmal die Auktionssite und die vom Anwender eingegebenen Artikel. Am kritischsten sind der Name und die Beschreibung des Artikels. Da die bekanntesten Auktionssites verschiedene Arten von Suchfunktionen anbieten, sollten Sie in diesem fiktiven Beispiel das gleiche bieten. Die einfache Suche nach Artikelbezeichnungen reicht im wirklichen Leben nicht aus; statt dessen sollte ein Satz von Schlüsselwörtern aus der Artikelbeschreibung extrahiert werden. Ich sage ausdrücklich Schlüsselwörter, weil Sie nicht nach »adirondack top« (für einen Gitarrenliebhaber ist es eindeutig das Holz auf der Oberseite des Gitarrenkorpus) suchen, um Informationen über Spielzeug (»top« = Kreisel) aus einer bestimmten Gebirgsregion (»Adirondack«) zu erhalten.
Die beste Möglichkeit, dies in dem bisher besprochenen Format zu erreichen, besteht darin, die Wörter zu extrahieren, die auf eine bestimmte Art und Weise hervorgehoben sind. Deshalb sind die fett oder kursiv gesetzten Wörter ideale Kandidaten. Natürlich könnten Sie auch einfach sämtliche Kindelemente des description-Elements herausfischen, bei denen es sich nicht um Text handelt. Allerdings müßten Sie sich dann durch Links (das Element a), Bildreferenzen (img) usw. kämpfen. Was Sie wirklich benötigen, ist die Festlegung einer benutzerdefinierten Art des Durchgangs. Gute Neuigkeiten; Sie sind hier genau richtig.
Das gesamte Traversal-Modul befindet sich im Package org.w3c.dom.traversal. So wie im Kern von DOM alles mit einem Document-Interface anfängt, beginnt in DOM-Traversal alles mit dem Interface org.w3c.dom.traversal.DocumentTraversal. Dieses Interface bietet zwei Methoden:
NodeIterator createNodeIterator(Node root, int whatToShow, NodeFilter filter, boolean expandEntityReferences); TreeWalker createTreeWalker(Node root, int whatToShow, NodeFilter filter, boolean expandEntityReferences);
Die meisten DOM-Implementierungen, die Traversal unterstützen, richten auch ihre Entsprechung der org.w3c.dom.Document-Implementierungsklasse so ein, daß sie das Interface DocumentTraversal implementiert; so funktioniert es jedenfalls in Xerces. Kurz gesagt stellt die Verwendung eines NodeIterators eine Listenansicht der Elemente zur Verfügung, die er iterativ durchgeht; die genaueste Entsprechung ist eine Standard-Java-List (im Package java.util). TreeWalker bietet eine Baumansicht, an die Sie bei der Arbeit mit XML bisher wahrscheinlich eher gewöhnt sind.
NodeIterator
Ich möchte die Begriffsbestimmungen jetzt hinter mir lassen und zu dem Codebeispiel kommen, das ich zuvor angekündigt hatte. Ich will auf diejenigen Inhalte innerhalb der Beschreibung eines Artikels der Auktionssite zugreifen können, die sich innerhalb bestimmter Formatierungs-Tags befinden. Um dies zu erreichen, benötige ich als erstes Zugriff auf den DOM-Baum selbst. Da dies nicht dem Servlet-Ansatz entspricht (Sie würden wahrscheinlich nicht wollen, daß ein Servlet die Suchausdrücke aufbaut, sondern lieber eine selbständige Klasse verwenden), benötige ich eine neue Klasse, ItemSearcher (Beispiel 6-5). Diese Klasse nimmt als Argumente eine beliebige Anzahl von Artikeldateien entgegen, die durchsucht werden sollen.
package javaxml2; import java.io.File; // DOM-Imports import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.traversal.DocumentTraversal; import org.w3c.dom.traversal.NodeFilter; import org.w3c.dom.traversal.NodeIterator; // Herstellerspezifischer Parser import org.apache.xerces.parsers.DOMParser; public class ItemSearcher { private String docNS = "http://www.oreilly.com/javaxml2"; public void search(String filename) throws Exception { // Parsing in einem DOM-Baum File file = new File(filename); DOMParser parser = new DOMParser( ); parser.parse(file.toURL().toString( )); Document doc = parser.getDocument( ); // Knoten erhalten, bei dem der Durchgang starten soll Element root = doc.getDocumentElement( ); NodeList descriptionElements = root.getElementsByTagNameNS(docNS, "beschreibung"); Element description = (Element)descriptionElements.item(0); // Einen NodeIterator erhalten NodeIterator i = ((DocumentTraversal)doc) .createNodeIterator(description, NodeFilter.SHOW_ALL, null, true); Node n; while ((n = i.nextNode( )) != null) { if (n.getNodeType( ) == Node.ELEMENT_NODE) { System.out.println("Gefundenes Element: '" + n.getNodeName( ) + "'"); } else if (n.getNodeType( ) == Node.TEXT_NODE) { System.out.println("Gefundener Text: '" + n.getNodeValue( ) + "'"); } } } public static void main(String[] args) { if (args.length == 0) { System.out.println("Keine Artikeldateien zum Durchsuchen angegeben."); return; } try { ItemSearcher searcher = new ItemSearcher( ); for (int i=0; i<args.length; i++) { System.out.println("Bearbeite Datei: " + args[i]); searcher.search(args[i]); } } catch (Exception e) { e.printStackTrace( ); } } }
Wie Sie sehen können, habe ich einen NodeIterator erzeugt und ihm das Element beschreibung übergeben, bei dem der Durchgang starten soll. Der konstante Wert, der als Filter übergeben wird, weist den Iterator an, alle Knoten anzuzeigen. Sie könnten genauso einfach Werte wie Node.SHOW_ELEMENT oder Node.SHOW_TEXT angeben, die nur Elemente bzw. nur Textknoten zeigen würden. Ich habe bisher noch keine NodeFilter-Implementierung angegeben (dazu komme ich als nächstes), und ich habe die Auflösung von Entity-Referenzen zugelassen. An alledem ist angenehm, daß der Iterator, nachdem er erstellt wurde, nicht nur die Kindelemente von beschreibung findet. Statt dessen findet er alle Knoten unterhalb von beschreibung, auch wenn sie mehrere Stufen tief verschachtelt sind. Das ist äußerst praktisch bei der Arbeit mit einer unbekannten XML-Struktur!
An dieser Stelle erhalten Sie noch immer alle Knoten, von denen Sie die meisten gar nicht brauchen. Ich habe etwas Code hinzugefügt (die letzte while-Schleife), um Ihnen zu zeigen, wie die resultierenden Element- und Textknoten ausgegeben werden. Sie können den Code in diesem Zustand starten, aber das wird nicht viel helfen. Statt dessen muß der Code einen Filter bereitstellen, so daß er nur Elemente mit der gewünschten Formatierung akzeptiert: den Text innerhalb von i- oder b-Blöcken. Sie können dieses angepaßte Verhalten zur Verfügung stellen, indem Sie eine selbstdefinierte Implementierung des Interfaces NodeFilter hinzufügen, das nur eine einzige Methode definiert:
public short acceptNode(Node n);
Diese Methode sollte NodeFilter.FILTER_SKIP, NodeFilter.FILTER_REJECT oder NodeFilter.FILTER_ACCEPT zurückgeben. Der erste dieser Rückgabewerte überspringt den untersuchten Knoten, aber fährt in der Iteration mit dessen Kindknoten fort; der zweite verwirft den untersuchten Knoten und dessen Kindknoten (nur in TreeWalker anwendbar); und der dritte akzeptiert den untersuchten Knoten und gibt ihn weiter. Das Ganze erinnert stark an SAX, da Sie in die Iteration einzelner Knoten eingreifen und jeweils entscheiden können, ob sie an die aufrufende Methode weitergegeben werden sollen. Fügen Sie die folgende nicht-öffentliche Klasse zu der Quelldatei ItemSearcher.java hinzu:
class FormattingNodeFilter implements NodeFilter { public short acceptNode(Node n) { if (n.getNodeType( ) == Node.TEXT_NODE) { Node parent = n.getParentNode( ); if ((parent.getNodeName( ).equalsIgnoreCase("b")) || (parent.getNodeName( ).equalsIgnoreCase("i"))) { return FILTER_ACCEPT; } } // Wenn wir hier angekommen sind: kein Interesse return FILTER_SKIP; } }
Dies ist einfach klarer, herkömmlicher DOM-Code und sollte Ihnen keinerlei Schwierigkeiten bereiten. Zuerst einmal akzeptiert der Code nur Textknoten; der Text der formatierten Elemente wird benötigt, nicht die Elemente selbst. Als nächstes wird der Elternknoten ermittelt, und da wir mit Sicherheit davon ausgehen können, daß Text-Knoten Element-Knoten als Eltern haben, ruft der Code unmittelbar getNodeName( ) auf. Wenn der Elementname entweder »b« oder »i« ist, hat der Code Suchtext gefunden und gibt FILTER_ACCEPT zurück. Andernfalls wird FILTER_SKIP zurückgegeben.
Jetzt müssen wir nur noch eine Änderung am Erzeugungsaufruf des Iterators vornehmen, die diesen anweist, die neue Filter-Implementierung zu verwenden, und eine weitere Änderung an der Ausgabe. Beide Änderungen erfolgen in der existierenden Methode search( ) der Klasse ItemSearcher:
// Einen NodeIterator erhalten
NodeIterator i = ((DocumentTraversal)doc)
.createNodeIterator(description, NodeFilter.SHOW_ALL,
new FormattingNodeFilter( ), true);
Node n;
while ((n = i.nextNode( )) != null) {
System.out.println("Suchbegriff gefunden: '" + n.getNodeValue( ) + "'");
}
Einige aufmerksame Leser werden sich fragen, was passiert, wenn eine NodeFilter-Implementierung mit der Konstante in Konflikt gerät, die der Methode createNodeIterator( ) übergeben wird (in diesem Fall ist diese Konstante NodeFilter.SHOW_ALL). Tatsächlich wird der einfache Konstantenfilter zuerst angewendet, und dann wird die resultierende Liste von Knoten an die Filter-Implementierung weitergegeben. Wenn ich die Konstante NodeFilter.SHOW_ELEMENT angegeben hätte, hätte ich gar keine Suchbegriffe erhalten, da mein Filter keine Text-Knoten zum Untersuchen erhalten hätte, sondern nur Element-Knoten. Passen Sie auf, daß Sie die beiden Filter auf eine Weise zusammen verwenden, die Sinn ergibt. In diesem Beispiel hätte ich auch gefahrlos NodeFilter.SHOW_TEXT verwenden können.
Nun ist die Klasse nützlich und startklar. Wenn ich sie mit der Datei bourgOM.xml ausführe, die ich im ersten Abschnitt beschrieben habe, erhalte ich die folgenden Ergebnisse:
bmclaugh@GANDALF ~/javaxml2/build $ java javaxml2.ItemSearcher ../ch06/xml/item-bourgOM.xml Bearbeite Datei: ../ch06/xml/item-bourgOM.xml Suchbegriff gefunden: 'wunderschöne' Suchbegriff gefunden: 'Sitka-Deckel' Suchbegriff gefunden: 'indischem Rosenholz' Suchbegriff gefunden: 'vollen Klang' Suchbegriff gefunden: 'großartiges Spielverhalten' Suchbegriff gefunden: 'fossilem Elfenbein' Suchbegriff gefunden: 'Ebenholz' Suchbegriff gefunden: 'hervorragende Gitarre'
Das ist perfekt: Alle fett und kursiv gesetzten Begriffe können nun zu einer Suchfunktion hinzugefügt werden. (Entschuldigung, aber die müssen Sie selbst schreiben!)
TreeWalker
Das Interface TreeWalker ist fast genau dasselbe wie das Interface NodeIterator. Der einzige Unterschied besteht darin, daß Sie eine Baumansicht statt einer Listenansicht erhalten. Dies ist vor allem dann nützlich, wenn Sie nur mit einer bestimmten Sorte von Knoten innerhalb eines Baums arbeiten möchten; zum Beispiel wenn Sie einen Baum nur mit Elementen oder ohne alle Kommentare haben wollen.
Durch die Verwendung des konstanten Filterwertes (wie NodeFilter.SHOW_ELEMENT) und einer Filter-Implementierung (etwa einer, die FILTER_SKIP für alle Kommentare zurückgibt) können Sie im wesentlichen eine Ansicht eines DOM-Baums ohne störende Informationen erhalten. Das Interface TreeWalker bietet alle grundlegenden Knotenoperationen, wie firstChild( ), parentNode( ), nextSibling( ) und natürlich die Methode getCurrentNode( ), die Ihnen mitteilt, wo Sie sich gerade befinden.
Ich werde Ihnen hier kein Beispiel geben. Mittlerweile sollten Sie verstehen, daß dies dem Umgang mit einem Standard-DOM-Baum entspricht, außer, daß Sie unerwünschte Bestandteile durch die Verwendung der NodeFilter-Konstanten herausfiltern können. Dies ist eine großartige, einfache Art und Weise, Ihre Ansicht von XML-Dokumenten auf die Informationen zu beschränken, die Sie sehen möchten. Verwenden Sie dieses Interface ruhig; es ist ein echter Gewinn, genau wie NodeIterator! Sie können die vollständige Spezifikation auch online unter http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ lesen.
Range
Das DOM Level 2-Modul Range gehört zu den weniger häufig genutzten Modulen, nicht weil es so schwer zu verstehen wäre, sondern weil es nicht besonders nützlich ist. Dieses Modul bietet ein Verfahren, um einen Block von Inhalten innerhalb eines Dokuments zu bearbeiten. Nachdem Sie diesen Inhaltsbereich definiert haben, können Sie in ihn einfügen, ihn kopieren, Teile davon löschen und ihn auf vielfältige Art und Weise manipulieren. Das Wichtigste ist für den Anfang zu verstehen, daß »Range« oder »Bereich« hier eine beliebige Anzahl zusammengruppierter Teile eines DOM-Baums beschreibt. Es bezieht sich nicht auf einen Satz erlaubter Werte, bei dem ein Maximum und ein Minimum oder ein Anfangs- und ein Endwert definiert sind. Insofern hat DOM-Range überhaupt nichts mit der Validierung von Datenwerten zu tun. Wenn Sie das begriffen haben, liegen Sie schon weit über dem Durchschnitt.
Wie Traversal schließt auch die Arbeit mit Range ein neues DOM-Package ein: org.w3c.dom.ranges. Es gibt in der Tat nur zwei Interfaces und eine Exception in dieser Klasse, es wird also nicht lange dauern, bis Sie sich zurechtfinden. Das erste Interface ist die Entsprechung zu Document (und DocumentTraversal): Das ist org.w3c.dom.ranges.DocumentRange. Wie die Klasse DocumentTraversal implementiert die Document-Implementierungsklasse von Xerces Range. Und genau wie DocumentTraversal hat sie sehr wenige interessante Methoden; genau genommen sogar nur eine:
public Range createRange( );
Alle anderen Bereichsoperationen arbeiten mit der Klasse Range (genauer gesagt einer Implementierung dieses Interfaces, aber Sie verstehen schon). Nachdem Sie eine Instanz des Interfaces Range erzeugt haben, können Sie einen Start- und einen Endpunkt festlegen und mit dem Editieren anfangen. Für ein Beispiel möchte ich noch einmal zu dem Update-ItemServlet zurückkehren.
Ich habe schon erwähnt, daß es ein wenig Mühe bereitet zu versuchen, alle Kindelemente des Elements beschreibung zu entfernen und dann den neuen Beschreibungstext einzustellen; das kommt daher, daß es keine Möglichkeit gibt zu unterscheiden, ob sich nur ein einzelner Text-Knoten in der Beschreibung befindet oder ob viele Elemente, Textknoten und verschachtelte Knoten in der Beschreibung existieren, die zuerst einmal HTML ist. Ich habe Ihnen gezeigt, wie Sie das alte Element beschreibung einfach entfernen und ein neues erzeugen können. DOM-Range macht dies jedoch unnötig. Schauen Sie sich die folgende Modifikation der Methode doPost( ) in diesem Servlet an:
// Dokument laden try { DOMParser parser = new DOMParser( ); parser.parse(xmlFile.toURL().toString( )); doc = parser.getDocument( ); Element root = doc.getDocumentElement( ); // Name des Artikels NodeList nameElements = root.getElementsByTagNameNS(docNS, "name"); Element nameElement = (Element)nameElements.item(0); Text nameText = (Text)nameElement.getFirstChild( ); nameText.setData(name); // Beschreibung des Artikels NodeList descriptionElements = root.getElementsByTagNameNS(docNS, "beschreibung"); Element descriptionElement = (Element)descriptionElements.item(0); // Beschreibung entfernen und neu erstellen Range range = ((DocumentRange)doc).createRange( ); range.setStartBefore(descriptionElement.getFirstChild( )); range.setEndAfter(descriptionElement.getLastChild( )); range.deleteContents( ); Text descriptionText = doc.createTextNode(description); descriptionElement.appendChild(descriptionText); range.detach( ); } catch (SAXException e) { // Fehler ausgeben PrintWriter out = res.getWriter( ); res.setContentType("text/html"); out.println("<HTML><BODY>Fehler beim Lesen von XML: " + e.getMessage( ) + ".</BODY></HTML>"); out.close( ); return; }
Um sämtlichen Inhalt zu entfernen, erzeuge ich als erstes einen neuen Range, indem ich ein Casting zum Typ DocumentRange durchführe. Sie müssen auch Import-Anweisungen für die Klassen DocumentRange und Range zu Ihrem Servlet hinzufügen (beide befinden sich im Package org.w3c.dom.ranges).
Im ersten Teil des Abschnitts über DOM Level 2-Module habe ich Ihnen gezeigt, wie Sie überprüfen können, welche Module eine Parser-Implementierung unterstützt. Mir fällt auf, daß Xerces gemeldet hat, er unterstütze Range nicht. Dennoch ist dieser Code mit Xerces 1.3.0, 1.3.1 und 1.4 jeweils völlig reibungslos gelaufen. Seltsam, nicht wahr?
Nachdem das Range-Objekt bereit ist, setzen Sie einen Start- und einen Endpunkt. Da ich sämtlichen Inhalt innerhalb des Elements beschreibung brauche, beginne ich vor dem ersten Kindelement dieses Element-Knotens (mit Hilfe von setStartBefore( )) und höre nach seinem letzten Kind auf (durch setEndAfter( )). Nachdem dies erledigt ist, kann einfach deleteContents( ) aufgerufen werden. Entsprechend bleibt kein bißchen Inhalt zurück. Anschließend erzeugt das Servlet den neuen Beschreibungstext und füllt ihn mit Inhalt. Zu guter Letzt weise ich die JVM an, daß sie durch den Aufruf von detach( ) sämtliche mit dem Range verbundenen Ressourcen freigeben kann. Auch wenn dieser Schritt üblicherweise vergessen wird, kann er bei längeren Codeteilen, die zusätzliche Ressourcen verwenden, wirklich weiterhelfen.
Eine weitere Option besteht darin, extractContents( ) statt deleteContents( ) zu verwenden. Diese Methode entfernt den Inhalt ebenfalls, gibt dann aber den entfernten Inhalt zurück. Sie könnten dies zum Beispiel als Archivierungselement einfügen:
// Beschreibung entfernen und neu erstellen Range range = ((DocumentRange)doc).createRange( ); range.setStartBefore(descriptionElement.getFirstChild( )); range.setEndAfter(descriptionElement.getLastChild( )); Node oldContents = range.extractContents( ); Text descriptionText = doc.createTextNode(description); descriptionElement.appendChild(descriptionText); // Den alten Inhalt zu einem anderen, einem Archivelement hinzufügen archivalElement.appendChild(oldContents);
Versuchen Sie das nicht in Ihrem Servlet; es gibt kein archivalElement in diesem Code, und das Beispiel dient nur Demonstrationszwecken. Aber was Ihnen davon im Bewußtsein bleiben sollte, ist, daß das DOM Level 2-Modul Range Ihnen beim Editieren von Dokumentinhalten helfen kann. Es bietet auch dann noch eine Möglichkeit, auf den Inhalt zuzugreifen, wenn Sie sich zum Zeitpunkt der Programmierung noch nicht über dessen Struktur im klaren sind.
Es gibt noch viel mehr mögliche Bereichsangaben in DOM; Sie können dies zusätzlich zu der Beschäftigung mit all den anderen DOM-Modulen, die in diesem Kapitel behandelt werden, selbst herausfinden. Allerdings sollten Sie jetzt die Grundlagen gut genug verstanden haben, um loslegen zu können. Insbesondere sollten Sie wissen, daß Sie an jedem Punkt in einer aktiven Range-Instanz einfach range.insertNode(Node newNode) aufrufen können, um neuen Inhalt hinzuzufügen – wo auch immer im Dokument Sie sich befinden! Genau diese robuste Editierfähigkeit macht die Bereichslösung so attraktiv.
Wenn Sie das nächste Mal eine Struktur, über die Sie wenig wissen, löschen, kopieren, extrahieren oder mit zusätzlichem Inhalt versehen müssen, sollten Sie darüber nachdenken, Bereiche zu verwenden. Die Spezifikation gibt Ihnen Informationen über all das und noch mehr. Sie befindet sich online unter http://www.w3.org/TR/DOM-Level-2-Traversal-Range/.
Events, Views und Style
Neben dem HTML-Modul, über das ich als nächstes sprechen werde, gibt es drei andere DOM Level 2-Module: Events, Views und Style. Ich werde sie in diesem Buch nicht tiefgreifend behandeln, insbesondere weil ich glaube, daß sie eher für die Client-Programmierung nützlich sind. Bisher habe ich mich auf die serverseitige Programmierung konzentriert und werde mich auch im Rest des Buches in diesem Bereich bewegen. Diese drei Module werden oft in Client-Software verwendet, etwa in IDEs, Webseiten und ähnlichem. Trotzdem möchte ich jedes davon kurz ansprechen; insofern werden Sie beim nächsten Treffen mit anderen Alpha-Geeks noch immer an der Spitze der DOM-Mannschaft stehen.
Events
Das Modul Events bietet genau das, was Sie wahrscheinlich erwarten: Eine Möglichkeit, nach Ereignissen in einem DOM-Dokument zu »lauschen«. Die entsprechenden Klassen sind im Package org.w3c.dom.events enthalten, und die Klasse, die das Ganze in Gang setzt, ist DocumentEvent. Hier gibt es keine Überraschung; kompatible Parser (wie Xerces) implementieren dieses Interface in der gleichen Klasse, die org.w3c.dom.Document implementiert. Das Interface definiert nur eine Methode:
public Event createEvent(String eventType);
Der übergebene String ist der Ereignistyp; gültige Werte in DOM Level 2 sind »UIEvent«, »MutationEvent« und »MouseEvent«. Jeder dieser drei Werte hat eine zugehörige Klasse: UIEvent, MutationEvent und MouseEvent. Wenn Sie in das Xerces-Javadoc hineinschauen, werden Sie bemerken, daß hier nur das Interface MutationEvent geboten wird, der einzige Ereignistyp, den Xerces unterstützt. Wenn ein Ereignis »ausgelöst« wird, kann es durch einen EventListener behandelt (oder »abgefangen«) werden.
Hierbei werden Sie vom DOM-Kern unterstützt; in einem Parser, der DOM-Events unterstützt, sollte eine Klasse, die das Interface org.w3c.dom.Node implementiert, auch das Interface org.w3c.dom.events.EventTarget implementieren. Auf diese Weise kann jeder Knoten Ziel eines Ereignisses sein. Das bedeutet, daß Sie bei diesen Knoten die folgende Methode zur Verfügung haben:
public void addEventListener(String type, EventListener listener, boolean capture);
Kommen wir nun zur Vorgehensweise. Sie können eine neue EventListener-Implementierung erzeugen (eine selbstdefinierte Klasse, die Sie schreiben müßten). Sie brauchen nur eine einzige Methode zu implementieren:
public void handleEvent(Event event);
Registrieren Sie diesen Listener mit allen Knoten, mit denen Sie arbeiten möchten. Code an dieser Stelle erledigt üblicherweise nützliche Aufgaben, zum Beispiel sendet er E-Mails an Benutzer, um ihnen mitzuteilen, daß ihre Informationen (in irgendeiner XML-Datei) geändert wurden, oder er validiert das XML erneut (denken Sie an XML-Editoren) oder fragt Benutzer, ob sie sicher sind, daß sie die Aktion durchführen möchten.
Gleichzeitig möchten Sie bei bestimmten Aktionen, daß Ihr Code ein neues Event-Objekt auslöst, etwa wenn der Anwender in einer IDE auf einen Knoten klickt und neuen Text eingibt oder ein anderes Element löscht. Wenn das Event ausgelöst wird, wird es an die verfügbaren EventListener-Klassen weitergegeben, angefangen beim aktiven Knoten und dann weiter nach oben. An dieser Stelle wird der Code Ihres Listeners ausgeführt, wenn die Ereignistypen gleich sind. Zusätzlich können Sie an dieser Stelle bestimmen, ob das Ereignis (nachdem Sie es behandelt haben) an der Weitergabe gehindert werden oder weiter nach oben in der Ereigniskette wandern soll, um möglicherweise noch von anderen registrierten Listenern behandelt zu werden.
Das war es also; wir haben Ereignisse auf etwas mehr als einer einzigen Seite abgehandelt! Und Sie haben gedacht, Spezifikationen wären schwierig zu lesen. Ernsthaft, dies ist sehr nützlicher Stoff, und wenn Sie mit clientseitigem Code arbeiten oder mit Software, die als in sich geschlossene Anwendung auf dem Desktop der Anwender verbreitet wird (wie der XML-Editor, von dem ich die ganze Zeit rede), sollte dies ein Teil Ihres DOM-Werkzeugkastens sein. Lesen Sie die vollständige Dokumentation online unter http://www.w3.org/TR/DOM-Level-2-Events/.
Views
Das nächste Modul auf der Liste ist das DOM Level 2-Modul Views. Der Grund, warum ich diese Ansichten nicht besonders detailliert behandle, ist, daß es wirklich sehr wenig darüber zu sagen gibt. Wie ich die (eine Seite lange!) Spezifikation lese, ist das Ganze einfach eine Basis für zukünftige Arbeit, vielleicht in vertikalen Märkten. Die Spezifikation definiert nur zwei Interfaces, die sich beide im Package org.w3c.dom.views befinden. Hier sehen Sie das erste:
package org.w3c.dom.views; public interface AbstractView { public DocumentView getDocument( ); }
Und hier das zweite:
package org.w3c.dom.views; public interface DocumentView { public AbstractView getDefaultView( ); }
Das sieht aus, als würde es im Kreis herum gehen, nicht wahr? Mit einem einzelnen Quelldokument (einem DOM-Baum) können mehrere Ansichten verknüpft sein. In diesem Fall bezeichnet »Ansicht« eine Präsentationsart, etwa ein mit Style versehenes Dokument (nachdem XSL oder CSS angewandt wurde) oder vielleicht eine Version mit Shockwave und eine weitere ohne. Durch die Implementierung des Interfaces AbstractView können Sie Ihre eigenen, selbstdefinierten Versionen der Darstellung eines DOM-Baums definieren. Sehen Sie sich zum Beispiel dieses abgeleitete Interface an:
package javaxml2; import org.w3c.dom.views.AbstractView; public interface StyledView extends AbstractView { public void setStylesheet(String stylesheetURI); public String getStylesheetURI( ); }
Ich habe die Methodenimplementierungen weggelassen, aber Sie können sehen, wie dies für die Bereitstellung mit Style versehener Ansichten eines DOM-Baums verwendet werden könnte. Zusätzlich würde eine kompatible Parser-Implementierung dafür sorgen, daß ihre org.w3c.dom.Document-Implementierung ihrerseits DocumentView implementiert, was es ihnen ermöglicht, ein Dokument nach dessen Standardansicht zu fragen. Es ist zu erwarten, daß Sie in einer späteren Version der Spezifikation in der Lage sein werden, mehrere Ansichten für ein Dokument zu registrieren und eine Ansicht bzw. mehrere Ansichten enger an ein Dokument zu binden.
Sie werden feststellen, daß dies allmählich mehr Substanz erhalten wird, da Browser wie Netscape, Mozilla und Internet Explorer diese Art von Ansichten für XML bieten. Zusätzlich können Sie die kurze Spezifikation online unter http://www.w3.org/TR/DOM-Level-2-Views/ lesen und wissen dann genau so viel wie ich.
Style
Als letztes betrachten wir das Modul Style, das auch einfach als CSS (Cascading Style Sheets) bezeichnet wird. Sie können diese Spezifikation unter http://www.w3.org/TR/DOM-Level-2-Style/ nachlesen. Dieses Modul bietet eine Anbindung für CSS-Stylesheets, so daß diese durch DOM-Konstrukte dargestellt werden. Alles Interessante befindet sich in den Packages org.w3c.dom.stylesheets und org.w3c.dom.css. Das erste enthält allgemeingültige Basisklassen und das zweite bietet spezifische Anwendungen von Cascading Style Sheets. Beide konzentrieren sich insbesondere darauf, einem Client ein mit Style versehenes Dokument zu zeigen.
Dieses Modul wird ganz genau so verwendet wie die DOM-Kern-Interfaces: Besorgen Sie sich einen Style-konformen Parser, führen Sie das Parsing eines Stylesheets durch, und verwenden Sie die CSS-Sprachbindungen. Das ist besonders praktisch, wenn Sie ein CSS-Stylesheet durch den Parser schicken und auf ein DOM-Dokument anwenden möchten. Sie arbeiten mit dem gleichen grundlegenden Satz von Konzepten, falls das für Sie einen Sinn ergibt (und das sollte es; wenn Sie mit einer API zwei Dinge statt einem tun können, ist das grundsätzlich gut!).
Auch das Style-Modul streife ich nur kurz, da es vollständig durch Javadoc dokumentiert ist. Die Klassen haben sehr treffende Namen (CSSValueList,
Rect,
CSSDOMImplementation) und sind ihren XML-DOM-Gegenstücken ähnlich genug, so daß ich zuversichtlich bin, daß Sie kein Problem haben werden, sie zu verwenden, wenn Sie das müssen.
HTML
Für HTML bietet DOM einen Satz von Interfaces, die die verschiedenen HTML-Elemente nachbilden. Zum Beispiel können Sie die Klassen HTMLDocument, HTMLAnchorElement und HTMLSelectElement verwenden (alle im Package org.w3c.dom.html), um ihre Entsprechungen in HTML darzustellen (in diesem Fall <html>, <a> und <selct>). Sie stellen allesamt bequeme Methoden zur Verfügung, wie setTitle( ) (bei HTMLDocument), setHref( ) (bei HTMLAnchorElement) und getOptions( ) (bei HTMLSelectElement). Sie alle erweitern DOM-Kernstrukturen wie Document und Element und können deshalb genau so verwendet werden wie jeder andere DOM-Knoten.
Es stellt sich jedoch heraus, daß die HTML-Bindungen (zumindest direkt) selten genutzt werden. Nicht etwa, weil sie nicht nützlich wären; statt dessen sind bereits viele Tools geschrieben worden, die diese Art des Zugriffs sogar auf noch benutzerfreundlichere Weise anbieten. XMLC, ein Projekt innerhalb des Enhydra Application Server Frameworks, ist ein Beispiel dafür (online unter http://xmlc.enhydra.org zu finden), ein anderes ist Cocoon, das im Kapitel Web Publishing Frameworks behandelt wird. Sie ermöglichen es Entwicklern, mit HTML und Webseiten auf eine Art und Weise zu arbeiten, die nicht einmal grundlegende DOM-Kenntnisse benötigt, und bieten Webdesignern und Neueinsteigern unter den Java-Entwicklern so einen leichteren Zugang.
Insgesamt ergibt sich aus der Verwendung solcher Tools, daß die HTML-DOM-Bindungen nur selten benötigt werden. Aber wenn Sie sie kennen, können Sie sie benutzen, wenn es nötig ist. Zusätzlich können Sie auf wohlgeformte HTML-Dokumente (XHTML) eine Standard-DOM-Funktionalität anwenden, indem Sie Elemente als Element-Knoten und Attribute als Attr-Knoten verwenden. Sogar ohne die HTML-Bindungen können Sie also DOM verwenden, um mit HTML zu arbeiten. Kinderleicht.
Dieses und jenes
Was bleibt neben diesen Modulen und der Namensraum-Fähigkeit noch über DOM Level 2 zu sagen? Sehr wenig, und das meiste davon haben Sie wahrscheinlich schon benutzt. Die Methoden createDocument( ) und createDocumentType( ) sind neu zur Klasse DOMImplementation hinzugekommen, und Sie haben sie beide verwendet. Außerdem sind die Methoden getSystemId( ) und getPublicId( ) ebenfalls DOM Level 2-Erweiterungen. Darüber hinaus gibt es da nicht mehr viel; z.B. einige neue DOMException-Fehlercode-Konstanten, und das war es beinahe. Sie können sich die vollständige Liste der Änderungen online unter http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/changes.html ansehen. Der Rest der Änderungen betrifft die zusätzlichen Module, von denen ich eins als nächstes behandeln werde.
DOM Level 3
Bevor ich das Thema DOM abschließe und zu der üblichen »Vorsicht Falle!«-Liste komme, werde ich ein wenig Zeit damit verbringen, Sie darüber zu informieren, was es in DOM Level 3, das gerade in der Entwicklung ist, neues geben wird. Genauer gesagt rechne ich damit, daß diese Spezifikation Anfang 2002 fertiggestellt sein wird, ungefähr zu der Zeit, zu der Sie wahrscheinlich dieses Buch lesen. Die Bestandteile, auf die ich hier eingehe, stellen nicht alle Änderungen und Erweiterungen in DOM Level 3 dar, aber es sind diejenigen, von denen ich glaube, daß sie von allgemeinem Interesse für die meisten DOM-Entwickler sind (dazu zählen Sie inzwischen auch, falls Sie sich das gefragt haben). Viele dieser Neuerungen gehören zu denen, die DOM-Programmierer sich schon seit mehreren Jahren wünschen, insofern können Sie sich auch auf sie freuen.
Die XML-Deklaration
Die erste Änderung in DOM, die ich herausstellen möchte, erscheint auf den ersten Blick ziemlich trivial: das Verfügbar-Machen der XML-Deklaration. Erinnern Sie sich? Hier ein Beispiel:
<?xml version="1.0" standalone="yes" encoding="UTF-8"?>
Es gibt hier drei wichtige Informationsaspekte, die zur Zeit in DOM noch nicht verfügbar sind: die Version, der Zustand des Attributs standalone und die Angabe der Codierung. Darüber hinaus besitzt der DOM-Baum selbst eine Codierung; diese kann mit dem XML-Attribut encoding übereinstimmen, muß es aber nicht tun. Zum Beispiel wird die »UTF-8« entsprechende Codierung in Java zu »UTF8«, und es sollte eine Möglichkeit geben, zwischen den beiden zu unterscheiden. Alle diese Probleme werden in DOM Level 3 gelöst, indem vier Attribute zum Interface Document hinzugefügt werden. Es handelt sich dabei um version (einen String), standalone (ein boolean), encoding (einen weiteren String) und actualEncoding (wiederum einen String). Die Zugriffs- und Änderungsmethoden, um diese Attribute zu modifizieren, sind keine Überraschung:
public String getVersion( ); public void setVersion(String version); public boolean getStandalone( ); public void setStandalone(boolean standalone); public String getEncoding( ); public void setEncoding(String encoding); public String getActualEncoding( ); public void setActualEncoding(String actualEncoding);
Das Wichtigste ist, daß Sie endlich in der Lage sein werden, auf die Informationen in der XML-Deklaration zuzugreifen. Das ist ein wirklicher Segen für alle, die XML-Editoren oder ähnliches schreiben und diese Informationen benötigen. Es hilft auch solchen Entwicklern, die an der Internationalisierung mit XML arbeiten, da sie die Codierung eines Dokuments (encoding) ermitteln, einen DOM-Baum mit seiner eigenen Codierung (actualEncoding) erzeugen und dann bei Bedarf übersetzen können.
Knotenvergleiche
In den Leveln 1 und 2 von DOM besteht die einzige Möglichkeit eines Vergleichs zweier Knoten darin, diesen manuell durchzuführen. Das führt letzten Endes dazu, daß Entwickler Hilfsmethoden schreiben, die mittels instanceof den Knotentyp ermitteln und dann alle verfügbaren Methodenwerte miteinander vergleichen. Es ist mit anderen Worten ein Krampf. DOM Level 3 bietet verschiedene Vergleichsmethoden, die diesen Krampf lindern. Ich teile Ihnen die vorläufigen Bezeichnungen mit und erläutere dann jede davon. Sie sind allesamt Ergänzungen zu dem Interface org.w3c.dom.Node und sehen folgendermaßen aus:
// Überprüfen, ob der übergebene Knoten dasselbe Objekt ist wie der aktuelle public boolean isSameNode(Node input); // Überprüfen auf Strukturgleichheit (nicht Objektgleichheit) public boolean equalsNode(Node input, boolean deep); /** Konstanten für die Dokumentenreihenfolge */ public static final int DOCUMENT_ORDER_PRECEDING = 1; public static final int DOCUMENT_ORDER_FOLLOWING = 2; public static final int DOCUMENT_ORDER_SAME = 3; public static final int DOCUMENT_ORDER_UNORDERED = 4; // Die Dokumentreihenfolge der Eingabe im Vergleich zum akt. Knoten ermitteln public int compareDocumentOrder(Node input) throws DOMException; /** Konstanten für die Baumposition */ public static final int TREE_POSITION_PRECEDING = 1; public static final int TREE_POSITION_FOLLOWING = 2; public static final int TREE_POSITION_ANCESTOR = 3; public static final int TREE_POSITION_DESCENDANT = 4; public static final int TREE_POSITION_SAME = 5; public static final int TREE_POSITION_UNORDERED = 6; // Die Baumposition der Eingabe im Vergleich zum akt. Knoten ermitteln public int compareTreePosition(Node input) throws DOMException;
Die erste dieser Methoden, isSameNode( ), ermöglicht Objektvergleiche. Es wird nicht ermittelt, ob die beiden Knoten die gleiche Struktur haben oder gleiche Daten beinhalten, sondern ob sie dasselbe Objekt in der JVM sind. Die zweite Methode, equalsNode( ), werden Sie in Ihren Anwendungen wahrscheinlich häufiger verwenden. Sie überprüft die Gleichheit von Node-Objekten bezüglich Daten und Typ (selbstverständlich wird ein Attr niemals gleich einem DocumentType sein). Sie stellt einen Parameter zur Verfügung, deep, der es ermöglicht, zwischen einem Vergleich nur des Nodes selbst oder aber einem Vergleich mitsamt all seinen Kind-Nodes zu wählen.
Die beiden nächsten Methoden, compareDocumentOrder( ) und compareTreePosition( ), ermöglichen es, die relative Position des aktuellen Nodes im Vergleich zu einem übergebenen Node zu ermitteln. Für beide Methoden sind einige Konstanten definiert, die als Rückgabewerte dienen. Ein Knoten kann im Dokument vor dem aktuellen Knoten, nach ihm, an der gleichen Position oder ohne Reihenfolge auftreten. Der Wert »ohne Reihenfolge« tritt auf, wenn ein Attribut mit einem Element verglichen wird oder auch in jedem anderen Fall, in dem der Begriff »Dokumentreihenfolge« keine Bedeutung hat. Und als letztes tritt eine DOMException auf, wenn die beiden Knoten, die überprüft werden, sich nicht im gleichen DOM-Document-Objekt befinden.
Die letzte neue Methode, compareTreePosition( ), bietet dieselbe Art des Vergleichs, fügt aber die Fähigkeit hinzu, Vorfahren und Nachkommen zu ermitteln. Zwei zusätzliche Konstanten, TREE_POSITION_ANCESTOR und TREE_POSITION_ DESCENDANT, machen dies möglich. Die erste gibt an, daß sich der übergebene Node vom Referenz-Node aus gesehen (also von demjenigen aus, dessen Methode aufgerufen wird) weiter oben in der Hierarchie befindet; die zweite zeigt dagegen an, daß der übergebene Node in der Hierarchie weiter unten liegt.
Mit Hilfe dieser vier Methoden können Sie jede beliebige DOM-Struktur isolieren und herausfinden, wie sie zu einer anderen steht. Diese Erweiterung in DOM Level 3 sollte Ihnen gute Dienste leisten, und Sie können sich darauf verlassen, all diese Vergleichsmethoden in Ihrem Code anwenden zu können. Behalten Sie aber sowohl die Namen als auch die Werte der Konstanten im Auge, da sie sich während der Entwicklung dieser Spezifikation noch ändern können.
Bootstrapping
Die letzte Erweiterung in DOM Level 3, die ich behandeln möchte, kann mit einer gewissen Berechtigung als die wichtigste bezeichnet werden: die Fähigkeit des Bootstrappings. Ich habe schon früher erwähnt, daß Sie beim Erzeugen von DOM-Strukturen auf die Verwendung von herstellerspezifischem Code angewiesen sind (es sei denn, Sie benutzen JAXP, das ich im gleichnamigen Kapitel JAXP behandeln werde). Das ist natürlich unerwünscht, da es die Herstellerunabhängigkeit verhindert. Zur Illustration wiederhole ich hier ein Codefragment, das ein DOM-Document-Objekt unter Verwendung einer DOMImplementation erzeugt:
import org.w3c.dom.Document; import org.w3c.dom.DOMImplementation; import org.apache.xerces.dom.DOMImplementationImpl; // Klassendeklaration und andere Java-Konstrukte DOMImplementation domImpl = DOMImplementationImpl.getDOMImplementation( ); Document doc = domImpl.createDocument( ); // Und so weiter...
Das Problem ist, daß es keine Möglichkeit gibt, eine DOMImplementation zu erhalten, ohne die Implementierungsklasse eines Herstellers zu importieren und zu benutzen. Die Lösung besteht darin, eine Factory zu verwenden, die DOMImplementation-Instanzen bereitstellt. Selbstverständlich ist das, was die Factory zur Verfügung stellt, eine Hersteller-Implementierung von DOMImplementation (ich weiß, ich weiß, das ist ein wenig verwirrend). Hersteller können Systemeigenschaften einstellen oder ihre eigenen Versionen dieser Factory anbieten, so daß sie die von ihnen gewünschte Implementierungsklasse zurückgibt. Der resultierende Code zur Erzeugung von DOM-Bäumen sieht dann so aus:
import org.w3c.dom.Document; import org.w3c.dom.DOMImplementation; import org.w3c.dom.DOMImplementationFactory; // Klassendeklaration und andere Java-Konstrukte DOMImplementation domImpl = DOMImplementationFactory.getDOMImplementation( ); Document doc = domImpl.createDocument( ); // Und so weiter...
Die hinzugefügte Klasse ist DOMImplementationFactory, und sie sollte die meisten Ihrer Schwierigkeiten mit der Herstellerabhängigkeit auf einmal beseitigen. Sie können diese Klasse als das Flaggschiff von DOM Level 3 ansehen, denn sie ist in den bisherigen Leveln von DOM einer der am häufigsten gewünschten Bestandteile.
Vorsicht Falle!
DOM besitzt einige besonders fehleranfällige Aspekte, genau wie SAX und genau wie die APIs, die wir in den nächsten paar Kapiteln behandeln werden. Ich werde Sie auf einige davon aufmerksam machen und Ihnen so hoffentlich einige Stunden an Debugging-Arbeit abnehmen, wenn Sie damit arbeiten. Genießen Sie es; dies waren nämlich Probleme, mit denen ich selbst konfrontiert wurde und gegen die ich eine Zeitlang kämpfen mußte, bevor ich das Ganze auf die Reihe bekommen habe.
Die gefürchtete WRONG DOCUMENT-Exception
Das Hauptproblem, das ich bei DOM-Entwicklern immer wieder beobachte, ist das, was ich »die gefürchtete WRONG DOCUMENT-Exception« nenne. Diese Exception tritt auf, wenn Sie versuchen, Knoten aus unterschiedlichen Dokumenten miteinander zu vermischen. Sie kommt am häufigsten vor, wenn Sie versuchen, einen Knoten von einem Dokument ins andere zu bewegen, was an und für sich eine häufige Aufgabe ist.
Das Problem kommt wegen des Factory-Ansatzes zustande, den ich schon weiter oben erwähnt habe. Da jedes Element, jedes Attribut, jede Processing Instruction und so weiter aus einer Document-Instanz heraus erzeugt wird, dürfen Sie nicht einfach voraussetzen, daß diese Knoten kompatibel zu anderen Document-Instanzen sind; zwei Instanzen von Document könnten von verschiedenen Herstellern mit unterschiedlichen unterstützten Features stammen, und der Versuch, Knoten aus dem einen Dokument mit Knoten aus dem anderen zu vermischen oder zu vergleichen, kann zu implementierungsabhängigen Problemen führen. Im Ergebnis erfordert die Verwendung eines Knotens aus einem anderen Dokument, daß dieser an die Methode insertNode( ) des Zieldokuments weitergereicht wird. Das Ergebnis dieser Methode ist ein neues Node-Objekt, das mit dem Zieldokument kompatibel ist. Mit anderen Worten wird der folgende Code Probleme bereiten:
Element otherDocElement = otherDoc.getDocumentElement( ); Element thisDocElement = thisDoc.getDocumentElement( ); // Hier kommt das Problem - Knoten aus verschiedenen Dokumenten werden vermischt thisDocElement.appendChild(otherDocElement);
Dadurch wird die folgende Exception ausgelöst:
org.apache.xerces.dom.DOMExceptionImpl: DOM005 Wrong document at org.apache.xerces.dom.ChildAndParentNode.internalInsertBefore( ChildAndParentNode.java:314) at org.apache.xerces.dom.ChildAndParentNode.insertBefore( ChildAndParentNode.java:296) at org.apache.xerces.dom.NodeImpl.appendChild(NodeImpl.java:213) at MoveNode.main(MoveNode.java:30)
Um dies zu vermeiden, müssen Sie den gewünschten Knoten zunächst in das neue Dokument importieren:
Element otherDocElement = otherDoc.getDocumentElement( ); Element thisDocElement = thisDoc.getDocumentElement( ); // Den Knoten in das richtige Dokument importieren Element readyToUseElement = (Element)thisDoc.importNode(otherDocElement); // Jetzt funktioniert es thisDocElement.appendChild(readyToUseElement);
Beachten Sie, daß das Ergebnis von importNode( ) vom Typ Node ist, deshalb muß ein Casting auf das richtige Interface (in diesem Fall Element) durchgeführt werden. Sparen Sie sich ein wenig Zeit und Mühe, und merken Sie sich das gut; schreiben Sie es sich auf einen Notizzettel, und legen Sie ihn unter das Kopfkissen. Glauben Sie mir, das ist so ziemlich der ärgerlichste Fehler, der der Menschheit bekannt ist!
Erzeugen, erweitern und einfügen
Die Lösung des gerade beschriebenen Problems führt oft zu einem anderen Problem. Ein Fehler, den ich häufig sehe, entsteht, wenn Entwickler daran denken, einen Knoten zu importieren und dann vergessen, ihn anzuhängen! Mit anderen Worten: Mitunter taucht Code auf, der so aussieht:
Element otherDocElement = otherDoc.getDocumentElement( ); Element thisDocElement = thisDoc.getDocumentElement( ); // Den Knoten in das richtige Dokument importieren Element readyToUseElement = (Element)thisDoc.importNode(otherDocElement); // Der Knoten wird nie angehängt!!
In diesem Fall erhalten Sie ein Element, das zum Zieldokument gehört, aber nie an irgend etwas im Dokument angehängt (oder irgend etwas vorangestellt) wird. Das Ergebnis ist ein weiterer schwer zu findender Fehler, bei dem das Element zwar zum Dokument gehört, sich aber nicht im eigentlichen DOM-Baum befindet. Die Ausgabe wird letzten Endes völlig ohne den importierten Knoten daherkommen, was ziemlich frustrierend sein kann. Passen Sie auf!
Und was kommt jetzt?
Nun, Sie sollten zu merken beginnen, daß Sie immer besser mit der ganzen XML-Geschichte zurechtkommen. Im nächsten Kapitel gehe ich auf der API-Strecke eine Station weiter, indem ich Ihnen JDOM vorstelle, eine weitere API für den Zugriff auf XML aus Java heraus. JDOM ähnelt DOM (ist aber nicht DOM) insofern, daß es Ihnen ein Baummodell von XML zur Verfügung stellt. Ich zeige Ihnen, wie es funktioniert, stelle klar, wann es verwendet werden sollte, und behandle die Unterschiede zwischen den verschiedenen XML-APIs, die wir uns bisher angeschaut haben. Werden Sie aber jetzt nicht übermütig; es gibt noch viel mehr zu lernen!