Komponentenorientierte Webentwicklung, die Spaß macht!

Tutorial: World Wide Wicket

Martin Dilger

Wie schon viele Male zuvor versuchte Herr K., seinen verdienten Feierabend mit einem guten Film aus seiner Onlinevideothek zu verbringen. Und wie immer ärgerte er sich über die langsame Weboberfläche seines Anbieters. Im Ärger kam Herrn K. eine Idee: Wieso nicht einmal selbst zum Anbieter werden und anderen dieses Ärgernis ersparen? Da Herr K. bisher nur wenig Erfahrung mit der Entwicklung von Webanwendungen hat, muss ein passendes Framework gefunden werden, um schnell und einfach eine performante, wartbare und funktionale Webanwendung entwickeln zu können.

Im vorliegenden Artikel wollen wir die Fortschritte von Herrn K. bei der Entwicklung seiner eigenen Onlinevideothek begleiten. Herr K. hat als regelmäßiger Leser des Java Magazins bereits eine Vielzahl an Frameworks kennen gelernt. Das vielversprechendste davon scheint Apache Wicket [4] zu sein.

Wicket bietet ein einfach zu erlernendes API, eine klare Trennung von Layout und Implementierung, die Möglichkeit, komponentenorientiert zu entwickeln und obendrein AJAX-Support, ohne selbst JavaScript entwickeln zu müssen. Also ideale Voraussetzungen für Herrn K.

Die ersten Schritte

Herr K. entscheidet sich für einen schnellen Einstieg und verwendet Apache Maven [5]  als Build-System. Maven bietet den Vorteil, dass die einzelnen Bibliotheken nicht manuell heruntergeladen werden müssen und bereits ein fertiger Archetype für Wicket-Projekte existiert. Hier [6] gibt es einen Generator, der sogar das Maven-Kommando hierfür bereits vorbereitet.

mvn archetype:create -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart 
-DarchetypeVersion=1.4.9 -DgroupId=de.pentasys.k -DartifactId=video

Obendrein bekommt Herr K. sogar den Jetty-Webserver geschenkt, der im Archetype bereits über das Maven-Jetty-Plug-in eingebunden ist. Der Webserver kann über das Kommando mvn jetty:run gestartet werden und lauscht auf dem Port 8080.

Herr K. betrachtet den generierten Archetype und findet erstaunlich wenig, was zum Betrieb einer Wicket-Anwendung nötig ist. Zunächst findet er nur eine einzige XML-Datei im gesamten Projekt, die für Java-Webanwendungen obligatorische web.xml (Listing 1).

<filter>
<filter-name>pentasys.video</filter-name>
<filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
<init-param>
<param-name>applicationClassName</param-name>
<param-value>de.pentasys.k.VideoStoreApplication</param-value>
</init-param>
</filter>

Der Wicket-Filter erhält als Parameter den vollqualifizierten Namen einer Applikationsklasse, die freundlicherweise gleich mit generiert wurde (Herr K. hat hier bereits einige Klassennamen angepasst):

public class VideoStoreApplication extends WebApplication{
public Class<HomePage> getHomePage() {
return HomePage.class;
}
}

Über die Applikationsklasse kann die Anwendung zentral konfiguriert werden. Sie definiert zunächst nichts weiter als die Methode getHomePage(), die die Einstiegsseite der Applikation liefert. Des Weiteren findet Herr K. die Klasse HomePage selbst (Listing 2).

public class HomePage extends WebPage {
public HomePage() {
add(new Label("message"
"If you see this message, " +
"K's Video Store is properly configured"));
}
}

Obwohl Herr K. bisher keine Erfahrung mit Wicket besitzt, scheint intuitiv klar, wie das Framework funktioniert. Die Klasse HomePage definiert ein neues Label im Konstruktor, das mit der Methodeadd()hinzugefügt wird. Doch Herrn K. fehlt noch ein wichtiger Aspekt, und zwar die Stelle, an der das Layout einer Wicket-Anwendung definiert wird.

Herr K. stöbert daraufhin noch ein wenig im generierten Archetype und stößt auf die Datei HomePage.html, die im selben Package wie die Klasse HomePage liegt und denselben Bezeichner hat (Listing 3).

<html xmlns:wicket="http: //wicket.apache.org/dtds.data/wicket-xhtml1.4-strict.dtd" >
<head>
<title>PENTASYS AG - K's Video Store Application</title>
</head>
<body>
<strong>K's Video Store Application</strong>
<br/><br/>
<span wicket:id="message">message will be here</span>
</body>
</html>

Herr K. sieht erfreut, dass es sich hier um ein reines HTML-Template ohne Taglibs, Scriptlets oder sonstige Logik handelt. Ein <span/>-Element im Template besitzt das Attribut wicket:id, das auf den Wert „message“ gesetzt wird. Dies entspricht genau der ID, mit dem das Label in der HomePage-Klasse hinzugefügt wurde. Sofort erschließt sich Herrn K. die Funktionsweise. Der Inhalt des<span/>-Elements wird durch die Java-Komponente mit der entsprechenden ID ersetzt.

Verlassen wir kurz Herrn K. und rekapitulieren die bisher gewonnenen Erkenntnisse.

Wicket-Komponenten bestehen aus nichts anderem als Java-Klassen und zugehörigen HTML-Templates (und evtl. zusätzlichen CSS und/oder JavaScript-Dateien). Die HTML-Templates müssen sich im gleichen Package wie die Java-Klassen befinden und den gleichen Bezeichner tragen. Komponenten können ineinander geschachtelt werden (mit der Methode add()und der Vergabe einer ID). Die Java-Komponentenhierarchie spiegelt sich in der Tag-Hierarchie des HTML-Templates wider. Die Zuordnung von Java-Komponenten zu HTML-Tags erfolgt über das Attribut wicket:id. Natürlich müssen nur diejenigen HTML-Tags eine ID erhalten, die auch tatsächlich mit einer Java-Komponente verknüpft werden sollen.

Herr K. ist mittlerweile auf das erste Problem gestoßen. Er ist ein hervorragender Entwickler aber leider auch ein lausiger Designer. Zum Glück befindet sich in seinem Bekanntenkreis Herr D., ein erfolgreicher Webdesigner. Herr K. skizziert kurz seine Vorstellung auf einem Blatt Papier und schickt den Entwurf an Herrn D.. Kurz darauf erhält er ein fertiges HTML-Template inkl. CSS-Styling, das er nun als Grundlage für die weitere Entwicklung verwendet.

Der nächste Schritt für Herrn K. besteht nun darin, die fachlichen Domänenobjekte für seine Videothek zu definieren. (Die Klasse Customer ist exemplarisch in Listing 4 dargestellt).

public class Customer implements Serializable {
private String name;
private Adress adress;
private String email;
private CustomerType type;

public Customer(){
}

public Customer(String name, String email, CustomerType type, Adress adress) {
this.email = email;
this.name = name;
this.adress = adress;
this.type = type;
}
/*
Getter und Setter
*/
}

Alle Domänenklassen sind einfache Pojos und für Herrn K. selbsterklärend. Die einzige Besonderheit: jede Domänenklasse implementiert das Serializable-Interface. Warum das notwendig ist, wird im Kasten Wicket und Serialisierung genauer erläutert.

[ header = Seite 2: Wicket und Serialisierung ]

Wicket und Serialisierung
Wicket serialisiert eine Page sowie deren Komponenten nach jedem Request per Default in den so genannten DiskPageStore auf die Festplatte und stellt so eine Lösung für das Back-Button-Problem im Browser bereit. Das bedeutet aber auch, dass wirklich jede Domänenklasse der Webanwendung zwingend das Serializable-Interface implementieren und serialisierbar sein sollte. Ist dies nicht möglich, kann das Problem umgangen werden, indem beispielsweise ein LoadableDetachableModel verwendet wird [7]. Über das LoadableDetachableModel wird Objekt detached (also z. B. das Objekt selbst auf null gesetzt und nur eine ID gespeichert), bevor der Komponentenbaum serialisiert wird. Beim nächsten Request kann das Objekt erneut geladen werden (z. B. aus der Datenbank anhand der gespeicherten ID).

Herr K. möchte die kundenspezifischen Informationen in einer Session speichern. Zunächst sollte der aktuell angemeldete Benutzer über die Session zugreifbar sein. Weiterhin ist es sinnvoll, eine Liste an bereits ausgewählten Filmen über die Session zugreifbar zu machen.

Diese Informationen scheinen für den Anfang zu genügen und Herr K. implementiert die Klasse VideoSession in Listing 5.

public class VideoSession extends WebSession{
public VideoSession(Request request) {
super(request);
}

private Customer customer;
private List<Movie> selectedMovies;

public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public List<Movie> getSelectedMovies() {
return selectedMovies;
}
public void setSelectedMovies(List<Movie> selectedMovies) {
this.selectedMovies = selectedMovies;
}

public static VideoSession get(){
return (VideoSession) Session.get();
}

}

Die Klasse VideoSession erweitert die Klasse WebSession des Wicket-Frameworks. WebSession ist eine Abstraktion für die aus der Servlet-Spezifikation bekannte HttpSession. Vorteil der Verwendung von Websession gegenüber der direkten Verwendung von HttpSession ist die Typsicherheit der in der Session verfügbaren Objekte. Anstatt eines Zugriffs der Art Customer customer = (Customer) httpSession.getAttribute(„customer“) kann Herr K. direkt und ohne zu Casten auf den Kunden zugreifen über Customer customer = VideoSession.get().getCustomer().

Damit für jeden Benutzer eine neue Session erzeugt wird, überschreibt Herr K. einfach die Methode newSession(Request request, Response response) in der Klasse VideoStoreApplication und liefert als Rückgabewert eine neue Instanz von VideoSession.

Da Herr K. ein von Natur aus gutgläubiger Mensch ist, verzichtet er zunächst auf die Implementierung eines Login-Mechanismus für seine Videothek. Stattdessen soll der Benutzer direkt auf der ersten Seite die Möglichkeit bekommen, seinen Namen, E-Mail-Adresse, Rechnungsadresse sowie seinen Kundentyp (privat oder business) zu hinterlegen.

Models auf der Titelseite

Herr K. beginnt gut gelaunt mit der Erweiterung der bereits vorhandenen Klasse HomePage. Zur Umsetzung von Formularen für Eingabefelder bietet Wicket entsprechende Komponenten wie beispielsweise TextField für <input type=“text“/>, RadioChoice für <input type=“radio“/> oder Form für das <form/>-Element selbst.

Herr K. erstellt zunächst die für die Eingabe der Benutzerdaten passende Html-Form (in Auszügen in Listing 6) und anschließend die korrespondierenden Wicket-Komponenten (Listing 7). Die Komponenten werden später im Artikel noch genauer betrachtet.

<form wicket:id="customerForm">
<table>
<tr>
<td>Name:</td>
<td colspan="2"><input type="text" wicket:id="name" /></td>
</tr>
<tr>
<td>Email:</td>
<td colspan="2"><input type="text" wicket:id="email" /></td>
</tr>
[...]
</form>

final Customer customer = createEmptyCustomer();
Form form = new Form<Customer>("customerForm", new CompoundPropertyModel<Customer>(customer));
final TextField<String> nameField = new TextField<String>("name");
final TextField<String> emailField = new TextField<String>("email");
final RadioChoice<CustomerType> customerTypeChoice = new RadioChoice<CustomerType>
("type", Arrays.asList(CustomerType.values()), new EnumChoiceRenderer<CustomerType>()); final TextField<String> streetField = new TextField<String>("adress.street"); final TextField<String> zipField = new TextField<String>("adress.zip"); final TextField<String> cityField = new TextField<String>("adress.city"); final TextField<String> houseNumberField = new TextField<String>( "adress.houseNumber"); form.add(nameField); form.add(emailField); [..]

Zunächst jedoch macht sich Herr K. mit einem weiteren wichtigen Konzept des Wicket-Frameworks, den Modellen, vertraut. Ein Wicket-Modell kann als Wrapper-Objekt betrachtet werden, das die Zugriffe auf die domänenspezifischen Klassen kapselt.

Modelle ermöglichen einer Wicket-Komponente den Zugriff auf das zugrunde liegende Domänenobjekt (wie beispielsweise einen Customer) über entsprechende Zugriffsmethoden (getObject ()/setObject()). Ein einfaches Beispiel für die Verwendung von Modellen ist in folgendem Codefragment dargestellt:

Model<String> model = new Model<String>(customer.getName());
Label label = new Label("labelId", model);

Das Label würde jetzt automatisch den im Model hinterlegten Namen des Kunden anzeigen. Die Verwendung von Models hat gegenüber der direkten Verwendung von fachlich motivierten Klassen einige Vorteile. Modelle abstrahieren von der zugrundeliegenden Datenquelle, was deren problemlosen Austausch zu einem späteren Zeitpunkt ermöglicht. Wicket bietet verschiedene Modellimplementierungen, die beispielsweise ein automatisches Binding an bestimmte Attribute einer Domänenklasse (PropertyModel) oder auch das Laden von Daten über ein Backend-System (LoadableDetachableModel) ermöglichen.

Ein naiver Ansatz von Herr K. wäre es, jeder Java-Komponente ein entsprechendes String-Model zuzuordnen und diese dann beim Abschicken des Formulars auszuwerten ähnlich dem folgenden Listing (in dem man auch direkt sieht, wie man einem Link ein Verhalten geben kann, aber dazu gleich mehr):

form.add(new SubmitLink("submit") {
@Override
public void onSubmit() {
customer.setName(nameField.getModelObject());
/**
Mehr Mappings
*/
}
});

Dieser Ansatz bietet gleich mehrere Nachteile. Zum einen produziert Herr K. unnötigen Code, der gewartet werden muss, die Klasse unnötig aufbläht und schlimmstenfalls sogar Fehler enthalten kann. Weiterhin müsste Herr K. genaugenommen für jedes Attribut auch prüfen, ob das Model überhaupt gesetzt ist, um NullPointerExceptions zu vermeiden. Ein nächster Schritt für Herrn K. könnte darin bestehen, die statischen String-Modelle gegen PropertyModels zu ersetzen. PropertyModels vermeiden die zuvor diskutierten Nachteile, indem sie die Möglichkeit bieten, ein automatisches Binding über eine einfache Expression zu realisieren. Exemplarisch hat Herr K. hierzu folgende Felder implementiert:

final TextField<String> emailField = new TextField<String>("email",
new PropertyModel<String>(customer, "email"));
final TextField<String> streetField = new TextField<String>("street",
new PropertyModel<String>(customer, "adress.street"));

Ein PropertyModel erwartet in seinem Konstruktor das Domänenobjekt, dessen Daten verwendet werden sollen, und eine Expression für den Zugriff auf die entsprechenden Daten. Für Felder, auf die mit einer Expression zugegriffen werden soll, muss zumindest eine Property mit dem passenden Namen existieren (Getter- und Setter sind optional, aber zu empfehlen). Diese können sogar geschachtelt sein, wie im Beispiel adress.street klar wird. Dies entspricht einem Aufruf von getAdress().getStreet(). PropertyModels übernehmen hier freundlicherweise auch NullChecks und erzeugen sogar nicht gesetzte Objekte (beispielsweise ein Adress-Objekt, wenn dieses noch nicht vorhanden sein sollte). Ein weiteres Model, das Herrn K. noch mehr Arbeit abnimmt, ist dasCompoundPropertyModel.

Wicket-Komponenten verhalten sich nämlich glücklicherweise wie Herrn K.s neugierige Kinder. Wenn diese nicht weiter wissen, fragen sie bei den Eltern nach. Hat eine Wicket-Komponente kein eigenes Model, wird automatisch das Model der Elternkomponente (in diesem Beispiel der Form) verwendet.

Zusätzlich entspricht in den meisten Fällen die Template-ID einer Komponente dem Attributnamen in der Domänenklasse (z. B. entspricht. <wicket:id=“email“/> namentlich dem Attribut email aus Customer). Diese Gesetzmäßigkeit macht sich das CompoundPropertyModel zu Nutze, indem hierdurch ein automatisches Binding erzeugt wird. Erzeugt Herr K. also eine Wicket-Komponente mit der ID „email“ wird diese automatisch an das E-Mail-Attribut des Customers im CompundPropertyModel der Form gebunden.

Herr K. muss zusätzlich zwingend die entsprechenden Wicket-IDs im HTML-Template anpassen, damit diese mit den vergebenen IDs in der Java-Komponente übereinstimmen. Übrigens lässt sich dieser Mechanismus über Model.bind()flexibel anpassen, falls die IDs aus irgendwelchen Gründen nicht angepasst werden können. Damit alle Komponenten mit demselben Model arbeiten können, wird dies nicht mehr in jede Wicket-Komponente einzeln gesetzt, sondern einmalig in der Form, wie in Listing 7 ersichtlich.

[ header = Seite 3: Wicket K. omponenten ]

Wicket K. omponenten

Die meisten in Listing 7 definierten Komponenten sind Herrn K. bereits aufgrund des zuvor erworbenen Wissens klar. Die wohl interessanteste Komponente ist die Klasse RadioChoice, die für die Auswahl des Kundentypen entsprechende Radiobuttons rendert:

RadioChoice<CustomerType>("type", Arrays.asList(CustomerType.values()),
new EnumChoiceRenderer<CustomerType>());

Die Klasse RadioChoice erwartet die obligatorische Komponenten-ID, ein optionales Modell (das in diesem Fall bereits aus der Form geladen wird), eine Liste an Werten, die als Radiobuttons gerendert werden sollen (CustomerType.values()) sowie einen optionalen ChoiceRenderer.

Der ChoiceRenderer bestimmt hierbei die für jeden RadioButton gerenderte Htmp-Markup-ID sowie den angezeigten Text. In diesem Fall verwendet Herr K. den bereits im Wicket-Framework verfügbaren EnumChoiceRenderer, der den anzuzeigenden Text für den CustomerType-Enum aus einer Property-Datei laden kann.

Es empfiehlt sich, zumindest für jede Wicket-Seite sowie für die Applikation selbst eine eigene Property-Datei anzulegen (hier auch das Stichwort Internationalisierung). Damit diese Dateien gefunden werden, gelten für sie dieselben Regeln wie für die zuvor bereits erläuterten Html-Templates, d. h. sie müssen im selben Package wie die zugehörige Klasse liegen und denselben Namen tragen. Herr K. erstellt also eine Property-Datei mit dem Namen VideoStoreApplication.properties und in dieser die beiden folgenden Einträge:

CustomerType.PRIVATE = Privatkunde
CustomerType.BUSINESS = Businesskunde

Für das Enum CustomerType.PRIVATE wird also automatisch der Text Privatkunde gerendert. Weitere Informationen zum Thema Ressourcen finden sich in [8].

Das passende Styling

Damit die Anwendung so aussieht, wie Herr K. sich dies vorstellt, muss nun auch der passende CSS-Style mit eingebunden werden. Herr D. hat hierfür bereits die Datei style.css geliefert. Über folgendes Codefragment in der Klasse HomePage bindet Herr K. die Datei style.css im Header der gerenderten Seite ein. add(CSSPackageResource.getHeaderContribution(HomePage.class, „style.css“));. Die Methode getHeaderContribution erwartet hierbei zwei Argumente, die Java-Klasse, zu der relativ die im zweiten Parameter definierte CSS-Datei gesucht wird. Die Datei style.csswürde also im gleichen Package wie die Klasse HomePage gesucht werden. Der gerenderte Header sieht für das inkludierte CSS in diesem Fall so aus:

<link rel="stylesheet" type="text/css" href="resources/de.pentasys.k.HomePage/style.css" />

Das Einbinden von eigenen JavaScript-Dateien würde übrigens für Herrn K. analog über die Klasse JavascriptPackageResource funktionieren.

Ein valides Argument

Benutzereingaben werden bisher nicht validiert. Würde sich Herr K. eine Stammdatenbank aufbauen wollen, hätte er wohl ziemlich bald mit leeren Feldern oder Emailadressen á la „test“ zu kämpfen.

Zunächst überlegt sich Herr K., dass von allen Eingaben lediglich der Name, die E-Mail-Adresse und der Typ zwingend erforderlich sein sollen. Dies ist in Wicket sehr einfach realisierbar, da jede Formkomponente über die Methode setRequired(boolean required) verfügt. Herr K. realisiert dies exemplarisch für das E-Mail-Feld:

final TextField<String> emailField = new TextField<String>("email");
emailField.setRequired(true);

Doch an welcher Stelle sollte die Fehlermeldung angezeigt werden? Wicket bietet hierfür die Komponente FeedbackPanel, die einfach der Form hinzugefügt werden kann und automatisch alle Fehlermeldungen anzeigt, die während der Formvalidierung gesammelt werden:

form.add(new FeedbackPanel("feedback"));
<div wicket:id="feedback"/>

Standardmäßig werden Fehlermeldungen durch die FeedbackPanel-Komponente mit der CSS-Klasse feedbackPanelERROR gerendert, für die Herr D. bereits einen passenden Style definiert hat. Zuletzt möchte Herr K. noch sicherstellen, dass immer eine korrekte E-Mail-Adresse eingegeben wird. Wicket bietet hierfür bereits die Klasse EmailAddressValidator. Ein Validator wird einer Komponente einfach über die Methode add()hinzugefügt:

emailField.add(EmailAddressValidator.getInstance());. Ein eigener Validator könnte von Herrn K. problemlos durch Implementierung des Interfaces IValidator erstellt werden (Eine eigene Beispielimplementierung hat Herr K. dem Demoprojekt beigefügt). Zur Veranschaulichung ist in Abbildung 1 die Form mit einer gerenderten Fehlermeldung dargestellt.

Abb. 1: Formularvalidierung

Das Abschicken der Form

Herr K. möchte gerne die Kundendaten in der aktuellen Session speichern, sobald der Kunde seine Daten eingegeben hat und die Form abschickt. Hierfür fügt Herr K. der Form einen entsprechenden SubmitLink hinzu:

form.add(new SubmitLink("submit") {
@Override
public void onSubmit() {
// store new customer in session
((VideoSession) getSession()).setCustomer(customer);
setResponsePage(SelectMoviePage.class);
}
});

Wicket bietet ein Herrn K. (und auch den meisten anderen Entwicklern) sehr geläufiges Event-basiertes Programmiermodell, das sich in den meisten GUI-Frameworks bewährt hat. Die onSubmit-Methode der SubmitLink-Komponente wird ausgeführt, sobald der Benutzer die Form abschickt.

Durch das zuvor definierte Binding kann Herr K. sicher sein, dass die eingegebenen Daten bereits im Customer vorhanden sind, wenn alle Validierungen erfolgreich waren. Schlägt eine Validierung fehl, wird nichts in das Domänenobjekt geschrieben.

Fazit

Herr K. musste sich initial viele Grundlagen zum Thema Wicket erarbeiten. So konnte er im ersten Teil dieses Artikels lediglich eine einzige Seite fertigstellen. Diese jedoch mit der Möglichkeit von Benutzereingaben, effizienter Verwendung von Wicket-Models und zusätzlicher Validierung von Eingaben.

Im zweiten Teil des Artikels wird Herr K. den Rest der Anwendung implementieren und sich hierfür u. a. die Navigation zwischen verschiedenen Wicket-Seiten, die Einbindung interessanter Komponenten wie ListViews, die Möglichkeit zur Strukturierung von Seiten und eine Anbindung an das Spring-Framework genauer betrachten.

Für heute jedoch hat sich Herr K. und der Autor das Feierabendbier redlich verdient und dasselbe gönnen wir auch Ihnen. Schönen Feierabend!

Geschrieben von
Martin Dilger
Kommentare

Schreibe einen Kommentar

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