15.4 Grundlagen der XML-Programmierung
Da XML innerhalb weniger Jahre zu einem der wichtigsten Datenformate geworden ist, wird es von fast allen Programmiersprachen unterstützt. Das wichtigste Instrument der XML-Programmierung ist ein XML-Parser, der die einzelnen Komponenten von XML-Dokumenten voneinander trennt und ihre Wohlgeformtheit überprüft. Die Ausgabe eines solchen Parsers kann anschließend durch ein selbst geschriebenes Programm verarbeitet werden.
Wie man einen XML-Parser selbst schreiben könnte, soll an dieser Stelle nicht erörtert werden. Im Grunde handelt es sich um ein sehr komplexes Gefüge regulärer Ausdrücke, die ein wohlgeformtes XML-Dokument beschreiben. Da es für die meisten Programmiersprachen unzählige fertige Parser gibt, müssen Sie sich das nicht antun.
In diesem Abschnitt werden die beiden gängigsten XML-Programmiermodelle beschrieben. Die verwendete Programmiersprache ist Java, obwohl beide Modelle auch von anderen Sprachen wie Perl, PHP oder dem .NET Framework unterstützt werden. Das erste Modell ist das ereignisbasierte SAX (Simple API for XML), das zweite das baumbasierte DOM (Document Object Model).
Falls Sie SAX und DOM in Java-Programmen verwenden möchten, benötigen Sie zuerst einen Parser, der diese Modelle unterstützt und die Parsing-Ergebnisse an Ihr Programm weitergibt. Sehr empfehlenswert ist der Parser Apache Xerces, den Sie unter http://xml.apache.org/xerces2-j/index.html herunterladen können. Wenn Sie ihn für eine andere Programmiersprache als Java benötigen, können Sie sich selbst unter xml.apache.org umsehen. Die aktuelle Version von Xerces für Java ist zurzeit 2.6.2. Sein Vorteil ist, dass er sowohl DOM als auch SAX unterstützt und die notwendigen Java-Klassen für beide enthält.
Um Xerces und die XML-Klassen aus Ihrem Java-Programm heraus zu verwenden, müssen Sie den Pfad der Datei xercesImpl.jar (bei älteren Versionen xerces.jar) in Ihren Java-Classpath aufnehmen. Wie die Umgebungsvariable CLASSPATH manipuliert wird, steht in Kapitel 9, »Grundlagen der Programmierung«.
Zur Demonstration der XML-Programmierung kommt ein weiteres XML-Dokument zum Einsatz; es handelt sich um die XML-Darstellung einer Liste von Büchern über XML. Ihr Aufbau ähnelt der Comic-Liste:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<xml-buecher>
<buch isbn="978-3898426947">
<autor>
<name>Bongers</name>
<vorname>Frank</vorname>
</autor>
<titel>XSLT 2.0 und XPath 2.0</titel>
<auflage>2</auflage>
<ort>Bonn</ort>
<jahr>2008</jahr>
<verlag>Galileo Computing</verlag>
</buch>
<buch isbn="978-3836213677">
<autor>
<name>Vonhoegen</name>
<vorname>Helmut</vorname>
</autor>
<titel>Einstieg in XML</titel>
<auflage>5</auflage>
<ort>Bonn</ort>
<jahr>2009</jahr>
<verlag>Galileo Computing</verlag>
</buch>
<buch isbn="978-3897213395">
<autor>
<name>Harold</name>
<vorname>Eliotte Rusty</vorname>
</autor>
<autor>
<name>Means</name>
<vorname>W. Scott</vorname>
</autor>
<titel>XML in a Nutshell</titel>
<auflage>3</auflage>
<ort>Köln</ort>
<jahr>2005</jahr>
<verlag>O'Reilly</verlag>
</buch>
<!-- und viele weitere Bücher -->
</xml-buecher>
15.4.1 SAX
Wie bereits erwähnt, ist die Simple API for XML ein ereignisbasiertes Programmiermodell für XML-Anwendungen. Der Parser verarbeitet ein XML-Dokument und ruft für bestimmte Ereignisse, die Sie festlegen können, Ihre Verarbeitungsfunktionen auf. Das Callback-Verfahren, bei dem Ihre eigenen Methoden von außen aufgerufen werden, wurde bereits in Kapitel 10, »Konzepte der Programmierung«, erläutert.
Um SAX zu verwenden, müssen Sie zunächst eine Reihe von Klassen importieren. Die schnellste und einfachste Importanweisung ist folgende:
Als Nächstes müssen Sie eine Instanz Ihres bevorzugten SAX-Parsers anlegen. Das folgende Beispiel erzeugt eine Instanz des Xerces-SAX-Parsers:
public String pclass =
"org.apache.xerces.parsers.SAXParser";
public XMLReader parser =
XMLReaderFactory.createXMLReader (pclass);
XMLReader ist ein Interface, das einen SAX-fähigen Parser beschreibt. Der Vorteil gegenüber einer konkreten Klasse liegt auf der Hand: Wenn Sie von Xerces auf einen anderen Parser umsteigen müssen oder wollen, brauchen Sie nur den Wert der Variablen pclass zu ändern; alles andere bleibt bestehen. Aus demselben Grund wird kein Konstruktor aufgerufen, sondern eine Methode einer Factory-Klasse.
Damit der Parser etwas zu tun hat, müssen Sie ihm nun ein XML-Dokument vorsetzen. Dies geschieht mithilfe einer InputSource-Instanz. Wenn Sie die Datei xml-buecher.xml im aktuellen Verzeichnis als Eingabedokument verwenden möchten, sieht der entsprechende Code folgendermaßen aus:
InputSource source =
new InputSource (new java.io.FileInputStream (
new java.io.File ("xml-buecher.xml")));
source.setSystemId ("xml-buecher.xml");
Der InputSource-Konstruktor erwartet die Übergabe eines InputStreams, der im vorliegenden Fall aus einer lokalen Datei gebildet wird. Die Methode setSystemId() des InputSource-Objekts stellt die XML-Datei auch gleich als SYSTEM-ID ein, damit relative Pfadangaben innerhalb des XML-Codes korrekt aufgelöst werden.
Nachdem Sie nun den Parser und das XML-Eingabedokument eingerichtet haben, können Sie die Methode parse() aufrufen, um das Dokument tatsächlich zu verarbeiten:
parser.parse (source);
Damit nun aber die vom Parser gemeldeten Ereignisse wie Beginn und Ende eines Elements oder das Auftreten eines Attributs von Ihrem Programm verarbeitet werden können, müssen Sie es als Handler für die jeweiligen Ereignisse registrieren. Der wichtigste Handler für SAX-Ereignisse ist org.xml.sax.ContentHandler, der sich um XML-Ereignisse wie Elemente und Attribute kümmert.
Die drei anderen möglichen Handler werden hier nicht weiter besprochen. Es handelt sich um org.xml.sax.EntityResolver, der – wie der Name vermuten lässt – Entity-Referenzen auflöst, ErrorHandler zur Bearbeitung von Parsing-Fehlern und DTDHandler zur Validierung des Dokuments anhand einer DTD.
Um Ihr eigenes Programm als ContentHandler zu registrieren, muss es dieses Interface und alle seine Methoden implementieren, ob Sie die Funktionalität dieser Methoden nun benötigen oder nicht. Diese spezielle Eigenschaft von Java-Interfaces wird in Kapitel 10, »Konzepte der Programmierung«, näher erläutert. Außerdem muss eine Instanz der Klasse vorliegen, die als Handler registriert wird. Sie müssen diese Instanz mithilfe der Methode setContentHandler() der XMLReader-Instanz angeben. Alternativ können Sie auch eine separate Klasse schreiben, eine Instanz davon erzeugen und als Handler registrieren. Schematisch sieht diese Registrierung, die noch vor dem parse()-Aufruf stehen muss, folgendermaßen aus:
parser.setContentHandler (handler);
In Tabelle 15.3 sehen Sie eine Liste aller Callback-Methoden, die Sie für einen ContentHandler implementieren müssen. Beachten Sie dabei, dass Sie keine dieser Methoden jemals selbst aufrufen werden. Stattdessen werden sie nacheinander vom XML-Parser aufgerufen, wenn er die betreffenden Komponenten eines XML-Dokuments antrifft.
SAX-Callback-Methode | Erläuterung |
public void setDocumentLocator (Locator locator) |
Wird zu Beginn des Parsings aufgerufen und richtet den Locator ein, der jeweils auf die Position im XML-Dokument verweist, an der sich der Parser gerade befindet. |
public void startDocument () throws SAXException |
Wird beim eigentlichen Beginn des Parsing-Prozesses aufgerufen. |
public void endDocument () throws SAXException |
Wird stets am Ende der Verarbeitung aufgerufen, sowohl wenn das Dokument fertig verarbeitet ist als auch bei einem Abbruch durch einen Fehler. |
public void startPrefixMapping (String prefix, String uri) throws SAXException |
Wird vor dem Start eines Elements aufgerufen, das ein Namensraum-Präfix besitzt. |
public void endPrefixMapping (String prefix) throws SAXException |
Wird nach dem Abschluss eines Elements mit Namensraum-Präfix aufgerufen. |
public void startElement (String namespaceURI, String localName, String qName, Attributes attrs) throws SAXException |
Eines der wichtigsten SAX-Callbacks. Es wird bei jedem Antreffen eines öffnenden Tags aufgerufen und liefert den Namensraum-URI, den einfachen Elementnamen, den Qualified Name (Name mit Namensraum-Präfix) und eine Aufzählung der Attribute. |
public void endElement (String namespaceURI, String localName, String qName) throws SAXException |
Wird bei jedem Antreffen eines schließenden Tags aufgerufen. Folgerichtig liefert die Methode dieselben Elementinformationen wie startElement(), außer den Attributen. |
public void characters (char ch[], int start, int length) throws SAXException |
Gibt einfachen Text aus dem Dokument als Array aus einzelnen Zeichen zurück. Zur Kontrolle werden die Nummer des Startzeichens und die Länge mit angegeben, da keine Garantie besteht, dass ein ganzer Textabschnitt auf einmal zurückgegeben wird. |
public void ignorableWhitespace (char ch[], int start, int length) throws SAXException |
Gibt ignorierbaren Whitespace als Array von Zeichen zurück. Wird nur aufgerufen, wenn das Dokument sich auf eine DTD oder ein Schema bezieht, deren Inhaltsdefinitionen Whitespace übrig lassen. Andernfalls wird Whitespace von characters() behandelt. |
public void processingInstruction (String target, String data) throws SAXException |
Reagiert auf Steueranweisungen im Dokument, allerdings nicht auf die <?xml?>-Steueranweisung am Dokumentbeginn. Sonstige Steueranweisungen sind für Befehle an XML-Anweisungen vorgesehen. |
public void skippedEntity (String name) throws SAXException |
Reicht alle Entity-Referenzen an den ContentHandler durch, die nicht vom Parser aufgelöst werden können. |
Die Beispielanwendung in Listing 15.1 macht etwas recht Nützliches mit der XML-Bücherliste: Sie schreibt die meisten Informationen aus dem XML-Dokument über JDBC in eine Datenbanktabelle in der MySQL-Datenbank buchliste, die über MySQL Connector/J (siehe Kapitel 12, »Datenbanken«) angebunden ist. Explizit weggelassen werden nur die Autoren: Ein Buch kann mehrere Autoren haben; dafür benötigt ein sauberes relationales Datenbankmodell mehrere Tabellen. Diese Komplexität wäre für ein einfaches Beispiel fehl am Platze.
import org.xml.sax.*;
import org.xml.sax.helpers.*;
import java.sql.*;
import java.io.*;
public class XMLBuecherDB implements ContentHandler {
Connection conn; // Die Datenbankverbindung
Statement st; // Ein SQL-Statement-Objekt
public String isbn; // ISBN des aktuellen Buches
public String info; // Aktuell verarbeitete Info
public boolean relevant;
// z.Z. relevantes Element?
public String rtext; // Text => Datenfeld
public static void main (String args[])
{
XMLBuecherDB buecherdb = new XMLBuecherDB();
System.out.println ("Willkommen!");
try {
buecherdb.makeDB(buecherdb);
}
catch (Exception e) {
System.out.println ("Ein Fehler ist aufgetreten");
e.printStackTrace(System.err);
}
}
public void makeDB (XMLBuecherDB bdb) throws Exception {
// MySQL/ConnectorJ-Treiber laden
try {
Class.forName("com.mysql.jdbc.Driver").newInstance();
}
catch (ClassNotFoundException e) {
System.out.println ("JDBC-ODBC-Treiber nicht gefunden.");
return;
}
// Verbindung zum MySQL-Server herstellen.
// Die Datenbank "buchliste" muss existieren.
// Die beiden leeren Strings müssen Sie mit
// einem Benutzernamen und einem Passwort
// eines Benutzers auffüllen, der in dieser
// Datenbank Tabellen erstellen darf!
conn = DriverManager.getConnection ("jdbc:mysql://localhost/
buchliste", "", "");
st = conn.createStatement();
// Datenbanktabelle BUECHER löschen, falls sie bereits existiert
st.execute ("DROP TABLE IF EXISTS buecher");
// Tabelle BUECHER neu anlegen
st.execute ("CREATE TABLE BUECHER (ISBN CHAR(11) PRIMARY KEY, TITEL
VARCHAR(30), JAHR INT, ORT VARCHAR(30), VERLAG VARCHAR(30))");
// SAX-Parser erzeugen
String pclass = "org.apache.xerces.parsers.SAXParser";
XMLReader parser = XMLReaderFactory.createXMLReader (pclass);
// XML-Input-Source erzeugen
InputSource source = new InputSource (new java.io.FileInputStream
(new java.io.File ("xml-buecher.xml")));
source.setSystemId ("xml-buecher.xml");
// Eine Instanz des Programms selbst als
// ContentHandler registrieren
parser.setContentHandler (bdb);
info = "";
// Parsing beginnen
parser.parse(source);
}
/* Implementierung der ContentHandler-Klassen */
public void setDocumentLocator (Locator locator) {
// Wird nicht benötigt
}
public void startDocument () throws SAXException {
// Wird nicht benötigt
}
public void endDocument () throws SAXException {
// Wird nicht benötigt
}
public void startPrefixMapping (String prefix, String uri) throws
SAXException {
// Wird nicht benötigt
}
public void endPrefixMapping (String prefix) throws SAXException {
// Wird nicht benötigt
}
public void startElement (String namespaceURI, String localName, String
qName, Attributes attrs) throws SAXException {
String abfr; // Hilfsvariable für SQL-Abfragen
if (localName.equals ("buch")) {
// ISBN ermitteln => in neuen Datensatz
isbn = attrs.getValue ("isbn");
System.out.println ("Buch: ISBN " + isbn);
try {
abfr = "INSERT INTO BUECHER (ISBN) VALUES (\"" + isbn + "\")";
System.out.println (" [SQL: " + abfr + "]");
st.execute (abfr);
}
catch (Exception e) {
System.out.println ("Konnte Datensatz " + isbn +
" nicht anlegen.");
}
relevant = false;
} else if (localName.equals ("titel") || localName.equals ("jahr") ||
localName.equals ("ort") || localName.equals ("verlag")) {
// Namen des alten u. neuen Elements speichern
relevant = true;
info = localName;
rtext = "";
} else {
// Unwichtiges Element
relevant = false;
}
}
public void endElement (String namespaceURI, String localName, String
qName) throws SAXException {
String abfr; // Hilfsvariable für SQL-Abfragen
if (relevant) {
// Whitespace am Anfang entfernen
while (rtext.charAt (0) == ' ') {
rtext = rtext.substring (1, rtext.length());
}
// Whitespace am Ende entfernen
while (rtext.charAt (rtext.length() -1) ==
' ') {
rtext = rtext.substring (0,
rtext.length() - 1);
}
System.out.println ("------Element: " + info);
// Bisherigen Text => aktuelles Feld
try {
abfr = "UPDATE BUECHER SET " + info +
"=\"" + rtext + "\"
WHERE ISBN=\"" + isbn + "\"";
System.out.println (" [SQL: " + abfr +
"]");
st.execute (abfr);
}
catch (Exception e) {
System.out.println (" !! Konnte " +
info + ": " + rtext +
" nicht einfuegen!");
}
rtext = "";
relevant = false;
}
}
public void characters (char ch[], int start, int length)
throws SAXException {
if (relevant) {
// Aktuelle Zeichen hinzufügen
String ntext = new String(ch, start, length);
rtext += ntext;
}
}
public void ignorableWhitespace (char ch[], int start, int length)
throws SAXException {
// Wird nicht benötigt
}
public void processingInstruction (String target, String data)
throws SAXException {
// Wird nicht benötigt
}
public void skippedEntity (String name) throws SAXException {
// Wird nicht benötigt
}
}
Listing 15.1 Eine XML-Struktur wird mit SAX ausgelesen und in eine MySQL-Datenbank geschrieben.
Die Komplexität des Beispiels ergibt sich insbesondere daraus, dass die einzelnen Bestandteile des XML-Dokuments, die für die Datenbank relevant sind, in verschiedenen Methoden des Programms gefunden werden: startElement() leitet den Beginn des jeweiligen Elements ein und beginnt damit die Sammlung der nachfolgenden Zeichen in characters(), um das entsprechende Feld der Datenbank mit Inhalt zu füllen. Bei endElement() ist der entsprechende Text vollständig und wird in die Datenbank geschrieben. Da sich vor und hinter dem eigentlichen Inhalt Whitespace befinden kann, wird dieser zunächst zeichenweise entfernt.
Auch die etwas seltsame Aufgabenverteilung zwischen der Methode main() und dem eigentlichen »Arbeitstier« makeDB() ist sicherlich erklärungsbedürftig: Die globale Variable info, die den Namen des aktuellen Elements speichert, könnte nicht in der statischen Methode main() verwendet werden. Umgekehrt kann das Programm selbst nicht statisch sein, weil der registrierte ContentHandler eine Instanz sein muss. Deshalb wird in main() eine Instanz von XMLBuecherDB selbst erzeugt und an makeDB() übergeben, da main() wiederum nicht auf ein globales XMLBuecherDB-Objekt zugreifen dürfte.
15.4.2 DOM
Das Document Object Model (DOM) wurde vom W3C standardisiert und kann von vielen verschiedenen Programmiersprachen aus verwendet werden. Beispielsweise wird in Kapitel 19, »JavaScript und Ajax«, die Anwendung von DOM in einem Browser zur Manipulation von HTML-Dokumenten besprochen.
In diesem kurzen Abschnitt wird dagegen gezeigt, wie Sie DOM in einem Java-Programm einsetzen können. Der große Unterschied zu SAX besteht darin, dass DOM beim Parsing zunächst ein vollständiges Baummodell des XML-Dokuments errichtet, das Sie anschließend in aller Ruhe durchqueren und modifizieren können. Die Äste, Zweige und Blätter des Baums entsprechen den Elementen, Attributen und Textinhalten eines XML-Dokuments.
Um DOM in einem Java-Programm zu verwenden, benötigen Sie zunächst einen DOM-fähigen Parser. Auch in diesem Fall ist Apache Xerces eine gute Wahl. Zu Beginn Ihres Programms müssen Sie die Parser-Klasse und die DOM-Klassen importieren:
Um ein XML-Dokument durch den DOMParser zu schicken, können Sie folgendermaßen verfahren:
Anschließend kann der gesamte XML-Dokumentbaum vom Parser entgegengenommen werden. Er befindet sich in einem Document-Objekt:
Document doc = parser.getDocument ();
Das Objekt doc besteht aus einer Reihe ineinander verschachtelter Knoten; dies sind Objekte vom Typ Node. Am sinnvollsten lassen sie sich in einer rekursiven Prozedur durchwandern:
recurseNode (doc);
Die entsprechende Prozedur recurseNode() kann beispielsweise folgendermaßen aussehen:
public void recurseNode (Node knoten) {
int typ = node.getNodeType(); // Knotentyp
switch (typ) {
// Je nach Knotentyp reagieren;
// bei Knoten mit Kindknoten recurseNode()
// rekursiv aufrufen
}
}
Erfreulicherweise brauchen Sie sich die verschiedenen Knotentypen nicht numerisch zu merken, sondern können auf eine Reihe symbolischer Konstanten zurückgreifen, die das Interface Node exportiert. Tabelle 15.4 enthält eine Übersicht über die verfügbaren Knotentypen.
Knotentyp | Bedeutung |
Node.ELEMENT_NODE |
ein XML-Element |
Node.TEXT_NODE |
einfacher Text |
Node.CDATA_SECTION_NODE |
ein CDATA-Abschnitt |
Node.COMMENT_NODE |
ein XML-Kommentar |
Node.PROCESSING_INSTRUCTION_NODE |
eine Steueranweisung |
Node.ENTITY_REFERENCE_NODE |
eine Entity-Referenz |
Node.DOCUMENT_TYPE_NODE |
eine DOCTYPE-Deklaration |
Die Rekursion über die Kindknoten eines Objekts ist übrigens auch keine schwierige Angelegenheit: Ein Aufruf der Methode getChildNodes() eines Knotens gibt eine Liste aller direkten Kindobjekte zurück. Diese Liste können Sie mithilfe einer einfachen for-Schleife bearbeiten:
public void recurseNode (Node knoten) {
// aktuellen Knoten verarbeiten ...
// Rekursion über seine "Kinder":
NodeList kinder = knoten.getChildNodes();
if (nodes != null) {
for (int i = 0; i < kinder.getLength(); i++) {
recurseNode (kinder.item(i));
}
}
}
Mit DOM lassen sich Unmengen sinnvoller Anwendungen schreiben. Einige Beispiele finden Sie in Kapitel 19, »JavaScript und Ajax«, für die in Webbrowsern eingebaute DOM-Variante.
Ihr Kommentar
Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.