Teil 2: Von dynamischen Projektionen und externen Dokumenten

Ein neuer Blick auf XML – Teil 2

Sven Ewald
©Shutterstock.com/jehsomwang

Dieses Mal gehen wir etwas tiefer auf die Features der Bibliothek XMLBeam ein. Wir zeigen, wie sich die Projektionsziele parametrisieren lassen, erklären die Verwendung von externen Dokumenten und werfen einen ersten Blick auf schreibende Projektionen.

Im ersten Teil dieser Serie lernten wir das Konzept der Datenprojektion und die Funktionsweise von Unterprojektionen kennen. Durch die Verknüpfung von XPath-Ausdrücken mit Methodendeklarationen wird die Java-Bibliothek zu einem erstaunlich vielseitigen Werkzeug. Heute verwenden wir XMLBeam noch etwas vielseitiger, indem wir Projektionsmethoden um Parameter erweitern. Diese Parameter können noch zur Laufzeit das Ziel der Projektionsmethoden beeinflussen. Wieder soll uns ein Beispiel aus der realen Welt den Einstieg erleichtern:

<profiles>
  <profile name="Some Profile" version="8">
    <setting id="org.eclipse.jdt.core.formatter..." value="false" />
  		... 
  </profile>
  <profile name="Another Profile" version="8">
    <setting id="org.eclipse.jdt.core.formatter..." value="true" />
    	 ...
  </profile>
</profiles>

Die XML-Datei enthält mehrere Konfigurationsprofile, für die wir ein Zugriffsinterface definieren möchten. Das Java-Interface soll in der Lage sein, uns eine Liste der Profile zu liefern und zu einem Profil die zugehörigen Settings auszugeben.

@XBDocURL("resource://eclipsecodeformatprofile.xml")
public interface EclipseFormatterConfigFile {
 
    interface Setting { 
        @XBRead("@id")
        String getName();
 
        @XBRead("@value")
        String getValue();         
    }
 
    @XBRead("//profile/@name")
    List<String> getProfileNames();
 
    @XBRead("//profile[@name="{0}"]/setting")
    List<Setting> getAllSettingsForProfile(String profileName);    
}

Die erste Zeile mit der Annotation @XBDocURL ignorieren wir vorerst, denn darauf kommen wir später zu sprechen. Das innere Interface Setting soll uns als Unterprojektion für jeden Konfigurationseintrag den entsprechenden Wert liefern. Die erste Anforderung, eine Liste der Profilnamen zu liefern, wird von der Methode getProfileNames() umgesetzt. Ihr XPath-Ausdruck selektiert von allen im Dokument vorkommenden Elementen „profile“ jeweils das Attribut „name“ und liefert damit genau das gewünschte Ergebnis.
Spannend wird es nun bei der Methode getAllSettingsForProfile(String), welche als dynamische Projektionsmethode umgesetzt ist. Sie definiert einen Parameter „profileName“,  der den Platzhalter „{0}“ in dem XPath-Ausdruck füllt. Das funktioniert, weil beim Aufruf einer Projektionsmethode der Ausdruck durch die Klasse java.text.MessageFormat mit allen Parametern der Methode formatiert wird. Dadurch ist es möglich, das eigentliche Ziel der Projektionsmethode durch eine beliebige Anzahl von Parametern bestimmen zu lassen. Die recht umfassende Syntax der Klasse MessageFormat wird hier ausführlich beschrieben. In unserem Beispiel soll nun ein einfacher String als Parameter genügen. Nachdem das Projektionsinterface definiert ist, reichen vier Zeilen Code, um die Liste der enthaltenen Profile und alle Einstellungen eines Profiles auszugeben.

EclipseFormatterConfigFile configFile = new XBProjector().io().fromURLAnnotation(EclipseFormatterConfigFile.class);

System.out.println("Profile names:" + configFile.getProfileNames());

for (Setting setting:configFile.getAllSettingsForProfile("Some Profile")) {
    System.out.println(setting.getName()+" -> "+setting.getValue()); 
}

Nun wird es Zeit, die Erklärung der Annotation @XBDocURL nachzuholen. Sie ordnet einer Projektion einen Ort zu, über den das Zieldokument gefunden werden kann. Genau das geschieht beim Aufruf von projector.io().fromURLAnnotation(…). Der Projektor akzeptiert dabei neben den von java.net.URL verarbeiteten Protokollen auch den speziellen Identifier „resource“. Dieser weist den Projektor an, das Dokument über den Classloader des Projektionsinterfaces zu laden. Es genügt also, die XML-Datei neben das Projektionsinterface zu legen, damit sie gefunden wird. Dabei ist es dann unerheblich, ob das Dokument über das Dateisystem, über das Netzwerk oder aus einem Jar geladen wird. Nach erfolgreichem Laden über eine @XBDocURL Annotation wird dieser Ort im DOM-Baum als BaseURI verwendet. Natürlich würde auch ein Aufruf von projector.io().url(„resource://eclipsecodeformatprofile.xml“).read(…) funktionieren. Die Annotation hat allerdings noch einen weiteren Verwendungszweck. Es ist auch erlaubt, sie an Projektionsmethoden zu verwenden. Dadurch ist das Ziel der XPath-Auswertung nicht mehr das Dokument des Projektionsinterfaces, sondern ein externes Dokument. So wird es möglich, mehrere Dokumente über ein einziges Javaobjekt anzusprechen. Ein einfaches Beispiel:

public interface ExternalDocumentsDemo {
 
  @XBDocURL("http://somewhere.examle/fooDocument.xml")
  @XBRead(“//foo”)
  String getFoo();

  @XBDocURL("http://somewhere.examle/barDocument.xml")
  @XBRead(“//bar”)
  String getBar();         
}

Das Interface ExternalDocumentsDemo definiert zwei Projektionsmethoden auf verschiedene externe Dokumente. Wenn diese Methoden aufgerufen werden, wird der zugehörige XPath-Ausdruck nicht mehr auf das dem Projektionsinterface zugehörigen Dokument angewendet. Das Projektionsinterface braucht sogar gar kein existierendes Dokument.

ExternalDocumentsDemo projection = new 
XBProjector().projectEmptyDocument(ExternalDocumentsDemo.class);
System.out.println(“Content of foo:”+projection.getFoo());
System.out.println(“Content of bar:”+projection.getBar());

Stattdessen wird jeweils das Zieldokument geladen, der XPath-Ausdruck ausgewertet, das Ergebnis als Rückgabetyp konvertiert und zurückgegeben. Dies lässt sich auch mit dynamischen Projektionen verbinden, da Methodenparameter auch für den Dokumentenort ausgewertet werden.

public interface DynamicExternalDemo {
  @XBDocURL("{0}")
  @XBRead("{1}")
  String evaluateXPathfromURL(String url, String xpath);
}

Beispiel 3 zeigt die Ausdrucksstärke von XMLBeam. Das Laden eines beliebigen Dokumentes und das Extrahieren eines Wertes sind nur wenige Javazeilen entfernt.
Bevor wir zu tief in Spielereien mit externen und dynamischen Projektionen eintauchen, wenden wir uns schnell noch einem anderen Thema zu: Bidirektionale Projektionen. Bereits im ersten Teil haben wir gelernt, dass Projektionen Sichten auf DOM-Bäume sind. Diese Sichten müssen sich allerdings nicht auf das Lesen von Elementen oder Attributen beschränken. Auch das Erzeugen, Schreiben und Löschen von Elementen und Attributen wird unterstützt. Dabei kommt zwar auch wieder XPath zum Einsatz, allerdings muss die Syntax für Schreiboperationen eingeschränkt werden und es steht kein vollwertiger Parser zur Verfügung. Es können z.B. keine Funktionen benutzt werden und der Zielpfad muss im Gegensatz zum Lesen immer eindeutig sein. Werden  diese Anforderungen an den XPath-Ausdruck erfüllt, sind Lesen und Schreiben symmetrische Operationen, d.h. beim Lesen erhält man die gleichen Daten, die man beim Schreiben hinterlegt hat. Das Schreiben von bereits gelesenen Daten ändert das Dokument nicht.

public interface Bidirectional {
  @XBRead("/a/b/c/foo")
  String getFoo();

  @XBWrite("/a/b/c/foo")
  void setFoo(String value)

  @XBDelete("/a/b/c/foo")
  void deleteFoo();
}

Eine schreibende Projektionsmethode wird analog zu einer Lesenden deklariert. Statt @XBRead wird die Annotation @XBWrite verwendet. Natürlich muss die Methode einen Parameter für den zu schreibenden Wert besitzen. Wird hier der Wert null übergeben, wird der Wert des Projektionszieles gelöscht. Das umgebene Element wird dabei jedoch nicht gelöscht, sondern bleibt einfach leer bestehen. Möchte man ganze Elemente oder Attribute entfernen, so gibt es dafür die Annotation @XBDelete. Der Parametertyp einer schreibenden Projektionsmethode ist im Prinzip beliebig. Zur Konvertierung als XML-Wert wird die Methode toString() benutzt, welche sinnvoll implementiert sein muss.
Was, wenn man schreibende und dynamische Projektionsmethoden kombinieren möchte? Also Methoden mit mehreren Parametern, von denen einer den zu schreibenden Wert enthält und die anderen den XPath-Ausdruck parametrisieren? Wie das geht, zeigt unser Beispiel 5.

public interface DynamicBidirectional {
  
 @XBWrite("/a/{1}/foo")
 void setFoo(String value,String parentElementName)

 @XBWrite("/a/b/foo[@key=”{0}”]/@value")
 void setValue(String key,@Value int value);
}

Bei der Methode setFoo() wird einfach der weitere Parameter angefügt und im XPath-Ausdruck referenziert. Der erste Parameter wird als Wert in das Element „foo“ geschrieben. Im zweiten Fall, bei der Methode setValue(), bevorzugen wir das API Design andersherum und markieren den zweiten Parameter als den zu schreibenden Wert. Dazu genügt es, die Annotation @Value an den Parameter zu setzen. Wir werden später, wenn es um die Erzeugung von kompletten Dokumenten geht, tiefer auf schreibende Projektionen eingehen.
 Im dritten Teil der Serie erklären wir den Umgang mit Namespaces, gehen auf die Konfigurationsmöglichkeiten des Projektors ein und zeigen, wie sich die XPath-Ausdrücke außerhalb des Java-Codes ablegen lassen.

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

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: