Wie kann man in Java auf XML-Dokumente zugreifen?
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:
<?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:
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].
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:
- Der Namensraum-URI des Elementes
- Der unqualifizierte Elementname
- Der qualifizierte Elementname
- 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].
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) 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.