Groovy: Templates
← Builder | ↑ Neue Konzepte | → Das Groovy-JDK
Relativ häufig besteht die Notwendigkeit, irgendwelche Daten, die innerhalb des Programms eingelesen oder berechnet werden, in einem zusammenhängenden Text auszugeben. Ein mögliches Vorgehen dabei ist, diesen zu erzeugenden Text als Muster zu erfassen, in dem für die konkreten Daten spezielle Platzhalter vorgesehen sind. Im Programm wird dann nur noch der Mustertext komplett eingelesen und mit den darzustellenden Daten gemischt, und schon haben Sie das fertig formatierte Resultat. Der Vorteil ist, dass das Formatieren von Texten auf diese Weise viel übersichtlicher ist, als wenn sie stückweise zusammengesetzt werden. Außerdem können Sie die Texte getrennt vom Programm speichern und bei bedarf ändern, ohne das Programm selbst anfassen zu müssen. Auch mehrsprachige Ausgabeformate sind so leicht zu realisieren.
Besonders beliebt ist dieses Vorgehen bei der Generierung von dynamischen Webseiten; in Java gibt es dafür beispielsweise JavaServer Pages oder Velocity, und viele andere Programmierumgebungen bieten vergleichbare Werkzeuge, die man generell als Template-Engines bezeichnet.
Ein einfaches Beispiel für das Mischen von Text und Daten kennen Sie bereits in der Form der GStrings, die ja ein fester Bestandteil der Programmiersprache Groovy sind. GStrings sind allerdings eine Art von Templates, die nur innerhalb des Programms erzeugt und nicht etwa als Datei eingelesen werden kann.
Da Template-Engines in vielen Situationen nützlich sein können, umfasst die Groovy-Umgebung ein abstraktes Framework für Template-Engines und drei konkrete Ausprägungen für unterschiedliche Anwendungsfälle. Alle eignen sich dazu, Texte und Daten zu mischen, wenden dabei aber unterschiedliche Mechanismen an. Wir werden hier auf die prinzipiellen Mechanismen eingehen und zeigen, wie Sie die Template-Engines in einem Groovy-Programm generell anwenden können. Auf spezifische Anwendungsfälle kommen wir im Kapitel ##LIBRARY## zu sprechen.
Das Vorgehen
BearbeitenDas Mischen, also das Verknüpfen von laufendem Text mit Daten, läuft immer so ab, dass Sie am Anfang einerseits eine Textvorlage mit eingebetteten Platzhaltern, und andererseits die dort einzufügenden Programmdaten haben. Das Ergebnis ist ein neuer Text mit dem Inhalt der Textvorlage und die darin eingefügten Programmdaten. Die einzelnen Schritte sind folgende:
- Sie besorgen sich eine Instanz der zu verwendenden Template-Engine.
- Die Template-Engine erzeugt aus der Textvorlage das eigentliche Template. Dies ist ein Objekt einer Klasse, die das Interface groovy.text.Template implementiert.
- Sie erzeugen eine Map, in die alle Daten, die dem Template zur Verfügung gestellt werden sollen, unter einem eindeutigen Namen eingetragen werden. Diese Map entspricht dem Binding eines Skripts und wird auch so bezeichnet.
- In einem Aufruf der Methode make() übergeben Sie Ihre Daten dem Template, das den eigentlichen Mischvorgang ausführt und als Ergebnis ein Writable-Objekt liefert.
- Das Endresultat in Textform erhalten Sie erst, wenn Sie das Writable-Objekt einem Writer übergeben oder einfach seine toString()-Methode aufrufen.
Die Templates sind wiederverwendbar, Sie können also die Schritte 3 bis 5 mehrmals hintereinander mit unterschiedlichen Daten ausführen, ohne jedes Mal ein neues Template erzeugen zu müssen.
Die Strategien zur Erzeugung des gemischten Textes können sehr unterschiedlich sein. So kann in einem Fall der gesamte Mischvorgang schon beim make() des Templates stattfinden, so dass das Writable-Objekt schon den kompletten Text enthält, der dann am Ende nur noch auszugeben ist. Es kann aber auch sein, dass der Mischvorgang überhaupt erst beim Herausschreiben des Writable-Objekts geschieht. Letzteres kann sinnvoll sein, wenn der Ergebnistext sehr lang ist, so dass es effizienter ist, wenn er gar nicht erst als kompletter String im Speicher gehalten, sondern gleich beispielsweise in eine Datei geschrieben oder über das Netzwerk versendet wird.
Wir wollen das Vorgehen an einem sehr einfachen Beispiel erläutern. Angenommen, wir haben programmieren ein einfaches Telefonverzeichnis. Jeder Eintrag in dem Verzeichnis wird im Groovy-Programm durch ein Objekt der folgenden simplen Klasse repräsentiert:
class Kontakt { def name def nummern }
Die Property name enthält den Namen einer Person und die Property nummern eine Liste mit Strings, in denen jeweils eine Telefonnummer und ggf. noch eine zusätzliche Bemerkung stehen. Das gesamte Verzeichnis ist wiederum eine Liste aus Kontakt-Objekten, die wir in einer Variablen namens kontaktliste gespeichert haben. Wir fangen klein an, daher hat unsere Kontaktliste erst einmal nur drei Einträge, wobei es zu einem der Einträge noch gar keine Nummern gibt.
kontaktliste = [ new Kontakt(name:'Klaus Schulz', nummern:['029281 dienstl.','028277 privat']), new Kontakt(name:'Jens Krause'), new Kontakt(name:'Erika Meier', nummern:['02827']) ]
Da sich Kontakte, wie wir wissen, schnell ändern können, haben wir auch den Stand der Liste als Datum in einer Variablen namens stand. Im Beispiel ist es einfach das heutige Datum.
stand = new Date()
Dieses Telefonverzeichnis wollen wir nun formatiert ausgeben. Ohne Template-Engine würden wir dies etwa so bewerkstelligen:
df = java.text.DateFormat.getDateInstance() println " TELEFONVERZEICHNIS vom ${df.format(stand)}" kontaktliste.each { kontakt -> println kontakt.name kontakt.nummern.each { println " $it" } }
Das Ergebnis sieht dann so aus:
TELEFONVERZEICHNIS vom Wed May 23 11:12:01 CEST 2007 Klaus Schulz 029281 dienstl. 028277 privat Jens Krause Erika Meier 02827
Das gleiche Ergebnis können wir auch mit einem Template erzielen, dann brauchen wir den Text nicht programmgesteuert zusammensetzen.
Die Template-Syntax
BearbeitenZunächst wird eine Textvorlage benötigt. Die Form, die Textvorlagen haben müssen, ähnelt sehr den verbreiteten Mustern wie sie etwa von JSP oder Velocity bekannt sind. Wenn Sie bereits mit einem vergleichbaren Werkzeug gearbeitet haben, werden Sie auch mit den Groovy-Templates keine Schwierigkeiten haben.
Prinzipiell gibt es zwei Arten von Platzhaltern: solche, die zum Einfügen von Daten dienen, und solche, mit denen bestimmte Aktionen ausgelöst werden, zum Beispiel die wahlweise oder wiederholte Ausführung eines Template-Abschnitts. In Groovy werden an diesen Stellen in dem einen Fall die Inhalte von Variablen oder die Ergebnisse von Ausdrücken eingefüllt, im anderen Fall können es beliebige Code-Schnipsel sein – die allerdings im Zusammenhang einen Sinn ergeben müssen.
Für das obige Telefonverzeichnis könnte die Textvorlage so aussehen:
<% df = java.text.DateFormat.getDateInstance() %>TELEFONVERZEICHNIS vom <%= df.format(stand) %> <% liste.each { eintrag -> %> $eintrag.name <% eintrag.nummern.each { nummer -> %> $nummer <% } } %>
In diesem Beispiel sollen anstelle der durch Dollar-Zeichen markierten Platzhalter die Inhalte der gleichnamigen Variablen eingetragen werden. Zwischen den spitzen Klammern mit Prozentzeichen befinden sich Teile von Programmcode, die ausgeführt werden sollen. Hier benötigen wir die Code-Abschnitte, um eine DateFormat-Instanz zu initialisieren und um die Schleifen formulieren zu können.
Tipp: Sie sehen schon an diesem sehr einfachen Beispiel, dass die Mischung aus Vorlagentext und Programmlogik nicht unbedingt übersichtlich ist. Insbesondere die korrekte Anordnung der schließenden Klammern ist schwer zu erkennen. Die Anwendung der eingebauten Template-Engines ist daher nur in sehr einfachen Situationen zu empfehlen. Außerdem sollten Sie immer darauf achten, dass Programmlogik und Anzeige sauber getrennt bleiben. Im obigen Beispiel könnte man daher überlegen, die Formatierung des Datums im Programm außerhalb der Vorlage vorzunehmen. |
Folgende Tabelle enthält eine kurze Übersicht der bei den mitgelieferten Template-Engines vorgesehenen Platzhalter.
Platzhalter | Bedeutung |
$variable $variable.property |
Wird durch den Inhalt der Variablen ersetzt. Anstelle des einfachen Variablennamens kann auch ein Ausdruck verwendet werden, dieser darf aber nur Namen und Punkte zur Verknüpfung enthalten. |
${ausdruck} | Wird durch das Ergebnis des zwischen den geschweiften Klammern stehenden Groovy-Ausdrucks ersetzt.
Auch nötig wenn auf einen Variablennamen direkt ein Buchstabe oder eine Zahl folgen sollen: ${variable}test |
<%= ausdruck %> | Wird ebenfalls durch das Ergebnis des Ausdrucks ersetzt. Im Unterschied zu dem Dollar-Platzhalter kann dieser Ausdruck über mehrere Zeilen hinweg reichen. |
<% programmcode %> | Ein Stück Programmcode, das direkt ausgeführt wird, ohne dass ein Wert in den Ergebnistext einzufügen ist. Kann ebenfalls mehrere Zeilen umfassen. |
Arbeiten mit der SimpleTemplateEngine
BearbeitenWie kommen aber nun die Textvorlage und die Daten zusammen? Das Vorgehen ist bei allen Template-Engines in Groovy gleich; am Beispiel der SimpleTextEngine zeigen wir, wie die einzelnen Schritte vonstatten gehen.
Holen Sie sich zunächst eine Instanz der TemplateEngine, die Sie verwenden möchten, in diesem Falle also der SimpleTemplateEngine.
engine = new SimpleTemplateEngine()
Lassen Sie die Template-Engine ein Template aus dem Vorlagetext erzeugen.
template = engine.createTemplate(textvorlage)
Legen Sie eine Map für die Daten an, die Sie dem Template zum Mischen übergeben möchten.
daten = [liste:kontaktliste, stand:new Date()]
Stoßen Sie den eigentlichen Mischvorgang an.
ergebnis = template.make(daten)
Das Ergebnis ist ein Writable-Objekt. Sie können es beispielsweise an einen Writer übergeben, um es in eine Datei zu schreiben.
new File('Telefonliste-Ergebnis.txt').withWriter { writer -> writer.write(ergebnis) }
Oder geben Sie einfach das Ergebnis auf der Konsole aus.
println ergebnis
Wenn Sie sich das Ergebnis ansehen, werden Sie möglicherweise feststellen, dass es nicht ganz Ihren Erwartungen entspricht. Das Problem besteht darin, dass die Template-Engine zwar den in <%...%> eingeschlossenen Groovy-Code entfernt, den Rest des Textes aber gänzlich unverändert lässt. Das hat zur Folge, dass die dort, wo ganze Zeilen aus Groovy-Code bestehen, möglicherweise unerwünschte Leerzeilen bestehen bleiben. Hier ist dies auch der Fall:
TELEFONVERZEICHNIS vom 23.05.2007
Klaus Schulz 029281 dienstl. 028277 privat Jens Krause Erika Meier 02827
Um dies zu verhindern, bleibt Ihnen nur eine Möglichkeit – den Vorlagentext so schreiben, dass die Groovy-Codeschnipsel keine ganzen Zeilen einnehmen. Etwa so:
<% df = java.text.DateFormat.getDateInstance() %>TELEFONVERZEICHNIS vom <%= df.format(stand) %> <% liste.each { eintrag -> %>$eintrag.name <% eintrag.nummern.each { nummer -> %> $nummer <% } } %>
Leider macht dies die Vorlage aber nicht gerade leichter lesbar.
Tipp: Wenn der in der Vorlage eingebettete Groovy-Code Fehler enthält, sind diese unter Umständen sehr schwer zu finden. Wie erwähnt, wandelt die SimpleTemplateEngine die Vorlage in ein Groovy-Skript um, das beim Aufruf der make()-Methode des Templates ausgeführt wird. Wenn dabei Fehler auftreten, verweisen die angegebenen Zeilennummern auf dieses intern generierte Skript – und helfen Ihnen daher herzlich wenig.
Um fehlerhafte Vorlagentexte debuggen zu können, bietet das SimpleTemplateEngine die Möglichkeit, sie einem „gesprächigen“ Modus laufen zu lassen. Dazu müssen Sie die Engine mit einem true als Argument instanziieren. engine = new SimpleTemplateEngine(true) Dann gibt sie bei jedem Erzeugen eines Templates den generierten Groovy-Code auf der Konsole aus. Den Fehler hier zu finden, ist dann wesentlich einfacher. |
Sehen wir uns das kurze Skript, mit dem wir das Telefonverzeichnis erstellen, noch einmal im Zusammenhang an. Folgendes Beispiel enthält das entsprechende Listing
/** * Skript zum Erzeugen einer Telefonliste mit SimpleTemplateEngine */ // Die Klasse Kontakt enthält die Telefonnumern zu einer Person class Kontakt { def name def nummern } // Liste der Kontakte initialisieren kontaktliste = [ new Kontakt(name:'Klaus Schulz', nummern:['029281 dienstl.','028277 privat']), new Kontakt(name:'Jens Krause'), new Kontakt(name:'Erika Meier', nummern:['02827']) ] // Die Textvorlage anlegen textvorlage = <% df = java.text.DateFormat.getDateInstance() %>TELEFONVERZEICHNIS vom <%= df.format(stand) %> <% liste.each { eintrag -> %>$eintrag.name <% eintrag.nummern.each { nummer -> %> $nummer <% } } %>''' // Template-Engine instanziieren engine = new groovy.text.SimpleTemplateEngine() // Template aus Textvorlage erzeugen template = engine.createTemplate(textvorlage) // Binding erstellen daten = [liste:kontaktliste, stand:new Date()] // Mischvorgant auslösen ergebnis = template.make(daten) // Ergebnis ausgeben. new File('Telefonliste-Ergebnis.txt').withWriter { writer -> writer.write(ergebnis) }
Achten Sie darauf, dass Sie, wenn Sie einen Vorlagentext wie hier als String-Literal definieren, dieses nicht in doppelte Anführungszeichen einschließen ("…" oder """…"""). In diesem Fall wird der Groovy-Compiler nämlich den Text als GString betrachten und versuchen, die Platzhalter wie $nummer schon beim Kompilieren zu ersetzen. Dies ist natürlich nicht erwünscht, denn das Mischen von Text und Daten soll dem Template vorbehalten bleiben. Einfache Anführungszeichen um einen String verhindern, dass dieser als GString interpretiert wird.
Weitere Skript-Engines in Groovy
BearbeitenWie erwähnt, liefert die Groovy-Umgebung noch zwei weitere Template-Engines aus. Eine von ihnen, die XmlTemplateEngine, ist auf das Verarbeiten und Generieren von XML-Dokumenten spezialisiert und wird im Kapitel ##LIBRARY## näher behandelt.
Schließlich gibt es noch die GStringTemplateEngine. Sie kann in genau der gleichen Weise verwendet werden. Sie brauchen im obigen Beispiel nur die Zeile
engine = new groovy.text.SimpleTemplateEngine()
durch diese Zeile ersetzen:
engine = new groovy.text.GStringTemplateEngine()
Das Ergebnis sieht genau so aus wie vorher. Die Templates beider Engines generieren Writable-Objekte, die denselben Ergebnistext über einen Writer ausgeben können. Allerdings unterscheidet sich die Implementierung gravierend. Während die SimpleTemplateEngine aus dem Vorlagentext ein Groovy-Skript erzeugt, das den Ergebnistext Zeile für Zeile in den Writer schreibt, baut die GStringTemplateEngine ein spezielles Closure-Objekt, das das gesamte Template als GString in einem Stück ausgibt. Der Unterschied kommt insbesondere dann zum Tragen, wenn das Ergebnis des Mischvorgangs nicht zeilenweise organisiert ist, denn es entfällt die Notwendigkeit, zunächst eine gesamte Zeile im Speicher zusammenzubauen, bevor sie ausgegeben werden kann. Ein Beispiel dafür ist die Übertragung von Streaming-Daten im Internet, die vom Empfänger ja schon während des laufenden Übertragungsvorgangs dargestellt werden sollen.
––––
Mit der Closure, dem Builder und der Template-Engine haben Sie drei Konzepte kennen gelernt, die in in der Groovy-Programmierung generell eine große Rolle spielen und die sich in vielen Teilen der Groovy-Bibliothek angewendet und umgesetzt werden, und denen wir in den folgenden Kapiteln immer wieder begegnen.
← Builder | ↑ Neue Konzepte | → Das Groovy-JDK