Das Spring-basierte Webframework Catapult stellt sich vor

W-JAX-Challenge: Zoccer

Wie verhalten sich ambitionierte Java-Entwickler, wenn sich ihre Idealvorstellungen von domänengetriebener Entwicklung mit den vorhandenen Webframeworks nicht realisieren lassen? Sie experimentieren mit einer Eigenentwicklung. Und wie verhalten sie sich, wenn diese Eigenentwicklung funktioniert? Sie stellen sich der W-JAX-Challenge.

Seit Ende 2008 entwickelt mindmatters, ein Hamburger Softwarehaus, im Kontext zweier Kundenprojekte das Open-Source-Framework Catapult. Als im Herbst 2009 die W-JAX-Challenge rief, entschieden wir fünf Tage vor dem Abgabetermin, dem Ruf zu folgen und die geforderte Tippspielanwendung für die Fu

ßball-WM 2010 mit Catapult zu schreiben. Die Herausforderung bestand für uns darin, in kürzester Zeit eine Webanwendung auf die Beine zu stellen, die sowohl technisch als auch inhaltlich herausragt. Wir wollten beweisen, dass dies mit Catapult sehr einfach möglich ist.

Und Catapult hat sich großartig bewährt. In insgesamt 16 Entwicklertagen (verteilt auf acht Personen) und 1430 Zeilen Java-Code entstand die Webanwendung Zoccer, die mit den folgenden Features aufwarten kann:

  • Einblick in den vollen Funktionsumfang, auch für nicht angemeldete Besucher
  • Zugriffskontrolle für die drei Rollen „Besucher“, „Nutzer“ und „Administrator“
  • Ajax-basierte Nutzerschnittstelle, Inline Editing
  • individuelle Gruppentipptabellen auf Basis der eigenen Tipps
  • Sparklines innerhalb der Gruppentabellen
  • Timer-Funktion für ungetippte Spiele
  • Bestenliste inklusive Gravatar-Icons für alle Nutzer
  • indizierte Schnellsuche in der Bestenliste
  • umfangreiche, mit Wikipedia verlinkte Länderübersicht inklusive der Landesflaggen
  • kontextsensitive Hilfe

Die Gruppenseite für den Administrator zum Einstellen der Mannschaften und der Spielergebnisse

Die Gruppentippseite mit den erhaltenen Punkten, der Gruppentabelle und der Tipptabelle

[ header = Seite 2: Catapult ]

Die Bestenliste nach Anklicken des Hilfebuttons rechts oben

Catapult

Catapult ist ein Spring-basiertes, nicht invasives Webframework für die Java-Plattform, das die domänengetriebene Entwicklung unterstützt. Catapult verzichtet dabei auf die in herkömmlichen MVC-Architekturen notwendigen Controller und stellt die Domänenobjekte stattdessen direkt dem View zur Verfügung (Naked-Objects-Pattern). Jegliche public-Methoden mit beliebiger Signatur können aus dem View heraus gerufen und selbstverständlich nach Bedarf mit Spring Security geschützt werden. An die Stelle der Controller-Logik treten in Catapult entsprechende Konventionen, auf deren Grundlage das Framework selbst die Aufgaben der Controller übernimmt. Alle Spring-MVC-relevanten Controller, RequestMappings etc. stehen jedoch nach wie vor zur Verfügung und können sofort ohne zusätzliche Konfiguration benutzt werden. Abhängigkeiten zu externen Services wie zur Persistenzschicht oder zum Mailversand lassen sich direkt in die Domänenobjekte injizieren. Catapult implementiert die grundlegenden CRUD-Operationen bereits selbst und stellt sie über eine REST-Schnittstelle zur Verfügung.

Catapult basiert auf Spring 3.0 – letzten Endes soll Catapult sogar als vorkonfiguriertes Spring-MVC verstanden werden. Der gesamte Spring-Funktionsumfang steht damit allen Catapult-Webanwendungen zur Verfügung.

Im View setzt Catapult auf XHTML (tatsächlich XML), das um nur wenige eigene Attribute erweitert wurde. So müssen keinerlei frameworkspezifische Tags erlernt und genutzt werden. Der Entwickler behält die volle Kontrolle über das generierte HTML. CSS und JavaScript können problemlos eingebunden werden. Darüber hinaus lassen sich durch Catapults leistungsstarken Templating-Mechanismus eigene, domänenspezifische Tags und damit letztendlich eine DSL für den View erstellen. Die Schnittstelle zu den Domänenobjekten wird über die seit Version 3.0 in Spring enthaltene Expression Language (SpEL) realisiert. Am Beispiel Zoccer wollen wir nun zeigen, wie Catapult funktioniert.

Die Domänenklassen

Die Domäne in Zoccer besteht u. a. aus den Klassen Zoccer (repräsentiert einen Mitspieler bzw. Nutzer der Anwendung), SoccerMatch (Fußballspiel), SoccerLeague (Vorrundenliga bzw. Gruppe),SoccerFinals (Finalrunden), den dazugehörigen und typgleichen Tippklassen SoccerMatchBet, SoccerLeagueBet und SoccerFinalsBet und schließlich dem Tippschein BetSlip. Diese Klassen bilden das Herz der Anwendung. Ihre Instanzen werden in der Datenbank persistiert und sind direkt im View zugreifbar.

Eine Java-Klasse wird zu einer Entitätsklasse, indem sie mit der Annotation @Entity aus der Java Persistence API (JPA) versehen wird. Damit kann ein Objekt dieser Klasse nun über die JPA (in unserem Fall mittels Hibernate) in der Datenbank abgelegt werden. In einer Catapult-Anwendung ist eine Entität gleichzeitig eine Ressource bzw. ein Naked Object, das per Request direkt verändert werden kann. Catapult stellt automatisch REST-URLs für diese Entität bereit. Sehen wir uns beispielhaft einen Ausschnitt der Klasse Zoccer an:

@Entity public class Zoccer implements Serializable { @Id @GeneratedValue private Long id; @NotEmpty public String displayName; @Column(unique = true, nullable = false) @Email @NotEmpty public String email; @OneToOne(cascade = CascadeType.ALL) public BetSlip betSlip; ... } 

Diese Klasse enthält nur die für das Datenbank-Mapping erforderlichen Annotationen aus der JPA (@Entity, @Id, @GeneratedValue, @Column, @OneToOne). Darüber hinaus ist Catapult Bean-Validation-kompatibel (JSR 303), indem es deren Annotationen und die Hibernate-Erweiterungen (@NotEmpty, @Email etc.) für die Validierung unterstützt. Member-Variablen, auf die frei lesend und schreibend zugegriffen werden kann, werden als einfache public-Felder modelliert – längliche und redundante Getter- und Setter-Listen werden so vermieden.

[ header = Seite 3: Der XHTML-View ]

Der XHTML-View

Die Seiten einer Catapult-Webanwendung werden in XHTML geschrieben. Sowohl in den Attributen als auch innerhalb des Textinhalts kann über die SpEL auf die Daten der Domäne zugegriffen werden. Ein SpEL-Ausdruck wird dabei durch die Zeichen #{ und } begrenzt. Variablen werden über ein vorangestelltes # referenziert, andere Bezeichner werden als Bean-Namen des Spring-Applikationskontexts interpretiert. Letzten Endes ist dies die gesamte Magie des Views: Jede Seite etabliert einen eigenen Spring-Applikationskontext, auf den über die SpEL zugegriffen werden kann.

Da Catapult alle mit @Entity annotierten Klassen automatisch einliest, kann der SpEL-Ausdruck #{zoccers} ohne weitere Konfiguration zur Liste alle Zoccer-Instanzen ausgewertet werden. Darüber hinaus lädt Catapult für einen URL /zoccers/5 die Zoccer-Instanz mit der ID 5 aus der Datenbank und stellt sie der Seite /zoccers/show.html unter dem Variablennamen zoccer zur Verfügung. Ein Text #{#zoccer.displayName}: #{#zoccer.betSlip.result} würde in diesem Fall den Namen des Mitspielers und seinen Punktestand ausgeben.

Da XHTML als statische Beschreibungssprache keine Sprachmittel enthält, um Schleifen oder Bedingungen auszudrücken, fügt Catapult diese Funktionalität über spezielle Attribute in einem eigenen Namensraum hinzu. Das folgende Beispiel produziert beispielsweise entweder eine Liste mit allen Mitspielernamen (sofern vorhanden) oder den Satz „ Es sind noch keine Mitspieler angemeldet.“, falls die Liste der geladenen Zoccer leer ist:

<html xmlns="http: //www.w3.org/1999/xhtml" xmlns:c="http: //www.catapultframework.org/xml/core"> <body> <ul c:if="#{!zoccers.empty}"> <li c:foreach="#{zoccers.iterator('z')}">#{#z.displayName}</li> </ul> <p c:if="#{zoccers.empty}">Es sind noch keine Mitspieler angemeldet.</p> </body> </html> 

Der Wert des c:if-Attributs bestimmt, ob das dazugehörige Element dargestellt wird oder nicht. Der Inhalt des Attributs c:foreach ergibt einen Iterator, der für jeden Iterator-Wert eine Variable (hier z) definiert und das dazugehörige li-Element wiederholt.

Darüber hinaus bietet Catapult die Möglichkeit, lokale Variablen im View zu definieren. Dies geschieht über Attribute, die dem speziellen Namensraum http://www.catapultframework.org/xml/paramangehören. So würde beispielsweise durch das folgende Beispiel die Variable topZoccers definiert werden, die nur Spieler mit einem Punktestand größer als 250 enthält:

<div xmlns:p="http: //www.catapultframework.org/xml/param"> p:topZoccers="#{zoccers.?[betSlip.result gt 250]}"> <p c:if="#{#topZoccers.empty}"> ... </p> ... 

Innerhalb des div-Elements kann dann wie gehabt auf die neue Variable zugegriffen werden.

Wie bereits angedeutet, kann Catapult nicht nur einfache XHTML-Seiten darstellen, sondern über einen leistungsstarken Templatemechanismus Seiten aus mehreren Teilen komponieren. In Zoccer legt ein Basistemplate das Grundlayout aller Seiten fest. Alle konkreten Seiten erben von diesem Basistemplate. Des Weiteren lassen sich wiederverwendbare Seitenfragmente ausgliedern und dann inkludieren. Das Interessante an diesen Mechanismen ist, dass sie ohne spezielle, Catapult-spezifische Tags funktionieren. Ein Beispiel:

<t:layout xmlns="http: //www.w3.org/1999/xhtml" xmlns:c="http: //www.catapultframework.org/xml/core" xmlns:t="/WEB-INF/templates" xmlns:leagues="/leagues" tab="leagues"> <t:content> <section class="column width4 first tint1"> <div class="..."> ... <div c:foreach="#{leagues.sort().iterator('league')}"> <leagues:overview league="#{#league}" /> </div> </div> </section> </t:content> </t:layout> 

Der Namensraum t verweist auf das Verzeichnis /WEB-INF/templates. Das Element t:layout erbt damit von der Datei /WEB-INF/templates/layout.html. Durch die Angabe beliebiger Attribute (wietab=“leagues“) können Variablen definiert werden, auf die im Basistemplate layout.html per SpEL zugegriffen werden kann. Innerhalb von t:layout werden dann Teile des Basistemplate überschrieben bzw. gefüllt. In diesem Fall überschreibt t:content den Inhalt des Elements mit dem Attribut id=“content“ aus layout.html.

Ganz analog verweist der Namensraum leagues auf das Verzeichnis /leagues. Dementsprechend inkludiert das Element leagues:overview die Datei /leagues/overview.html. Innerhalb dieses Elements können bei Bedarf weitere Elemente aus dem leagues-Namensraum stehen, die damit einzelne Elemente aus der inkludierten Datei überschreiben. Auch hier wird mithilfe des Attributsleague=“#{#league}“ eine Variable an die inkludierte Datei übergeben. Wie man sieht, funktionieren Vererbung und Inklusion nach dem gleichen Prinzip.

Da auf die inkludierten Dateien auch direkt per URL zugegriffen werden kann (sofern man es nicht mit Mitteln wie Spring Security verhindert), lassen sich Ajax-basierte Rich Internet Applications (RIAs) ohne Weiteres mit Catapult realisieren. In diesem Fall würde per Ajax der inkludierte Teil der Seite separat nachgeladen und im Browser aktualisiert werden. Wie dies in der Praxis funktioniert, zeigt sich am Beispiel Zoccer auf den Tippseiten der Vorrunden, in der sich die Gruppentipptabelle bei jeder Änderung eines Tipps automatisch aktualisiert.

[ header = Seite 4: Aktionen ]

Aktionen

Wir haben bisher einen Blick auf die Domänenklassen und den View geworfen. Doch wie lassen sich nun Aktionen (bzw. Methoden) in der Domäne ausführen? Betrachten wir dazu den Registrierungsprozess:

<form action="#{zoccer.register(#self.password)}" method="post"> <label for="email">#{i18n['zoccer.email']}</label> <input type="text" id="email" name="email"/> <span class="error" c:if="#{error.email}">#{error.email}</span> <label for="password">#{i18n['zoccer.password']}</label> <input type="password" id="password" name="password"/> <button type="submit">#{i18n['zoccer.register']}</button> </form> 

Auch hier kommen keine Catapult-spezifischen Elemente vor. Stattdessen werden bekannte Elemente semantisch erweitert. So findet sich im action-Attribut des Formulars der SpEL-Ausdruck #{zoccer.register(#self.password)}, der bewirkt, dass beim Abschicken des Formulars die Methode register auf einer (neuen) Zoccer-Instanz gerufen wird. Als Methodenparameter wird der Wert desinput-Elementes mit dem Attribut name=“password“ übergeben. Die Methode register sieht folgendermaßen aus:

@Entity public class Zoccer implements Serializable { ... @PersistenceContext private transient EntityManager entityManager; @Transactional public void register(String password) { this.account = new Account(this.email, password); this.entityManager.persist(this); System.out.println("zoccer.register.success"); } ... } 

Catapult versucht, alle übertragenen Request-Parameter an Felder der aufgerufenen Klasse zu binden. So würde der Wert des input-Feldes email an die Member-Variable email gebunden werden. Schlägt allerdings zuvor die Validierung fehl (das Feld email war mit den Annotationen @Email und @NotEmpty versehen), fängt Catapult dies ab und stellt eine entsprechende Fehlermeldung in der Map error unter dem Key email bereit. Der Fehlertext kann dabei passend in der Datei i18n.properties definiert werden, die auch die anderen zu internationalisierenden Texte enthält.

Bemerkenswert an der register-Methode ist zum einen der automatisch zur Verfügung stehende JPA-EntityManager, den Catapult in die Entity-Klassen injiziiert. Zum anderen wird über einen einfachen System.out.println-Aufruf eine (internationalisierte) Message erzeugt, die im View mittels #{message} ausgegeben werden kann.

Fazit

Dank Catapult haben wir die Herausforderung, Zoccer in nur 5 Tagen zu programmieren, glänzend gemeistert. Im Ergebnis entstand binnen kürzester Zeit eine funktional vollständige, lauffähige und ajaxifizierte Webanwendung. Weitere Features von Catapult sind Folgende:

  • Templating für jegliche XML-basierten Formate (z. B. RSS, ATOM, SVG)
  • Umfangreiche SpEL-Erweiterungen
  • Mail-Templates
  • Voller Support von Scala und Groovy
  • Comet-Unterstützung mit integriertem Observer-Pattern/PropertyChangeListener

Weitere Informationen unter: http://www.catapultframework.org.

Die Autoren sind Softwareentwickler bei mindmatters und entwickeln seit vielen Jahren Webanwendungen in Java. Ihre ersten Open-Source-Sporen haben sie sich mit dem Framework JSF-Spring verdient.
Geschrieben von
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.