Aus Linux-Magazin 02/2005

Template-Engine Velocity für die Java-Beans-Programmierung

Velocity aus dem Jakarta-Projekt hilft nicht nur den Webprogrammierern dabei, statische Vorlagen mit dynamischen Inhalten zu füllen. Das spart viel Zeit beim Entwickeln einander ähnelnder Beans.

Typisch für Webcontent ist, dass ein vorgegebener Rahmen das Layout vorgibt, aber die Inhalte variieren. Diesem Ansatz folgt Velocity aus dem Jakarta-Projekt, das Java-Lösungen für den Apache-Webserver sammelt. So behandeln die Beispiele des Velocity User Guide sowie die meisten anderen Anleitungen Web-bezogene Aufgaben.

Um diesen festgetretenen Pfad geht es im Folgenden aber nicht, sondern um ein anderes Anwendungsszenario. Das Prinzip bleibt aber unverändert und lässt sich auf Web- und beliebige andere Anwendungen übertragen. Das Beispiel erzeugt eine Java-Bean-Klasse dynamisch aus einer vorgegebenen Beschreibungsdatei. Mit Hilfe des Velocity-Template »bean.vm« stellt »BeanGen.java« (Listing 3) eine Bean-Klasse mit einigen Properties bereit.

Wer Java-Beans kodiert, muss immer wieder ähnliche Codeabschnitte tippen. Wenigstens für die Getter- und Setter-Methoden liefern die gängigen IDEs inzwischen Vorlagen mit, aber bei weiter gehenden Bean-Features wie Changelistener für Bound und Constrained Properties, Events und Eventlistener helfen sie nicht weiter. Selbst einfache Klassen mit wenigen Properties arten unnötig aus. Velocity beschleunigt solche mühsamen Routinearbeiten deutlich.

Templates und Daten

Die Template-Engine von Velocity erzeugt aus Daten und Vorlagen – den Templates – fertige Ausgabedateien für praktisch jeden Zweck. Diese Vorlagen bestehen aus drei Teilen: statischem Text, Referenzen und Direktiven. Die Referenzen sind Platzhalter für Daten, die Direktiven steuern das Verhalten der Template-Engine. Velocity verwendet für seine Templates eine eigene Programmiersprache: die Velocity Template Language (VTL).

Die Einstiegshürde beim Lernen der Sprache liegt niedrig. VTL beschränkt sich auf die unbedingt notwendigen Elemente. Wer JSPs und Shell- oder Perl-Programmierung kennt, dem wird VTL kaum Mühe bereiten. Die Daten für die Templates definiert man entweder innerhalb des jeweiligen Template selbst, oder stellt sie über ein Rahmenprogramm zur Verfügung. Letzteres ist der übliche Weg, erstere Methode eignet sich hauptsächlich für Konstanten.

Bean-Template

Listing 1, ein Auszug aus dem Beispiel-Template, definiert seine Daten fest. Es mischt statischen Text, in diesem Fall Java-Code, mit Direktiven und Referenzen. Die Zeilen 14 und 15 ziehen weitere Templates heran, dabei bewirkt »#parse« das Verarbeiten des importierten Template. Die Anweisung »#include« fügt einen Inhalt hinzu, ohne darin enthaltene Anweisungen zu verarbeiten.

Listing 1 verwendet fast alle Sprachelemente von VTL: Loops, einfache Bedingungen, Makro-Aufrufe, Kommentare und Schachtelung von Templates. Die Platzhalter für die Daten, also die Referenzen, fangen mit »$« an – die Bash lässt grüßen. Referenzen bieten aber mehr Möglichkeiten als Variablen.

Sprachelemente

Listing 1 beginnt mit einem mehrzeiligen Kommentarblock, es folgt ein einzeiliger Kommentar. VTL unterscheidet zwischen Java- und VTL-Kommentaren: Erstere behandelt es wie alle anderen Java-Anweisungen. Die »#parse«-Kommandos binden – wie beschrieben – externe Templates ein.

Die »#if«…»#elseif«…»#else«…»#endif«-Direktive erlaubt alternative Textkonstrukte. Das Beispiel benötigt dies, da sowohl »package«- als auch »extends«- und »implements«-Statements in Java optional sind (Zeilen 19 bis 21, 30 bis 35). Schleifen ermöglichen die Kommandos »#foreach« und »#end« (Zeilen 23 bis 25). Velocity erhöht automatisch einen Zähler, seinen aktuellen Wert gibt »$velocityCount« aus. Bei geschachtelten Schleifen enthält »$velocityCount« stets den Wert der innersten Schleife.

Velocimacros

Eine weitere wichtige Funktion erfüllen die so genannten Velocimacros. In Zeile 41 von Listing 1 ruft das »bean.vm«-Template in der »#foreach«-Schleife das »#property«-Makro für jede definierte Bean-Property auf; den gleichen Effekt hätte eine entsprechende »#parse«-Anweisung. Listing 2 zeigt das Makro »#property« ohne die erwähnten komplexeren Bean-Features wie Bound und Constrained Properties und Listener. Hier kommen sowohl Velocity- als auch Java-Kommentare vor, nur Letztere gelangen in den generierten Code.

Nur zwei Direktiven verwendet Listing 1 nicht: »#set« belegt Variablen innerhalb von Templates mit Werten und »#stop« unterbricht die Verarbeitung eines Template, dieses Feature nützt vor allem bei rekursiven Templates. Die Kernaufgabe der vorgestellten Direktiven ist es aber, Textbausteine bedingt oder repetitiv einzufügen. Wirklich dynamischer Inhalt gelangt über die schon erwähnten Referenzen in die Textausgabe.

Es gibt drei Typen von Referenzen: Variablen, Eigenschaften (Properties) und Methoden. Die Variablen-Referenzen sind das einfachste Konstrukt, sie bestehen aus einem $-Zeichen gefolgt vom Namen der Variablen. Anders als in der Shell darf eine Velocity-Variable nur mit einem Buchstaben beginnen und neben Zahlen und dem Unterstrich auch Bindestriche enthalten. Ein weiterer Unterschied: Wurde einer Variablen »foo« noch kein Wert zugewiesen, gibt Velocity als Wert der Referenz »$foo« den String »$foo« zurück. In der Shellprogrammierung wäre das Ergebnis ein leerer String. Wer dies bevorzugt, schreibt im VM-Template »$!foo«.

Analog zu Bash-Programmen lassen sich Referenzen auch in der Form »${foo}« beschreiben. Beim Verketten von Strings ist dies notwendig, da der Ausdruck »${foo}bar« ein anderes Ergebnis ausgibt als »$foobar«. Auch dies kennen Bash-Programmierer von ihrer Shell.

Dynamik durch Reflection

Die Stärke von Java kommt mit den weiteren Typen von Referenzen zum Tragen. Eine typische Referenz in einem VM-Template verwendet die Punktnotation wie bei »$clazz.package«, um auf eine Property des Java-Objekts »clazz« zuzugreifen (Listing 1, Zeile 19). Die Klasse des »clazz«-Objekts muss entweder eine »getPackage()«-Methode haben oder ein Hashtable sein. Im letzteren Fall sollte der Hashtable ein Objekt mit dem Key (Namen) »package« enthalten. Über die Java-Reflection-API bestimmt Velocity die richtige Alternative.

Die dritte Variante der Referenzen verwendet Methoden: »$page.setTitle(“Der Titel”)«. Hier muss der Programmierer darauf achten, dass die Methode existiert und die Parameter kompatibel sind. Velocity wandelt das Ergebnis der Methode in einen String um. Das ist immer möglich, da jede Java-Klasse von »java .lang.Object« erbt, die wiederum eine Default-Methode implementiert.

Listing 1: »bean.vm«

01 #******************************************
02 #
03 # This Velocity-template creates Java-Bean classes.
04 #
05 # Copyright (c) 2004 by Bernhard Bablok (mail@bablokb.de)
06 #
07 # $Revision: 1.1 $
08 # $Author: bablokb $
09 #
10 ******************************************#
11
12 ## include copyright and simple GPL-header in generated bean
13
14 #parse ("copyright.vm")
15 #parse ("gpl.vm")
16
17 ## add (optional) package and import-statements
18
19 #if ($clazz.package)
20 package $clazz.package;
21 #end
22
23 #foreach ($import in $clazz.imports)
24 import $import;
25 #end
26
27 ## start of class-definition
28
29 $clazz.qualifier class $clazz.name
30   #if ($clazz.extends)
31     extends $clazz.extends
32   #end
33   #if ($clazz.implements)
34     implements $clazz.implements
35   #end
36                                         {
37 ##
38 ## add bean-properties using a macro
39
40 #foreach ($p in $clazz.properties)
41 #property ($p)
42 #end
43
44 ## end-of-class
45
46 }

Listing 2: »bean-macros.vm«

01 #*******************************************
02 #
03 # This Velocity-macro creates Java-code for individual properties.
04 #
05 # Copyright (c) 2004 by Bernhard Bablok (mail@bablokb.de)
06 #
07 # $Revision: 1.1 $
08 # $Author: bablokb $
09 #
10 # TODO: Add support for bound and constrained properties and events.
11 #
12 *******************************************#
13
14 #macro (property $prop)
15    /////////////////////////////////////////
16
17    private $prop.type i$prop.name;
18<c>19    public $prop.type get${prop.name}() {
20      return i$prop.name;
21    }
22
23    public void set${prop.name}($prop.type value) {
24      i$prop.name = value;
25    }
26
27 #end

Velocity in IDEs

Viele Entwickler legen Wert auf Integration von Velocity in die beliebten Entwicklungsumgebungen. Neben den Editoren JEdit, Ultraedit und (X)Emacs (siehe Abbildung 1), gibt es Velocity auch in Eclipse[3].

Integration hat aber noch einen weiteren Aspekt: Velocity als Hilfsmittel für andere Tools. Wer auf[4] nach Velocity sucht, findet einige Eclipse-Plugins, die Code mittels Velocity generieren. Auf der Velocity-Homepage gibt es eine Reihe von Links zu weiteren Tools, die Velocity verwenden.

Diese Technik funktioniert auch mehrstufig. Die Beispielanwendung verwendet einen Hashtable für die Klassenbeschreibung. Mehrere Objekte im Hashtable sind selbst wiederum Hashtables, beispielsweise für die »import«-Statements und -Properties. Wie die Zeilen 23 bis 25 zeigen, lässt sich sowohl über Hashtables als auch über Arrays und andere Collections iterieren.

Ihren Nutzen entfalten die Templates erst, wenn sie mit Daten gefüllt werden. Velocity mischt die Daten aus einem so genannten Context mit den Templates. Es stellt dafür die Klasse »VelocityContext« bereit, »Context« heißt ein Interface dieser Klasse. Ein Context bietet ähnliche Möglichkeiten wie ein Hashtable.

Füllen des Context

Um Daten zum Füllen eines Template zusammenzutragen, erzeugt man entsprechende Objekte und fügt sie in den Context ein: »put(Name,Objekt)«. Template-Autoren und Java-Programmierer brauchen sich also nur über die Namen der Objekte und die verfügbaren Properties oder Methoden zu einigen.

Das genaue Aussehen der Objekte ist dann noch den Gegebenheiten anzupassen. Gibt es schon eine Klasse »person«, zum Beispiel mit den Methoden »getName()« und »getAge()«, fügt man ein Objekt dieser Klasse in den Context ein. Andernfalls ist es einfacher, einen Hashtable zu erzeugen und Namen und Alter über die entsprechenden Methoden der Hashtable-Klasse zu definieren:

VelocityContext ct = 
  new VelocityContext();
Hashtable p = new Hashtable();
p.put("name","Tux");
p.put("age","12");
ct.put("person",p);

Listing 3: »BeanGen.java«

022 import java.util.*;
023 import java.io.*;
024
025 import org.apache.velocity.app.*;
026 import org.apache.velocity.*;
027
035 public class BeanGen {
036
041   private static final String BEAN_TEMPLATE = "bean.vm",
042     TEMPLATE_ENC = "ISO-8859-15";
043
044   private VelocityContext iContext;
045
046   //////////////////////////////////////////
047
052   private BeanGen() throws Exception {
053     Properties prop = new Properties();
054     prop.put("velocimacro.library","bean-macros.vm");
055     prop.put("runtime.log","/tmp/velocity.log");
056     Velocity.init(prop);
057     iContext = new VelocityContext();
058   }
059
060   //////////////////////////////////////////
061
066   private void fillContext() {
067     iContext.put("year",
068       new Integer(Calendar.getInstance().get(Calendar.YEAR)));
069
070     // add author description
071     Hashtable ad = new Hashtable();
072       ad.put("name","B.Bablok");
073       ad.put("email","mail@bablokb.de");
074     iContext.put("author",ad);
075
076     // add class description
077     Hashtable cd = new Hashtable();
078       cd.put("name","Person");
079       cd.put("package","de.bablokb.beans");
080       cd.put("qualifier","public final");
081       cd.put("extends","Object");
082       cd.put("implements","Serializable");
083
084       Hashtable imports = new Hashtable();
085         imports.put("1","java.io.*");
086         imports.put("2","java.util.*");
087       cd.put("imports",imports);
088
089       Hashtable properties = new  Hashtable();
090         Hashtable prop1 = new Hashtable();
091           prop1.put("type","String");
092           prop1.put("name","Name");
093       properties.put("a",prop1);
094         Hashtable prop2 = new Hashtable();
095           prop2.put("type","ArrayList");
096           prop2.put("name", "EmailAddresses");
097       properties.put("b",prop2);
098       cd.put("properties",properties);
099     iContext.put("clazz",cd);
100   }
101
102   //////////////////////////////////////////
103
108   private void merge() throws Exception {
109     PrintWriter out = new PrintWriter(System.out);
110     Velocity.mergeTemplate(BEAN_TEMPLATE,TEMPLATE_ENC,iContext,out);
111     out.flush();
112   }
113
114   //////////////////////////////////////////
115
120   public static void main(String[] args) {
121     try {
122       BeanGen gen = new BeanGen();
123       gen.fillContext();
124       gen.merge();
125     } catch (Exception e) {
126       e.printStackTrace();
127     }
128   }
129 }

Programmieren mit Velocity

Das Paket von[1] enthält zwei Jar-Dateien, »velocity-1.4.jar« und »velocity-dep-1.4.jar«. Letztere stellt alle Bibliotheken bereit, von denen der Velocity-Code abhängt. Die entsprechende Jar-Datei gehört zum Kompilieren und Ausführen in den Classpath.

Programmtechnisch sind drei Schritte auszuführen: Die Velocity-Runtime initialisieren und konfigurieren, einen Context erzeugen und ihm Daten übergeben und schließlich ein Template mit den Daten aus dem Context füllen. In der Beispielanwendung in Listing 3 schafft der Konstruktor die Grundlagen (Zeilen 52 bis 58). Zeile 54 legt den Namen der Makrobibliothek fest, Zeile 55 den Ort für das Runtime-Log. Ohne diese Angaben sucht Velocity die Makros in der Datei »VM_global_library.vm« und schreibt das Log in die Datei »velocity.log« im aktuellen Verzeichnis.

Abbildung 1: Syntax Highlighting mit dem VTL-Mode von Emacs.

Abbildung 1: Syntax Highlighting mit dem VTL-Mode von Emacs.

Der zweite Schritt füllt den Context. Im Beispiel erledigt dies die Methode »fillContext()« in den Zeilen 66 bis 100. In einer realen Anwendung kämen die Daten aus einer Konfigurationsdatei und ein Parser erzeugte die notwendigen Objekte für den Context. Hier sind diese Angaben fest einprogrammiert, der komplette Quelltext ist wieder auf dem Listing-Server[2] zu finden.

Die Methode »merge()« mischt Context und Template in den Zeilen 108 bis 112. Das eigentliche Velocity-Programm umfasst hier weniger als zehn Zeilen – davon beansprucht das Aufbereiten der Daten den Hauptteil. Zwar bietet ein längeres Programm mehr Möglichkeiten zur Kontrolle des Ergebnisses, aber Velocity richtet sich nach der Unix-Kernphilosophie – ein Tool für eine Aufgabe.

finally{}

Mit seiner übersichtlichen Programmiersprache generiert Velocity aus wenigen Zeilen Code beliebige Ausgabedateien. Wer tiefer einsteigen will, sollte sich zunächst an die mitgelieferte Dokumentation halten. Dazu gibt es den User’s Guide in mehreren Sprachen, den Developer’s Guide und eine Referenz.

Nützlich ist es in vielen Bereichen, aber auch Velocity eignet sich nicht für jeden Zweck. Webseiten lassen sich zwar damit sehr leicht erzeugen – solange sie keine zu komplexen Aufgaben stellen. Für höhere Anforderungen ist eher das klassische, aber komplexere JSP zu empfehlen. Das Mischen von JSP und VTL ermöglichen spezielle Taglibs; Details zu dieser Option liefert die Dokumentation.

Vorschau

Code generieren ist auch für klassische Tools eine Hauptaufgabe, etwa für Lexer und Parser. Die nächste Folge der Coffeeshop-Reihe beschäftigt sich deshalb mit JavaCC, einem reinen Generator für Java-Parser. (csc)

Infos

[1] Velocity: [http://jakarta.apache.org/velocity/index.html]

[2] Listings dieses Coffeeshops: [https://www.linux-magazin.de/Service/Listings/2005/02/Coffeeshop]

[3] Velocity-UI für Eclipse: [http://veloedit.sourceforge.net]

[4] Eclipse-Plugins: [http://www.eclipse-plugins.info/]

Der Autor

Bernhard Bablok arbeitet bei der AGIS mbH als Anwendungsentwickler. Wenn er nicht Musik hört, mit dem Radl oder zu Fuß unterwegs ist, beschäftigt er sich mit Themen rund um Objektorientierung. Er ist unter [coffee-shop@bablokb.de] zu erreichen.

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben