Aus Linux-Magazin 04/2006

Webanwendungen mit Trails entwickeln

© photocase.com

Trails mischt eine abgestimmte Sammlung bewährter J2EE-Frameworks mit Ideen von Ruby on Rails und minimiert so die manuelle Programmierarbeit. Diese Kombination bringt Java-basierte Webapplikationen leicht und schnell auf die richtige Schiene.

Irgendwann im Frühsommer 2005 war es, als Jim Weirich seinem Freund Chris Nelson bei einem Treffen der Java-Usergroup von Cincinnati das Video von Ruby on Rails [1] vorspielte. Chris war beeindruckt von dem schnellen und einfachen Weg, Webanwendungen zu entwickeln, und fasste den Entschluss, dies auch in seiner Lieblingsprogrammiersprache Java hinzubekommen.

Nach einigen Monaten war es so weit: Chris stellte die erste Version von Trails [2] fertig. Der Name Trails lehnt sich an Rails an und setzt noch das T von Tapestry [3] davor. Seine Kernidee ist es, die Entwicklung von J2EE-Anwendungen zu vereinfachen und zu beschleunigen. Chris wollte nicht das Rad neu erfinden und setzte bei der Entwicklung von Trails auf bereits erprobte Frameworks wie Apache Ant, Tapestry, Aspect-J [4], Spring [5] und den objektrelationalen Mapper Hibernate [6].

Voraussetzung, um mit Trails arbeiten zu können, sind ein installiertes Ant und das fast 20 MByte große Trails-Archiv von [2], das alle weiteren Frameworks enthält. Tomcat 5.5 als Application Server sollte bereits installiert haben, wer sofort loslegen möchte. Ist »ANT_HOME« gesetzt und Trails in ein entsprechendes Verzeichnis entpackt, kann mit Hilfe von »ant install-apt« die nötige Ant-Bibliothek für Trails in das Ant-Lib-Verzeichnis installiert werden.

Schnellstart

Ein Beispiel demonstriert im Folgenden die Verwendung von Trails: eine kleine Webanwendung, die Videofilme und die entsprechenden Schauspieler verwaltet. Trails generiert das Skelett einer solchen Anwendung (in Rails auch als Scaffolding bezeichnet) vollautomatisch und bestückt es mit Bibliotheken und Konfigurationsdateien. Dazu genügt es, im Trails-Verzeichnis »ant create-project« auszuführen (Abbildung 1).

Abbildung 1: Mit Trails lassen sich Java-Webanwendungen wie diese Schauspielerdatenbank fast ohne manuelles Programmieren erstellen.

Abbildung 1: Mit Trails lassen sich Java-Webanwendungen wie diese Schauspielerdatenbank fast ohne manuelles Programmieren erstellen.

Nach dem Eingeben des Basisverzeichnisses und des Namens der neuen Anwendung, im Beispielfall »meinevideos«, legt Trails ein entsprechendes Verzeichnis, alle nötigen Unterverzeichnisse und Bibliotheken an, einschließlich einer »build.xml« für die Erzeugung und das Deployment der neuen Webanwendung. Die angelegte Verzeichnisstruktur sieht folgendermaßen aus:

  • »Basisverzeichnis/meinevideos/« ist das
    Hauptverzeichnis der Webanwendung. Es kann als Startpunkt für
    den Import in eine IDE wie Eclipse dienen.
  • »Basisverzeichnis/meinevideos/src« ist das
    Quellcodeverzeichnis der Applikation. Hier werden die eigenen
    Klassen implementiert.
  • »Basisverzeichnis/meinevideos/context« ist das
    Verzeichnis der eigentlichen Webapplikation.
  • »Basisverzeichnis/meinevideos/context/WEB-INF«
    enthält die Konfigurationsdateien »web.xml«,
    »hibernate.properties« und so fort sowie alle
    Tapestry-Pages und HTML-Fragmente.
  • »Basisverzeichnis/meinevideos/lib« enthält
    alle Archive der einzelnen Frameworks (Hibernate, Tapestry, Apache
    Commons und so fort).

Zusätzlich ist noch der Ort der Tomcat-Installation in der Datei »build.properties« einzutragen.

Domänen-getriebene Entwicklung

Startpunkt jeder Trails-Webapplikation sind ein oder mehrere POJOs (Plain Old Java Objects), die den Domäne(n)-Objekten der Anwendung entsprechen. Mit diesem ersten wichtigen und entscheidenden Unterschied zu Ruby on Rails wird der Programmierer bereits vor dem ersten Schritt konfrontiert: Trails startet nicht mit einer Datenbanktabelle, sondern erwartet als Ausgangspunkt eine einfache Java-Klasse. Die Klasse in Listing 1, die im Beispiel Videos verwalten soll, lässt sich in einer modernen IDE wie Eclipse leicht erzeugen (Abbildung 3). Dazu deklariert man die Klassenattribute »id«, »title« und »year« und erzeugt mit dem Getters- und Setters-Generator die Zugriffsmethoden.

Um Hibernate mitzuteilen, welche Klasse es persistieren soll, muss der Entwickler Java-5-Annotationen im Sinne des neuen EJB-3.0/JSR-220-Standards ([7], [8]) benutzen. Die Annotation »@Entity« (Listing 1, Zeile 12) gibt dabei an, dass Hibernate die Klasse »Movie« in der Datenbank speichern soll. Dann fehlt noch eine eindeutige Identifikationsnummer, die durch die Annotation »@Id(generate=GeneratorType.AUTO)« über das »id«- Attribut erzeugt wird.

Um einzelne Filme in der Datenbank zu unterscheiden, bedarf es noch einer Methode »equals«, die sich jedoch trivial mit Hilfe eines »EqualsBuilder« aus dem Framework »apache.commons.lang.builder« implementieren lässt (Zeilen 10 und 44). Nach dem Aufruf des Ant-Targets »ant deploy« findet sich die Webanwendung prompt unter der Adresse »http://localhost:8080/meinevideos/« im Browser – vorausgesetzt der Tomcat-Pfad in »build.properties« stimmt.

Listing 1:
»Movie«-Version 1

01 package de.wartala.meinevideos;
02 
03 import java.util.HashSet;
04 import java.util.Set;
05 
06 import javax.persistence.Entity;
07 import javax.persistence.GeneratorType;
08 import javax.persistence.Id;
09 
10 import org.apache.commons.lang.builder.EqualsBuilder;
11 
12 @Entity
13 public class Movie {
14     private Integer id;
15     private String title;
16     private Integer year;
17 
18     @Id(generate=GeneratorType.AUTO)
19     public Integer getId() {
20         return this.id;
21     }
22 
23     public void setId(Integer id) {
24         this.id = id;
25     }
26 
27     public String getTitle() {
28         return title;
29     }
30 
31     public void setTitle(String title) {
32         this.title = title;
33     }
34 
35     public Integer getYear() {
36         return year;
37     }
38 
39     public void setYear(Integer year) {
40         this.year = year;
41     }
42 
43     public boolean equals(Object obj) {
44         return EqualsBuilder.reflectionEquals(this, obj);
45     }
46 
47     public String toString() {
48         return this.getTitle();
49     }
50 }
Abbildung 2: Der Aufruf von »ant create-project« erzeugt das Grundgerüst.

Abbildung 2: Der Aufruf von »ant create-project« erzeugt das Grundgerüst.

Abbildung 3: Trails-Webanwendungen lassen sich bequem in der Eclipse-IDE editieren.

Abbildung 3: Trails-Webanwendungen lassen sich bequem in der Eclipse-IDE editieren.

CRUD

Die Eingangsseite von Trails grüßt mit einem freundlichen “Welcome”. Ein Klick auf den Link »List Movies« verschafft eines der ersten, von Rails bekannten Aha- Erlebnisse: Schon in diesem frühen Entwicklungsstadium der Webanwendung kann der Benutzer auf einfache Weise Datensätze erzeugen, suchen, ändern oder löschen (Abbildung 1). Dieses als CRUD (Create, Retrieve, Update, Delete) bekannte Konzept ist eines der Highlights bei jeder Rails-Demonstration.

Den Plural »Movies« des »Movie«-POJOs erzeugt eine einfache Funktion von Trails, der so genannte Pluralizer, der den Namen eines im Singular angegebenen englischen Klassennamens in seine Mehrzahl überführt.

Die Beispielanwendung vermag nicht nur Filme zu verwalten, sondern auch Schauspieler. Dazu ist die Implementierung eines passenden Domänen-Objekts nötig. Die Schauspieler-Klasse »Actor« soll die Attribute Name und Geburtstag enthalten (Listing 2). Hibernate wäre kein objektrelationaler Wrapper, wenn er nur Objekte abbilden könnte. Natürlich kann er auch Relationen zwischen einzelnen Entitäten ausdrücken. In der Videoverwaltung sollen für einen Film mehrere Schauspieler zu nennen sein (eine klassische 1-zu-n-Relation). Um das innerhalb der Klasse »Movie« auszudrücken, kommen die in Java 5 eingeführten Generics zum Zuge.

Listing 2:
»Actor«

01 package de.wartala.meinevideos;
02 
03 import java.util.Date;
04 
05 import javax.persistence.Entity;
06 import javax.persistence.GeneratorType;
07 import javax.persistence.Id;
08 
09 @Entity
10 public class Actor {
11     private Integer id;
12     private String name;
13     private Date birthday;
14 
15     @Id(generate=GeneratorType.AUTO)
16     public Integer getId() {
17         return id;
18     }
19 
20     public void setId(Integer id) {
21         this.id = id;
22     }
23 
24     public String getName() {
25         return name;
26     }
27 
28     public void setName(String name) {
29         this.name = name;
30     }
31 
32     public Date getBirthday() {
33         return birthday;
34     }
35 
36     public void setBirthday(Date birthday) {
37         this.birthday = birthday;
38     }
39 
40     public String toString() {
41         return this.getName();
42     }
43 }

Ein Hashset von »Actor« implementiert in der Klasse »Movie« die 1-zu-n-Relation der Schauspieler (Listing 3, Zeile 25). Getters und Setters dieses Klassenattributs drücken über JSR-220-Annotationen die gewünschte Beziehung aus (Listing 3, Zeilen 56 bis 59). Ein »ant redeploy« macht die Änderungen wirksam: Wenn nun der Benutzer einen Film eingibt, kann er mit »Add New« gleich neue Schauspieler hinzufügen.

Listing 3:
»Movie«-Version 2

01 package de.wartala.meinevideos;
02 
03 import java.util.HashSet;
04 import java.util.Set;
05 
06 import javax.persistence.CascadeType;
07 import javax.persistence.Entity;
08 import javax.persistence.GeneratorType;
09 import javax.persistence.Id;
10 import javax.persistence.JoinColumn;
11 import javax.persistence.OneToMany;
12 
13 import org.apache.commons.lang.builder.EqualsBuilder;
14 import org.hibernate.validator.NotNull;
15 import org.trails.descriptor.annotation.Collection;
16 import org.trails.descriptor.annotation.PropertyDescriptor;
17 import org.trails.validation.ValidateUniqueness;
18 
19 @Entity
20 @ValidateUniqueness(property="title")
21 public class Movie {
22     private Integer id;
23     private String title;
24     private Integer year;
25     private Set<Actor> actors = new HashSet<Actor>();
26 
27     @PropertyDescriptor(index=0)
28     @Id(generate=GeneratorType.AUTO)
29     public Integer getId() {
30         return this.id;
31     }
32 
33     public void setId(Integer id) {
34         this.id = id;
35     }
36 
37     @PropertyDescriptor(index=1)
38     @NotNull
39     public String getTitle() {
40         return title;
41     }
42 
43     public void setTitle(String title) {
44         this.title = title;
45     }
46 
47     @PropertyDescriptor(index=2)
48     public Integer getYear() {
49         return year;
50     }
51 
52     public void setYear(Integer year) {
53         this.year = year;
54     }
55 
56     @PropertyDescriptor(index=3)
57     @OneToMany(cascade=CascadeType.ALL)
58     @JoinColumn(name="movieId")
59     @Collection(child=true)
60     public Set<Actor> getActors()
61     {
62         return actors;
63     }
64 
65     public void setActors(Set<Actor> actors)
66     {
67         this.actors = actors;
68     }
69 
70     public boolean equals(Object obj) {
71         return EqualsBuilder.reflectionEquals(this, obj);
72     }
73 
74     public String toString() {
75         return this.getTitle();
76     }
77 }

Die bisher sichtbaren Webseiten sind von Trails erzeugt und dementsprechend schlicht. Einfache Änderungen wie die Reihenfolge der Eingabefelder oder der Titel der einzelnen Attribute lassen sich über Trails-Annotations modifizieren. So legt die folgende Annotation sowohl die Position des Eingabefelds für den Geburtstag des Schauspielers innerhalb des Formulars fest (Position 2) als auch das Ausgabeformat für das Datum selber und das angezeigte Label:

@PropertyDescriptor(index=2,format="dd.MM.yyyy",displayName="Geburtstag")
public Date getBirthday() {
        return birthday;
}

Solche Anpassungen sollten natürlich nicht direkt in der Klasse stehen, sondern außerhalb des Quellcode. Dazu müssen jedoch die Seiten selber parametrisiert werden.

Um strukturelle Änderungen an der zugrunde liegenden Seite zu ermöglichen, setzt Trails auf das Webframework für Model-View-Controller namens Tapestry. Mit seiner Hilfe lässt sich neben der Validierung auch die Internationalisierung von Webseiten einfach realisieren. Ebenso wie Java Server Faces (JSF) trennt Tapestry zwischen den Strukturkomponenten der Sei- te und ihrer Darstellung. Alle angezeigten Eingabeelemente in Trails sind Tapestry- oder Trails-Komponenten.

Wer die generierten Seiten verändern möchte, sollte sich also mit der Aufteilung der einzelnen Bestandteile in Tapestry beschäftigen. Trails generiert für die CRUD-Fälle Default-Seiten (Endung ».html«) und entsprechende Seitenmodelle (Endung .»page«). Beide Teile kontrolliert das Tapestry-Applicationservlet, das von »web.xml« eingebunden wird.

Lokalisierung

Um statische Inhalte der Seiten zu lokalisieren, müssen die entsprechenden HTML-Seiten geändert werden. Hier kommen die von Java gewohnten Message-Bundles zum Einsatz, also einfache Properties-Dateien der Form Parameter= Wert. Um auf der Startseite »Home.html« der Beispielanwendung das »Welcome« abhängig von der eingestellten Webbrowser-Lokalisierung anzuzeigen, ist die Datei »messages_Länderkürzel.properties« erforderlich. Im Falle von »meinevideos« also »messages_de.properties« in »/meinevideos/context/WEB-INF«:

org.trails.welcome=Willkommen zu Trails

In »Home.html« muss der Entwickler die Zeile »<h1>Welcome to Trails</h1>« durch »<h1><span key=”org.trails.welcome”>Welcome to Trails</span></h1>« ersetzen, um die lokalisierte Form des Willkommensgrußes anzuzeigen.

Leider funktioniert dieses Vorgehen nicht beim Scaffolding: In der Startseite der Beispielanwendung steht immer noch »List Movies«. Der Trails-Entwickler verspricht in seinem Blog [8] eine Lösung für die nächste Version. Daher sollte man schon jetzt beim Scaffolding die landesprachlichen Feinheiten bedenken.

Die Datei »/meinevideos/context/WEB-INF/applicationContext.xml« bindet das Spring-Framework in Trails ein. Sie referenziert nicht nur alle für Trails und Hibernate nötigen Beans und dekoriert sie mit den verschiedensten Properties, hier ist auch der richtige Ort, um Message-Bundles wie in Listing 4 einzubinden.

Listing 4:
Message-Bundle

01 <!-- Message source for this context, loaded from localized "messages_xx" files -->
02 <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
03     <property name="basename">
04         <value>messages</value>
05     </property>
06 </bean>
07 
08 <bean id="trailsMessageSource" class="org.trails.i18n.DefaultTrailsResourceBundleMessageSource">
09     <property name="messageSource"><ref local="messageSource"/></property>
10 </bean>

Referenzieren lassen sich die Parameterwerte in Tapestry-Seiten auf unterschiedliche Arten. Da die Validierung innerhalb des Domänen-Objekts erfolgt, ist es auch möglich, die Ausgaben an Feldvalidatoren zu hängen:

@NotNull(message="{error.emptyMessage}")
@Pattern(regex="[A-z|\s]+",message="{error.letterOrSpace}")

Um die Konfiguration von Trails möglichst einfach zu halten, ist als Default-Datenbank die In-Memory-Version von HSQL [10] integriert, die auch hinter Open Office steckt. Für andere Datenbanken muss der Entwickler die Datei »hibernate.properties« in »Basisverzeichnis/meinevideos/context/WEB-INF« editieren. Welche Datenbanken für Hibernate geeignet und getestet sind, verraten die Sites [11] und [12].

Ajax-Unterstützung

Innerhalb des erzeugten Anwendungsskeletts konfiguriert eine Datei namens »[Anwendungsname].application« die Tapestry-Umgebung. In ihr ist bereits die Ajax-Bibliothek Tacos [13] referenziert. Ein wesentlicher Vorteil von Ajax ist das partielle Rendering von Seitenteilen. Das macht sich besonders bei Seiten mit vielen Ein- oder Ausgabe-Komponenten bemerkbar: Ob der Browser 20 Eingabefelder eines Webformulars wiederholt darstellen muss oder nur eins, ist für den wartenden Anwender ein großer Unterschied.

Tacos und damit auch Trails bietet dafür eine Lösung. Leider muss der Webentwickler nicht nur die HTML-Seite anpassen, sondern auch eine eigene Model-Klasse für Tapestry implementieren. Für die nächste Release ist hierfür aber eine Vereinfachung angekündigt. Wer so lange nicht warten möchte, sollte sich das Ajax-Beispiel auf [2] anschauen.

Schon recht gut

Ebenso wie bei Rails nichts um Ruby herumführt, geht bei Trails nichts ohne Tapestry, wenn größere Anwendungen zu realisieren sind. Rails verdankt große Teile seiner Leistungsfähigkeit der dynamischen Sprache Ruby. Mit Java sind dynamische Attribute und Methoden (noch) nicht zu haben. Ebenfalls fehlen etliche Schmankerl, die in Rails bereits vorhanden sind.

So steht im Moment noch eine E-Mail-Anbindung aus, die sich jedoch mit Java Mail einfach ersetzen lassen sollte. Ebenso fehlt die Anbindung an Webservices, die aber mit dem Apache-Webservice-Framework Axis [14] realisierbar ist. Auf der anderen Seite kann Trails im Gegensatz zu Rails\’ O/R-Mapper Active Record durch Hibernate auch bestehende Datenbanken anbinden.

Zweifellos bleibt noch eine Menge zu tun. Aber Trails ist mit Version 0.8 noch zwei Trippelschritte von seiner Release 1.0 entfernt. Wer sich die Mühe macht und die aktuelle Trails-Version aus dem CVS-Repository auscheckt, findet bereits einige Spuren von Features, die in der nächsten Version von Trails an Bord sein werden. Neben der schon erwähnten I18N-Unterstützung enthält das Trails-Framework ein neues Security-Package, das auf dem Acegi-Security-Framework für Spring aufbaut [15].

Damit lassen sich in gewohnter Spring-Manier sicherheitsrelevante Deklarationen in die Applicationcontext-Definition verfrachten. Das geht ganz ohne Änderungen am bestehenden Code der Trails-Applikation. Wenn dann noch Chris Nelson etwas an der Dokumentation feilt und findige Entwickler eine Möglichkeit finden, Java Server Faces statt Tapestry zu integrieren, stünde modernen J2EE-Webapplikationen auf Knopfdruck nichts mehr im Weg. (ofr)

Infos

[1] Armin Röhrl, “Wie auf Schienen – Webprogrammierung mit Ruby und Rails”: Linux-Magazin 12/04, S. 100

[2] Trails: [http://trails.dev.java.net]

[3] Apache Tapestry: [http://jakarta.apache.org/tapestry]

[4] Aspect-J: [http://www.eclipse.org/aspectj]

[5] Spring-Framework: [http://www.springframework.org]

[6] M. Schmidt, T. Ehlers, “Überwintern im Eis, Objekt-relationales Mapping mit Hibernate”: Linux-Magazin 09/04, S. 104

[7] EJB 3.0 Annotation (JSR-220): [http://www.jcp.org/aboutJava/communityprocess/edr/jsr220]

[8] Hibernate Annotations: [http://www.hibernate.org/hib_docs/annotations/reference/en/html]

[9] Chris Nelsons Weblog: [http://jroller.com/page/ccnelson/Weblog?catname=/Trails]

[10] HSQL: [http://hsqldb.org]

[11] Offiziell unterstützte Datenbanken für Hibernate: [http://www.hibernate.org/260.html]

[12] Von der Community unterstützte Datenbanken für Hibernate: [http://www.hibernate.org/80.html]

[13] Tacos: [http://tacos.sourceforge.net]

[14] Apache Axis: [http://ws.apache.org/axis]

[15] Acegi Security Framework für Spring: [http://acegisecurity.org]

Der Autor

Ramon Wartala ist Software Engineer bei AOL Deutschland in der Abteilung Development & Architecture. Wenn er sich nicht gerade mit dem Internet beschäftigt, verbringt er seine Zeit am liebsten mit seiner Frau.

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