Wie kann man in Java auf XML-Dokumente zugreifen?

Unter Java existieren mehrere Möglichkeiten, um auf XML-Dokumente zuzugreifen. Alle besitzen spezifische Vor- und Nachteile. Die zwei möglicherweise gängigsten werden hier kurz vorgestellt.

Vorbereitung

Um die Bibliotheken JDOM bzw. SAX nutzen zu können, müssen sie vorher unter https://www.jdom.org/downloads/index.html bzw. https://sourceforge.net/projects/sax/files/ heruntergeladen und wie unter https://javabeginners.de/Grundlagen/Bibliothek_einbinden.php erläutert in das Projekt eingebunden werden.

JDOM vs. SAX

JDOM und SAX unterscheiden sich grundlegend in ihrer Herangehensweise beim Auslesen und Verarbeiten von XML-Texten. Die Unterschiede sind durchaus relevant für die Auswahl des jeweiligen Verfahrens:
JDOM liest den kompletten Baum einer XML-Datei ein, sodass anschließend ein wahlfreier Zugriff auf seine Elemente möglich ist. Der Nachteil: besonders bei großen XML-Dateien kann die Größe des Speicherbedarfs dafür durchaus relevant sein. Zudem kann eine Auswertung der eingelesenen Daten erst nach vollständigem Parsen erfolgen.
Das "Simple API for XML Parsing (SAX)" basiert auf einem Ereignismodell. Sie liest eine XML-Datei wie einen Datenstrom und reagiert jeweils zu Beginn und am Ende eines neuen Baumelementes, indem an diesen Stellen Callback-Funktionen eines Handlers aufgerufen werden. Der XML-Text wird also nicht als ganzes vor der Verarbeitung eingelesen, sondern die Verarbeitung beginnt bereits beim Lesen des ersten Zeichens. Dieses ausgesprochen ressourcenschonende Vorgehen bewirkt u.a., dass selbst nicht wohlgeformte XML-Dekumente zumindest bis zur Fehlerstelle verarbeitet werden können. Hier ist also besondere Vorsicht geboten.

Das XML-Beispiel

Als Demonstrationsbeispiel verwenden wir eine einfache XML-Datei wie in listing 1 gezeigt:

listing 1
<?xml version="1.0" encoding="UTF-8"?>
<adressen>
    <adresse>
        <name>
            <nachname>Meier</nachname>
            <vorname>Willi</vorname>
        </name>
        <strasse>
            <name>Schlossallee</name>
            <nr>34</nr>
        </strasse>
        <ort name="Bullerbü" plz="43210" />
    </adresse>
    <adresse>
        <name>
            <nachname titel="Dr.">Schmitz</nachname>
            <vorname>Hans</vorname>
        </name>
        <strasse>
            <name>Badstr.</name>
            <nr>5</nr>
        </strasse>
        <ort name="Posemuckel" plz="01234" />
    </adresse>
</adressen>

JDOM

Um die XML-Datei auszulesen, muss zunächst mit Hilfe der Klasse SAXBuilder ein JDOM-Dokument erzeugt werden, dessen Methode build() das File-Objekt der XML-Datei übergeben bekommt [listing 3/23]. Durch ein XMLOutputter-Objekt kann dies schließlich als Byte-Strom, hier über System.out, auf der Kommandozeile ausgegeben werden. Die Methode output() sorgt dabei für die Ausgabe und erhält als Parameter das durch den SAXBuilder erzeugte Dokument und wahlweise einen OutputStream oder Writer [27].

Jetzt lässt sich problemlos auf das Wurzelelement, die Kindelemente, Attribute und Werte zugreifen:
Die Methode getRootElement() des Document-Objektes liefert das Wurzel-Element des XML-Eintrags als Element-Objekt [30].

Das Auslesen einzelner XML-Knoten und Attribute wird in der Folge in der Methode readElements() demonstriert. Ihr wird das Objekt des Wurzel-Elements als Parameter übergeben [33].
Mittels getName() können der Bezeichner eines Element-Objektes abgefragt [49] und durch seine Methode getChildren() eine Liste der enthaltenen Kindelemente ermittelt werden [51]. Sind keine Kindelemente vorhanden, wird die Liste zwar erzeugt, bleibt aber leer. Die Liste wird durchlaufen und die ggfs. in ihr gespeicherten Elemente daraufhin überprüft, ob sie ihrerseits Kindelemente aufweisen. Dies geschieht dadurch, dass geprüft wird, ob durch Aufruf von getChildren() eine List mit mindestens einem Element erstellt wurde [55]. Ist dies der Fall, wird die Methode rekursiv mit dem geprüften Element wieder aufgerufen, sodass die bisherige Prozedur statt mit dem Wurzelelement nun mit dem ermittelten durchlaufen wird [56].
Weist das Listenelement jedoch kein Kindelement auf, wird es ausgegeben, wobei durch seine Methoden getName() wieder der Bezeichner und durch getValue() der zugehörige Wert ermittelt werden [58].

Um eventuell vorhandene Attribute auszulesen, wird, ähnlich der Ermittlung von Kindelementen, nun versucht, durch getAttributes(), von jedem Element eine Liste von Attributen zu erstellen [60]. Und wiederum wird über die Länge der Liste festgestellt, ob das geprüfte Element überhaupt Attribute aufweist [61]. Ist dies nicht der Fall, wird der Vorgang abgebrochen und mit continue innerhalb der Schleife zum nächsten Element gewechselt [62]. Ist ein Attribut vorhanden, wird die Schleife fortgesetzt, indem Attribut-Bezeichner und -Wert wiederum mittels getName() [65], bzw. getValue() [66] ausgelesen und anschließend ausgegeben [67] werden.

Die im Folgenden aufgerufene MethodereadFirstNamedElement() demonstriert, wie auf ein benanntes Element zugegriffen werden kann [36]. Dies wird im Wesentlichen durch die Methode getChild() ermöglicht, die mit oder ohne String-Parameter für den Element-Bezeichner verwendet werden kann. Für den Zugriff auf den Bezeichner wird der XML-Baum jedoch nicht selbstständig durchlaufen, vielmehr müssen, ausgehend von einem vorgegebenen Knoten, die dem Pfad zum Zielknoten entsprechenden Kindknoten nacheinander aufgerufen werden. Bei mehrfachem Vorhandensein eines Bezeichners im XML-Baum wird nur der erste passende berücksichtigt. Um z.B. den Wert des Elementes nachname zu ermitteln, wäre etwa der folgende Ausdruck geeignet:

listing 2
rootElement.getChild("adresse").getChild("name").getChild("nachname");

Ausgehend vom Wurzelelement ist der Knoten adresse zwei Mal im Baum vorhanden, und das erste Vorkommen führt zum Nachnamen Meier.

Bei der Verwendung von getChild() muss beachtet werden, dass die Methode null liefert, falls das gesuchte Element nicht vorhanden sein sollte. Das kann bei kaskadierten Abfragen ohne angemessene Vorsichtsmaßnahmen zu einer schwer aufzulösenden NullPointerException führen. Eine Prüfung auf null ist daher dringend angeraten. Dies kann leider zu recht aufgeblähtem Code führen, wie hier in readFirstNamedElement() gezeigt [73].

Sollen z.B. alle Knoten "nachname" ermittelt werden, muss rekursiv oder entsprechend mit Schleifen vorgegangen werden. Dies ist in readNamedElements() realisiert [92]. Auch hier wird, ähnlich wie oben, eine Liste der vorhandenen Kindelemente des übergebenen Element-Objektes erstellt. Die Bezeichner werden beim Listendurchlauf mittels getName() ermittelt und auf den String-Parameter der Methode hin geprüft [98]. Bei Erfolg wird dieser ausgelesen, ansonsten wird die Methode rekursiv mit dem aktuellen Element aufgerufen [101].

listing 3
import java.io.File;
import java.io.IOException;
import java.util.List;

import org.jdom.Attribute;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.XMLOutputter;

class JDOMLesen {

    public static void main(String[] args) {

        Document doc = null;

        File f = new File("my_xml_file.xml");

        try {
            // Das Dokument erstellen
            SAXBuilder builder = new SAXBuilder();
            doc = builder.build(f);
            XMLOutputter oput = new XMLOutputter();

            // Komplettes Dokument ausgeben
            oput.output(doc, System.out);

            // Wurzelelement ermitteln
            Element rootElement = doc.getRootElement();
            
            // Alle Elemente auslesen
            readElements(rootElement);

            // Wert eines bestimmten Elementes ausgeben
            readFirstNamedElement(rootElement);

            // Benannte Nachname-Elemente ausgeben
            readNamedElements(rootElement, "nachname");

        } catch (JDOMException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void readElements(Element e) {
        System.out.println(e.getName());
        @SuppressWarnings("unchecked")
        List<Element> elements = e.getChildren();
        for (Element elem : elements) {
            @SuppressWarnings("unchecked")
            List<Element> children = elem.getChildren();
            if (children.size() > 0) {
                readElements(elem);
            } else {
                System.out.println(elem.getName() + ": " + elem.getValue());
                @SuppressWarnings("unchecked")
                List<Attribute> attributes = elem.getAttributes();
                if (attributes.size() == 0)
                    continue;
                System.out.println("\tAttribut: ");
                for (Attribute att : attributes) {
                    String name = att.getName();
                    String val = att.getValue();
                    System.out.println(name + ": " + val);
                }
            }
        }
    }
    
    private static void readFirstNamedElement(Element e) {
        Element nName = e.getChild("adresse");
        if (nName != null) {
            nName = nName.getChild("name");
            if (nName != null) {
                nName = nName.getChild("nachname");
                if (nName != null) {
                    System.out.println("Nachname: " + nName.getValue());
                }else {
                    System.out.println("Keine Element 'nachname' vorhanden.");
                }
            } else {
                System.out.println("Keine Element 'name' vorhanden.");
            }
        } else {
            System.out.println("Keine Element 'adresse' vorhanden.");
        }
    }

    private static void readNamedElements(Element e, String name) {
        @SuppressWarnings("unchecked")
        List<Element> elements = e.getChildren();
        Element elem;
        for (int i = 0; i < elements.size(); ++i) {
            elem = elements.get(i);
            if (elem.getName().equals(name)) {
                System.out.println(name + ": " + elem.getValue());
            } else {
                readNamedElements(elem, name);
            }
        }
    }
}

SAX/SAX2

Die Basis der Verarbeitung einer XML-Datei mit SAX/SAX2 bildet ein EventHandler-Objekt, das durch Erweitern der Klasse DefaultHandler dem Entwicklungsziel angepasst wird. Zur reinen Ausgabe des XML-Textes werden üblicherweise die folgenden fünf Methoden überschrieben, die per default nichts unternehmen. Ihre Implementierung wird gemäß der Verarbeitung des im jeweiligen Methodennamen angesprochenen Dateibereiches vorgenommen:

startDocument()
Wird bei Erkennen des Dokumentanfangs ausgeführt.
endDocument()
Wird nach Erreichen des Dokumentendes ausgeführt.
startElement()
Wird bei Erkennen des Öffnungstags eines Elementes ausgeführt.
endElement()
Wird bei Erkennen des Schlusstags eines Elementes ausgeführt.
characters()
Liest den Inhalt des aktuellen Elementes.

Die Klasse SAXLesen erweitert selbst DefaultHandler und stellt somit die o.a. Methodenimplementierungen bereit. Das entsprechende Objekt wird zu Beginn in main() erzeugt [21]. Ein XMLReader zum Durchlaufen des XML-Textes wird gebildet [24] und erhält hierzu das Handler-Objekt [25] zur Verarbeitung des Inhaltes als Parameter übergeben. Das selbe Objekt wird auch zur Fehlerbehandlung registriert [26], da DefaultHandler die Schnittstelle ErrorHandler imlementiert. Sie definiert drei Methoden zur Behandlung von Fehlern, von denen zwei hier überschrieben werden [77, 82].
Zum Start der Verarbeitung wird die Methode parse() auf dem Reader-Objekt aufgerufen und bekommt den Pfad zur XML-Datei als String-Parameter übergeben [27]. Zum Abfangen von Fehlern müssen die Reader-Methoden innerhalb eines try-catch-Blockes ausgeführt werden.

Am Ende des Quelltextes sind zwei Hilfsmethoden deklariert, die in der Folge mehrfach aufgerufen werden: textPuffer() leitet lediglich den Inhalt eines StringBuffer-Objektes als String an die Methode ausgabe() weiter [93] und löscht den Puffer anschließend [94].
Die Methode ausgabe() nimmt den String entgegen, bildet einen OutputStreamWriter [100] und schreibt mit diesem den String auf die Konsole [101] bevor der Writer wieder gelöscht wird [102].

In den Methoden startDocument() [36] und endDocument() [40] wird zu Demonstrationszwecken hier lediglich jeweils ein String ausgegeben.

In startElement() stehen vier Parameter zur Verfügung, auf die im Methodenkörper zugegriffen werden kann:

  1. Der Namensraum-URI des Elementes
  2. Der unqualifizierte Elementname
  3. Der qualifizierte Elementname
  4. Die Attribute des Elementes

Zunächst wird der Name des beim Durchlaufen der Datei erreichten Elementes mit vorangehender öffnender spitzer Klammer ausgegeben [48], wobei der einfache, lokale Name verwendet wird [47], falls wie hier kein qualifizierter Name vorhanden sein sollte. In der Folge werden dann, innerhalb einer Schleife, die Attribute des Elementes mit Name und Wert ausgegeben [55], wobei wiederum zwischen dem einfachen und dem qualifizierten Namen unterschieden wird [53]. Die Methode schließt mit der Ausgabe der schließenden spitzen Klammer des Starttags ab [58].

Die Methode characters() dient der Verarbeitung von Elementinhalten. Sie bekommt ein char-Array, einen String-Offet und eine String-Länge als Parameter übergeben. Das Array dient als Puffer, im vorliegenden Beispiel mit der Länge 2024, und speichert den gesamten XML-Text. Der String-Offset entspricht jeweils der ersten Position nach der schließenden Klammer des aktuellen Starttags und der Längenparameter derjenigen des dort beginnenden Eintrags. Der Inhalts-String wird somit aus dem diesen Werten entsprechenden Anteil des char-Arrays gebildet [68] und anschließend in einen StringBuffer geschrieben [70, 72].

In endElement() [61] wird zunächst die Hilfsmethode textPuffer() aufgerufen, die die Verarbeitung des vorher gebildeten Puffers ausführt [62]. Anschließend wird der Schlusstag des aktuellen Elementes ausgegeben [64].

listing 4
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

public class SAXLesen extends DefaultHandler {
    static final String neueZeile = System.getProperty("line.separator");

    static private Writer out = null;

    private StringBuffer textBuffer = null;

    public static void main(String[] argv) {

        DefaultHandler handler = new SAXLesen();
        XMLReader reader = null;
        try {
            reader = XMLReaderFactory.createXMLReader();
            reader.setContentHandler(handler);
            reader.setErrorHandler(handler);
            reader.parse("my_xml_file.xml");
        } catch (IOException e) {
        } catch (SAXException e) {
            e.printStackTrace();
        }
    }

    /** ******* SAX EventHandler Methoden ************ */

    public void startDocument() throws SAXException {
        ausgabe("auf geht's!" + neueZeile);
    }

    public void endDocument() throws SAXException {
        ausgabe("\ndas war's!" + neueZeile);
    }

    public void startElement(String namespaceURI, String localName, String qName, Attributes attrs)
            throws SAXException {
        textPuffer();
        String eName = ("".equals(localName)) ? qName : localName;
        ausgabe("<" + eName);

        if (attrs != null) {
            for (int i = 0; i < attrs.getLength(); i++) {
                String aName = attrs.getLocalName(i);
                if ("".equals(aName))
                    aName = attrs.getQName(i);
                ausgabe(" " + aName + "=\"" + attrs.getValue(i) + "\"");
            }
        }
        ausgabe(">");
    }

    public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
        textPuffer();
        String eName = ("".equals(localName)) ? qName : localName;
        ausgabe("</" + eName + ">");
    }

    public void characters(char[] buf, int offset, int len) throws SAXException {
        String s = new String(buf, offset, len);
        if (textBuffer == null)
            textBuffer = new StringBuffer(s);
        else
            textBuffer.append(s);
    }
    
    /************* SAX ErrorHandler Methoden *************/
    
    public void warning(SAXParseException spe) {
        System.err.println("Warnung: Zeile " + spe.getLineNumber() + " Spalte " + spe.getColumnNumber());
        System.err.println(spe.getMessage());
    }
    
    public void fatalError(SAXParseException spe) throws SAXParseException {
        System.err.println("Fataler Fehler: Zeile " + spe.getLineNumber() + " Spalte " + spe.getColumnNumber());
        System.err.println(spe.getMessage());
        throw spe;
    }

    /** ************** Hilfsmethoden ******************* */

    private void textPuffer() throws SAXException {
        if (textBuffer == null)
            return;
        ausgabe(textBuffer.toString());
        textBuffer = null;
    }

    private void ausgabe(String s) throws SAXException {
        try {
            if (out == null)
                out = new OutputStreamWriter(System.out, "UTF8");
            out.write(s);
            out.flush();
        } catch (IOException ex) {
            throw new SAXException("Ein-/Ausgabefehler", ex);
        }
    }
}

Die beiden Methoden warning() [77] und faralError() entstammen dem Interface ErrorHandler, das von DefaultHandler implementiert wird. Sie dienen der Fehlerbehandlung mit entsprechenden Ausgaben.

Quellen
  1. http://www.saxproject.org/event.html

1) Bei Verwendung von JDOM2 müssen die Import-Aweisungen von "import org.jdom..." auf "import org.jdom2..." angepasst werden.

Wenn Ihnen javabeginners.de gefällt, freue ich mich über eine Spende an diese gemeinnützigen Organisationen.

letzte Änderung an:
XML-Datei lesen


Javabeginners News-Feed News-Feed abonnieren