Teil 1: Datenprojektion mit XMLBeam

Ein neuer Blick auf XML

Sven Ewald
©Shutterstock.com/jehsomwang

Der Industriestandard XML ist so allgegenwärtig, dass es verwundern mag, noch etwas Neues zu entdecken. Die Java-Bibliothek XMLBeam setzt ein neues Konzept im Umgang mit DOM-Bäumen um. Anstatt die Java-Welt per Datenbindung mit XML zu koppeln, erlaubt sie es, Objekte aus der einen Welt in die jeweils andere  „zu projizieren“.

Datenbindung-Frameworks verbinden XML-Dokumente mit Java-Objekten, indem sie die Struktur der XML-Entitäten in einer Objekthierarchie nachbilden. Diese sind mitunter recht sperrig und können das Design einer Java-API erschweren. Um eine größere Trennung zwischen XML-Struktur und Java-Code zu erreichen, geht XMLBeam einen anderen Weg: Die Daten verbleiben im DOM-Baum und sind direkt in Form von Javatypen nutzbar. Dazu wird ein Projektionsinterface mit Projektionsmethoden definiert. Diese legen fest, wie Daten aus dem Dokument gelesen oder in das Dokument geschrieben werden. Gleichzeitig können Instanzen dieser Projektionsinterfaces wie normale Javaobjekte verwendet werden. Sie sind das Bindeglied zwischen Javacode und DOM-Baum.
Den Zugriff auf das Dokument spezifizieren XPath-Ausdrücke, die über Annotationen den Projektionsmethoden zugeordnet werden. Dies erlaubt es, mehrere Elemente oder Attribute zu einem Javaobjekt zu bündeln oder ein Element auf mehrere Javaobjekte aufzuteilen. Statt der Struktur des XML-Dokumentes zu folgen, lässt sich eine eigene Sicht auf den zugehörigen DOM-Baum definieren. Dabei sind sowohl lesende als auch schreibende Methoden erlaubt.
Ein erstes Beispiel soll jetzt den Einstieg erleichtern: Der MSN Wetter Dienst (Tutorial) liefert Wetterdaten in XML für eine erste Demonstration:

<weatherdata>
      <weather ... degreetype="F"
        lat="50.5520210266113" lon="6.24060010910034" 
        searchlocation="Monschau, Stadt Aachen, NW, Germany" ... >
        <current ... skytext="Clear" temperature="46"/>
      </weather>
</weatherdata>

Wir definieren ein Projektionsinterface um Wetterbeschreibung, Ort und Temperatur auszulesen. Das XML Attribut „searchlocation“ soll als Java String, die Temperatur hingegen als Integer konvertiert werden. Entsprechend wird der Rückgabewert unserer Projektionsmethoden definiert. Jeder Methode wird über die Annotation @XBRead ein XPath-Ausdruck zugeordnet, der die gewünschten Elemente oder Attribute selektiert. Der Name der Projektionsmethoden kann frei gewählt werden, es empfiehlt sich jedoch bei der Javabean-Spezifikation zu bleiben und Getter-Methoden entsprechende Namen zu geben.

public interface WeatherData {
	@XBRead("/weatherdata/weather/@searchlocation")
	String getLocation();

	@XBRead("/weatherdata/weather/current/@temperature")
	int getTemperature();

	@XBRead("/weatherdata/weather/@degreetype")
	String getDegreeType();

	@XBRead("/weatherdata/weather/current/@skytext")
	String getSkytext();

	interface Location {
		@XBRead("@lon")
		double getLongitude();
 		
		@XBRead("@lat")
		double getLatitude();
	}
	@XBRead("/weatherdata/weather")
	Location getCoordinates();
	}

Was hat es nun mit dem inneren Interface „Location“ auf sich? Während sich die Projektionsmethoden in dem Interface WeatherData auf das gesamte Dokument beziehen, ist Location eine Unterprojektion auf ein einzelnes XML-Element und dient der Methode getCoordinates() als Rückgabewert. Dadurch ist es möglich, die Geokoordinaten in einem Objekt zu bündeln, auch wenn es in dem Dokument gar kein Element Location gibt. Man beachte, dass die XPath-Ausdrücke nun relativ zu einem Element evaluiert werden. An welches Element eine Unterprojektion gebunden wird, hängt von Ausdruck der erzeugenden Methode ab.

private void printWeatherData(String location) throws IOException {
	final String BaseURL = "http://weather.service.msn.com/find.aspx?	
outputview=search&weasearchstr="; WeatherData weatherData = new XBProjector().io().url(BaseURL + location).read(WeatherData.class); System.out.println("The weather in " + weatherData.getLocation() + ":"); System.out.println(weatherData.getSkytext()); System.out.println("Temperature: " + weatherData.getTemperature() + "°" + weatherData.getDegreeType()); Location coordinates = weatherData.getCoordinates(); System.out.println("The place is located at " + coordinates.getLatitude() + "," + coordinates.getLongitude()); }

Als Einstiegspunkt zum Erzeugen, Lesen oder Schreiben von Projektionen dient eine Instanz der Klasse XBProjektor. Da für unser Beispiel die Defaulteinstellungen des Projektors genügen, reicht ein Aufruf des Standardkonstruktors.
Die Eingabe und Ausgabeoperationen sind hinter der Methode io() thematisch gruppiert. So führt io().stream() zu den Operationen mit InputStream, bzw. OutputStream und io().file() zu Dateioperationen. Auf diese Weise spart die Code-Completition oft den Blick in die Dokumentation. In unserem Beispiel wird über io().url(…).read(…) eine HTTP-GET Anfrage gestellt und die Antwort als Projektion mit dem Typen WeatherData abgebildet.
Gleich darauf kann die Projektion weatherData wie ein gewöhnliches Javaobjekt verwendet werden. Sie besitzt sogar implizite equals() und hashcode-Methoden, die eine intuitive Verwendung von Projektionen in Java-Collections erlauben. Bei der Ausgabe der Wetterbeschreibung und der Temperatur werden die per XPath selektierten Werte entsprechend ihres gewünschten Rückgabetypes automatisch konvertiert. Falls der XPath-Ausdruck einer Projektionsmethode keinen Treffer im Dokument findet, wird null zurückgegeben.
Die Unterprojektion Location wird über die Methode getCoordinates() erzeugt. Im Unterschied zu den anderen Methoden ist der Rückgabetyp dieser Methode ein Interface und der zugehörige  Xpath-Ausdruck selektiert kein Attribut, sondern ein Element. Da Projektionen nur „Sichten“ auf den DOM-Baum sind, können sie andere Projektionen enthalten oder sich mit anderen Projektionen überlappen.
Wie geht man nun damit um, wenn der XPath-Ausdruck mehrere Attribute oder Elemente im Dokument selektiert? In diesem Fall wird nur der erste Treffer zurückgeliefert. Wie alle selektierten Elemente oder Attribute ausgelesen werden können, zeigt das zweite Beispiel.

<example2>
	<elment>
		<subelement> 100 </subelement>
		<subelement> 200 </subelement>
		<subelement> 300 </subelement>
		<subelement> 400 </subelement>
	</element>
</example2>

Im zweiten Beispiel beleuchten wir Projektionen zum Auslesen von Wertemengen. Zur Auswahl steht die Rückgabe als Array oder Java-List.

public interface Example2 {
  @XBRead(“//subelement”)
  int[] getAllSubelementValues();

  @XBRead(value=“//subelement”, targetComponentType=Integer.class)
  List<Integer> getAllSubElemetValuesAsList();
}

Entsprechend des deklarierten Rückgabetyps der Methode getAllSubelementValues() werden alle selektierten Werte der Elemente „subelement“ in einem Integer-Array zurückgegeben. Hier ignorieren wir einfach die vorgegebene Struktur und lesen die Werte elementübergreifend aus. Soll nun statt eines Arrays eine Java-Liste erzeugt werden, ist ein Trick notwendig, damit die Projektionsmethode die Konvertierung wie gewünscht durchführen kann. Zur Ausführungszeit bleibt nach der Typlöschung (Type Erasure) von den Generics nichts mehr übrig. Um die Liste dennoch mit den gewünschten Typen füllen zu können, muss der Zieltyp in der Annotation der Methode getAllSubElemetValuesAsList() mit angegeben werden. Die Projektionsmethode wird nun beim Aufruf eine LinkedList erzeugen und mit allen Werten der selektierten Elemente füllen.
Im zweiten Teil dieser Reihe werden wir bidirektionale, also lesende und schreibende Projektionen sowie dynamische Projektionen, die erst zur Laufzeit ihren  XPath-Ausdruck zusammensetzen, kennenlernen.

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

4 Kommentare auf "Ein neuer Blick auf XML"

avatar
400
  Subscribe  
Benachrichtige mich zu:
Oliver Gierke
Gast
Sehr nett. Gibt es Erfahrungen wie das mit der Performance ausschaut? Das proxying kostet bestimmt etwas. Ich kann mir aber gut vorstellen, dass man dadurch, dass man eben sehr gezielt Werte aus dem Dokument greift gegenüber einfachem JAXB mapping wieder gut etwas spart. Ach, evtl. ein kleiner Hinweis noch: die generischen Typen für getAllSubElemetValuesAsList() sind natürlich *nicht* einfach Weg. Man da bequem mit getMethod().getGenericReturnType() einfach auf den ParameterizedType zugreifen. Das ist oft ein misverstandener Aspekt von type-erasure. Man kann aus einer List *Instanz* nicht mehr den Component-Type bestimmen. Sobald man aber Reflection-Artefakte in die Hand bekommt (Felder, Methoden, Parameter) geht… Read more »
SvenEwald
Gast

Vielen Dank für`s Feedback 🙂
Die Performance hängt vor allem vom verwendeten XPath-Interpreter ab. Wie man einen anderen als den des JDK verwendet, das wird in einem späteren Teil erklärt.
Ein SAX-Parser wird aber in jedem Fall schneller sei.
Was den generischen Typen der Liste angeht,
so habe ich einfach übersehen dass ich mich an der Stelle darauf verlassen kann, dass getGenericReturnType() einen ParameterizedType zurück liefert. Diese Verbesserung fliesst auf jeden Fall in die Version 1.1 mit ein.

Oliver Gierke
Gast

Ah, ich wusste nicht, dass du der Autor der Library. Dann gleich mal doppelt danke: ein sehr nützliches Helferlein.

Zu dem Rückgabewert: getGenericReturnType() gibt einen Type. Den muss mann dann per instanceof weiterbehandeln. Aber die verschiedenen Möglichkeiten die ihr da unterstützt sind ja überschaubar: einfache Class<?>, Arrays davon oder ParameterizedTypes, wobei die dann vmtl. erstmal nur Collections sein können.

Macht es evtl. noch Sinn bei einem leeren value-Attribut einfach aus dem Methodennamen das zu lesende Element abzuleiten? getFoo() liest eben "/foo" by default usw.

SvenEwald
Gast
Ich denke in diesem Fall reicht sogar ein Cast: "((ParameterizedType) type).getActualTypeArguments()[0] " müsste immer ein Class Objekt liefern. Denn es sind nur Listen auf Java-Primitives-Klassen, String, DOM-Nodes und Unterprojektionen zugelassen. Also nur Zeug was der Projektor auch erzeugen kann. Ich habe es soweit eingebaut und die Testfälle sind noch grün 😉 Den XPath Ausdruck aus dem Methodennamen abzuleiten ist sogar mit XMLBeam-Bordmitteln machbar. Dafür mach ich mal am Wochenende ein Tutorial. Die XPath Ausdrücke können externalisiert werden, z.B. in eine Properties-Datei, Datenbank oder auch eine Implementierung die den Pfad aus dem Methodenobjekt herleitet. Wenn man das tut, dann hat man… Read more »