Template-basierte Dokumentenerstellung mit XMLBeam

Ein neuer Blick auf XML: XMLBeam Teil 4

Sven Ewald
©Shutterstock.com/jehsomwang

Im vierten und letzten Teil zeigen wir wie sich aus Templates ein Dokument zusammensetzen lässt und wie Projektionen um eigene Methodenimplementierungen erweitert werden können. Zum Abschluss dieser Serie werfen wir noch einen Blick auf die Typenkonvertierung.

Bereits im zweiten Teil dieser Serie wurde gezeigt, dass Projektionen auch schreiben können. Nun gehen wir tiefer darauf ein und lernen, dass sich XMLBeam beim Schreiben nicht nur auf das Verändern von Element- und Attributinhalten beschränkt. Mit Hilfe der im ersten Teil gezeigten Unterprojektionen besteht die Möglichkeit, ganze Elemente und Unterelemente in den DOM-Baum zu schreiben. Das lässt sich nutzen, um bei der Erstellung von Dokumenten statische und dynamische Anteile getrennt zu behandeln und damit den Programmieraufwand gering zu halten. Dazu definieren wir Templates, die als Projektionen eingelesen, im Javacode verändert und dann zu einem neuen Dokument zusammengefügt werden.
Um das Prinzip zu zeigen, dient hier ein auf das Wesentliche vereinfachte SVG Dokument.

<svg>
   <!-- simple document template -->
	<g style="stroke:black; stroke-width:3;"/>
</svg>

Das Element „<svg>“ wird später die Basis unseres Dokumentes darstellen, in das per Java weitere Elemente einfügt werden. Wir erzeugen drei weitere Templates mit sinnvollen Vorbelegungen.

<rect width="100" height="100" style="fill:blue" />
<circle r="50" style="fill:red" />
<ellipse rx="50" ry="100" style="fill:yellow" />

Anschließend definieren wir einige Projektionsinterfaces, um mit den Templates arbeiten zu können. Um Methoden gemeinsam nutzen zu können, verwenden wir für die Projektionen Shape, Rectangle, Circle und Ellipse Java-Typvererbung. Dass dies innere Interfaces von SVG sind, dient nur der Übersichtlichkeit und hat keine Bedeutung für die Ausführung.

@XBDocURL("resource://svg_document_template.svg")
public interface SVG {

    public interface Shape {
        @XBWrite("./@x")
        Shape setX(int x);

        @XBWrite("./@y")
        Shape setY(int y);
    }

    @XBDocURL("resource://rect_template.svg")
    public interface Rect extends Shape {
        @XBWrite("./@width")
        Rect setWidth(int w);

        @XBWrite("./@heigth")
        Shape setHeight(int h);
    }

    @XBDocURL("resource://circle_template.svg")
    public interface Circle extends Shape {
        @XBWrite("./@r")
        Shape setRadius(int r);
    }

    @XBDocURL("resource://ellipse_template.svg")
    public interface Ellipse extends Shape {
        @XBWrite("./@rx")
        Shape setRadiusX(int rx);
        
        @XBWrite("./@ry")
        Shape setRadiusY(int ry);
    }

    @XBWrite("/svg/g/*")
    SVG setShapes(List<Shape> shapes);
}

Eine Besonderheit stellt die Methode setShapes(List<Shape>) dar. Sie nimmt Projektionen des Typs Shape und schreibt sie als Unterelemente des Elements „<g>“ in den DOM-Baum der Projektion SVG. Der eigentliche Trick findet versteckt statt, denn die zu schreibenden Objekte sind in diesem Fall gar keine Unterprojektionen auf Elemente, sondern Projektionen auf Dokumente. XMLBeam lässt dies zu und erkennt automatisch, dass in diesem Fall nicht das Projektionsziel in den DOM-Baum eingefügt wird, sondern eine Kopie des Root-Elementes. Die Kopie ist nötig, weil das Element beim Schreiben den Besitzer wechselt. Der Platzhalter „*“ im XPath-Selektor weist XMLBeam an, den Namen des Elementes beizubehalten. Stünde dort „/svg/g/shape“, würden die geschriebenen Elemente zusätzlich noch in „shape“ umbenannt.

XBProjector projector = new XBProjector(Flags.TO_STRING_RENDERS_XML);

Rect rect = projector.io().fromURLAnnotation(Rect.class);
Circle circle = projector.io().fromURLAnnotation(Circle.class);
Ellipse ellipse = projector.io().fromURLAnnotation(Ellipse.class);

List<Shape> shapes = new LinkedList<Shape>();
shapes.add(rect.setX(10).setY(120));
shapes.add(circle.setX(60).setY(60));
shapes.add(ellipse.setX(180).setY(120));

SVG svgDoc = projector.io().fromURLAnnotation(SVG.class);
svgDoc.setShapes(shapes);

System.out.println(svgDoc.toString());

Als Ausgabe unseres Beispielprogrammes erhalten wir das folgende SVG Dokument.

<svg>
  <!-- simple document template -->
  <g style="stroke:black; stroke-width:3;">
    <rect height="100" style="fill:blue" width="100" x="10" y="120"/>
    <circle r="50" style="fill:red" x="60" y="60"/>
    <ellipse rx="50" ry="100" style="fill:yellow" x="180" y="120"/>
  </g>
</svg>

Benutzerdefiniertes Verhalten von Projektionen

Wir haben in den vorherigen Teilen bereits in einigen Beispielen gesehen, dass sich Projektionen wie herkömmliche Javaobjekte (POJOs) verwenden lassen. Projektionen können dank sinnvoller Implementierung von equals() und hashcode() direkt in Collections verwendet werden. Sie unterstützen Vererbung und können sogar von Serializable erben um dann wie gewöhnliche Objekte serialisiert zu werden. Nun zeigen wir, wie es möglich ist, eigene Methodenimplementierungen zu den Projektionsmethoden hinzuzufügen.
Verwendet man XMLBeam mit Java 8, so lassen sich im Projektionsinterface einfach Default-Methoden hinzufügen. Diese können Projektionsmethoden oder andere Default-Methoden aufrufen, obwohl XMLBeam abwärtskompatibel bis Java 6 ist.

Falls man auf eine ältere Javaversion angewiesen ist und keine Default-Methoden zur Verfügung stehen, ist es dennoch möglich, Methoden zu überschreiben oder hinzuzufügen. Dazu bedient sich XMLBeam eines Konzeptes, das an die Mixins anderer Programmiersprachen angelehnt ist. Natürlich erweitert XMLBeam nicht den Java-Sprachumfang um echte Mixins. Der Begriff stellt in diesem Kontext aber eine gute Metapher dar und wird der Einfachheit halber im Folgenden für ein Bündel von Methodenimplementierungen verwendet, welches einer Projektion hinzugefügt werden soll. Zunächst wird ein Mixin-Interface definiert:

public interface Validatable {
	boolean isValid();	
}

Nun lassen wir unser Projektionsinterface von dem Interface Validatable erben, damit die Verwender der Projektion diese Methode aufrufen können:

public interface Person extends Validatable {
	@XBRead(„/person/@age“)
	int getAge();	
}

Was noch fehlt, ist die Definition der Methode „isValid()“. Sie wird in einem eigenem „Mixin-Objekt“ implementiert und vor dem Erzeugen der Projektion beim Projektor registriert:

projektor.mixins().addProjectionMixin(Person.class,new Validatable() {
	private Person me;
	@Override
	public boolean isValid() {
	   return me.getAge() >= 18;
	}
});

XMLBeam sorgt nun dafür, dass Aufrufe von person.isValid() an die zu dem Projektionsinterface Person zugeordnete Implementierung weitergeleitet werden. Um in dieser Implementierung Zugriff auf die Projektionsmethoden zu erhalten, wird die jeweils aktuelle Projektion vorher in das Attribut „me“ injeziert. Auf diese Weise können Mixin-Interfaces bei verschiedenen Projektionsinterfaces mit unterschiedlichen Implementierungen registriert werden. Es ist auch möglich, bereits vorhandene Methoden wie „toString()“ zu überschreiben oder schon bestehende Interfaces wie Comparable als Mixin-Interface zu verwenden.

Konfiguration der Typenkonvertierung

XMLBeam sorgt automatisch dafür, dass gelesene XML-Daten in den gewünschten Rückgabetypen der Projektionsmethoden konvertiert werden. Diese Konvertierung lässt sich leicht eigenen Bedürfnissen anpassen:

public static class HexToLongConversion extends Conversion<Long> {

        private HexToLongConversion(final Long defaultValue) {
            super(defaultValue);
        }

        @Override
        public Long convert(final String data) {
            return Long.parseLong(data, 16);
        }
    }
public interface Projection {
        @XBRead("/foo")
        long getData();
    }

In unserem Beispiel sollen hexadezimale Zahlen aus einem XML-String als Long konvertiert werden. Dazu implementieren wir das Interface Conversion und übergeben unsere Instanz dem DefaultTypeConverter als gewünschte Konvertierung für Long.

XBProjector projector = new XBProjector();
DefaultTypeConverter converter = projector.config().getTypeConverterAs(DefaultTypeConverter.class);
converter.setConversionForType(Long.TYPE, new HexToLongConversion(0L));
Projection projection = projector.projectXMLString("<foo>CAFEBABE</foo>", Projection.class);
assertEquals(3405691582L, projection.getData());

So ist es möglich, bestehende Konvertierungen zu überschreiben oder Neue für eigene Java-Typen hinzuzufügen.

Hiermit endet der letzte Teil dieser Serie über XMLBeam. Weitere Beispiele und Tutorials befinden sich auf der Projektseite.

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: