Teil 3: Nahtloser Umgang mit Namespaces, einfache Projektor-Konfiguration

Ein neuer Blick auf XML – Teil 3

Sven Ewald
©Shutterstock.com/jehsomwang

Im dritten Teil dieser Reihe zeigen wir den Umgang mit Namespaces, erklären, wie sich der Datenprojektor konfigurieren lässt und demonstrieren, dass die XPath-Ausdrücke nicht in den Annotationen der Projektionsmethoden stehen müssen.

Die Verbindung von XML-Namespaces und XPath ist sicher schon für manches graues Haar verantwortlich gemacht worden, denn es sind gleich mehrere Stolperfallen zu überwinden. So gelten im Dokument enthaltene Namespace-Präfixe nicht automatisch für XPath-Ausdrücke. Der Xpath-Parser besteht aus einem eigenem Präfix-Mapping. Default-Namespaces gelten in XML zwar für Unterelemente, ganz entgegen der ersten Erwartung aber nicht für Attribute. Ein XPath-Selektor ohne Präfix ist laut XPath 1.0 Spezifikation immer ein Element ohne Namespace, auch wenn es ein solches Element mit Default-Namespace gibt. Zu allem Überfluss ist die Verarbeitung von Namespaces im XML-Parser des JDK per Voreinstellung unterbunden und die XPath-Verarbeitung scheitert mit einer Exception falls ein nicht vorhandenes Präfix selektiert wird.

Den zweiten Teil dieser Serie, der vor zwei Wochen erschien, finden Sie übrigens hier auf JAXenter.

<root xmlns:h="http://www.w3.org/TR/html4/"
      xmlns="http://www.w3schools.com/furniture">
<!-- Beispieldokument zur demonstration von XPath 1.0 -->

<!--  Das erste table element hat einen Namespace mit dem Präfix "h". -->
	<h:table>
		<h:tr>
			<h:td>Apples</h:td>
			<h:td>Bananas</h:td>
		</h:tr>
	</h:table>
	
<!--  Das Zweite Element hat immer noch einen Namespace, allerdings ohne Präfix -->	
	<table>
		<name>African Coffee Table</name>
		<width>80</width>
		<length>120</length>
	</table>
</root>

XMLBeam bietet in Form von sogenannten Namespace-Philosophien (siehe DefaultXMLFactoriesConfig.NamespacePhilosophy) gleich drei verschiedene Lösungsansätze um sowohl einen flexiblen, als auch bequemen Umgang mit Namespaces zu ermöglichen. Eine Namespace-Philosophie ist eine Sammlung von Voreinstellung, die beim Erzeugen der verwendeten XML-Tools angewendet werden.

Die erste Möglichkeit ist das komplette Ignorieren von Namespaces, welches dem Default-Verhalten des JDK am nächsten kommt. Dabei sind für den Parser und den XPath-Auswerter alle Elemente und Attribute mit Namespaces unsichtbar. Der verwendete DocumentBuilder wird über die Methode „setNamespaceAware(false)“ angewiesen, Namespaces zu ignorieren. Entsprechend heißt das Enum für diese Namespace-Philosophie: „NIHILISTIC“.

Die zweite Möglichkeit besteht darin, die Entscheidung über Namespaces den verwendeten Factory-Methoden zu überlassen. XMLBeam wird in diesem Fall keine Änderungen an den Einstellungen vornehmen und keine Aussage über die Existenz von Namespaces treffen. Dies wird über das Enum „AGNOSTIC“ gewählt. Der Benutzer kann bzw. muss sich nun um die Konfiguration der verwendeten Tools selbst kümmern und eigene Präfix-Mappings für die XPath-Auswertung festlegen.

Die dritte Möglichkeit ist die Defaultkonfiguration des XBProjectors. XMLBeam übernimmt automatisch vorhandene Namespace-Prefixe aus dem Dokument für die Xpath-Auswertung und konfiguriert den XML-Parser entsprechend. Nichtexistierende Namespaces lassen die XPath-Auswertung nicht mehr abbrechen, sondern werden wie nichtexistierende Elemente behandelt.  Schließlich werden auch Elemente mit Default-Namespace über ein automatisch vorhandenes Prefix „xbdefaultns:“ ansprechbar. Mit diesen Voreinstellungen sollten sich die meisten Anwendungsfälle schmerzfrei lösen lassen. Entsprechend heißt diese Namespace-Philosophie: „HEDONISTIC“.

public interface NameSpaceProjection {

    // Automatisches Präfix Mapping aus dem Dokument
    @XBRead("//h:table")
    String getNamepsacedTable();

    // Automatisch erstelltes Präfix für den Default-Namspace
    // XPath 1.0 besteht auf einem Präfix, auch wenn das Element im
    // Dokument keinen Präfix besitzt.
    @XBRead("//xbdefaultns:table")
    String getDefaultNamepsacedTable();
}

Nun haben wir schon einen Aspekt der Klasse DefaultXMLFactoriesConfig kennengelernt. Wie der Name suggeriert, stellt sie die Defaulteinstellungen des XBProjectors dar, dessen API dafür ausgelegt ist, dass es mehrere XMLFactoriesCofigs geben kann. Diese steuern die Erstellung des DocumentBuilders zur Erzeugung des DOM-Baumes, der XPath-Instanz für die Projektionsmethoden und des Transformers zur Ausgabe des DOM-Baumes als XML-Dokument. Über die Klasse DefaultXMLFactoriesConfig lässt sich wählen, ob das erzeugte XML-Dokument bei der Ausgabe lesbar formatiert werden soll (Pretty-Printing) und ob im Header des Dokumentes eine XML-Deklaration erzeugt werden soll (siehe W3C Recommendation).

Zugriff auf die aktuelle Projektorkonfiguration erhält man über die Methode projector.config(), die mit ein bisschen Syntax-Sugar hilft, die Klammerung bei dem nötigen Cast zu vereinfachen: So lassen sich die Methoden der Defaultkonfiguration direkt über projector.config().as(DefaultXMLFactoriesConfig.class).* ansprechen. Der eigentliche Cast wird im Fluent-Interface des Projektors versteckt. Nebenbei: Ein Fluent-Interface über Method-Chaining wird auch von allen Projektionen unterstützt. Statt void dürfen Setter- und Deleter-Methoden auch ihr eigenes Interface als Rückgabewert deklarieren und die Methodenaufrufe einfach hintereinander gehängt werden.

Der Projektor selbst verfügt noch über zwei weitere Flags, die das Verhalten von Projektionen beeinflussen. So lässt sich über das Flag „SYNCHRONIZE_ON_DOCUMENTS“ steuern, ob Projektionen in einer synchronisierten Variante erzeugt werden sollen. Da die Synchronisierung auf dem Root-Dokument des DOM-Baumes geschieht, sind damit Zugriffe auf eine Projektion oder Unterprojektion thread-safe. Das Flag „TO_STRING_RENDERS_XML“ steuert das Verhalten der toString()-Methode von Projektionen. So kann es praktisch sein, das XML-Dokument von Projektionen oder das Element von Unterprojektionen einfach über diese Methode ausgeben zu lassen. Dies muss allerdings erst explizit eingeschaltet werden, denn falls das Dokument etwas umfangreicher ist, kann ein zu langes toString()-Resultat beim Debuggen die IDE lahm legen. Die Default-Implementierung der toString()-Methode gibt lediglich eine kurze, debug-freundliche Information über Projektionsinterface und Projektionsziel zurück. Im nächsten Teil dieser Serie werden wir lernen, wie sich eine eigene Implementierung der toString()-Methode realisieren lässt.

In den bisher gezeigten Beispielen hatten wir die Metadaten unserer Projektionen, also die Definition des Dokumentenursprungs und die XPath-Ausdrücke im Java-Sourcecode abgelegt. Für mache Anwendungsfälle mag es wünschenswert sein, diese Metadaten separat von den Javaquellen abzulegen. Daher betrifft eine weitere Konfigurationsmöglichkeit die Ursprünge der Dokumente und der Xpath-Ausdrücke. Bei den bisher gezeigten Beispielen waren die XPath-Ausdrücke den zugehörigen Projektionsmethoden über die @XBRead, bzw. @XBWrite Annotationen zugeordnet. Über die Methode projector.config().setExtenalizer(…) lassen sich jedoch auch verschiedene andere Implementierungen realisieren. Das Interface Externalizer ist dafür zuständig, einem Methodenaufruf einen XPath-Ausdruck zuzuordnen. Woher dieser Ausdruck stammt, ist der Implementierung durch den Benutzer überlassen. Eine Beispiel liegt als PropertyFileExternalizer der Bibliothek bei. Damit ist es leicht möglich, die XPath-Ausdrücke außerhalb des Java-Codes in einer Property-Datei abzulegen und sogar noch während der Laufzeit zu ändern. Eine andere Möglichkeit wäre, die Ausdrücke aus den Namen der Projektionsmethoden abzuleiten. Dies demonstriert das folgende Beispiel.

<Department>
  <Users>
    <Name>John Doe</Name>
    <Name>Tommy Atkins</Name>
  </Users>
</Department>
@XBDocURL(“resource://extexample.xml“)
public interface ExampleProjection {        
    @XBRead
    List<String> getDepartmentUsersName();
  }
XBProjector projector = new XBProjector();
projector.config().setExternalizer(new ExternalizerAdapter() {
  @Override
  public String resolveXPath(String annotationValue, Method method, Object[] args) {
    // Simplest conversion of camel case getter name to xpath expression.
    return method.getName().substring(3).replaceAll("[A-Z]","/$0");
  }
});
List<Sting> deparmentUsers = projector.io().fromURLAnnotation(ExampleProjection.class).getDepartmentUsersName();

Im vierten und letzten Teil dieser Serie werden wir auf die Erzeugung von komplexen XML-Dokumenten eingehen. Außerdem erklären wir, wie man Projektionen um eigene Methodenimplementierungen bereichert und wie sich die Typkonvertierung erweitern lässt.

Aufmacherbild: Abstract lighting flare von Shutterstock / Urheberrecht: jehsomwang

Geschrieben von
Sven Ewald
Sven Ewald
Sven Ewald ist Autor der Bibliothek XMLBeam. Seit 15 Jahren setzt er Java zur Lösung unterschiedlichster Anforderungen ein und noch ist kein Ende in Sicht. Momentan entwickelt er IDEs für domainspezifische Sprachen bei der DSA Daten und Systemtechnik GmbH.
Kommentare

Hinterlasse einen Kommentar

2 Kommentare auf "Ein neuer Blick auf XML – Teil 3"

avatar
400
  Subscribe  
Benachrichtige mich zu:
Oliver Gierke
Gast

Verdammt, diesen Artikel hätte ich am Wochenende brauchen können :). Ich war diesem Beispiel [0] gefolgt, mit einer pom.xml die eben den default namespace verwendet. In diesem Fall funktioniert die Standardkonfiguration leider nicht OOTB, weil man eben alle Ausdrücke mit xbdefault: präfixen muss. Wäre es nicht sinnvoller by default eben den Default-Namespace auf einen ohne Präfix abzubilden?

Gruß,
Ollie

[0] http://xmlbeam.org/t04.html

SvenEwald
Gast

Sorry, ich werde mich bemühen, das nächste Problem schneller vorherzusehen 😉
Ich hab mir das auch gedacht, dass das sinnvoll wäre. Allerdings müsste dafür der XPath-Ausdruck gebarst werden um die Element-Selektoren zu finden. Und es würde kein XPath 1.0 mehr sein. Daher habe ich mich dagegen entschieden und hoffe auf einen gescheiten XPath 2.0-Parser. Die Version 2.0 hat nämlich genau das eigentliche erwartete Verhalten bei Default-Namespaces.
PS: Danke für's Ausprobieren und für's Feedback!