Tapestry 5
Igor Drobiazko
Tapestry 5 Die Entwicklung von Webanwendungen mit Leichtigkeit Mit einem Vorwort vom Tapestry-Erfinder Howard M. Lewis Ship
An imprint of Pearson Education München • Boston • San Francisco • Harlow, England Don Mills, Ontario • Sydney • Mexico City Madrid • Amsterdam
Bibliografische Information der Deutschen Nationalbibliothek Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.
Die Informationen in diesem Produkt werden ohne Rücksicht auf einen eventuellen Patentschutz veröffentlicht. Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zusammenstellung von Texten und Abbildungen wurde mit größter Sorgfalt vorgegangen. Trotzdem können Fehler nicht vollständig ausgeschlossen werden. Verlag, Herausgeber und Autoren können für fehlerhafte Angaben und deren Folgen weder eine juristische Verantwortung noch irgendeine Haftung übernehmen. Für Verbesserungsvorschläge und Hinweise auf Fehler sind Verlag und Herausgeber dankbar. Alle Rechte vorbehalten, auch die der fotomechanischen Wiedergabe und der Speicherung in elektronischen Medien. Die gewerbliche Nutzung der in diesem Produkt gezeigten Modelle und Arbeiten ist nicht zulässig.
Fast alle Hardware- und Softwarebezeichnungen und weitere Stichworte und sonstige Angaben, die in diesem Buch verwendet werden, sind als eingetragene Marken geschützt. Da es nicht möglich ist, in allen Fällen zeitnah zu ermitteln, ob ein Markenschutz besteht, wird das ® Symbol in diesem Buch nicht verwendet.
Umwelthinweis: Dieses Produkt wurde auf chlorfrei gebleichtem Papier gedruckt. Um Rohstoffe zu sparen, haben wir auf Folienverpackung verzichtet.
10 9 8 7 6 5 4 3 2 1 12 11 10
ISBN 978-3-8273-2844-1
© 2010 by Addison-Wesley Verlag, ein Imprint der Pearson Education Deutschland GmbH Martin-Kollar-Straße 10–12, D-81829 München/Germany Alle Rechte vorbehalten Einbandgestaltung: Marco Lindenbeck, webwo GmbH (
[email protected]) Lektorat: Brigitte Bauer-Schiewek,
[email protected] Fachlektorat: Ulrich Stärk Korrektorat: Sandra Gottmann Herstellung: Monika Weiher,
[email protected] Satz: Reemers Publishing Services GmbH, Krefeld (www.reemers.de) Druck: Bercker Graph. Betrieb, Kevelaer Printed in Germany
Inhaltsübersicht Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
17
Vorwort von Howard M. Lewis Ship . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
21
Danksagungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
23
Teil I
Tapestry für Einsteiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
25
1
Von Servlets bis MVC-Frameworks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
27
2
Tapestry-Schnelleinstieg . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35
3
Tapestry als ereignisgetriebenes MVC-Framework . . . . . . . . . . . . . . . . . . . .
61
4
Navigation zwischen Seiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
73
5
Entwicklung von zustandsbehafteten Anwendungen . . . . . . . . . . . . . . . . . .
87
6
Lokalisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
7
Formulare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
Dynamische Formulare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
9
Arbeiten mit JavaBeans . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
117
10 Multimedia-Inhalte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209 Teil II
Tapestry für Fortgeschrittene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
11 Entwicklung wiederverwendbarer Komponenten . . . . . . . . . . . . . . . . . . . . . 219 12 Mixins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245 13 Ajax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255 14 Integration von Hibernate und Spring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281 15 Testen von Tapestry-Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299 16 Sicherheit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319
Inhaltsübersicht
Teil III
Tapestry für Profis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 325
17 Tapestry IoC und Dependency Injection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327 18 AOP mit Tapestry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 365 19 Typumwandlung/Type Coercion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375 20 Verarbeitung von Anfragen durch Tapestry . . . . . . . . . . . . . . . . . . . . . . . . . . . 381 21 Bytecodemanipulation mit Tapestry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 389 A
Anhang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 397
Stichwortverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 425
6
Inhaltsverzeichnis Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Vorwort von Howard M. Lewis Ship . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 Danksagungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 Teil I 1
Tapestry für Einsteiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Von Servlets bis MVC-Frameworks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 1.1 Wie alles begann . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 1.2 Model View Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 1.3 Tapestry und das Model View Controller Pattern . . . . . . . . . . . . . . . . . . . . . 31 1.4 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2
Tapestry-Schnelleinstieg . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 2.1 Starten von Tapestry-Anwendungen mit Eclipse Web Tools Platform . . 35 2.2 Starten von Tapestry-Anwendungen mit Run-Jetty-Run . . . . . . . . . . . . . . . 40 2.3 Struktur von Tapestry-Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 2.3.1
Tapestry-Seiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
2.3.2
Tapestry Markup Language . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
2.3.3
Tapestry-Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
2.3.4
Tapestry IoC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
2.4 Nonstop-Entwicklung mit Tapestry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 2.5 Fehlerberichte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 2.6 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 3
Tapestry als ereignisgetriebenes MVC-Framework . . . . . . . . . . . . . . . . . . . . . . . . 61 3.1 Behandeln der Benutzeraktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 3.2 Namenskonvention vs. Annotationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 3.3 Kontext eines Ereignisses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 3.4 Erzeugen einer Antwort mit Handler-Methoden . . . . . . . . . . . . . . . . . . . . . . 65 3.5 Auslösen von Ereignissen mit der Komponente EventLink . . . . . . . . . . . . 67 3.6 Programmatisches Auslösen eigener Ereignisse . . . . . . . . . . . . . . . . . . . . . 68 3.7 Event Bubbling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 3.8 Abfangen von Exceptions aus Handler-Methoden . . . . . . . . . . . . . . . . . . . . 70 3.9 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
Inhaltsverzeichnis
4
Navigation zwischen Seiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 4.1 Erstellen von Verweisen zwischen Seiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 4.2 Übermitteln von Informationen an eine Zielseite . . . . . . . . . . . . . . . . . . . . . 74 4.3 Übermitteln von Informationen an eine Tapestry-Seite . . . . . . . . . . . . . . . 76 4.3.1
Generieren von Handler-Methoden für den Aktivierungskontext . . . . . . 81
4.4 Navigation durch Aktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 4.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 5
Entwicklung von zustandsbehafteten Anwendungen . . . . . . . . . . . . . . . . . . . . . . . 87 5.1 Lebenszyklus eines Servlets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 5.2 Lebenszyklus einer JavaServer Page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 5.3 Lebenszyklus einer Tapestry-Seite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 5.3.1
Teilnehmen am Lebenszyklus einer Tapestry-Seite . . . . . . . . . . . . . . . . . 89
5.3.2
Wann werden pageLoaded(), pageAttached() und pageDetached() aufgerufen? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
5.4 Verwalten des Seitenzustandes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 5.4.1
Strategien zum Zwischenspeichern der Eigenschaften . . . . . . . . . . . . . . 95
5.4.2
Verwerfen des Zustands einer Seite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
5.4.3
Unterbinden des Verwerfens des Seitenzustands . . . . . . . . . . . . . . . . . . 98
5.5 Verwaltung des Anwendungszustandes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98 5.5.1
Erzeugen eines Session State Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
5.5.2
Überprüfung der Existenz eines Session State Objects . . . . . . . . . . . . . 100
5.5.3
Arbeiten mit dem Dienst ApplicationStateManager . . . . . . . . . . . . . . . . 101
5.5.4
Eingreifen in den Prozess der Instanziierung eines Session State Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
5.6 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 6
Lokalisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 6.1 Internationalisierung und Lokalisierung in Java . . . . . . . . . . . . . . . . . . . . . . 105 6.2 Anwendungsweiter Nachrichtenkatalog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 6.3 Komponenten-Nachrichtenkatalog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 6.4 Lokalisierte Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 6.5 Zugreifen auf den Nachrichtenkatalog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 6.6 Unterstützte Sprachen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 6.7 Lokalisierung statischer Ressourcen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 6.8 Umschalten zwischen unterstützten Sprachen einer Anwendung . . . . . . 112 6.9 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
8
Inhaltsverzeichnis
7
Formulare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 7.1 Erzeugen eines einfachen Login-Formulars . . . . . . . . . . . . . . . . . . . . . . . . . . 118 7.2 Behandelung der Ereignisse der Komponente Form . . . . . . . . . . . . . . . . . . 121 7.3 Mehrere Formulare auf einer Seite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125 7.4 Überblick über Tapestrys Formularkomponenten . . . . . . . . . . . . . . . . . . . . 126 7.4.1
Texteingabe in Text- und Passwortfelder . . . . . . . . . . . . . . . . . . . . . . . . . 126
7.4.2
Checkboxen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
7.4.3
Radiobuttons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
7.4.4
Auswahllisten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
7.4.5
Palette . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
7.4.6
Eingabe eines Datums . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
7.4.7
Hochladen von Dateien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
7.4.8
Abschicken von Formularen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
7.5 Labels für Formularfelder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148 7.6 Eingabevalidierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150 7.6.1
Überschreiben von Validierungsmeldungen . . . . . . . . . . . . . . . . . . . . . . 154
7.6.2
Eigene Validatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
7.7 Null-Werte in Formularfeldern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160 7.8 Umwandlung von Eingaben zwischen Client und Server . . . . . . . . . . . . . . 162 7.9 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167 8
Dynamische Formulare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169 8.1 Variable Anzahl von Formularfeldern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169 8.1.1
Beispielszenario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
8.1.2
Implementierung des Beispielszenarios . . . . . . . . . . . . . . . . . . . . . . . . . 171
8.2 Erweitern der Formulare durch Benutzerinteraktionen . . . . . . . . . . . . . . . 176 8.2.1
Beispielszenario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
8.2.2
Implementierung des Beispielszenarios . . . . . . . . . . . . . . . . . . . . . . . . . 176
8.3 Teilformulare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179 8.4 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182 9
Arbeiten mit JavaBeans . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183 9.1 Formulare für JavaBeans . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183 9.1.1
Don’t Repeat Yourself . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
9.1.2
Generieren von Formularen mit BeanEditForm . . . . . . . . . . . . . . . . . . . . 185
9.1.3
Verstecken bestimmter Eigenschaften von JavaBeans . . . . . . . . . . . . . . 187
9
Inhaltsverzeichnis
9.1.4
Angepasste Formularfelder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
9.1.5
Reihenfolge der Formularfelder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
9.1.6
Virtuelle Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
9.1.7
Programmatisches Ändern von Metadaten einer JavaBean . . . . . . . . . . 192
9.1.8
Eingabenvalidierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
9.2 Darstellen von JavaBeans . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196 9.3 Darstellen mehrerer JavaBeans . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197 9.3.1
Paging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
9.3.2
Zugriff auf die Werte der aktuellen Iteration . . . . . . . . . . . . . . . . . . . . . . 201
9.3.3
Überschreiben der Darstellung von Spalten . . . . . . . . . . . . . . . . . . . . . . . 202
9.4 Datentypen von Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203 9.4.1
Neue Datentypen zum Editieren von Eigenschaften . . . . . . . . . . . . . . . . 203
9.4.2
Neue Datentypen zum Darstellen von Eigenschaften . . . . . . . . . . . . . . . 205
9.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207 10 Multimedia-Inhalte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209 10.1 Senden eines einfachen Textes als Bytestrom . . . . . . . . . . . . . . . . . . . . . . . 209 10.2 Anbieten einer Download-Funktionalität . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210 10.3 Anzeigen eines Diagramms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212 10.4 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215 Teil II
Tapestry für Fortgeschrittene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
11 Entwicklung wiederverwendbarer Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . . 219 11.1 Bestandteile einer Komponente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219 11.2 Parameter von Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219 11.3 Bidirektionale Parameter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221 11.4 Standard-Bindings der Parameter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222 11.4.1
Standard-Binding-Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
11.4.2
Generierung von Standard-Binding-Methoden . . . . . . . . . . . . . . . . . . . . 225
11.5 Vererbung von Bindings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226 11.6 Eigene Binding-Präfixe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228 11.7 Markup-Erzeugung einer Komponente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232 11.8 Informelle Parameter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236 11.9 Vererben von Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237 11.10 Umgebungsdienste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239
10
Inhaltsverzeichnis
11.11 Komponentenbibliotheken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241 11.12 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243 12 Mixins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245 12.1 Was sind Mixins? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245 12.2 Instanz-Mixins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249 12.3 Implementierungs-Mixins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251 12.4 Parameter von Mixins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252 12.5 Mixins in Tapestry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253 12.6 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254 13 Ajax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255 13.1 Einfache Ajax-Funktionalität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255 13.2 Aktualisieren von Zones mit zusätzlichem Inhalt . . . . . . . . . . . . . . . . . . . . . 258 13.3 Visuelle Rückmeldung über Ajax-Updates . . . . . . . . . . . . . . . . . . . . . . . . . . . 261 13.4 Aktualisierung mehrerer Zones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261 13.5 Ajax-Unterstützung in Tapestry-Komponenten . . . . . . . . . . . . . . . . . . . . . . . 264 13.6 Ajax und JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265 13.7 Optimierung der Ladezeit durch Ajax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266 13.8 Erzeugen eines Autovervollständigers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269 13.8.1
Erweitern von Autocomplete . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
13.9 Eigene Ajax-Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275 13.10 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 278 14 Integration von Hibernate und Spring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281 14.1 Integration von Hibernate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281 14.1.1
Konfiguration von Hibernate in Tapestry-Anwendungen . . . . . . . . . . . . 281
14.1.2
Zugriff auf die Hibernate-Session . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 282
14.1.3
Commit von Transaktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
14.1.4
Hibernate-Entitäten als Aktivierungskontext . . . . . . . . . . . . . . . . . . . . . . 284
14.1.5
Zwischenspeichern von Entitäten mit @Persist . . . . . . . . . . . . . . . . . . . . 284
14.1.6
Darstellen der Hibernate-Entitäten mit Komponente Grid . . . . . . . . . . . 285
14.1.7
Zugriff auf Hibernate-Metriken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
14.1.8
Suche nach Hibernate-Entitäten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 287
14.1.9
Teilnahme an der Hibernate-Konfiguration . . . . . . . . . . . . . . . . . . . . . . . 288
14.1.10 Konfiguration der Hibernate-Integrationsbibliothek . . . . . . . . . . . . . . . . 289
11
Inhaltsverzeichnis
14.2 Integration von Spring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289 14.2.1
Konfiguration von Spring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 290
14.2.2 Benutzen von Spring-Beans in Tapestry-Anwendungen . . . . . . . . . . . . . 292 14.2.3 Benutzung von Tapestry-Diensten in Spring-Beans . . . . . . . . . . . . . . . . 294 14.2.4 Benutzung von Spring-Beans in Seiten und Komponenten . . . . . . . . . . 296
14.3 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 296 15 Testen von Tapestry-Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299 15.1 Unit-Tests für Dienste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299 15.2 Unit-Tests für Seiten und Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 302 15.2.1
Einfacher Unit-Test für eine Seite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 302
15.2.2 Unit-Tests zur Überprüfung von Markup . . . . . . . . . . . . . . . . . . . . . . . . . 304 15.2.3 Testen von Links . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305 15.2.4 Testen von Formularen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 306 15.2.5 Überschreiben von Diensten zur Verbesserung der Testbarkeit . . . . . . 309 15.2.6 Testen von Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311
15.3 Integrationstests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 312 15.3.1
Ein einfacher Integrationstest . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 312
15.3.2 Klicken auf Links in Integrationstests . . . . . . . . . . . . . . . . . . . . . . . . . . . . 313 15.3.3 Testen von Formularen in Integrationstests . . . . . . . . . . . . . . . . . . . . . . . 314 15.3.4 Testen von Ajax-Funktionalität in Integrationstests . . . . . . . . . . . . . . . . 316
15.4 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318 16 Sicherheit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319 16.1 Schützen der Seiten durch HTTPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319 16.1.1
Basis-URLs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 320
16.1.2
HTTPS und Entwicklungsmodus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322
16.2 Authentifizierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322 16.3 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 324 Teil III
Tapestry für Profis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 325
17 Tapestry IoC und Dependency Injection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327 17.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327 17.2 Inversion of Control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327 17.3 Dependency Injection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 328 17.3.1
Setter-Injektion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 328
17.3.2 Konstruktor-Injektion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 329
12
Inhaltsverzeichnis
17.3.3 Feld-Injektion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 329 17.3.4 Singleton-Antipattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
17.4 Warum braucht Tapestry IoC? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330 17.5 Überblick über Tapestry IoC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331 17.5.1
Modul . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332
17.5.2 Bind-Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332 17.5.3 Build-Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 334 17.5.4 Abhängigkeiten von Diensten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 334
17.6 Lebenszyklus eines Dienstes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 336 17.7 Eifriges Laden von Diensten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337 17.8 Scope eines Dienstes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338 17.9 Eindeutigkeit von Diensten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339 17.9.1
Eindeutigkeit durch explizite IDs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
17.9.2 Eindeutigkeit durch Marker-Annotationen . . . . . . . . . . . . . . . . . . . . . . . . 341
17.10 Konfiguration von Diensten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343 17.10.1 Erweiterungspunkte in Tapestry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 344 17.10.2 Ungeordnete Konfigurationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345 17.10.3 Geordnete Konfigurationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347 17.10.4 Schlüssel-Wert-Paare als Konfiguration . . . . . . . . . . . . . . . . . . . . . . . . . . 349
17.11 Konfiguration von Anwendungen mit Symbolen . . . . . . . . . . . . . . . . . . . . . 350 17.11.1 Symbole . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 350 17.11.2 Bereitstellen von Symbolen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
17.12 Überschreiben von Diensten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357 17.13 Diensterzeugung für Fortgeschrittene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357 17.13.1 Implementieren von Chains . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357 17.13.2 Implementieren von Pipelines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 359
17.14
Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
18 AOP mit Tapestry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 365 18.1 Grundbegriffe von AOP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 365 18.2 Tapestry und AOP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 366 18.3 Autorisierung der Methodenaufrufe von Diensten . . . . . . . . . . . . . . . . . . . 368 18.4 Überschreiben von Methodenparametern der Dienste . . . . . . . . . . . . . . . 369 18.5 Überschreiben der Rückgabewerte von Dienstmethoden . . . . . . . . . . . . . 370 18.6 Dekorieren von Komponentenmethoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371 18.7 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372
13
Inhaltsverzeichnis
19 Typumwandlung/Type Coercion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375 19.1 Verstehen der Typumwandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375 19.2 Erweitern des Umwandlungsgraphen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 376 19.3 Durchführen der Typumwandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377 19.4 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379 20 Verarbeitung von Anfragen durch Tapestry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 381 20.1 Die HttpServletRequestHandler-Pipeline . . . . . . . . . . . . . . . . . . . . . . . . . . . . 381 20.1.1
Bereitstellen eines eigenen HttpServletRequestFilters . . . . . . . . . . . . . . 383
20.2 Die RequestHandler-Pipeline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 384 20.2.1 Bereitstellen eines eigenen RequestHandlers . . . . . . . . . . . . . . . . . . . . . 384
20.3 Chain of Command des Dienstes Dispatcher . . . . . . . . . . . . . . . . . . . . . . . . . 385 20.3.1 Behandlung von Anfragen an den Kontextpfad . . . . . . . . . . . . . . . . . . . . 385 20.3.2 Behandlung von Anfragen an Tapestry-Seiten . . . . . . . . . . . . . . . . . . . . . 386 20.3.3 Auslösen von Komponentenereignissen . . . . . . . . . . . . . . . . . . . . . . . . . 386 20.3.4 Behandlung von Anfragen an statische Ressourcen . . . . . . . . . . . . . . . . 387 20.3.5 Bereitstellen eines eigenen Dispatchers . . . . . . . . . . . . . . . . . . . . . . . . . 387
20.4 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 387 21 Bytecodemanipulation mit Tapestry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 389 21.1 Das zu implementierende Szenario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 389 21.2 Bytecodemanipulation von Seitenklassen . . . . . . . . . . . . . . . . . . . . . . . . . . . 390 21.3 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395 A
Anhang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 397 A.1 Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 397 A.1.1
14
ActionLink . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 397
A.1.2
AddRowLink . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 397
A.1.3
AjaxFormLoop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 398
A.1.4
Any . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 399
A.1.5
BeanDisplay . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 399
A.1.6
BeanEditForm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 400
A.1.7
BeanEditor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 401
A.1.8
Checkbox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 402
A.1.9
DateField . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
A.1.10
Delegate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 404
A.1.11
Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 404
A.1.12
EventLink . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 404
Inhaltsverzeichnis
A.1.13
ExceptionDisplay . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 405
A.1.14
Form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 405
A.1.15
FormFragment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406
A.1.16
FormInjector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407
A.1.17
Grid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 408
A.1.18
Hidden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 410
A.1.19
If . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 410
A.1.20 Label . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411 A.1.21
LinkSubmit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411
A.1.22 Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 412 A.1.23 Output . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413 A.1.24 OutputRaw . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413 A.1.25 PageLink . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 414 A.1.26 Palette . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 414 A.1.27 PasswordField . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 415 A.1.28 ProgressiveDisplay . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 416 A.1.29 Radio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 416 A.1.30 RadioGroup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417 A.1.31
RemoveRowLink . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417
A.1.32 RenderObject . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417 A.1.33 Select . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418 A.1.34 Submit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 419 A.1.35 SubmitNotifier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 419 A.1.36 TextArea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 419 A.1.37 TextField . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 420 A.1.38 TextOutput . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 421 A.1.39 Unless . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 421 A.1.40 Upload . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 421 A.1.41
Zone . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 422
A.2 Mixins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 422 A.2.1
Autocomplete . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 422
A.2.2
DiscardBody . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 423
A.2.3
NotEmpty . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 423
A.2.4
RenderDisabled . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 423
A.2.5
RenderInformals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 423
A.2.6
TriggerFragment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 424
15
Inhaltsverzeichnis
A.3 Seiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 424 A.3.1
PropertyDisplayBlocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 424
A.3.2
PropertyEditBlocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 424
A.3.3
ServiceStatus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 424
A.3.4
Statistics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 424
Stichwortverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 425
16
Einleitung Tapestry ist ein Open-Source-Framework der Apache Software Foundation zur Entwicklung von dynamischen und skalierbaren Webanwendungen in Java. Tapestrys Design ermöglicht die Entwicklung von Webanwendungen mit minimalem Aufwand. Der Einstiegsaufwand in Tapestry ist relativ klein, sodass der Leser innerhalb kürzester Zeit zu sichtbaren Ergebnissen kommt. Tapestry nimmt dem Entwickler viel Arbeit ab und reduziert damit die Menge des benötigten Codes. Es ist so konzipiert, dass beinahe jeder Teil des Frameworks durch Anwendungsentwickler überschrieben werden kann. Tapestry ist ein komponentenorientiertes Framework. Im Gegensatz zu anfrageorientierten Frameworks geht es bei der Entwicklung nicht um die Beantwortung von Anfragen, sondern um das Platzieren von Komponenten auf Seiten und das Behandeln von Ereignissen, die von den Komponenten ausgelöst werden. Tapestry bringt eine Reihe von Komponenten mit und ermöglicht es dem Benutzer, leicht eigene Komponenten zu entwickeln. Durch Tapestrys testfreundliches Design und zusätzliche Funktionalität, die speziell auf das Testen von Komponenten, Seiten und ganzen Anwendungen ausgerichtet ist, können Anwendungen besonders leicht getestet werden. Beim Einsatz von Tapestry müssen Sie auch nicht auf Frameworks verzichten, die Sie bereits im Einsatz haben. Tapestry ist so konstruiert, dass die Integration von anderen Frameworks mit wenig Aufwand realisiert werden kann. Beste Beispiele dafür sind die Integrationen von Hibernate und Spring, die in diesem Buch besprochen werden. In den letzten Jahren haben dynamische Sprachen wie Ruby, Groovy usw. viel Aufsehen erregt. Die Webframeworks Ruby on Rails oder Grails scheinen in puncto Produktivität Java-Frameworks einige Schritte voraus zu sein. Der Erfolg dieser Frameworks liegt unter anderem in der Anwendung von Konzepten wie Convention over Configuration, DRY (Don’t Repeat Yourself), RESTful URLs usw. Die Entwickler von Tapestry, darunter auch der Autor dieses Buchs, sehen ihr Framework als Javas Antwort auf Ruby on Rails und Co. Ziele von Tapestry sind unter anderem die Steigerung der Entwicklerproduktivität durch Minimierung des benötigten Codes, aussagekräftige Fehlerberichte und Features wie Live Class Reloading. Getreu dem Prinzip Convention over Configuration verzichtet Tapestry – im Gegensatz zu allen anderen Webframeworks – völlig auf XML-Konfigurationen.
Einleitung
In diesem Buch wird die aktuelle Version 5.1.0.5 von Tapestry behandelt, die am 5. Mai 2009 veröffentlicht wurde. Tapestry wird unter der Lizenz Apache 2.01 veröffentlicht.
An wen richtet sich dieses Buch? Dieses Buch richtet sich an alle Java-Entwickler, sowohl Einsteiger als auch Profis, die Webanwendungen mit minimalem Aufwand und einem hohen Maß an Produktivität entwickeln möchten. Es wird vorausgesetzt, dass der Leser fundierte Kenntnisse in Java und HTML besitzt. Kenntnisse in JavaServer Pages oder Servlet-API sind von Vorteil, jedoch nicht erforderlich.
Über dieses Buch Dieses Buch bietet dem Leser einen schnellen und praxisnahen Einstieg in das Tapestry-Framework. Anhand vieler Beispiele, Kochrezepte und Praxistipps wird Tapestry schrittweise erläutert. Das Buch besteht aus den drei folgenden Teilen: Im ersten Teil dieses Buches werden die grundsätzlichen Konzepte von Tapestry vorgestellt. Es wird empfohlen, die ersten fünf Kapitel in der gegebenen Reihenfolge durchzulesen, da sie die Grundlage für das Verständnis aller folgenden Kapitel darstellen. Alle anderen Kapitel dieses Teils können unabhängig voneinander durchgearbeitet werden. Nach dem Lesen dieses Teils sind Sie in der Lage, einfache Tapestrybasierte Webanwendungen zu entwickeln. Das zweite Teil richtet sich an alle, die sich mit Tapestry bereits auskennen und erste Schritte mit dem Framework gemacht haben. Es werden weiterführende Themen wie die Erstellung von neuen Komponenten, die Integration von Hibernate und Spring sowie das Testen von Tapestry-Anwendungen behandelt. Am Ende dieses Teils sind Sie ein erfahrener Tapestry-Anwender und für das Profi-Wissen bereit. In Teil III des Buches werden Themen behandelt, die für Tapestry-Profis interessant sind. Das Herz von Tapestry, Tapestry IoC, wird in diesem Kapitel behandelt. Aus diesem Grund lohnt es sich, beim Lesen der ersten beiden Teile ab und zu Teil III durchzublättern, um eventuelle Verständnisprobleme zu beseitigen. Der Anhang enthält eine Komponentenreferenz, die zum Nachschlagen von verfügbaren Komponenten und deren Parametern benutzt werden kann.
1
18
http://www.apache.org/licenses/LICENSE-2.0
Einleitung
Tapestry im Web Zu den meisten Kapiteln des Buches ist auf der beiliegenden CD der Quellcode der Beispiele zu finden. Diese Beispiele sind aber auch online unter der URL http:// tapestrybook.googlecode.com zu erreichen. Über die neusten Entwicklungen in Tapestry können Sie sich auf der Homepage des Frameworks unter http://tapestry.apache.org/ informieren. Außerdem erhalten Sie tatkräftige Unterstützung im Tapestry-Mailverteiler. Viele Tapestry-Benutzer mit unterschiedlichem Wissensstand helfen sich gegenseitig aus, indem sie ihre Probleme und deren Lösungen austauschen. Sie können sich beim Verteiler anmelden, indem Sie eine Mail an
[email protected] schreiben. Auch der Mailverteiler des Entwicklungsteams könnte von Interesse sein. Falls Sie sich auf dem letzten Stand halten möchten, sollten Sie eine Registrierungsmail an
[email protected] schicken.
Über den Autor Dipl.-Informatiker Igor Drobiazko ist als Softwareentwickler bei der Internationalen Kapitalanlagegesellschaft mbH (HSBC INKA) im Bereich JEE, OSGi und Web tätig. In den letzten Jahren hat er Erfahrung in sämtlichen Java-Webtechnologien (JSP, Servlets, JSF, Seam und Wicket) gesammelt. Igor Drobiazko hat Tapestry bei seinem ehemaligen Arbeitgeber Nokia im Jahr 2006 kennengelernt und setzt das Framework seitdem zur Entwicklung von Webanwendungen ein. Er ist ein aktives Mitglied der Tapestry-Gemeinschaft und gehört seit Mai 2008 dem Entwicklungsteam von Tapestry an. Er ist Autor mehrerer Artikel über Tapestry und hält Vorträge über das Framework.
Über den Fachlektor Ulrich Stärk, M. Sc., ist wissenschaftlicher Mitarbeiter am Lehrstuhl für Software Engineering am Institut für Informatik der Freien Universität Berlin (www.inf.fuberlin.de). Zuvor arbeitete er am Hasso Plattner Institut in Potsdam im Bereich Internettechnologien und Systeme und für die Siemens AG. Ulrich Stärk benutzt Tapestry seit 2005 in fast allen seiner Webprojekte und schrieb, unter anderem für die Siemens AG, diverse Webanwendungen auf Basis von Tapestry und anderen Java-Technologien der JEE-Plattform. Außerdem besitzt er Erfahrung in der Entwicklung mit Spring, Hibernate, Servlets, JSPs, Struts und vielen anderen Technologien. Ulrich Stärk ist aktives Mitglied der Tapestry-Gemeinschaft, hilft auf der Benutzer-Mailingliste und steuerte schon mehrere Patches für das Framework bei.
19
Vorwort von Howard M. Lewis Ship Innovation creates opportunity. I think that’s a wonderful message for us software developers, because who (besides us) are really encouraged to innovate on a day-in, day-out basis? It’s also been my personal experience over the last seven years, the story of the public history of Tapestry. What began as a kind of mental doodle, a first experiment at writing a big Java project, and a way to keep busy while »on the bench« at my consulting job has taken on a life of its own. Even the oldest versions of Tapestry, stone knives and bear skins by comparison to Tapestry 5, had a certain spark, an insight-made-code, a sense of order and consistency that Java developers have craved for as long as we have been tasked with writing web applications. The opportunities that developing Tapestry have opened to me have been many-fold; I’ve enjoyed the chance to build many sites with this tool of my own devising. I’ve also had the pleasure of teaching Tapestry to developers all over the U. S. and Europe. Even so, I’m surprised and pleased when others in the community step up to the bat, so the speak, and become active committers on the project, as Igor has. But his work on this book represents an even greater effort, and one of greater value to the Tapestry community. I first become aware of Igor after he asked me to review an article he was writing for the online magazine InfoQ. I was impressed with the ambition of the article; in a short space going from Tapestry’s theory, to realistic data-base driven examples, right up to creating an Ajax in-place editor component, all in just a few pages. This book represents an expansion of that concept with more room to comfortably flesh out explanations of how your code and Tapestry work together. Tapestry is a very rich environment combining a state-of-the-art web framework with an equally advanced inversion-of-control container. The two layers working together support customizations and other solutions that are powerful and efficient, yet subtle. That’s a lot of power and it can take time to appreciate all that it is going on. As Evil Spock said in the »Mirror, Mirror« episode of Star Trek: »A man must also have the power.« To be more specific; the power to understand the Tapestry framework, and the power to know how to gracefully extend it to meet your specific needs. This book is the way to obtain that power.
Vorwort von Howard M. Lewis Ship
I hope you’ll enjoy reading this book and that it will open up the possibilities of just what can be accomplished with Tapestry. In the end, you’ll have a chance to innovate on your own projects, and create your own opportunities. Enjoy! Howard M. Lewis Ship Aug 4, 2009 Portland, Oregon
22
Danksagungen Zunächst möchte ich mich herzlich bei Howard M. Lewis Ship für die Erschaffung von Tapestry bedanken. Howard, vielen Dank für deine wertvollen Ratschläge und Tipps, die du mir für dieses Buch gegeben hast. Schon bei meinem ersten Artikel über Tapestry auf InfoQ2, den ich zusammen mit meinem guten Freund Renat Zubairov geschrieben habe, warst du eine große Hilfe. Ein besonders großer Dank gilt meiner Verlobten Katerina Vysotina. Katerina, ich möchte mich für dein Verständnis und deine Unterstützung während der Zeit bedanken, in der ich dieses Buch schrieb. Du hast die Idee von einem Tapestry-Buch von Anfang unterstützt und warst bereit, viele Wochenenden zu opfern. Dieses Buch wäre ohne deine Unterstützung nie erschienen. Weiterhin möchte ich mich beim Fachlektor dieses Buches, Ulrich Stärk, bedanken. Ulrich, deine Ratschläge bzgl. des Buchaufbaus und der Didaktik waren eine große Hilfe. Deine fachlichen Rezensionen haben wesentlich zur Steigerung der Qualität dieses Buches beigetragen. Außerdem möchte ich mich bei Renat Zubairov bedanken, der einige Kapitel dieses Buch durchgelesen und mir wertvolle Tipps gegeben hat. Schließlich möchte ich mich bei Brigitte Bauer-Schiewek vom Addison-Wesley Verlag für ihre Unterstützung und vor allem für ihre Geduld bedanken.
2
http://www.infoq.com/articles/tapestry5-intro
Teil I Tapestry für Einsteiger
1
Von Servlets bis MVC-Frameworks
Falls Sie ein Quereinsteiger ohne Erfahrung in Webentwicklung mit Java sind, benötigen Sie einen kleinen Exkurs über die Werkzeuge, die in den letzten Jahren in der Java-Gemeinde eingesetzt wurden. Obwohl Tapestry keine Kenntnisse über die Servlet-API voraussetzt, ist es sinnvoll, die geschichtliche Entwicklung der Java-Webtechnologien zu kennen. Nach einem kurzen Überblick über die Java-Webtechnologien der letzten Jahre folgt eine Vorstellung des Model View Controller-Patterns (MVC). Das Kapitel wird mit der Beschreibung von Tapestrys Ansatz zur Umsetzung des MVC-Patterns abgeschlossen.
1.1
Wie alles begann
Mit der Veröffentlichung der Version 1.0 der Servlet-Spezifikation durch Sun Microsystems begann im Jahre 1997 die serverseitige Existenz von Java. Ein Servlet ist ein plattformunabhängiges Java-Programm, das serverseitig die Verarbeitung einer HTTP-Anfrage übernimmt. Dieses Programm wird in einem Servlet-Container wie Apache Tomcat ausgeführt. HTTP (Hypertext Transfer Protocol) ist ein zustandsloses Protokoll auf der Anwendungsschicht des OSI-Referenzmodells für Kommunikationsprotokolle. Dieses Protokoll spezifiziert, wie ein Client die Inhalte eines Webservers abfragen kann und wie diese Anfragen von dem Server bearbeitet werden. Für die Anfragen werden in HTTP acht Methoden spezifiziert, die in diesem Buch nicht einzeln besprochen werden. Jede der acht Methoden basiert auf einem Request und der dazugehörigen Response. Die wichtigsten Methoden sind: GET, POST, PUT und DELETE. Jedes Servlet muss die abstrakte Servlet-Superklasse HttpServlet erweitern, die für die Behandlung der Anfragen unterschiedlicher Typen spezielle Methoden implementiert. Typischerweise sollte ein Servlet eine der Methoden doGet(), doPost(), doPut() oder doDelete()dieser Superklasse überschreiben. Im Listing 1.1 ist ein einfaches Servlet zu sehen, das für die Behandlung der Anfragen vom Typ GET die Methode doGet() überschreibt. Für jede ankommende Anfrage gibt das Servlet innerhalb dieser Methode eine einfache HTML-Seite aus. Somit agiert ein Servlet als eine Erweiterung des Webservers, die dynamische Inhalte an die Clients zurückschicken kann.
1 Von Servlets bis MVC-Frameworks
Listing 1.1: Einfaches Servlet public class HelloServlet extends HttpServlet { public void doGet (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ PrintWriter out = response.getWriter(); out.println(""); + out.println("Hello Servlet"); out.println(""Hello, world!"); out.println(""); out.close(); } }
Das Problem der Servlets ist, dass die Response durch Aufrufe der Methode println() der Klasse PrintWriter wie im Listing 1.1 erzeugt werden muss. Dies führte in der Vergangenheit dazu, dass ganze Webseiten innerhalb der Servlets programmiert wurden. Inhalte, die üblicherweise in HTML-Ressourcen abgelegt werden, wurden in Java-Klassen kodiert, was bei steigender Komplexität einer Webanwendung unausweichlich zu einem Chaos führte. Um dieses Problem zu lösen, hat Sun Microsystems die JavaServer Pages eingeführt. Diese Technologie ermöglicht es, statische Elemente von HTML mit dynamisch generierten Inhalten aus Servlets zu mischen. Typischerweise ist eine JSP eine HTMLSeite, in die Java-Quellcode (Scriptlets) eingebettet werden kann. Sobald eine JSP zum ersten Mal aufgerufen wird, wird sie von dem Container zu einem Servlet kompiliert. Der Einsatz von JavaServer Pages wird in vielen Büchern als Model-1-Architektur bezeichnet. In einer Model-1-Architektur (Abbildung 1.1) greift ein Client direkt auf eine JSP zu. Eine JSP ihrerseits kommuniziert direkt mit dem Model der Applikation, das durch JavaBeans repräsentiert wird. Die Entscheidung darüber, welche Folgeseite angezeigt wird, wird alleine von der aktuellen JSP getroffen. Damit ist im Model 1 die Kontrolle des Seitenflusses dezentralisiert. Schnell haben viele Entwickler eingesehen, dass die Entwicklung von Webanwendungen mit JavaServer Pages zwar angenehmer ist als mit Servlets, jedoch eine Menge Selbstdisziplin erfordert. Viele Entwickler haben sich damals das Leben leicht gemacht, indem sie die Geschäftslogik der Anwendungen innerhalb der JSPs implementiert haben. Da Scriptlets schlecht wiederverwendbar sind, wurde oft der Quellcode aus einer JSP in die anderen kopiert, falls eine ähnliche Funktionalität benötigt wurde. Ein weiteres Problem stellte die dezentralisierte Kontrolle des Seitenflusses dar. Viele JSPs enthielten Scriptlets, in denen abhängig von den Anfrageparametern die Folgeseite ausgewählt wurde. Um das Problem der Dezentralisierung zu beseitigen, fingen viele Entwickler an, JavaServer Pages mit Servlets zu kombinieren, was als Model-2-Architektur bezeichnet wurde.
28
1.1 Wie alles begann
Anfrage Browser
JSP Antwort
Datenbank JavaBean
Application Server
Enterprise Server
Abbildung 1.1: Model-1-Architektur In einer Model-2-Architektur (Abbildung 1.2) wird ein Controller-Servlet zwischen einem Client und einer JSP geschaltet. Dieses Servlet stellt eine zentrale Stelle für die Auswahl einer JSP dar, an die eine Benutzeranfrage weitergeleitet wird. Diese Auswahl geschieht üblicherweise abhängig von der angefragten URL, den Anfragenparametern innerhalb dieser URL und dem Applikationszustand.
Anfrage
Controller (Servlet) Wähle View aus
Browser
Antwort
Instanziiere
Model (JavaBean)
Datenbank
View (JSP)
Application Server
Enterprise Server
Abbildung 1.2: Model-2-Architektur Die Model-2-Architektur wird oft als Synonym für das Model View Controller-Pattern (MVC) eingesetzt. Dies stimmt aber nicht ganz. Wie gleich erläutert wird, baut das MVC auf dem Observer-Pattern auf, das im Web nicht zu realisieren ist. Aus diesem Grund wird die Model-2-Architektur oft als MVC-2 oder Web-MVC bezeichnet. Struts war das erste Java-Framework, welches das Web-MVC-Pattern implementiert hat.
29
1 Von Servlets bis MVC-Frameworks
Seit der Einführung von Struts im Jahre 2000 wurde eine Menge von MVC-basierten Frameworks zur Webentwicklung in Java veröffentlicht. Man hatte teilweise den Eindruck, dass alle drei bis sechs Monate ein neues Framework herausgekommen ist. Nur eine kleine Teilmenge dieser Neuerscheinungen konnte sich auf dem Markt behaupten. Als Webentwickler steht man bei der Auswahl eines Frameworks vor einer sehr großen Herausforderung. Lassen Sie uns zunächst das MVC-Pattern anschauen, bevor wir zur Umsetzung des Patterns durch Tapestry übergehen.
1.2
Model View Controller
Model View Controller (MVC) ist ein Architekturmuster, das zum ersten Mal in den 80er-Jahren in der Programmiersprache Smalltalk zur Implementierung der grafischen Benutzerschnittstellen eingesetzt wurde. Beim MVC-Pattern wird jede Benutzerinteraktion innerhalb einer Anwendung in drei Komponenten bzw. Schichten unterteilt, die in der Abbildung 1.3 dargestellt sind: Model: Komponente zum Speichern von Daten, die den Benutzern präsentiert wer-
den. Ein Model stellt den Vertrag zwischen Controller und View dar und wird oft als JavaBeans implementiert. View: Komponente zur Darstellung der Daten aus dem Model, die vom Controller
bereitgestellt werden. Controller: Komponente zum Lesen der Daten aus dem Model und Bereitstellen
dieser Daten zur Darstellung in einer View. Weiterhin ist die Controller-Schicht zur Interpretation der Benutzereingaben zuständig, die zur Ausführung der Geschäftslogik eingesetzt werden. Als Ergebnis einer Ausführung wird das Model aktualisiert. In einer Desktop-Anwendung, deren Benutzerschnittstelle beispielsweise mit Swing oder Eclipse Rich Client Platform realisiert ist, existieren öfter mehrere Views zur alternativen Darstellung der gleichen Daten des Models. Wann immer das Model sich ändert, müssen alle Views aktualisiert werden. Die klassische Variante des MVC-Patterns setzt das Observer-Pattern zur Benachrichtigung der Views über die Veränderungen des Models ein. Bei diesem Pattern registrieren sich die Views als Beobachter des Models und werden über alle Zustandsänderungen benachrichtigt. Wenn ein Client eine Anfrage verschickt, werden dessen Eingaben durch den Controller interpretiert. Es folgt eine Aktualisierung des Models, dessen Änderungen an die View weitergegeben werden. Vorteile von MVC sind: Separation of Concerns (Trennung von Zuständigkeiten) Wiederverwendbarkeit des Codes Zentralisierung der Kontrolle Erweiterbarkeit der Anwendung
30
1.3 Tapestry und das Model View Controller Pattern
Model
Zustandsabfrage
Zustandsänderung
View
Controller Auswahl der View
Abbildung 1.3: Model View Controller Da HTTP ein Pull-Protokoll ist, funktioniert das Observer-Pattern im Webbereich nicht. Wenn ein Server keine Anfrage erhalten hat, wird er von sich aus keine Informationen an einen Client senden (push). Um die Einschränkungen des HTTP-Protokolls aufzuheben, wird im Web eine modifizierte Version des MVC-Patterns eingesetzt, bei der die View die Daten aus dem Model nach dem Pull-Prinzip abholt. Dabei muss der Controller immer noch die Verbindung zwischen der View und dem Model herstellen, indem er die Benutzereingaben interpretiert und die Zustandsänderungen des Models durchführt. Der wesentliche Unterschied ist, dass die View vom Model entkoppelt wird. Die View hat lediglich die Kenntnis, wo die Daten abgeholt werden können und wohin sie gegebenenfalls geschrieben werden. Damit ergibt sich eine 3-SchichtenArchitektur wie in der Abbildung 1.4.
View (Präsentationsschicht)
Controller (Logikschicht)
Model (Datenschicht)
Abbildung 1.4: 3-Schichten-Architektur
1.3
Tapestry und das Model View Controller Pattern
Wie in vielen anderen MVC-Frameworks übernehmen die anwendungsspezifischen Domainobjekte die Rolle des Models in einer Tapestry-Applikation. Sie werden von den Entwicklern der jeweiligen Anwendungen modelliert und implementiert.
31
1 Von Servlets bis MVC-Frameworks
Die Rolle des zentralen Controllers übernimmt in Tapestry ein Filter3, der innerhalb der web.xml konfiguriert ist. Dieser Filter untersucht jede Anfrage an eine TapestryAnwendung und wählt unter anderem einen Controller zur Behandlung dieser Anfrage aus. Die Controllerlogik wird in Tapestry in sogenannten Handler-Methoden der Komponentenklassen umgesetzt, daher können Tapestry-Komponenten als Controller angesehen werden. Eine Handler-Methode dient der Behandlung eines Ereignisses, das beispielsweise durch eine Benutzerinteraktion ausgelöst wird. Wird beispielsweise ein Link angeklickt oder ein Formular verschickt, veröffentlicht Tapestry ein Ereignis. Sie können dieses Ereignis behandeln, indem Sie Ihre Geschäftslogik in einer HandlerMethode einer Komponente ausführen. Tapestry wird diese Handler-Methode anhand einer Namenskonvention (siehe Kapitel 3) finden und aufrufen. Im Gegensatz zu vielen MVC-Frameworks (z. B. Struts) werden Sie von Tapestry nicht gezwungen, URLs mit Controller der Anwendung innerhalb vorgeschriebener Konfigurationsdateien zu verknüpfen. Sie müssen sich niemals über die angefragten URLs kümmern. In Tapestry wird in jeder ankommenden Anfrage der Name der Zielseite, eventuell eine ID einer Komponente innerhalb dieser Seite und der sogenannte Kontext (falls vorhanden) kodiert. Sie müssen diese Informationen aus einer URL nicht extrahieren, dies wird von Tapestry erledigt. Sie implementieren lediglich eine Handler-Methode, die von Tapestry aufgerufen wird. Da eine Seite sich von einer Komponente nur minimal unterscheidet, übernehmen auch Seiten einer Tapestry-Anwendung die Aufgaben des Controllers im MVCPattern. Die Darstellung der Daten, die aus dem Model gewonnen wurden, wird in Tapestry in Templates umgesetzt. Damit repräsentieren die Templates die View des MVC-Patterns. Eine Seite verbindet das Model und die View mithilfe von Expansions (siehe Kapitel 2). Expansions sind mit der EL (Expression Language) in JSPs vergleichbar. Sie erlauben, dass Eigenschaften des Models in die Parameter der Komponenten innerhalb eines Templates gebunden werden können. Über die Punktnotation können Seiten und Komponenten die Eigenschaften des Models navigieren und damit ihre Werte lesen oder auch überschreiben.
1.4
Zusammenfassung
In den letzten Jahren wurde die Java-Gemeinde mit einer großen Menge von MVCFrameworks überflutet. Der Projekt Tapestry wurde im Jahre 2001 in das Leben gerufen. Zwei Jahre später wurde die Version 3 des Frameworks zu einem offiziellen Apache-Jakarta-Projekt. Im Jahre 2006 bekam Tapestry den Status eines Top-LevelProjektes bei Apache.
3
32
http://java.sun.com/products/servlet/Filters.html
1.4 Zusammenfassung
In diesem Kapitel wurde das Model View Controller Entwurfsmuster vorgestellt und Tapestrys Ansatz zur Implementierung von Web-MVC kurz erläutert. Sie haben gelernt, dass in Tapestry die Controllerlogik in sogenannten Handler-Methoden implementiert wird. Diese Handler-Methoden dienen der Behandlung von Ereignissen, die von Tapestry ausgelöst werden. Ereignisse werden in Kapitel 3 im Detail besprochen. Des Weiteren haben Sie gelernt, dass Tapestry im Gegensatz zu vielen anderen MVC-Frameworks nicht von der JSP-Technologie abhängig ist. Stattdessen werden in Tapestry Templates als View einsetzt. Nach einer theoretischen Einführung in diesem Kapitel folgt im nächsten Kapitel ein praktischer Schnelleinstieg in Tapestry, mit dem Sie die Grundlagen des Frameworks lernen werden.
33
2
Tapestry-Schnelleinstieg
Die wohl einfachste Methode zum Einstieg in ein neues Framework ist »Lerning by Doing«. Aus diesem Grund ist dieses Kapitel ein Schnelleinstieg, mit dem Sie Ihre erste Tapestry-Anwendung zum Laufen bringen. Für den Einstieg kümmern wir uns zunächst um die Einrichtung der Entwicklungsumgebung und diskutieren anschließend die Struktur von Tapestry-Anwendungen. Sie werden lernen, dass Tapestry durch das Prinzip Convention over Configuration den Konfigurationsaufwand auf ein Minimum reduziert und einen enormen Beitrag zu Ihrer Produktivität leistet.
2.1
Starten von Tapestry-Anwendungen mit Eclipse Web Tools Platform
Zur Installation Ihrer ersten Tapestry-Anwendung benutzen Sie das Archiv helloworld.war auf der beiliegenden CD. Es handelt sich um eine Hallo-Welt-Anwendung, die Sie in ein IDE (Integrated Development Environment) Ihrer Wahl importieren können. In diesem Buch wird Eclipse 3.5 eingesetzt, doch Sie können selbstverständlich auch NetBeans oder IDEA einsetzen. An dieser Stelle wird angeraten, das Paket Eclipse IDE for Java EE Developers zu installieren. Gehen Sie dazu auf die DownloadSeite von Eclipse (http://www.eclipse.org/downloads/) und wählen Sie eine aktuelle Version aus. Nach einer erfolgreichen Installation von Eclipse führen Sie folgende Schritte durch, um das Webarchiv hello-world.war zu importieren: 1. Starten Sie Eclipse. 2. Wählen Sie die Menüfolge FILE > IMPORT. 3. Im geöffneten Dialog wählen Sie WAR FILE in der Kategorie WEB, und klicken Sie auf NEXT (siehe Abbildung 2.1). 4. Im nächsten Schritt wählen Sie die WAR-Datei von der beiliegenden CD oder Ihrem Dateisystem aus, und vergeben Sie Ihrem Eclipse-Projekt einen Namen (siehe Abbildung 2.2). Gegebenenfalls müssen Sie eine TARGET RUNTIME wie Tomcat 6.0 auswählen. Klicken Sie dazu auf die Schaltfläche NEW… 5. Es erscheint ein Fenster wie in der Abbildung 2.3. Wählen Sie APACHE TOMCAT 6.0 aus, und klicken Sie auf NEXT. Im folgenden Dialog (Abbildung 2.4) geben Sie das Installationsverzeichnis von Tomcat an und bestätigen mit FINISH.
2 Tapestry-Schnelleinstieg
6. Das erzeugte Projekt ist in der Abbildung 2.5 zu sehen. Zum Starten der Applikation klicken Sie auf das Projekt mit der rechten Maustaste, und wählen Sie RUN AS > RUN ON SERVER. 7. Die Applikation wird gestartet. Öffnen Sie einen Browser Ihrer Wahl, und geben Sie die URL http://localhost:8080/hello-world/index ein. Das Ergebnis sollte wie in der Abbildung 2.6 aussehen.
Abbildung 2.1: Import einer WAR-Datei
Abbildung 2.2: Angabe des Pfades zur WAR-Datei der Hello-World-Anwendung
36
2.1 Starten von Tapestry-Anwendungen mit Eclipse Web Tools Platform
Abbildung 2.3: Auswahl einer Server Runtime
Abbildung 2.4: Angabe des Installationsverzeichnisses von Tomcat 6
37
2 Tapestry-Schnelleinstieg
Jetzt ist es an der Zeit, die Struktur des Projektes unter die Lupe zu nehmen. Zunächst betrachten wir die Datei web.xml (Listing 2.1) im Verzeichnis WebContent/WEB-INF. Zwei Aspekte sind nennenswert; zum einen der Name des Tapestry-Filters, zum anderen der Context-Parameter tapestry.app-package. Der Tapestry-Filter ist eine Implementierung des Interface javax.servlet.Filter4, die wie bei vielen Java-Webframeworks die Rolle des zentralen Controllers übernimmt. Die Benennung des Tapestry-Filters ist entscheidend für den Namen des IoC-Moduls der Anwendung, des sogenannten Anwendungsmoduls. Dieses Thema wird in Kapitel 17 erläutert. Zunächst reicht es zu wissen, dass der Name der Klasse AppModule im Paket de.t5book.services sich aus dem Namen des Tapestry-Filters und dem Wort Module zusammensetzt. Dies ist die erste Namenskonvention von Tapestry, die Sie sich merken sollten. Den Grund für diese Konvention erfahren Sie später, wenn Tapestry-IoC im Detail beschrieben wird.
Abbildung 2.5: Struktur des Eclipse-Projektes
4
38
http://java.sun.com/products/servlet/Filters.html
2.1 Starten von Tapestry-Anwendungen mit Eclipse Web Tools Platform
Abbildung 2.6: Ihre erste Tapestry-Seite Im Gegensatz zu fast allen anderen Webframeworks verzichtet Tapestry fast völlig auf XML-Konfiguration. Der Context-Parameter tapestry.app-package ist die einzige Information für Tapestry, die mittels XML konfiguriert wird. Der Wert des Parameters gibt das Wurzelpaket der Anwendung an. Ausgehend von diesem Paket sucht Tapestry nach Klassen der Seiten und Komponenten in vordefinierten Unterpaketen. Nach der Namenskonvention erwartet Tapestry die Klassen der Seiten im Unterpaket pages des Wurzelpakets. In Listing 2.1 ist das Paket de.t5book als Wurzelpaket der Anwendung angegeben. Damit wird nach der Seite Index im Paket de.t5book.pages gesucht, wenn die URL http://localhost:8080/hello-world/index angefragt wird. Tapestry interpretiert den Teil der URL nach dem letzten / als einen Namen der angefragten Seite. Das Paket pages darf auch Unterpakete enthalten, sodass bei einer großen Anzahl der Seiten eine Strukturierung möglich ist. Das jeweilige Unterpaket taucht dann auch in der URL der Seite auf. Die Seite de.t5book.pages.view.Details ist dann dementsprechend über die URL http://localhost:8080/hello-world/view/details zu erreichen. Bei einer Seite mit dem Namen Index handelt es sich um eine Startseite einer TapestryAnwendung. Falls in der angefragten URL kein Seitenname angegeben wurde, wird die Startseite angezeigt. Dieses Verhalten entspricht den Standards der meisten Webserver. Listing 2.1: web.xml (Konfiguration des Tapestry-Filters) Tapestry 5 Buch tapestry.app-package de.t5book app org.apache.tapestry5.TapestryFilter
39
2 Tapestry-Schnelleinstieg
app /*
Als Nächstes betrachten wir den Quellcode der Klasse Index im Unterpaket pages. Wie in Listing 2.2 zu sehen ist, handelt es sich um eine leere, unspektakuläre Java-Klasse. Zunächst geben wir uns damit zufrieden, denn wir haben gerade unsere erste Tapestry-Applikation installiert und erfolgreich zum Laufen gebracht. In den folgenden Kapiteln wird diese Klasse um verschiedene Funktionalitäten erweitert. Die Unterpakete components und services werden ebenso in den späteren Kapiteln besprochen. Listing 2.2: Index.java (Startseite) public class Index { }
2.2
Starten von Tapestry-Anwendungen mit Run-Jetty-Run
Falls Sie Eclipse WTP zusammen mit Tomcat in Ihren vorherigen Projekten bereits eingesetzt haben, dann kennen Sie sicherlich das Feature des automatischen Neuladens der Webmodule. Dieses Feature überwacht die Modifikation Ihrer Klassen und lädt automatisch den Kontext einer Anwendung neu, sobald eine der Klassen geändert und abgespeichert wurde. Das automatische Neuladen einer Applikation wird auch Hot Deployment genannt und ist bei der Entwicklung mit den herkömmlichen Frameworks wie Struts oder JavaServer Faces sehr nützlich. Doch Tapestry ist auf dieses Hot Deployment nicht angewiesen, da das Framework einen eigenen Mechanismus (siehe Kapitel 2.4) zum Laden der modifizierten Ressourcen mitbringt. Leider macht Tomcat den Einsatz des Tapestry-eigenen Mechanismus zum Nachladen der Klassen unmöglich. An dieser Stelle möchte ich Ihnen raten, den Jetty Web Server in einer Kombination mit dem Run-Jetty-Run-Eclipse-Plug-In für Ihre Entwicklung von Tapestry-Anwendungen zu wählen. Mit Jetty können Sie das Feature zum Nachladen der Ressourcen einsetzen, das in Kapitel 2.4 näher beschrieben wird. Zum Starten von Jetty Web Server unter Eclipse müssen Sie Run-Jetty-Run-Plug-In für Eclipse installieren. Dazu führen Sie folgende Schritte durch: 1. Starten Sie Eclipse. 2. Wählen Sie HELP > INSTALL NEW SOFTWARE... 3. Im geöffneten Dialog klicken Sie die Schaltfläche ADD…, geben Sie die UpdateSite des Plug-Ins Run-Jetty-Run5, und klicken Sie auf ADD. 5
40
http://run-jetty-run.googlecode.com/svn/trunk/updatesite/
2.2 Starten von Tapestry-Anwendungen mit Run-Jetty-Run
4. Im nächsten Schritt wählen Sie in der Auswahlliste WORK WITH: die hinzugefügte Update-Site des Plug-Ins aus. Es erscheinen die verfügbaren Features auf der Update-Site. Wählen JETTY INTEGRATION, und klicken Sie auf NEXT. Die Installation beginnt. 5. Starten Sie Eclipse neu.
Abbildung 2.7: Konfigurieren des Plug-Ins Run-Jetty-Run Nach einer erfolgreichen Installation sollten Sie unter RUN > RUN CONFIGURATIONS… den Eintrag JETTY WEBAPP wie in der Abbildung 2.7 sehen. Zum Konfigurieren von Run-Jetty-Run führen Sie folgende Schritte durch: 1. Klicken Sie auf JETTY WEBAPP mit der rechten Maustaste, und wählen Sie im Context-Menü die Option NEW aus. 2. Im geöffneten Dialog vergeben Sie der Konfiguration einen Namen, und wählen Sie Ihr Eclipse-Projekt aus. 3. Im Bereich WEB APPLICATION geben Sie einen Kontextnamen, unter dem Sie Ihre Applikation erreichen möchten, und den relativen Pfad zum Wurzelverzeichnis der Anwendung innerhalb des Projektes an. 4. Klicken Sie auf RUN, und öffnen Sie die URL http://localhost:8080/hello-world/ in Ihrem Browser. Das Ergebnis sollte wie in der Abbildung 2.6 aussehen.
41
2 Tapestry-Schnelleinstieg
2.3
Struktur von Tapestry-Anwendungen
Eine Tapestry-Anwendung besteht aus einer Menge von Seiten, die jeweils durch eine Java-Klasse repräsentiert werden. Jede Seite kann aus mehreren Komponenten bestehen, wobei die Komponenten ihrerseits aus mehreren einfacheren Komponenten zusammengebaut werden können. Dieser hierarchische Aufbau einer TapestryAnwendung ist in der Abbildung 2.8 zu sehen. Applikation
Seite
Komponente
Seite
Komponente
Komponente
Komponente
Komponente
Komponente
Komponente
Abbildung 2.8: Struktur von Tapestry-Anwendungen
2.3.1
Tapestry-Seiten
Eine Tapestry-Seite ist eine Java-Klasse, auf die durch eine URL zugegriffen werden kann. Anders als in beinahe allen Java-Webframeworks sind die Seitenklassen reine POJOs (Plain Old Java Objects). Das Framework zwingt uns nicht, die Klasse der Seiten oder der Komponenten von vorgeschriebenen Superklassen abzuleiten oder bestimmte Interfaces zu implementieren, um an die Framework-Funktionalität zu gelangen. Stattdessen wird die benötigte Funktionalität mittels eingebauter IoC-Container (siehe Kapitel 17) in die Klassen injiziert. Der zweite optionale Bestandteil einer Tapestry-Seite ist das Template. Templates sind wohlgeformte (X)HTML-Dateien und an der Erweiterung *.tml zu erkennen. TML steht für Tapestry Markup Language und repräsentiert das Pendant zu JSPs (JavaServer Pages). Ein Vorteil der Templates gegenüber JSPs besteht darin, dass sie nicht durch einen Servlet-Container zu Servlets übersetzt werden müssen. Außerdem kann in einem Template kein Java-Code mit HTML-Elementen vermischt werden, wie es durch JSP-Scriptlets möglich ist. Wie Sie bald lernen werden, können Templates ohne den Start eines Servers vorangeschaut werden.
42
2.3 Struktur von Tapestry-Anwendungen
Das Matching zwischen einer Klasse und dem zugehörigen Template erfolgt über die Namen der beiden Dateien. Für jede Seitenklasse sucht Tapestry nach einem Template mit dem gleichen Namen im Kontextverzeichnis der Applikation. Trägt beispielsweise eine Seite den Namen Page.java, so ist das zugehörige Template die Datei Page.tml. Man betrachte die Klasse der Startseite der Hello-World-Anwendung in Listing 2.3. Dabei handelt es sich um eine reine POJO-Klasse. Das Template der Seite in Listing 2.4 ist eine (X)HTML-Datei. Der Namensraum im Wurzelelement des Templates ist an dieser Stelle irrelevant. Tapestry-Namensräume in den Templates werden in Abschnitt 2.3.2 behandelt. Der Ausdruck ${hello} wird als Expansion bezeichnet. Expansions sind mit der EL (Expression Language) in JSPs vergleichbar. Sie werden angewendet, um statische Templates mit dynamischen Verhalten zu versehen. Der Ausdruck zwischen den geschweiften Klammern in Listing 2.4 wird benutzt, um auf die Methode getHello() der Klasse Index zuzugreifen. Damit kann man sich Expansions als Platzhalter vorstellen, die zur Laufzeit durch Werte von Variablen der dazugehörigen Seite belegt werden. Weitere Informationen über Expansions sind in Abschnitt 2.3.2 zu finden. Listing 2.3: Index.java public class Index{ public String getHello(){ return "Hello, World!"; } }
Listing 2.4: Index.tml Tapestry 5 Buch ${hello}
Wie schon erwähnt wurde, sind Templates nur optionale Bestandteile einer Seite. Der Grund dafür ist, dass das Markup einer Seite auch programmatisch mittels TapestryAPI erzeugt werden kann. In Kapitel 11 werden Sie lernen, wie mithilfe von RenderPhase-Methoden das Markup von Seiten und Komponenten im Java-Code erzeugt werden kann. An dieser Stelle wird nur eine kleine Vorschau auf dieses Feature gegeben. Die Seite in Listing 2.5 benötigt kein Template. Ihr Markup wird programmatisch erzeugt.
43
2 Tapestry-Schnelleinstieg
Listing 2.5: PageWithoutTemplate.java public class PageWithoutTemplate { void beginRender(MarkupWriter writer) { writer.element("html"); writer.defineNamespace( "http://tapestry.apache.org/schema/tapestry_5_1_0.xsd", "t"); writer.element("body"); writer.writeRaw("Hello, World!"); writer.end(); writer.end(); } }
Jede Seite ist durch eine URL zugreifbar. Ist der Kontextpfad der Applikation app, so ist die Seite aus dem Listing 2.4 unter URL http://localhost:8080/app/index erreichbar. Dabei spielt die Klein- oder Großschreibung keine Rolle. Über die URL http://localhost:8080/app/InDeX wird die gleiche Seite erreicht. Ein weiterer Vorteil der Templates ist, dass sie im Browser vorgeschaut werden können, ohne dass die Anwendung auf einen Server installiert werden muss. Der Browser wird die Tapestry-Elemente ignorieren, sodass Sie die Entwicklung der Templates einem HTML-Designer übergeben können.
2.3.2
Tapestry Markup Language
HTML (HyperText Markup Language) ist die am meisten verbreitete, textbasierte Auszeichnungssprache zur Strukturierung der Inhalte in Webdokumenten, die von einem Webbrowser interpretiert und in Form von Webseiten dargestellt wird. Dabei wird der zugrunde liegende Inhalt mithilfe von Auszeichnungen, auch Tags genannt, strukturiert. Anfang 2000 hat W3C die Spezifikation von XHTML (Extensible HyperText Markup Language) veröffentlicht, in der HTML 4 als XML 1.0 (Extensible Markup Language) formuliert wird. Die Umformung von HTML in XML hat zur Folge, dass XHTML-Dokumente sämtlichen Regeln einer XML-Sprache gerecht werden müssen. Beispielsweise muss die Wohlgeformtheit eines XHTML-Dokumentes sichergestellt werden. Dies impliziert, dass ein XHTML-Dokument genau ein Wurzelelement besitzen darf, das die Angabe eines Namensraums enthalten muss. Des Weiteren muss jedes Element ohne Inhalt entweder in sich geschlossen sein oder sowohl einen öffnenden als auch einen schließenden Tag besitzen. Beispielsweise ist die Variante des Break-Tags verboten. Stattdessen sollte entweder oder verwendet werden. Jedes Attribut eines Elements darf nur ein einziges Mal innerhalb dieses Elements vorkommen und muss in Anführungszeichen angegeben werden. TML (Tapestry Markup Language) kann als eine Erweiterung von XHTML verstanden werden. Das Wurzelelement eines Tapestry Templates muss den TapestryNamensraum definieren, der zur Identifikation der Tapestry-Elemente innerhalb des
44
2.3 Struktur von Tapestry-Anwendungen
Templates benutzt wird. Typischerweise wird für den Namensraum das Präfix t: gewählt. In Listing 2.4 ist das -Tag die Wurzel des Templates und definiert den Namensraum. Elemente des Templates mit dem Präfix t: werden vom Framework interpretiert, von einem Browser dagegen ignoriert. Dies ermöglicht, dass TMLDokumente ohne einen Start des Webservers im Browser vorangeschaut werden können. Falls Sie beispielsweise an dem Design Ihrer Tapestry-Anwendung arbeiten, können Sie Ihre Templates in einem HTML-Editor wie Dreamweaver bearbeiten und sofort durch einen Browser Ihrer Wahl darstellen lassen. Dieser Vorteil gegenüber JSP oder JSF wird zur Steigerung Ihrer Produktivität beitragen
Benutzen von Expansions Bei Expansions handelt es sich um eine Tapestry-spezifische Erweiterung von XHTML. Expansions sind Instruktionen an Tapestry, bestimmte Platzhalter innerhalb eines TML-Dokuments zur Laufzeit mit Werten von Variablen zu belegen. Das Format einer Expansion sieht wie folgt aus: ${Ausdruck}
Eine Expansion beginnt mit ${ und wird mit } beendet. Der Inhalt zwischen den geschweiften Klammern wird als ein Ausdruck interpretiert und zur Laufzeit von Tapestry ausgewertet. In Listing 2.4 wurde eine Expansion benutzt, um das Template mit der Eigenschaft hello der Seitenklasse (Listing 2.3) zu verbinden. Falls die jeweilige Eigenschaft eine JavaBean ist, kann mittels Punktnotation auf die Eigenschaften dieser JavaBean zugegriffen werden. Dabei wird angenommen, dass die JavaBean-Konventionen von SUN6 gelten. In Listing 2.6 ist eine JavaBean User zu sehen. Die Seite Index (Listing 2.7) erzeugt eine Instanz von dieser Klasse innerhalb der Methode getUser(). Mittels des Ausdrucks ${user.userName} wird aus dem Template (Listing 2.8) auf die Eigenschaft userName dieser Instanz zugegriffen. Listing 2.6: JavaBean User.java public class User{ private String userName; public String getUserName(){ return this.userName; } public void setUserName(String name){ this.userName = name; } }
6
http://java.sun.com/docs/books/tutorial/javabeans/
45
2 Tapestry-Schnelleinstieg
Listing 2.7: Index.java public class Index{ ... public User getUser(){ User user = new User(); user.setUserName("Tapestry User"); return user; } }
Listing 2.8: Index.tml mit einer Expansion Tapestry 5 Buch Hallo, ${user.userName}!
Falls die Methode getUser() einen null-Wert zurückgibt, führt die Auswertung der Expansion aus dem Listing 2.8 zu einer NullPointerException. Um diese Exception zu vermeiden, können Sie den Operator ? einsetzen. In Listing 2.9 wird Tapestry während der Auswertung des Ausdrucks überprüfen, ob ein User vorhanden ist. Falls dem so ist, wird der Wert seiner Eigenschaft userName ausgegeben. Ansonsten wird nichts dargestellt. Listing 2.9: Überprüfung auf Null-Wert innerhalb einer Expansion ${user?.userName}
Expansions werden nicht nur für den Zugriff auf die Eigenschaften der Seiten oder Komponenten verwendet. Mithilfe eines Präfixes können wir Tapestry mitteilen, wie eine Expansion interpretiert werden soll. Ist kein Präfix angegeben, wird das DefaultPräfix prop eingesetzt. Damit sind die beiden Ausdrücke ${prop:user} und ${user} äquivalent: Sie ermöglichen den Zugriff auf die Eigenschaft user einer Seite oder einer Komponente. In der Tabelle 2.1 sind alle möglichen Präfixe der Expansions zusammengefasst. In diesem Kapitel werden zunächst nur die wichtigsten besprochen, da eine komplette Erläuterung aller Präfixe mehr Tapestry-Wissen voraussetzt, als Sie möglicherweise zu diesem Zeitpunkt besitzen.
46
2.3 Struktur von Tapestry-Anwendungen
Präfix
Beschreibung
asset
Zugriff auf eine Eigenschaft vom Typ Asset.
block
Zugriff auf eine Komponente vom Typ Block.
component
Zugriff auf eine Komponente.
literal
Der Ausdruck wird als ein String interpretiert.
nullfieldstrategy
Zugriff auf eine Strategie zur Behandlung der Null-Werte der Formularfelder.
message
Der Ausdruck hinter dem Präfix wird als ein Schlüssel innerhalb eines java.util.ResourceBundle nachgeschlagen. Der Wert des Eintrages wird
ausgegeben. prop
Zugriff auf eine Eigenschaft der Seite oder der Komponente.
translate
Identifiziert den Translator, der zur Transformation des Wertes eines Formularfeldes benutzt werden soll.
validate
Identifiziert den Validator, der zur Validierung der Eingabe in einem Formularfeld benutzt werden soll.
var
Zugriff auf eine sogenannte Render-Variable, die nur während des Renderings verfügbar sind. Sobald das Rendering abgeschlossen ist, werden ihre Werte verworfen. Render-Variablen werden zum Zwischenspeichern von einfachen Werten verwendet und benötigen keine Eigenschaften in Java-Code.
Tabelle 2.1: Präfixe der Expansions Falls Sie einen konstanten String mittels einer Expansion ausgeben möchten, können Sie das Präfix literal anwenden. Im Listing 2.10 ist zu erkennen, dass die Auswertung einer Expansion mit dem Präfix literal das gleiche Ergebnis liefert wie, als wenn man den String ohne eine Expansion schreiben würde. Der Inhalt des -Tags ist in diesem Beispiel Hello World!. Dies mag den Eindruck erwecken, dass das Präfix literal redundant ist, da ein String auch ohne eine Expansion angegeben werden kann. In Kapitel 11 werden Sie sehen, dass bei der Spezifikation der Parameter von Komponenten auch dieses Präfix von Bedeutung ist. Listing 2.10: Literal-Präfix in Expansions Tapestry 5 Buch ${literal:Hello} World!
47
2 Tapestry-Schnelleinstieg
In Listing 2.11 wird der Ausdruck einer Expansion benutzt, um ein Schlüssel-WertPaar innerhalb eines java.util.ResourceBundle zu finden. Jede Seite und jede Komponente in Tapestry kann einen Nachrichtenkatalog besitzen, der zur Internationalisierung der jeweiligen Klasse eingesetzt wird. Nachrichtenkataloge werden in Kapitel 6 ausführlich behandelt. Hier reicht es zu wissen, dass Tapestry die Übersetzungen der Seiten in den Dateien mit der Erweiterung *.properties sucht, die im gleichen Paket wie die Seitenklasse anzulegen sind. Im Falle der Seite Index (Listing 2.11) werden also die Dateien Index.properties, Index_de.properties usw. nach dem Schlüssel some-key-inresource-bundle durchsucht. Der Wert dieses Schlüssels wird anstelle der Expansions dargestellt. Listing 2.11: Index.tml (Message-Präfix in Expansions) Tapestry 5 Buch ${message:some-key-in-resource-bundle}
Das Präfix var hat eine ähnliche Funktion wie das Präfix prop: Es wird für den Zugriff auf Variablen der jeweiligen Klasse eingesetzt. Der kleine Unterschied zwischen den beiden Präfixen ist, dass mittels var ein Zugriff auf sogenannten Render-Variablen stattfindet. Eine Render-Variable ist eine künstliche Eigenschaft einer Seiten- oder Komponentenklasse, die nicht in der jeweiligen Klasse definiert werden muss. Beim ersten Zugriff auf eine Render-Variable wird diese von Tapestry erzeugt und in einer internen Map verwaltet. Nach Abschluss des Renderns wird diese Variable entfernt. RenderVariablen werden zur Zwischenspeicherung von temporären Werten eingesetzt. In Listing 2.12 wird eine Render-Variable mit dem Namen myVar erzeugt, in die der aktuelle Wert der Iteration durch die Komponente geschrieben wird. Diese Komponente iteriert über alle Zahlen des Arrays, das in der Methode getNumbers() der Seitenklasse (Listing 2.13) erzeugt wird. Beachten Sie, dass die dazugehörige Seitenklasse keine Eigenschaft myVar besitzt. Listing 2.12: Einsatz von Render-Variablen Index
- ${var:myVar}
48
2.3 Struktur von Tapestry-Anwendungen
Listing 2.13: Index.java public class Index{ public int[] getNumbers(){ return new int[]{1,2,3,4,5}; } }
Übrigens, die Angabe der Eigenschaft numbers in Listing 2.12 kann durch einen ListenAusdruck ersetzt werden. Die Komponente Loop in Listing 2.14 erzeugt die gleiche Ausgabe wie die in Listing 2.12. Die Methode getNumbers() kann aus der Klasse Index in Listing 2.13 entfernt werden. Listing 2.14: Angabe der Listen-Ausdrücke ...
Alternativ kann auch ein Intervall-Ausdruck wie in Listing 2.15 angegeben werden. Listing 2.15: Angabe der Intervall-Ausdrücke ...
Unsichtbare Instrumentierung der Template-Elemente Um die Voranschaulichkeit der Templates zu verbessern, wurde in Tapestry die sogenannte unsichtbare Instrumentierung der Elemente eingeführt. Dabei handelt es sich um eine alternative Syntax für die Benutzung der Komponenten innerhalb eines TML-Dokumentes. In Listing 2.16 werden zwei Links zu der Seite Index erzeugt. Beim ersten der beiden Links signalisiert das Präfix t:, dass es sich um eine Tapestry-Komponente handelt. Da kein gewöhnliches HTML-Element ist, wird Ihr Browser den Link nicht korrekt darstellen, wenn Sie das Template ohne einen Start des Servers voranschauen möchten. Wie in der Abbildung 2.9 zu sehen ist, wird nur der Text »Index« angezeigt. Um diese Problematik umzugehen, wurde in Tapestry eine alternative Syntax für die Definition der Elemente eingeführt. Dabei wird der Typ einer Komponente hinter einem HTML-Element versteckt, das von einem Browser problemlos interpretiert werden kann. Der zweite PageLink ist das gewöhnliche
49
2 Tapestry-Schnelleinstieg
-Tag, das mit dem Attribute t:type aus dem Tapestry-Namensraum versehen wurde. Ein Browser wird das -Element erkennen und als einen Link darstellen. Auf diese Weise unterstützt Tapestry das WYSIWG-(What You See Is What You Get-)Prinzip für die Entwicklung der Templates.
Listing 2.16: Unsichtbare Instrumentierung Tapestry 5 Buch Index Index
Abbildung 2.9: Vorschau eines Templates ohne einen Start des Servers
2.3.3
Tapestry-Komponenten
Komponenten sind wiederverwendbare Bausteine einer Tapestry-Anwendung, die gemeinsame Funktionalität mehrerer Seiten kapseln. Ähnlich wie Seiten können Komponenten HTML-Ausgaben erzeugen und sogar ihre eigenen Templates besitzen. Grundsätzlich ist der Unterschied zwischen einer Seite und einer Komponente minimal. Der einzig wahre Unterschied ist das Unterpaket, in dem die jeweilige Klasse platziert ist. Zu Erinnerung: Seiten werden im Unterpaket pages abgelegt. Komponenten werden dagegen im Unterpaket components erwartet, wobei die Templates zusammen mit den Klassen der Komponenten im gleichen Paket liegen müssen. Das Paket components darf Unterpakete zur Gruppierung der Komponenten enthalten. Wie minimal der Unterschied zwischen Seiten und Komponenten ist, können Sie nachvollziehen, indem Sie aus der Seite Index eine Komponente erzeugen. Kopieren Sie die Klasse Index aus dem Paket de.t5book.pages, fügen Sie diese in das Paket de.t5book.components ein, und nennen Sie sie anschließend in HelloWorldComponent (Listing 2.17) um.
50
2.3 Struktur von Tapestry-Anwendungen
Listing 2.17: HelloWorldComponent.java public class HelloWorldComponent{ public String getHello(){ return "Hello World!"; } }
Weiterhin muss im Unterpaket components das Template HelloWorldComponent.tml angelegt werden, indem mittels Expansion auf die Methode getHello() der Komponentenklasse zugegriffen wird. Beachten Sie, dass dieses Template ebenfalls ein Wurzelelement erfordert. In Listing 2.18 wird als Wurzelelement eingesetzt, damit nach unserem kleineren Refactoring das Ergebnis wie in der Abbildung 2.6 aussieht. Falls Sie innerhalb dieses Templates weitere Tapestry-Komponenten benutzen möchten, müssen Sie in dem Wurzelelement den Tapestry-Namensraum wie in Listing 2.4 angeben. Listing 2.18: HelloWorldComponent.tml ${hello}
Der Inhalt der Klasse Index kann jetzt entfernt werden, sodass die Seite wieder ziemlich einfach wird. Listing 2.19: Index.java public class Index{ }
Im Template der Indexseite (Listing 2.20) wird die Expansion ${hello} durch ersetzt. Anhand des Namensraumes t erkennt Tapestry, dass in der Seite die Komponente HelloWorldComponent benutzt wird. Anhand des Namens der Komponente wird Tapestry die entsprechende Klasse und gegebenenfalls ein Template zum Erzeugen der Ausgabe einsetzen. Wie fast überall in Tapestry, spielt Großund Kleinschreibung keine Rolle. Sie können im Template schreiben oder auch die Komponentenklasse in HELLOWORLDCOMPONENT.java umbenennen. Tapestry wird immer noch die gleiche Komponente identifizieren. Weiterhin soll erwähnt sein, dass im Gegensatz zu JavaServer Faces keine Beschreibungsdateien für Komponenten erzeugt werden müssen. Sie erzeugen eine Komponentenklasse, gegebenenfalls ein Template und können die Komponente sofort einsetzen. Anhand der Namenskonvention zum Anlegen der Komponenten weiß Tapestry, wo die Komponenten zu suchen sind, und muss nicht darüber innerhalb einer XML-Datei informiert werden. Nun starten Sie Ihre Applikation, und Sie werden sehen, dass das Ergebnis wie in der Abbildung 2.6 aussieht.
51
2 Tapestry-Schnelleinstieg
Listing 2.20: Index.tml Tapestry 5 Buch
An diesem einfachen Beispiel haben Sie gelernt, wie einfach Komponenten in Tapestry erzeugt werden können. Mehr zur Erzeugung der Komponenten erfahren Sie in Kapitel 11.
Parameter von Komponenten Eine Komponente kann eine Menge von Parametern enthalten, die der Konfiguration der Komponente dienen. Anhängig vom Default-Präfix eines Parameters werden die angegebenen Werte unterschiedlich interpretiert. So wird in Listing 2.21 der Wert des Parameters source als eine Eigenschaft der enthaltenen Klasse interpretiert, da prop das Default-Präfix des Parameters ist. Folglich wird die Methode getNumbers() der Seitenklasse aufgerufen, um den Wert des Parameters zu bestimmen. Der Wert des Parameters element besitzt das Default-Präfix literal, sodass der Wert li als ein String interpretiert wird. Die Komponente Loop setzt diesen Wert ein, um das HTML-Element
um den Inhalt zwischen und zu schachteln. Listing 2.21: Parameter der Komponenten
Geschachtelte Komponenten Wie schon erwähnt wurde, können Komponenten aus weiteren Komponenten bestehen. Dabei wird die schachtelnde Komponente als Container für die geschachtelten Komponenten bezeichnet. Auch eine Seite, die Komponenten enthält, wird als Container bezeichnet. In Listing 2.22 wird innerhalb der Komponente HelloWorldComponent die Komponente Loop eingesetzt, um den Begrüßungstext fünf Mal auszugeben. Der aktuelle Wert der Iteration wird in der Eigenschaft currentValue der Seite gespeichert und mittels der Expansion ${currentValue} ausgegeben. Listing 2.22: Geschachtelte Komponente ${currentValue}: ${hello}
52
2.3 Struktur von Tapestry-Anwendungen
Die Annotation @Property der Eigenschaft currentValue stellt den Zugriff auf diese Eigenschaft bereit (Listing 2.23). Sie erspart Ihnen das Schreiben der Getter- und Setter-Methoden, die zum Setzen und Lesen des aktuellen Wertes der Iteration benötigt werden. Falls Sie selbst geschriebene Zugriffsmethoden bevorzugen, müssen Sie diese Annotation entfernen. Listing 2.23: HelloWorldComponent.java public class HelloWorldComponent { @Property private int currentValue; public String getHello() { return "Hello World!"; } }
Die innerhalb eines Templates definierte Komponente kann in ihren Container mittels der Annotation @InjectComponent injiziert werden. Dies ist vor allem dann nützlich, wenn Sie innerhalb des Containers eine der Methoden der geschachtelten Komponente aufrufen möchten. In Listing 2.24 wird die Komponente Loop aus dem Template in Listing 2.22 in die Eigenschaft helloLoop der Komponente HelloWorldComponent injiziert. Wie erkennt aber Tapestry, welche Komponente aus dem Template injiziert werden soll? Wenn eine Eigenschaft einer Seiten- oder Komponentenklasse mit der Annotation @InjectComponent markiert ist, sucht Tapestry in dem Template der Seite oder der Komponente nach einer geschachtelten Komponente mit der ID, die dem Namen dieser Eigenschaft entspricht. Wird eine Komponentendefinition gefunden und stimmt der Typ der Eigenschaft und der gefundenen Komponente überein, wird diese injiziert. In Listing 2.24 ist die Eigenschaft helloLoop mit @InjectComponent annotiert. Also sucht Tapestry im Template nach der geschachtelten Komponente mit der ID helloLoop (siehe Listing 2.22). Listing 2.24: Injizieren einer geschachtelten Komponente public class HelloWorldComponent { @Property private int currentValue; @InjectComponent private Loop helloLoop; public String getHello() { return "Hello World!"; } }
53
2 Tapestry-Schnelleinstieg
t:id vs. id Bitte beachten Sie, dass es einen großen Unterschied ausmacht, ob Sie in einem Template id oder t:id verwenden. Bei id handelt es sich um ein Attribut aus dem XHTML-Schema, das zur Zuweisung einer clientseitigen ID zu einem Tag verwendet wird. Eine Client-ID wird für die clientseitige Logik benötigt. Beispielsweise, um ein HTMLElement aus einer JavaScript-Funktion anzusprechen. Tapestry-Komponenten besitzen ebenfalls eindeutige IDs, die mit t:id angegeben werden. Eine Client-ID kann sich von der Kompo-
nenten-ID unterscheiden. Zur Injektion einer Komponente mittels @InjectComponent sollte immer t:id (also Tapestry-ID) eingesetzt wer-
den. Alternativ können Sie die ID der zu injizierenden Komponente in der Annotation @InjectComponent angeben. In Listing 2.25 wird die Komponente mit der ID helloLoop in die Eigenschaft myLoop injiziert. Listing 2.25: Injizieren einer geschachtelten Komponenten public class HelloWorldComponent { ... @InjectComponent("helloLoop") private Loop myLoop; ... }
Weiterhin ist es möglich, geschachtelte Komponenten innerhalb des Java-Codes zu definieren. So wird in Listing 2.26 die Komponente Loop definiert, indem eine Eigenschaft vom Typ Loop mit der Annotation @Component annotiert wird. Diese Definition entspricht der aus dem Listing 2.22, wobei die Parameter der geschachtelten Komponente in der Annotation @Component angegeben werden. Listing 2.26: Definieren von Komponenten innerhalb des Java-Codes public class HelloWorldComponent{ ... @Component(id="helloLoop", parameters={"source=1..5","value=currentValue"}) private Loop myLoop; ... }
54
2.3 Struktur von Tapestry-Anwendungen
Um diese geschachtelte Komponente innerhalb des Templates des Containers einzusetzen, muss lediglich ein Element definiert werden, dessen t:id dem Wert helloLoop entspricht. Das folgende Listing stellt eine Alternative für das Template aus dem Listing 2.22 dar. Listing 2.27: Einsetzen der außerhalb des Templates definierten Komponenten ${currentValue}: ${hello}
Erzeugen einer Layout-Komponente Viele Java-Webframeworks setzen SiteMesh oder Tiles für die Dekorierung der Seiten einer Anwendung ein. In Tapestry kann dies mithilfe einer einfachen Komponente realisiert werden. Wir erzeugen eine Komponente Layout, die den gemeinsamen Inhalt aller Seiten (Titel, Header, Footer, Navigation usw.) um den spezifischen Inhalt der einzelnen Seiten erzeugt. Dazu legen wir im Paket de.t5book.components die Klasse Layout an. Es handelt sich um eine leere Klasse, die nur die Annotation @IncludeStyleSheet besitzt. Diese Annotation bindet in jede Seite, die von Layout dekoriert wird, die Cascading Style Sheets zur Formatierung der Inhalte ein. Die einzubindende Datei ist style.css. Das Präfix context: informiert Tapestry, dass die Datei in dem Kontextverzeichnis der Webapplikation zu suchen ist. Ein weiteres mögliches Präfix ist classpath:. Dieses wird für den Zugriff auf Ressourcen aus dem Klassenpfad verwendet. Listing 2.28: Layout.java @IncludeStylesheet("context:style.css") public class Layout { }
Im gleichen Paket wird das Template der Komponente (Listing 2.29) angelegt, in das der Inhalt der Startseite kopiert wird. Der spezifische Inhalt der Startseite wird durch ersetzt. Dieses Element stellt den Platzhalter für den Inhalt der dekorierten Seiten dar. Wenn die Ausgabe einer Seite erzeugt wird, wird durch den spezifischen Inhalt der jeweiligen Seite ersetzt. Listing 2.29: Layout.tml Tapestry 5 Buch
55
2 Tapestry-Schnelleinstieg
Nun kann der Code für die Erzeugung des HTML-Gerüstes aus dem Template der Startseite entfernt werden. Der gemeinsame HTML-Code wird durch die Komponente Layout erzeugt. Das dekorierte Template der Startseite ist in Listing 2.30 zu sehen. Listing 2.30: Index.tml (Dekoriert mit Komponente Layout) ${hello}
2.3.4
Tapestry IoC
Wie schon erwähnt wurde, sind die Seiten- und Komponentenklassen in Tapestry reine POJOs (Plain Old Java Objects). Im Gegensatz zu beinahe allen Java-Webframeworks werden Sie von Tapestry nicht gezwungen, die Klassen der Seiten oder der Komponenten von vorgeschriebenen Superklassen abzuleiten oder bestimmte Interfaces zu implementieren, um an die Framework-Funktionalität zu gelangen. Tapestry wählt einen anderen Ansatz, bei dem unterschiedliche Bausteine des Frameworks in die Seiten oder Komponenten injiziert werden können. Falls Sie mit dem Begriff Inversion of Control nichts anfangen können, machen Sie sich keine Sorgen. In Kapitel 17 folgt eine ausführlich Beschreibung mit einer Menge von Beispielen. Zunächst reicht es, Tapestry IoC als einen Teil von Tapestry vorzustellen, der auf eine magische Weise Dienste, Seiten oder Komponenten in Ihre Klassen injiziert. Diese Injektion erfolgt mithilfe von Annotationen, wie beispielsweise in Listing 2.31 zu sehen ist. Die Annotation @InjectPage wird eingesetzt, um eine Seite in eine andere zu injizieren. @InjectComponent injiziert dagegen eine Komponente in eine Seite. Mithilfe der Annotation @Inject werden Dienste injiziert. In diesem Beispiel wird eine Instanz von HttpServletRequest in eine Seite injiziert. Beachten Sie die Eleganz dieser Lösung: Bei allen anderen Frameworks müssten Sie für den Zugriff auf eine Komponente oder die Bestandteile der Servlet-API Ihre Seitenklasse von bestimmten Superklassen ableiten und vordefinierte Methoden überschreiben. Listing 2.31: Injektion public class InjectionExample{ ... @InjectPage private MyPage page; @InjectComponent private MyComponent component; @Inject private HttpServletRequest request; ... }
56
2.4 Nonstop-Entwicklung mit Tapestry
Es wurde bereits erwähnt, dass die Klasse AppModule eine besondere Rolle in einer Tapestry-Anwendung spielt. Sie repräsentiert ein Modul des IoC-Containers, der mit Tapestry mitgeliefert wird. In vielen Beispielen in den folgenden Kapiteln wird diese Klasse eingesetzt, um Tapestry durch neue Funktionalität zu erweitern oder zu konfigurieren. Möglicherweise werden Sie diese Beispiele nur bedingt verstehen, da Ihnen das Verständnis von Tapestry IoC fehlt. An dieser Stelle möchte ich Sie bitten sich zu gedulden. Nachdem Sie das Kapitel 17 durchgelesen haben, blättern Sie einfach zu den Beispielen zurück, um eventuelle Verständnisprobleme zu beseitigen.
2.4
Nonstop-Entwicklung mit Tapestry
Eines der wichtigsten Unterscheidungsmerkmale von Tapestry ist die Steigerung der Produktivität der Entwickler. Das neue Live Reloading-Feature von Tapestry leistet einen großen Beitrag, um die Entwicklungszyklen zu verkürzen. In den herkömmlichen Java-Webtechnologien (aber auch in den älteren Versionen von Tapestry) sind die Entwickler gezwungen, ihre Applikation während der Entwicklung erneut zu deployen, wenn die Modifikationen der Klassen sichtbar werden sollen. Sie erinnern sich sicherlich, wie oft Sie pro Stunde Ihren Webserver erneut starten mussten, nur um kleinste Änderungen Ihres Codes zu sehen. Diese unnötige Wartezeit, summiert über mehrere Monate und mehrere Entwickler, macht die Entwicklung Ihrer Webapplikationen viel teuerer als nötig. Die Entwickler, die ihre Webanwendungen mit dynamischen Sprachen wie PHP oder Ruby implementieren, sind in diesem Sinne gegenüber ihren Java-Kollegen im Vorteil, da sie nicht den Webserver neu starten müssen. Sie ändern ihren Code, kehren zum Browser zurück, laden die Seite erneut und sehen die Folgen der Codeänderungen sofort. Es kann also von einer Nonstop-Entwicklung gesprochen werden. Tapestry bringt die Nonstop-Entwicklung in die Java-Welt. Mit Tapestry werden die Klassen der Seiten und Komponenten sofort nachgeladen, sobald eine Modifikation dieser Klassen festgestellt wurde. Folgende Unterpakete des Wurzelpakets einer Anwendung werden nach Modifikationen der Klassen überwacht: base components mixins pages
Neben den Klassen werden auch Templates und alle Ressourcen überwacht, sodass deren Modifikationen an ihnen sofort sichtbar werden. Um das Live Reloading auszuprobieren, ändern Sie eine Ihrer Seitenklassen, und laden Sie diese Seite im Browser erneut. Sie werden sehen, dass Sie eine ziemlich lange Zeit ohne einen Neustart des Webservers arbeiten können, was Ihre Produktivität enorm steigern wird.
57
2 Tapestry-Schnelleinstieg
Wie in Kapitel 2.2 bereits beschrieben wurde, funktioniert das Live Reloading-Feature nicht unter Tomcat. Aus diesem Grund wird angeraten, Jetty für die Entwicklung von Tapestry-Anwendungen einzusetzen.
2.5
Fehlerberichte
Ein weiteres Feature zur Steigerung der Produktivität der Entwickler stellen die Fehlerberichte dar, die vom Framework im Falle einer Exception generiert werden. Mithilfe dieser Berichte versucht Tapestry, die Entwickler bei der Fehlersuche so weit wie möglich zu unterstützen. In der Abbildung 2.10 ist ein Fehlerbericht zu sehen, der durch einen Tippfehler im Template verursacht wurde. Wie dem Bericht zu entnehmen ist, wird nicht nur die Fehlernachricht dargestellt, sondern auch Hinweise zur Beseitigung des Problems. Die Nachricht besagt, dass im Template eine Expansion ${hello123} benutzt wurde, für die keine Eigenschaft der Seite gefunden werden konnte. Stattdessen wurden die Eigenschaften class, componentResources und hello gefunden. Damit ein Entwickler nicht zwischen dem Browser und seiner IDE umschalten muss, wird das komplette Template der Seite angezeigt. Die Zeile des Templates, die den Fehler verursacht hat, ist farblich hervorgehoben, sodass die Suche nach dem Fehler auf ein Minimum reduziert wird. Es ist offensichtlich, dass der Ausdruck der Expansion von ${hello123} in ${hello} geändert werden muss.
Abbildung 2.10: Fehlerbericht
58
2.6 Zusammenfassung
Alternativ können Sie auch die Eigenschaft der Seite von hello in hello123 ändern. Tapestry würde diese Modifikation genau so schnell erkennen, ohne dass die Anwendung erneut deployt werden muss. Die Darstellung eines Fehlerberichts ist nur während der Entwicklungszeit sinnvoll. Deshalb kann dieses Feature ausgeschaltet werden, wenn Ihre Anwendung auf dem Produktivsystem läuft. In diesem Falle bekommen die Anwender eine Fehlermeldung, die keine internen Details Ihrer Applikation preisgibt. Wie der Produktivmodus eingeschaltet wird, erfahren Sie, wenn Tapestry IoC in Kapitel 17 besprochen wird.
2.6
Zusammenfassung
Eine Tapestry-Anwendung besteht aus einer Menge von Seiten, die aus mehreren Komponenten zusammengebaut werden können. Eine Seite besteht aus zwei Artefakten: einer erforderlichen Java-Klasse und einem optionalen Template. Komponenten bestehen ebenfalls aus einer Java-Klasse und einer TML-Datei und können mehrere geschachtelte Komponenten enthalten Getreu dem Prinzip Convention over Configuration verzichtet Tapestry völlig auf XML-Konfiguration. Ausgehend von dem Wurzelpaket einer Tapestry-Anwendung sucht das Framework nach den Klassen der Seiten, Komponenten und weiteren Bestandteilen einer Tapestry-Anwendung in folgenden Unterpaketen: base: Unterpaket für Basisfunktionalität. Hier können Superklassen von Seiten
und Komponenten angelegt werden. components: Unterpaket für Komponenten. mixins: Unterpaket für Mixins (siehe Kapitel 12). pages: Unterpaket für Seiten. services: Unterpaket für Dienste. Auch das Applikationsmodul AppModule wird per
Namenskonvention in diesem Paket erwartet. Tapestry IoC ermöglicht, dass Seiten und Komponenten reine POJOs sind, und wird zur Injektion der Dienste eingesetzt. Die Konfiguration des IoC-Containers wird ebenfalls gemäß einer vorgegebenen Namenskonvention vorgenommen. Der Name des Applikationsmoduls setzt sich aus dem Namen des TapestryFilter, der in web.xml vergeben wurde, und dem Suffix Module zusammen. Das Live Reloading-Feature und der Fehlerbericht steigern Ihre Produktivität während der Entwicklung von Tapestry-Anwendungen und sind gerade beim Einstieg in das Framework unverzichtbar.
59
3
Tapestry als ereignisgetriebenes MVC-Framework
Die meisten Java-Webframeworks können in Hinsicht auf ihr Vorgehen bei der Verarbeitung von Benutzerinteraktionen in anfrage- und ereignisgetriebene Frameworks unterteilt werden. In einem anfragegetriebenen MVC-Framework wird jede HTTP-Anfrage durch ein Dispatcher-Servlet untersucht und an einen Controller weitergereicht. Die Wahl eines Controllers erfolgt typischerweise anhand einer Konfigurationsdatei, in der URLs mit Controllern verknüpft sind. Der Controller verarbeitet die Anfrage durch Ausführung der Geschäftslogik, Manipulation des Zustands, Vorbereitung des Modells zu Darstellung in der View usw. und leitet die Anfrage an die View weiter. Struts als Vorreiter aller MVC-Frameworks in Java gehört zu der Gruppe der anfragegetriebenen Frameworks. Auch Spring MVC verfolgt diesen Ansatz. Zu den ereignisgetriebenen MVC-Frameworks gehören unter anderem JavaServer Faces und Tapestry. In Gegensatz zum anfragegetriebenen Ansatz werden bei ereignisgetriebenen MVC-Frameworks keine URLs auf Controller der Anwendung innerhalb vorgeschriebener Konfigurationsdateien verknüpft. Stattdessen werden durch Benutzerinteraktionen Ereignisse ausgelöst, die im sogenannten Listener behandelt werden. In Tapestry werden diese Listener durch sogenannte Handler-Methoden von Seiten- und Komponentenklassen repräsentiert. In diesem Kapitel lernen Sie den ereignisorientierten Ansatz von Tapestry zur Behandlung von Benutzeraktionen. Außerdem erfahren Sie, wie die Kommunikation zwischen einer Komponenten und ihrem Container erfolgt. Ereignisse bilden in Tapestry eine wichtige Grundlage für viele Funktionalitäten, sodass Sie mit diesem Konzept relativ früh konfrontiert werden müssen.
3.1
Behandeln der Benutzeraktionen
Die Controller-Logik einer Tapestry-Anwendung wird innerhalb der HandlerMethoden der Seiten und Komponenten implementiert. Diese Handler-Methoden können beispielsweise eingesetzt werden, um die Aktionen eines Benutzers, wie einen Klick auf einen Link, zu behandeln. In Tapestry können Sie mit der Komponente ActionLink einen Link zum Auslösen einer Aktion erzeugen. Implementieren Sie
3 Tapestry als ereignisgetriebenes MVC-Framework
eine Seite wie in Listing 3.1, und rufen Sie diese in Ihrem Browser auf. Sie werden erkennen, dass Tapestry einen gewöhnlichen Hyperlink erzeugt hat. Nun klicken Sie auf den Link. Es sollte eine Exception auftauchen, die Ihnen mitteilt, dass das Ereignis action nicht behandelt wurde. Listing 3.1: Index.tml (Beispiel eines ActionLinks) Klick mich
Wenn ein Benutzer auf den Link klickt, wird die Komponente ActionLink darüber informiert. Sie veröffentlicht ein Ereignis mit dem Namen action, für das Tapestry nach einer Handler-Methode in der enthaltenen Seitenklasse sucht. Um die Exception zu beseitigen, implementieren Sie also in der Klasse der Seite eine Handler-Methode wie in Listing 3.2, die die Behandlung dieses Ereignisses übernimmt. Gemäß der folgenden Namenskonvention benötigen Sie für das Ereignis action eine Methode onAction() bzw. onActionFromMyLink(). Die letztere Variante hört exklusiv auf das Ereignis action, das von der Komponente mit der ID myLink veröffentlicht wurde. Da im Template der Seite nur ein einziges ActionLink zu finden ist, können Sie sich frei für einen der beiden Namen entscheiden. Listing 3.2: Index.java public class Index { void onAction() { System.out.println("onAction()"); } }
Namenskonvention für Handler-Methoden Ein Ereignis einer Seite oder einer Komponente wird durch eine Handler-Methode behandelt, deren Name sich aus dem Präfix on und dem Namen des Ereignisses zusammensetzt, wobei für den Ereignisnamen zwischen Groß- und Kleinschreibung nicht unterschieden wird. So können Sie beispielsweise für das Ereignis abc eine HandlerMethode mit dem Namen onABC(),onabc() oder auch onAbc() implementieren.
62
3.2 Namenskonvention vs. Annotationen
Falls mehrere Komponenten innerhalb einer Seite ein Ereignis mit dem gleichen Namen auslösen, wird die gleiche Handler-Methode für die Behandlung dieser Ereignisse aufgerufen. Dies ist dadurch bedingt, dass Tapestry nur anhand eines Ereignisnamens die dazugehörige Handler-Methode findet. Falls also zwei Komponenten das Ereignis abc auslösen, kann Tapestry anhand der Methode onAbc() die Quelle des Ereignisses nicht unterscheiden. Um eine exklusive Behandlung der Ereignisse einer bestimmten Komponente sicherzustellen, müssen Sie die ID der Komponente innerhalb des Namens der Handler-Methode kodieren. Dabei wird an den Namen der Methode das Suffix From, gefolgt von der ID der Komponente, angefügt. So werden zum Beispiel die Methoden onAbcFromX() bzw. onAbcFromY() zur exklusiven Behandlung des Ereignisses abc aufgerufen, das von der Komponente mit der ID X bzw. Y veröffentlicht wurde. Falls eine Methode mit der erwarteten Signatur nicht gefunden wird, bleibt das Ereignis unbehandelt. Jedes Ereignis kann einen Kontext besitzen. Ein Kontext wird durch eine Menge von Objekten repräsentiert, die in einer URL kodiert sind. Die Elemente eines Kontextes werden als Argumente in die jeweilige Handler-Methode übergeben.
3.2
Namenskonvention vs. Annotationen
Neben der Namenskonvention existiert in Tapestry ein weiterer Mechanismus zum Matchen der Ereignisse und deren Handler-Methoden. Falls Sie keine Namenskonvention befolgen und Ihre Methoden anders benennen möchten, als Tapestry Ihnen vorschreibt, können Sie die Annotation @OnEvent einsetzen. Der Parameter value der Annotation dient zur Angabe des Ereignisnamens, für dessen Behandlung die Handler-Methode zuständig ist. Falls kein Ereignisname angegeben wurde, wird action als Standardwert genommen. So ist die Methode foo() in Listing 3.3 zusammen mit ihrer Annotation @OnEvent äquivalent zur Methode onAction(). Der Parameter component wird zur Identifikation der Komponente eingesetzt, die das Ereignis veröffentlich hat. Damit ist die Methode bar() zu einer Methode mit dem Namen onActionFromMyLink() äquivalent. Listing 3.3: Verbinden von Ereignissen und deren Handler-Methoden mit @OnEvent public class Index { @OnEvent void foo() { System.out.println("onAction()"); } @OnEvent(value="action", component="myLink")
63
3 Tapestry als ereignisgetriebenes MVC-Framework
void bar() { System.out.println("onAction()"); } }
Die Wahl des Ansatzes zum Verbinden der Ereignisse und deren Handler-Methoden bleibt natürlich dem Leser überlassen. Es wird jedoch empfohlen, die Namenskonvention zu befolgen. Damit sorgen Sie für den Wiedererkennbarkeit und damit eine bessere Lesbarkeit Ihres Codes. Außerdem bieten die meisten IDEs eine Funktionalität zum Suchen nach Methoden innerhalb einer Klasse an. Beispielsweise kann in Eclipse mit der Tastenkombination Strg + O ein Popup-Menü zur Suche nach einer Methode aufgerufen werden. Bei der Verwendung von Annotationen wird die Suche erschwert. Weiterhin hat die Namenskonvention den Vorteil, dass die Handler-Methoden in JavaDoc alle direkt untereinander auftauchen, da deren Namen alle mit dem Präfix on anfangen.
3.3
Kontext eines Ereignisses
Jedes Ereignis in Tapestry kann einen Kontext besitzen. Ein Kontext wird durch eine Menge von Objekten angegeben und zusammen mit dem Ereignis veröffentlicht. So kann der Kontext des Ereignisses action der Komponente ActionLink über den Parameter context spezifiziert werden. In Listing 3.4 wird die Zahl Pi als Kontext an übergeben. Listing 3.4: Angabe des Kontextes Klick mich
Wenn durch einen Klick auf den Link eine Anfrage an den Server verschickt wird, wird der Kontext in der Anfrage-URL mitkodiert und von Tapestry ausgewertet. Um auf diesen Kontext innerhalb der Handler-Methode zuzugreifen, muss die Methode onAction() um ein Argument vom Typ java.lang.Double wie in Listing 3.5 erweitert werden. Tapestry wird die Angabe 3.14159 automatisch zu einem Double konvertieren. Dies erfolgt mithilfe des Dienstes TypeCoercer, der später besprochen wird. Listing 3.5: Handler-Methode mit einem Kontext public class Index { void onAction(Double context) { System.out.println("Die Zahl Pi ist: "+context); } }
64
3.4 Erzeugen einer Antwort mit Handler-Methoden
Ein Kontext kann auch aus mehreren Objekten bestehen. Beispielsweise wird in Listing 3.6 ein Kontext erzeugt, indem in der Methode getContext() ein Array mit zwei Zahlen, die in der Mathematik eine wichtige Rolle spielen, zurückgegeben wird. Damit die Komponente ActionLink auf dieses Array zugreifen kann, muss im Template (Listing 3.7) mycontext als Wert des Parameters context angegeben werden. Listing 3.6: Kontext mit mehreren Bestandteilen public class Index{ public Object[] getMyContext(){ return new Object[]{new Double(3.14159), new Double(2.7182)}; } void onAction(Double pi, Double e) { System.out.println("Die Zahl Pi ist: "+pi); System.out.println("Die eulersche Zahl ist: "+e); } }
Listing 3.7: Referenziert den Kontext aus der Seitenklasse Klick mich
3.4
Erzeugen einer Antwort mit Handler-Methoden
Im Gegensatz zu Struts7 oder JavaServer Faces8 benötigt Tapestry keine Konfigurationsdateien, in denen Views mit Namen assoziiert werden. Stattdessen identifiziert Tapestry die Seiten über ihre Klassennamen. Eine Handler-Methode wie onAction() in Listing 3.6 kann entweder eine void-Methode oder auch eine Methode mit einem Rückgabewert sein. Falls vorhanden, ist der Rückgabewert für die Art der Antwort verantwortlich. Tapestry wertet den Typ des zurückgegebenen Wertes aus und entscheidet sich daraufhin, auf welche Weise die Antwort zurück zum Client geschickt wird. Folgende Rückgabetypen sind möglich:
7 8
http://struts.apache.org/ http://java.sun.com/javaee/javaserverfaces/
65
3 Tapestry als ereignisgetriebenes MVC-Framework
void: Das Markup der aktuellen Seite wird als Antwort zum Client geschickt. null-Wert: Falls eine Handler-Methode einen null-Wert zurückgibt, wird das
Markup der aktuellen Seite als Antwort zum Client geschickt. Dieses Verhalten entspricht einer void-Methode. java.lang.String: Ein Name einer Seitenklasse, zu der weitergeleitet werden soll. java.lang.Class: Klasse der Seite, zu der weitergeleitet werden soll. Diese Variante
ist gegenüber dem String-Rückgabewert vorzuziehen, da sich bei einem Refactoring der Name der Klasse ändern kann. Instanz einer Seite: Anstelle des Namens oder der Klasse einer Seite kann auch eine
Instanz dieser Seite zurückgegeben werden. Dieses Vorgehen ist dann nützlich, wenn Sie diese Instanz beispielsweise mit Werten vorbelegen oder bestimmte Methoden aufrufen möchten. Beachten Sie, dass Sie niemals eine Seite oder Komponente manuell erzeugen sollten, um diese Instanz in einer Handler-Methode zurückzugeben. Diese manuell erzeugte Instanz ist nicht einsatzbereit, weil ihr sämtliche Abhängigkeiten fehlen. Stattdessen wird eine Seiteninstanz mit @InjectPage injiziert. Dazu mehr in Kapitel 4. org.apache.tapestry5.Link: Dieses Interface repräsentiert in Tapestry eine URL. org.apache.tapestry5.StreamResponse: Dieses Interface ist zuständig zum Versenden
eines Bytestroms (z. B. ein Bild, eine PDF-Datei etc.) an den Client. java.net.URL: Externe URL außerhalb einer Tapestry-Anwendung
Wir betrachten nun einige Beispiele, welche die Benutzung der unterschiedlichen Rückgabetypen demonstrieren. In Listing 3.8 sind zwei ActionLinks mit den IDs pageLink und externalLink zu sehen. In der dazugehörigen Seitenklasse (Listing 3.9) sind zwei Handler-Methoden zur exklusiven Behandlung des Ereignisses action der beiden Komponenten implementiert. Durch einen Klick auf den Link mit der ID pageLink wird die Methode onActionFromPageLink() aufgerufen und anschließend zur Seite MyPage weitergeleitet. Ein Klick auf den Link mit der ID externalLink resultiert in einer Weiterleitung zu Google. Listing 3.8: Seite mit zwei Links Gehe zu MyPage Google
Listing 3.9: Unterschiedliche Rückgabewerte der Handler-Methoden public class Index { Object onActionFromPageLink() {
66
3.5 Auslösen von Ereignissen mit der Komponente EventLink
return MyPage.class; } Object onActionFromExternalLink() throws MalformedURLException { return new URL("http://www.google.de"); } }
Weitere Rückgabetypen werden später detailliert in den Kapiteln 4 und 10 besprochen.
3.5
Auslösen von Ereignissen mit der Komponente EventLink
Neben der Komponente ActionLink stellt Tapestry eine weitere Komponente bereit, mit deren Hilfe Ereignisse zur Behandlung der Benutzeraktionen ausgelöst werden. Die Komponente EventLink ist eine generische Variante von ActionLink. Sie ermöglicht es Ihnen, den Namen des Ereignisses, das sie veröffentlichen wird, über den Parameter event selbst zu bestimmen (Listing 3.10). Listing 3.10: Bestimmen des Ereignisnamens mit EventLink Klick mich
Listing 3.11: Behandelt das Ereignis myevent public class Index { void onMyEvent() { System.out.println("onMyEvent()"); } }
Die Angabe des Parameters event ist optional. Falls kein Ereignisname angegeben wird, verwendet Tapestry die ID der Komponente als Ereignisnamen. In Listing 3.12 wird der erste EventLink das Ereignis mit dem Namen xyz veröffentlichen. Folglich wird eine Methode mit dem Namen onXyz() zur Behandlung des Ereignisses benötigt. Für den zweiten EventLink ist weder eine ID noch der Name des Ereignisses angegeben. Also wird in diesem Fall eine Methode onEventLink() benötigt. Wenn für eine Komponente der Parameter t:id nicht angegeben wird, erzeugt Tapestry automatische eine eindeutige ID und weist sie dieser Komponente zu. Dabei nimmt Tapestry den Namen der jeweiligen Komponente und fügt einen Zähler
67
3 Tapestry als ereignisgetriebenes MVC-Framework
hinzu. Sie sollten sich aber niemals auf diese automatische Generierung der IDs verlassen, sondern ihre eigene ID bereitstellen. Die Funktionalität zur automatischen Generierung der IDs gehört zum internen API und kann sich jederzeit ohne Ankündigung ändern. Damit Ihre Handler-Methoden bei jedem Upgrade auf eine neue Tapestry-Version funktionieren, sollten Sie beim Einsatz der Komponente EventLink immer einen Wert entweder für t:id oder für den Parameter event bereitstellen. Listing 3.12: ID der Komponente EventLink als Name des Ereignisses Klick mich Mich auch
Listing 3.13: Behandelt Ereignisse xyz und eventlink public class Index { void onXyz() { System.out.println("onXyz()"); } void onEventlink() { System.out.println("onEventlink()"); } }
3.6
Programmatisches Auslösen eigener Ereignisse
Von einem ereignisgetriebenem Framework erwartet man nicht nur die Funktionalität zum Behandeln der Ereignisse, die vom Framework selbst ausgelöst werden, sondern auch die Mittel zum Triggern eigener Ereignisse. In Listing 3.14 wird in der Handler-Methode des Ereignisses action, das von einem ActionLink stammt, ein Ereignis mit dem Namen add veröffentlicht. Dies geschieht mithilfe des Dienstes ComponentResources, der mittels @Inject in die Seite injiziert wird. Die Methode triggerEvent() dieses Dienstes erwartet drei Parameter, die zum Auslösen eines Ereignisses notwendig sind: Name, Kontext und Callback. Der Name des Ereignisses legt fest, wie die Handler-Methode zu benennen ist, falls der Ansatz der Namenskonventionen angewendet wird. Der Kontext definiert die Anzahl und Typen der Parameter der Handler-Methode. Die anonyme Implementierung des Interface ComponentEventCallback wird zur Kontrolle des Ergebnisses der HandlerMethode eingesetzt, die das Ereignis add behandelt hat. Die Methode handleResult() des Callbacks erhält über ihren Parameter den Rückgabewert der Handler-Methode
68
3.6 Programmatisches Auslösen eigener Ereignisse
onAdd(), falls dieser nicht null ist, und soll die Entscheidung treffen, ob dieser Wert akzeptiert wird. Falls der Wert nicht akzeptiert werden kann, sollte eine Exception
geworfen werden. Falls die Handler-Methode onAdd() einen booleschen Wert zurückgeben würde, würde Tapestry die Methode handleResult() des Callbacks nicht aufrufen, ähnlich wie bei einem zurückgegebenen null-Wert. Der Wert true signalisiert, dass das Ereignis add behandelt wurde und die Suche nach einer Handler-Methode abgebrochen werden muss. Im Falle des Rückgabewertes false würde Tapestry die Suche nach einer weiteren Handler-Methode fortsetzen. In diesem Beispiel wird das Ereignis add mit zwei numerischen Werten 1 und 2 als Kontext verschickt. Die Handler-Methode des Ereignisses wird in der gleichen Seite implementiert und führt eine Addition der beiden Werte des Kontextes aus. Das Ergebnis wird von dem Callback abgefangen und auf die Konsole geschrieben. In einer realen Anwendung wird eine Komponente selten ihr eigenes Ereignis behandeln. Stattdessen wird das Ereignis einer Komponente zur Kommunikation mit ihrem Container verwendet. Dies erfolgt mithilfe des Konzeptes Event Bubbling, das in Abschnitt 3.7 beschrieben wird. Listing 3.14: Programmatisches Veröffentlichen von Ereignissen public class PublishEvent{ @Inject private ComponentResources componentResources; void onAction() { ComponentEventCallback callback = new ComponentEventCallback(){ public boolean handleResult(Object result) { System.err.println("Addition:" + result); return true; }}; componentResources.triggerEvent( "add", new Object[]{1, 2}, callback); } Object onAdd(Integer x, Integer y) { return x+y; } }
69
3 Tapestry als ereignisgetriebenes MVC-Framework
3.7
Event Bubbling
Zur Behandlung der Ereignisse wird in Tapestry ein Konzept namens Event Bubbling eingesetzt. Dabei steigt ein Ereignis durch die Hierarchie der Komponenten hoch wie eine Luftblase im Wasser. Wird ein Ereignis von einer Komponente verschickt, versucht Tapestry, eine Handler-Methode innerhalb der gleichen Komponente zu finden. Falls keine Methode gefunden wurde, geht Tapestry in der Hierarchie der Komponenten eine Ebene hoch und startet die Suche erneut. So wird ein Ereignis weitergereicht, bis es bei der enthaltenen Seite angekommen ist. Falls auch die Seite keine Handler-Methode zur Verfügung stellt, wird das Ereignis abgebrochen. In Listing 3.15 ist die Seite PublishEventDemo zu sehen, die die Komponente PublishEvent (Listing 3.14) einbettet und das Ereignis add behandelt. Die Handler-Methode dieser Seite wird nur dann aufgerufen, wenn die Komponente PublishEvent ihr Ereignis selbst nicht behandelt hat. Damit die Methode onAdd() der Seite PublishEventDemo aufgerufen wird, muss die gleichnamige Methode aus der Komponente PublishEvent entfernt werden. Listing 3.15: PublishEventDemo.java public class PublishEventDemo { @Component private PublishEvent publishEvent; Object onAdd(Integer x, Integer y) { return x+y; } }
Der Rückgabewert einer Handler-Methode ist entscheidend dafür, ob ein Ereignis abgebrochen wird oder nicht. Wird in einer Handler-Methode ein Wert ungleich null zurückgegeben, gilt das Ereignis als abgebrochen. Ansonsten steigt es in der Hierarchie hoch. Falls der Wert true zurückgegeben wird, wird das Ereignis abgebrochen, ohne dass ein entsprechender Callback (siehe Kapitel 3.6) aufgerufen wird. Die Rückgabe des Wertes false entspricht der Rückgabe eines null-Wertes.
3.8
Abfangen von Exceptions aus Handler-Methoden
Wird innerhalb einer Handler-Methode eine Exception geworfen, wird diese von Tapestry abgefangen und dem Benutzer in Form eines Fehlerberichts präsentiert. Zusätzlich veröffentlicht das Framework das Ereignis exception, das Sie wiederum durch eine entsprechende Handler-Methode behandeln können. Der Kontext dieses Ereignisses ist eine Instanz von ComponentEventException, welche die ursprüngliche Exception kapselt und Informationen über das Ursprungsereignis bereitstellt.
70
3.9 Zusammenfassung
In Listing 3.16 wird in der Handler-Methode des Ereignisses action eine RuntimeException geworfen. Die Methode onException() fängt diese Exception ab und gibt auf der Konsole den Typ des Ursprungsereignisses (action) und die beiden Parameter der Methode onAction() aus. Der Rückgabetyp von onException() bestimmt die Art der Antwort auf die Anfrage. In diesem Beispiel bewirkt die Handler-Methode des Ereignisses exception anschließend eine Weiterleitung zur Seite MyExceptionDisplayPage. Wäre die RuntimeException in der Methode onAction() nicht geworfen, würde die Seite CatchException (Listing 3.16) die Antwort erzeugen, da die Methode onAction() eine void-Methode ist. Die Methode onException() würde selbstverständlich auch die Exceptions der geschachtelten Komponenten einer Seite abfangen. Listing 3.16: Behandlung des Ereignisses exception public class CatchException { ... void onAction(Double pi, Double e) { throw new RuntimeException("Catch me if you can!"); } Object onException(ComponentEventException cause) { System.err.println("Exception in der Behandlung" +" des Ereignisses '"+ cause.getEventType() + "'"); EventContext context = cause.getContext(); System.err.println("pi: " + context.get(Double.class, 0)); System.err.println("e: " + context.get(Double.class, 1)); return MyExceptionDisplayPage.class } }
3.9
Zusammenfassung
In Tapestry wird die Kommunikation zwischen einer Komponente und ihrem Container (Seite oder Komponente) mithilfe der Ereignisse realisiert. Ein von einer Komponente ausgelöstes Ereignis kann entweder durch diese Komponente oder ihren Container behandelt werden. Das Konzept namens Event Bubbling ermöglicht, dass Ereignisse in der Komponentenhierarchie hochsteigen können, bis sie behandelt werden. Die Identifikation einer Handler-Methode zur Behandlung eines Ereignisses erfolgt per Namenskonvention oder mithilfe der Annotation @OnEvent, mit der HandlerMethoden versehen werden können. Handler-Methoden werden in Tapestry zur Implementierung von Controller-Logik verwendet. Mit Komponenten ActionLink und EventLink können Benutzeraktionen behandelt werden.
71
4
Navigation zwischen Seiten
Verweise (Hyperlinks) zwischen unterschiedlichen Webseiten haben mit Sicherheit enorm zum Erfolg des Internets beigetragen. Es ist für uns selbstverständlich, über einen Link von einer Webseite zur nächsten zu navigieren. Genauso selbstverständlich ist es, unterschiedliche Seiten einer Tapestry-Anwendung miteinander zu verknüpfen. In diesem Kapitel lernen Sie, wie Tapestry die Navigation zwischen den Seiten einer Applikation realisiert.
4.1
Erstellen von Verweisen zwischen Seiten
In Tapestry werden zwei Seiten mithilfe der Komponente PageLink verknüpft. Diese Komponente besitzt den erforderlichen Parameter page und mehrere optionale Parameter. Der Parameter page dient der Angabe des Namens einer Seite, auf die ein Verweis erzeugt werden soll. In Listing 4.1 wird das Template der Indexseite der HelloWorld-Anwendung um einen Link zur Seite MyPage erweitert. Listing 4.1: Komponente PageLink Weiter
Wenn Sie diese Seite in Ihrem Browser aufrufen, sollten Sie einen gewöhnlichen Link mit dem Text »Weiter« sehen. Nun klicken Sie auf den Link. Sie werden erkennen, dass die Adresse der Seite MyPage wie folgt lautet: http://localhost:8080/app/mypage. Der Name der Zielseite ist in der URL codiert, wobei nicht zwischen Groß- und Kleinschreibung unterschieden wird. In Kapitel 2 wurde erläutert, dass im Paket pages für Seiten auch Unterpakete angelegt werden können. Beim Erzeugen der Links auf Seiten in diesen Unterpaketen führt Tapestry eine kleine Optimierung der URLs durch. Falls der Name des Unterpakets ein Präfix bzw. Suffix des Seitennamens darstellt, wird dieses Präfix bzw. Suffix aus der URL entfernt. Für die Seite de.t5book.pages.book.ViewBook würde Tapestry also eine URL .../book/View und nicht .../book/ViewBook erzeugen. Analog wird für die Seite
4 Navigation zwischen Seiten
de.t5book.pages.view.ViewBook eine URL .../view/Book erzeugt. Damit wird erreicht, dass die URLs der Seiten kürzer werden und an Lesbarkeit gewinnen.
Zum Setzen einer Sprungmarke kann der Parameter anchor der Komponente PageLink eingesetzt werden. Dieser Parameter erwartet den Namen eines Sprungziels in der Zielseite. So wird beispielsweise in Listing 4.2 eine Sprungmarke mit dem Namen ende genutzt. An die URL zur Seite MyPage wird #ende angefügt, sodass der Browser zur entsprechenden Stelle in der Zielseite springt. Listing 4.2: PageLink mit einer Sprungmarke Weiter
Sie werden sich möglicherweise fragen, ob Tapestry die Komponente PageLink wirklich benötigt. Würde ein herkömmlicher Hyperlink nicht ausreichen? Selbstverständlich können Sie die Navigation zwischen Seiten auch per HTML-Links realisieren, doch in diesem Fall müssen Sie zusätzliche Arbeit bewältigen, die sonst Tapestry für Sie übernimmt. Beispielsweise überprüft das Framework, ob die angegebene Zielseite wirklich existiert, und zwar sobald die verweisende Seite aufgerufen wird. Falls die Seite nicht existiert, wird eine Exception geworfen. Mit herkömmlichen Hyperlinks müssten Sie alle Verweise durchklicken, um die Links zu finden, deren Zielseiten nicht existieren. Spätestens wenn Sie einige Seiten umbenennen, werden Sie dieses Feature zu lieben lernen. Ein weiterer Vorteil der Komponente PageLink ist, dass Sie keine Angabe des Kontextpfades Ihrer Anwendung benötigen. Sie müssen sich keine Gedanken machen, ob Ihre Links absolut oder relativ sein sollten. Stattdessen geben Sie den eindeutigen Namen der Seitenklasse an, und Tapestry sorgt für den Rest.
4.2
Übermitteln von Informationen an eine Zielseite
Wenn Sie im Internet unterwegs waren, haben Sie sicherlich gemerkt, dass viele Seiten kryptische Adressen besitzen. Wenn Sie beispielsweise bei Google den Suchbegriff »Tapestry 5« eingeben, so werden Sie feststellen, dass die Adresse der Ergebnisseite wie folgt aussieht: http://www.google.de/search?hl=de&q=Tapestry+5&btnG=Google+Search An diesem Beispiel können Sie sehen, wie Daten per URL übertragen werden können. Das Trennzeichen (?) teilt die URL in zwei Bestandteile: die Adresse und die Daten. Die einzelnen Daten sind voneinander durch das Ampersand (&) getrennt. So enthält die obige URL drei Name-Wert-Paare, wobei der Name und der Wert durch das Gleichheitszeichen (=) getrennt sind. Die mit einer URL übertragenen Daten werden
74
4.2 Übermitteln von Informationen an eine Zielseite
auch als Anfrageparameter bezeichnet. Der Anfrageparameter hl in der obigen URL gibt beispielsweise an, in welcher Sprache die Ergebnisse der Suche dargestellt werden sollen. Der Parameter q enthält als Wert unseren Suchbegriff. Wenn Sie bei Google arbeiten würden und die Suche mithilfe eines Servlets implementieren müssten, so würde dieses Servlet in etwa wie in Listing 4.3 aussehen. Bevor Sie mit der Suche anfangen würden, müssten Sie ein Objekt der Klasse HttpServletRequest nach den Parametern befragen. Sobald Sie den Suchbegriff aus der URL extrahiert haben, können Sie die Suche durchführen, deren Ergebnisse Sie dann in der gewünschten Sprache dem Benutzer präsentieren. Listing 4.3: SearchServlet.java public class SearchServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String language = request.getParameter("hl"); Locale locale = getLocale(language); String query = request.getParameter("q"); // Führe die Suche anhand der beiden Parameter aus. } private Locale getLocale(String str){ ... } ... }
Dieses Buch handelt von Tapestry und nicht von Googles Suchmaschine. Der Zweck dieses Kapitels ist es, Ihnen einen ersten Eindruck zu vermittelten, wie Sie die Funktionalität aus dem Listing 4.3 mit Tapestry realisieren würden. In Tapestry würde sich die Signatur der Methode zur Behandlung der Suchanfrage deutlich von der eines Servlets unterscheiden. Wie dies funktioniert, erfahren Sie in den folgenden Kapiteln. Listing 4.4: Search.java public class Search { Object onSearch(Locale locale, String queryString) { // Führe die Suche anhand der beiden Parameter aus. ... } ... }
75
4 Navigation zwischen Seiten
4.3
Übermitteln von Informationen an eine Tapestry-Seite
Betrachten Sie folgendes Beispiel: Sie haben den Auftrag, einen Online-Shop zum Verkauf von Büchern mit Tapestry zu entwickeln. Die Anwendung soll eine Startseite besitzen, auf der einige Bücher präsentiert werden. Jedes der Bücher ist mit einem Verweis versehen, der zu einer Detailseite über das jeweilige Buch führt. Wir beginnen mit der Entwicklung der Anwendung, indem wir ein einfaches Domain-Modell wie in Listing 4.5 implementieren. Die Klasse Book repräsentiert ein Buch, dessen Informationen wie Titel, Autor, ISBN usw. in einer Datenbank abgelegt sind. Die Eigenschaft id ist der Primärschlüssel aus der Datenbank, der zur Identifikation einer Instanz von Book dient. Getreu der Konvention für die Struktur einer Tapestry-Anwendung wird diese Klasse in dem Unterpaket entities des Wurzelpakets der Anwendung abgelegt. In diesem Beispiel heißt das Paket de.t5book.entities. Listing 4.5: Book.java public class Book { private Long id; private String title; private String author; private Date publicationDate; ... }
Als Nächstes entwickeln wir einen Dienst für den Zugriff auf die Instanzen von Book. Es handelt sich um ein typisches DAO (Data Access Object) mit den Methoden zum Suchen, Speichern und Löschen von Büchern. DAO ist das JEE-Standard-Pattern zur Abstraktion und Kapselung von Zugriffen auf die Daten aus einer Datenbank. In Listing 4.6 ist die Schnittstelle des Dienstes zu sehen, die die benötigten Methoden spezifiziert. In einer produktiven Anwendung würden Sie für die Implementierung dieser Methoden JDBC (Java Database Connectivity) oder ein ORM-Framework (Object Relational Mapping) wie Hibernate einsetzen. Doch an dieser Stelle sind die Implementierungsdetails uninteressant. Es reicht zu wissen, dass dieser Dienst eine zentrale Stelle für den Zugriff auf den Bücherbestand darstellt und mittels der Annotation @Inject in die Seiten des Shops injiziert werden kann. Listing 4.6: BookService.java public interface BookService { List findAllBooks(); Book findBookById(Long id); void save(Book book); void delete(Book book); }
76
4.3 Übermitteln von Informationen an eine Tapestry-Seite
In Listing 4.7 ist eine einfache Startseite des Shops zu sehen, in der mithilfe der Komponente Loop eine Liste der erhältlichen Bücher dargestellt wird. Die Komponente iteriert über die Menge der Bücher, die in der Methode getBooks() der Seitenklasse (Listing 4.8) zurückgegeben wird. Diese Methode bezieht ihrerseits die Liste der Bücher vom Dienst BookService. In jeder Iteration wird eine Instanz von Book der Eigenschaft currentBook der Seite zugewiesen, sodass im Rumpf der Komponente Loop auf diese Instanz mittels Expansions zugegriffen werden kann. So wird für jedes Buch ein Link zur Seite ViewBook erzeugt, dessen Text sich aus dem Titel des Buches und dem Namen des Autors zusammensetzt. Der Parameter context der Komponente PageLink dient der Übermittlung von Informationen an die Zielseite des Links. Wir benutzen diesen Parameter, um den Primärschlüssel einer Instanz von Book an die Seite ViewBook zu übertragen. Listing 4.7: Index.tml
${currentBook.title} : ${currentBook.author}
Listing 4.8: Index.java public class Index { @Property private Book currentBook; @Inject private BookService bookService; public List getBooks() { return bookService.findAllBooks(); } }
Um unsere Indexseite ausprobieren zu können, erzeugen wir zunächst eine leere Klasse ViewBook.java und ein leeres Template ViewBook.tml. Dies ist nötig, da sich ansonsten die Komponente PageLink beschweren würde, dass die Zielseite des Links noch nicht existiert. Rufen Sie die Startseite in Ihrem Browser auf, und klicken Sie auf eines der Bücher. Sie werden sehen, dass Tapestry einen Link wie http://localhost:8080/
77
4 Navigation zwischen Seiten
app/view/book/273 erzeugt hat. In dieser URL ist der Primärschlüssel 273 des ausgewählten Buches kodiert, an dem die Seite ViewBook erkennt, über welches Buch die Informationen angefragt werden. Es handelt sich um eine REST-konforme (Representational State Transfer) URL, die jeder Benutzer zu den Favoriten seines Browsers hinzufügen kann, um das jeweilige Buch auch einige Wochen oder Monate später wieder anschauen zu können. Lassen Sie uns nun die leere Seitenklasse und das Template der Seite ViewBook implementieren. Jede Seite in Tapestry kann einen sogenannten Aktivierungskontext besitzen. Ein Aktivierungskontext beinhaltet den Zustand der Seite, der zwischen zwei unterschiedlichen Anfragen aufbewahrt wird. Um das Setzen von Lesezeichen zu ermöglichen, darf dieser Zustand nicht in der HttpSession abgespeichert werden. Sobald die Session abgelaufen ist, ist die Seite ViewBook nicht mehr nutzbar. Sie müssten zurück zur Startseite navigieren, um von da aus ein Buch wieder auszuwählen. Sie haben sich sicherlich beim Browsen im Internet schon etliche Male darüber geärgert, dass Ihre Lesezeichen nach gewisser Zeit nicht mehr funktionieren. Damit Ihren Kunden dieser Ärger erspart bleibt, bietet Tapestry einen eleganten Ansatz zum Speichern des Zustandes einer Seite ohne eine HttpSession. Der Kontext der Komponente PageLink entspricht dem Aktivierungskontext der Zielseite ViewBook. Beim Laden dieser Seite löst Tapestry das Ereignis activate aus, das von der Seite zum Wiederherstellen des internen Zustandes verwendet werden kann. Zum Behandeln des Ereignisses muss eine Handler-Methode implementiert werden, die gemäß der Konvention zur Benennung von Handler-Methoden den Namen onActivate() tragen kann. Mithilfe des Parameters context der Komponente PageLink wurde der Primärschlüssel des ausgewählten Buches an die URL der Seite ViewBook angefügt. Also wird erwartet, dass die Aktivierungsmethode auch ein Argument zum Empfangen dieses Schlüssels besitzt. Im Listing 4.10 enthält die Aktivierungsmethode ein Argument vom Typ java.lang.Long, da auch der Primärschlüssel der Klasse Book von diesem Typ ist. Sobald eine URL wie http://localhost:8080/app/view/book/273 aufgerufen wird, wird Tapestry den String 273 aus der URL entnehmen und in ein java.lang.Long transformieren. Der erzeugte Primärschlüssel wird beim Aufruf der Methode onActivate() als Argument übergeben. Innerhalb dieser Methode wird der interne Zustand der Seite wiederhergestellt, indem der Dienst BookService zur Suche nach einem Buch eingesetzt wird. Das gefundene Buch wird der Eigenschaft book zugewiesen, sodass Sie im Template (Listing 4.9) mittels Expansion darauf zugreifen können. Sobald die Seite die Anfrage bearbeitet hat, wird sie zurück in den Seitenpool abgelegt, wo sie bis zur nächsten Anfrage, die möglicherweise von einem anderen Benutzer stammt, verweilt. Der Seitenpool wird im Detail in Kapitel 5 behandelt. Die Methode onPassivate() wird zur Behandlung des Ereignisses passivate eingesetzt. Dieses Ereignis wird ausgelöst, sobald eine Seite einen Link auf die Seite ViewBook erzeugen will, um ViewBooks über den Aktivierungskontext zu befragen. Weitere Informationen über das Ereignis passivate folgen in Abschnitt 4.4.
78
4.3 Übermitteln von Informationen an eine Tapestry-Seite
Listing 4.9: ViewBook.tml Titel: ${book.title} Author: ${book.author} ...
Listing 4.10: ViewBook.java public class ViewBook { @Inject private BookService bookService; @Property private Book book; void onActivate(Long bookId) { this.book = bookService.findBookById(bookId); } Long onPassivate() { return book.getId(); } }
In einer CRUD-Anwendung haben die Seiten für die Operationen Retrieve, Update und Delete den gleichen Aktivierungskontext, nämlich den Primärschlüssel der Entität zum Holen, Editieren und Löschen. Um die Wiederverwendbarkeit des Codes zur Wiederherstellung des internen Zustandes dieser Seiten zu gewährleisten, können Sie einen ValueEncoder implementieren, der für die Umwandlung zwischen server- und clientseitiger Repräsentation eines Objektes zuständig ist. In Listing 4.11 ist ein ValueEncoder für die Klasse Book zu sehen. Die Methode toClient() ist zuständig für die Umwandlung einer Instanz von Book in einen clientseitigen String. Typischerweise wird dazu der Primärschlüssel einer Entität eingesetzt. Der Rückgabewert dieser Methode wird an die URL zur Seite ViewBook (Listing 4.10) angehängt. Sobald die URL aufgerufen wird, extrahiert Tapestry diesen Wert aus der URL und erzeugt daraus eine Instanz von Book. Dies erfolgt dadurch, dass die clientseitige Repräsentation eines Buches an die Methode toValue() der Klasse BookEncoder übergeben wird. Mithilfe des Dienstes BookService wird das jeweilige Buch aus der Datenbank geholt und zurückgegeben.
79
4 Navigation zwischen Seiten
Listing 4.11: BookEncoder.java public class BookEncoder implements ValueEncoder { private BookService bookService; public BookEncoder(BookService bookService) { this.bookService = bookService; } public String toClient(Book book) { return String.valueOf(book.getId()); } public Book toValue(String clientValue) { Long id = Long.valueOf(clientValue); return bookService.findBookById(id); } }
Damit Tapestry diesen ValueEncoder benutzt, müssen Sie ihn dem Framework im IoCModul Ihrer Anwendung zur Verfügung stellen. Dies erfolgt, indem Sie eine Methode zur Konfiguration des Dienstes ValueEncoderSource wie in Listing 4.12 implementieren. Innerhalb dieser Methode wird eine Factory zur Erzeugung des ValueEncoder für die Klasse Book konstruiert und der Konfiguration von ValueEncoderSource hinzugefügt. Sie müssen diesen Zusammenhang zu diesem Zeitpunkt noch nicht verstehen. Tapestry IoC wird später ausführlich erläutert. Kopieren Sie einfach diese Methode in Ihr Applikationsmodul, und kehren Sie zu diesem Kapitel zurück, sobald Sie die Grundlagen über Tapestry IoC in Kapitel 17 durchgelesen haben. Sie werden feststellen, dass der Code in Listing 4.12 einfacher zu verstehen ist, als es zu diesem Zeitpunkt möglicherweise den Anschein erweckt. Listing 4.12: Bereitstellen eines ValueEncoders für die Klasse Book public class AppModule { ... public static void contributeValueEncoderSource( final MappedConfiguration config, final BookService bookService) { ValueEncoderFactory factory = new ValueEncoderFactory(){ public ValueEncoder create(Class clazz) { return new BookEncoder(bookService); }}; configuration.add(Book.class, factory); } }
80
4.3 Übermitteln von Informationen an eine Tapestry-Seite
Nun können Sie die Signatur der Aktivierungsmethode ändern, indem Sie den Typ des Argumentes von Long auf Book ändern. Dieses Argument kann der Eigenschaft book der Seite zugewiesen werden, sodass der Dienst BookService nicht mehr benötigt wird. Das Feld bookService kann aus der Seitenklasse entfernt werden. Auch der Inhalt der Methode zur Behandlung des Ereignisses passivate bedarf einer Änderung. Die Methode gibt ab sofort eine Instanz von Book zurück und nicht deren Primärschlüssel. Das Ergebnis ist in Listing 4.13 zu sehen. Listing 4.13: ViewBook.java public class ViewBook { @Property private Book book; void onActivate(Book book) { this.book = book; } Book onPassivate() { return book; } }
In Kapitel 14 werden Sie lernen, dass das Bereitstellen eines ValueEncoder für eine Hibernate-Entität nicht notwendig ist. Falls Sie Hibernate für die Persistenz der Klasse Book einsetzen, werden Sie höchstwahrscheinlich von dem Tapestry-Unterprojekt tapestry-hibernate Gebrauch machen. Dieses Projekt sorgt für eine elegante Integration von Tapestry und Hibernate, die in Kapitel 14 beschrieben wird. Unter anderem sorgt dieses Projekt für das Bereitstellen der ValueEncoder für die persistenten Klassen.
4.3.1
Generieren von Handler-Methoden für den Aktivierungskontext
Ein wichtiges Unterscheidungsmerkmal von Tapestry ist, dass das Framework einen enormen Beitrag zur Steigerung der Produktivität der Anwendungsentwickler leistet. Tapestry versucht, so viel Arbeit wie möglich für Sie zu übernehmen. Der Aktivierungskontext einer Seite ist ein hervorragendes Beispiel dafür. Sie können bequem auf die Anfrageparameter in der URL zugreifen, indem Sie einfach einen Aktivierungskontext definieren. Tapestry sorgt für die Umwandlung der Anfrageparameter in die richtigen Typen und stellt die umgewandelten Werte bereit. Sie müssen lediglich diese Werte innerhalb der Aktivierungsmethode den Eigenschaften der Seite zuweisen. Aber auch dieser letzte Schritt kann von Tapestry übernommen werden. Versehen Sie dazu die Eigenschaft book der Seite ViewBook mit der Annotation @PageActivationContext, und entfernen Sie die beiden Methoden onActivate() und onPassivate()
81
4 Navigation zwischen Seiten
wie in Listing 4.14. Sobald diese Seite zum ersten Mal aufgerufen wird, wird Tapestry die beiden Methoden zur Laufzeit generieren. Dies sorgt dafür, dass die Seite mit viel weniger Code auskommt und viel einfacher zu verstehen ist. Listing 4.14: Lässt die Handler-Methoden für den Aktivierungskontext generieren public class ViewBook { @PageActivationContext @Property private Book book; }
Die Annotation @PageActivationContext hat eine kleine Einschränkung: Sie können nur eine einzelne Eigenschaft einer Seite mit dieser Annotation versehen. Falls sich der Kontext Ihrer Seite aus mehreren Bestandteilen zusammensetzt, müssen Sie die Aktivierung und Passivierung dieser Seite auf herkömmliche Weise durchführen oder eine neue Klasse einführen, in der alle Bestandteile des Aktivierungskontextes gekapselt werden.
4.4
Navigation durch Aktionen
Neben der Komponente PageLink bietet Tapestry einen weiteren Mechanismus zur Navigation zwischen Seiten: Navigation durch Komponentenereignisse. Wie Sie bereits gelernt haben, können Ereignisse durch die Komponenten ActionLink und EventLink ausgelöst werden. Der Rückgabewert der Handler-Methoden zur Behandlung der Ereignisse bestimmt die Art der Antwort, die zurück zum Client geschickt wird. In Kapitel 3 wurden mehrere Rückgabetypen vorgestellt, die eine HandlerMethode zurückgeben darf. In diesem Kapitel werden einige dieser Typen besprochen, die zur Navigation zwischen Seiten verwendet werden. Die einfachste Weise, aus einer Handler-Methode heraus zu einer anderen Seite weiterzuleiten, ist die Rückgabe der Klasse dieser Seite. Alternativ kann der Klassenname der Zielseite zurückgegeben werden, was ebenfalls in einer Weiterleitung zu dieser Seite resultiert. Im Listing 4.15 sind zwei ActionLinks mit den IDs x und y zu sehen. In der dazugehörigen Seitenklasse (Listing 4.16) sind zwei Handler-Methoden zur exklusiven Behandlung des Ereignisses action der beiden Komponenten implementiert. Durch einen Klick auf den Link mit der ID x wird die Methode onActionFromX() aufgerufen und anschließend zur Seite MyPage weitergeleitet. Ein Klick auf den Link mit der ID y resultiert ebenfalls in einer Weiterleitung zur Seite MyPage.
82
4.4 Navigation durch Aktionen
Listing 4.15: Seite mit zwei ActionLinks Link X Link Y
Listing 4.16: Rückgabewerte der Handler-Methoden public class Index { Object onActionFromX() { System.out.println("onActionFromX()"); return "MyPage"; } Object onActionFromY() { System.out.println("onActionFromY()"); return MyPage.class; } }
Eine weitere Möglichkeit, aus einer Handler-Methode zu einer Seite weiterzuleiten, ist, diese Seite in die Ausgangsseite zu injizieren. Dazu müssen Sie eine Eigenschaft vom Typ der Zielseite mit der Annotation @InjectPage versehen. In Listing 4.17 wird in die Seite Index eine Instanz der Seite MyPage injiziert. Diese Instanz wird in der Methode onAction() zurückgegeben, sodass der Klick auf den ActionLink in einer Weiterleitung zur Seite MyPage resultiert. Dieses Vorgehen ist analog zu dem in Listing 4.16. Zur Erinnerung: Sie sollten niemals eine Instanz einer Seite manuell erzeugen, sondern immer mit @InjectPage injizieren. Listing 4.17: Injektion der Zielseite public class Index { @InjectPage private MyPage myPage; Object onAction() { System.out.println("onAction()"); return myPage; } }
83
4 Navigation zwischen Seiten
Die Injektion einer Instanz einer Seite ermöglicht eine Art Vorinitialisierung dieser Seite, bevor eine Weiterleitung erfolgt. Jetzt lassen Sie uns zu unserem Online-Shop zurückkehren. Wir entwickeln das gleiche Szenario wie in Kapitel 4.3 mithilfe der Komponente ActionLink. Das Template der Startseite des Shops (Listing 4.18) ändert sich minimal, indem ein PageLink durch einen ActionLink ersetzt wird. Die Klasse dieser Seite (Listing 4.19) injiziert eine Instanz der Seite ViewBook, um ein Buch zu setzen, das über den Kontext der Handler-Methode empfangen wurde. Anschließend wird die vorinitialisierte Instanz zurückgegeben. Listing 4.18: Index.tml ${currentBook.title} : ${currentBook.author}
Listing 4.19: Vorinitialisieren einer Seite vor einer Weiterleitung public class Index { ... @InjectPage private ViewBook viewBook; Object onAction(Book book) { viewBook.setBook(book); return viewBook; } }
Beachten Sie, dass Sie zum Setzen eines Buches in die Seite ViewBook eine SetterMethode implementieren müssen. In Listing 4.20 wird Tapestry mithilfe der Annotation @Property informiert, dass keine Setter-Methode generiert werden soll. Außerdem müssen Sie die Eigenschaft book mit der Annotation @Persist versehen, da das gesetzte Buch ansonsten verloren geht. Wenn Sie diese Annotation auslassen und auf den Link zur Seite ViewBook klicken, werden Sie sehen, dass beim Laden der Seite ViewBook eine NullPointerException auftritt, da das gesetzte Buch nicht mehr vorhanden ist. Details über @Persist werden in Kapitel 5 besprochen. An dieser Stelle reicht es zunächst zu wissen, dass der Wert einer mit @Persist annotierten Eigenschaft in der Session abgespeichert wird.
84
4.4 Navigation durch Aktionen
Listing 4.20: ViewBook.java public class ViewBook { @Persist @Property(write=false) private Book book; public void setBook(Book book) { this.book = book; } }
Das Zwischenspeichern des gesetzten Buchs mittels @Persist hat folgende Nachteile: javax.servlet.http.HttpSession wird benötigt, um das gesetzte Buch zwischen zwei
Anfragen aufzubewahren. Die URL der Seite ViewBook kann nicht als Lesezeichen abgespeichert werden. So-
bald die Session abgelaufen ist, ist die Seite nicht mehr benutzbar und präsentiert dem Besucher einen Fehler. Es benötigt mehr Code.
Falls Skalierbarkeit Ihrer Anwendung ein wichtiges Kriterium für Sie darstellt, so sollten Sie diesen Ansatz eher vermeiden. Stattdessen sollten Sie den Aktivierungsmechanismus einer Seite nutzen und die Parameter an Ihre Zielseite mit einer URL übertragen. In Kapitel 4.3 haben Sie gelernt, dass diese Aufgabe von der Komponente PageLink übernommen wird. Wie setzen Sie aber diese Funktionalität innerhalb einer Handler-Methode eines Ereignisses um? Die Lösung ist das Interface org.apache. tapestry5.Link, das eine URL oder URI innerhalb Tapestry repräsentiert. Um einen Link zu erzeugen, müssen Sie den Dienst org.apache.tapestry5. services.PageRenderLinkSource in Ihre Seite wie in Listing 4.21 injizieren. Die Methode createPageRenderLinkWithContext() des Dienstes ist die, die für die Konstruktion eines org.apache.tapestry5.Link zu einer Tapestry-Seite zuständig ist. Diese Methode besitzt zwei Argumente. Das erste Argument ist die Klasse der Seite, zu der ein Link erzeugt werden soll. Das zweite Argument hat eine variable Länge und stellt den Aktivierungskontext der Zielseite dar. Falls der Kontext nicht bereitgestellt wird, wird die onPassivate()-Methode der Zielseite aufgerufen, um den Kontext für die onActivate()Methode zu erzeugen. Sie merken sicherlich, dass die Signatur dieser Methode einige Gemeinsamkeiten mit der Signatur der Komponente PageLink besitzt. In der Tat benutzt die Komponente PageLink die gleiche Funktionalität wie der Dienst PageRenderLinkSource. Rufen Sie diese Methode auf, um einen Link zu erzeugen, den Sie dann als Rückgabewert der Handler-Methode verwenden. Die Annotation @Persist kann nun aus der Seite ViewBook entfernt werden, da der Zustand dieser Seite nicht mehr in der HttpSession gespeichert werden muss. Stattdessen muss die Eigenschaft book wieder wie in Listing 4.14 mit der Annotation @PageActivationContext versehen
85
4 Navigation zwischen Seiten
werden. Wenn Sie die Startseite in Ihrem Browser aufrufen und einen der Links klicken, werden Sie erkennen, dass die URL der Zielseite wieder eine clientseitige Repräsentation des übergebenen Kontextes besitzt. Damit ermöglichen Sie den Benutzern Ihrer Anwendung, Lesezeichen auf die Seite ViewBook anzulegen. Listing 4.21: Erzeugen eines Links mithilfe von PageRenderLinkSource public class Index { ... @Inject private PageRenderLinkSource pageRenderLinkSource; Object onAction(Book book) { Link link = pageRenderLinkSource.createPageRenderLinkWithContext( ViewBook.class, book); System.out.println(link.toAbsoluteURI()); return link; } }
4.5
Zusammenfassung
Unterschiedliche Seiten einer Tapestry-Anwendung können miteinander durch Hyperlinks verknüpft werden. Dabei kann die Navigation auf unterschiedliche Weise realisiert werden: Navigation durch die Komponente PageLink. Navigation durch Aktionen.
Bei der Navigation durch Aktionen ist der Rückgabewert einer Handler-Methode dafür verantwortlich, auf welche Weise die Antwort erzeugt wird. Im Falle einer voidHandler-Methode oder der Rückgabe eines null-Wertes ist die aktuelle Seite für das Erzeugen der Antwort zuständig. Weiterhin sind folgende Rückgabetypen möglich: Klasse oder Klassenname einer Seite. Injizierte Instanz einer Seite. org.apache.tapestry5.Link.
Jede Seite kann einen Aktivierungskontext besitzen, der zum Wiederherstellen des Seitenzustands benutzt wird. Zur Transformation zwischen server- und clientseitigen Repräsentation eines Aktivierungskontextes kann der Dienst ValueEncoder benutzt werden. Ferner kann Tapestry die Erzeugung des Kontexts übernehmen, sodass die Anwendungsentwickler weniger Code schreiben müssen.
86
5
Entwicklung von zustandsbehafteten Anwendungen
Die meisten Webanwendungen sind zustandsbehaftet. Im Falle eines Online-Shops loggt sich ein Käufer ein, schaut sich einige Artikel an, fügt einen oder anderen Artikel seinem Warenkorb zu und geht zur Kasse. Nach der Abwicklung eines Ankaufs loggt sich der Käufer wieder aus. Damit mehrere Interaktionen eines Käufers sich aufeinander beziehen, muss der Kaufvorgang mit einem Zustand behaftet werden. Das HTTP (Hypertext Transfer Protocol) ist jedoch zustandslos: D.h., jede Anfrage eines Clients an einen Server wird isoliert und unabhängig von vorherigen Anfragen bearbeitet. Wie kann dann ein Server wissen, welche Artikel in dem Warenkorb eines Käufers enthalten sind? Die Standardlösungen für dieses Problem sind Cookies, URLRewriting und verborgene Formularfelder. In der Servlet-API wird die Zustandsbehaftung von dem Interface javax.servlet.http.HttpSession gekapselt, sodass ein Entwickler nicht mit Cookies oder URL-Rewriting konfrontiert werden muss. Beim Einsatz von Tapestry müssen Sie sich nicht mit der Servlet-API auseinandersetzen, wenn Sie Sitzungsverwaltung umsetzen möchten. Stattdessen müssen Sie den Lebenszyklus von Tapestry-Seiten verstehen. Dieser wird in diesem Kapitel vorgestellt. Für Leser ohne Erfahrung in Servlet-API wird am Anfang des Kapitels der Lebenszyklus von Servlets und JSPs kurz angesprochen, um den Unterschied zum Zyklus von Tapestry-Seiten zu verdeutlichen. Anschließend wird erläutert, wie Tapestry-Anwendungen mit Zustand behaftet werden können. Neben den Variablen, die den Zustand einer Seite oder einer Komponente darstellen, existiert in Tapestry der Begriff eines Session State Objects, der einen applikationsweiten Zustand einer Anwendung darstellt.
5.1
Lebenszyklus eines Servlets
Von jedem Servlet wird nur eine einzelne Instanz angelegt, die für die Behandlung aller Benutzeranfragen an dieses Servlet zuständig ist. Jede neue Anfrage eines Clients wird in einem neuen Thread abgewickelt, sodass eine parallele Verarbeitung mehrerer Anfragen möglich ist. Da alle Threads auf die gleiche Instanz eines Servlets zugreifen, sollte sichergestellt sein, dass die Instanzvariablen eines Servlets keine anfragespezifische Informationen enthalten. Es ist die Aufgabe des Servlet-Entwicklers, den Zugriff auf die Instanzvariablen zu synchronisieren.
5 Entwicklung von zustandsbehafteten Anwendungen
Sobald ein Servlet-Container wie Tomcat ein Servlet erzeugt hat, ruft er die Methode init() des erzeugten Servlets auf. Diese Methode wird zur einmaligen Initialisierung des Servlets benutzt. Beispielsweise können in dieser Methode Ressourcen wie Datenbankverbindung belegt werden. Wann immer ein Server eine Benutzeranfrage erhält, ruft er in einem neuen Thread die Methode service() des Servlets auf. Diese Methode delegiert den Aufruf abhängig vom HTTP-Anfragetyp (GET, POST, PUT oder DELETE) an eine der Methoden doGet(), doPost(), doPut() oder doDelete(). Kurz bevor die Singleton-Instanz eines Servlets zerstört wird, ruft der Server die Methode destroy() auf, in der die belegten Ressourcen freigegeben werden können.
5.2
Lebenszyklus einer JavaServer Page
Eine JavaServer Page wird von einem Container wie Tomcat zu einem Servlet kompiliert und besitzt somit den gleichen Lebenszyklus wie ein Servlet. Mit der Version 1.1 wurden in JSP die Tag Handler eingeführt, die die Möglichkeit bieten, eigene JSP-Tags zu definieren. Bei einem Tag Handler handelt es sich um eine Java-Klasse, die einen vordefinierten Tag innerhalb einer JSP behandelt. Tag Handler können zu Bibliotheken gebündelt werden und sind mit Tapestry-Komponenten vergleichbar. Aus Performancegründen wird ein Tag Handler nicht jedes Mal neu erzeugt, sobald die JSP-Engine ein Tag im Rumpf einer JSP entdeckt hat. Stattdessen werden Tag Handler in einem Pool verwaltet, aus dem Sie bei Bedarf entnommen werden können. Eine Instanz eines Tag Handlers wird von der JSP-Engine erst dann benutzt, wenn der Handler von einer vorangegangenen Benutzung in den Pool zurückkehrt. Kurz bevor der Container einen Tag Handler freigibt, ruft er die Methode release() des Handlers auf. Diese Methode wird zum Freigeben der vom Handler belegten Ressourcen eingesetzt.
5.3
Lebenszyklus einer Tapestry-Seite
Im Gegensatz zu Servlets und JavaServer Pages sind Tapestry-Seiten keine Singletons. Stattdessen wird eine Seite exklusiv an eine Benutzeranfrage und damit an einen Thread gebunden. Ähnlich wie Tag Handler in JSP werden Seiten in einem Pool verwaltet, in dem sie bis zu ihrem Einsatz verweilen. Der Pool ist ein assoziativer Speicher, in dem jede Seite durch ihren Namen und eine java.util.Locale identifiziert wird. Von jeder Seite können mehrere Instanzen im Pool vorhanden sein, wobei die Anzahl dieser Seiten konfiguriert werden kann. Alle Instanzen einer Seite innerhalb des Pools sind äquivalent. Der Lebenszyklus einer Tapestry-Seite sieht wie folgt aus: Falls eine Seite benötigt wird, sucht Tapestry im Pool nach einer freien Instanz. Falls eine freie Instanz vorhanden ist, wird sie aus dem Pool entnommen und der
aktuellen Benutzeranfrage zugewiesen.
88
5.3 Lebenszyklus einer Tapestry-Seite
Falls die Anzahl der Instanzen einer Seite im Pool unter dem Softlimit (untere Ober-
grenze) ist, wird eine neue Instanz dieser Seite unverzüglich erzeugt und der aktuellen Anfrage zugewiesen. Der Standardwert liegt bei fünf Instanzen. Erreicht die Anzahl der Instanzen einer Seite im Pool das Softlimit, verzögert Tapes-
try die Erzeugung der neuen Instanz einer Seite. Anstatt eine neue Instanz zu konstruieren, wartet das Framework eine vorgegebene Zeit (per Default 10 ms) auf eine frei werdende Instanz. Ist diese Zeit verstrichen, ohne dass eine freie Instanz in den Pool zurückgekehrt ist, wird eine neue Instanz erzeugt. Erreicht die Anzahl der Instanzen einer Seite im Pool den Hardlimit (die obere
Obergrenze), werden keine neuen Instanzen mehr erzeugt. Wird eine weitere Instanz verlangt, wirf Tapestry eine Exception. Der Standardwert liegt bei 20 Instanzen. Wird eine Seiteninstanz freigegeben, wandert diese in den Pool zurück.
Die Zeit, die Tapestry beim Erreichen des Softlimits vor der Erzeugung einer neuen Instanz abwartet, ist ein sinnvoller Mechanismus, um die Anzahl der erzeugten Instanzen zu minimieren. Das Wiederverwenden von bereits erzeugten Instanzen stellt sicher, dass das Hardlimit erst dann erreicht wird, wenn die Anwendung unter Last steht. Bei Applikationen, für die Reaktionsgeschwindigkeit ein wichtiges Kriterium ist, sollten Sie sowohl das Soft- als auch das Hardlimit vergrößern. Die Soft-Wartezeit sollten Sie dagegen verkürzen. Die Konfiguration des Pools wird im Kapitel über Tapestry IoC besprochen. Außerdem prüft Tapestry periodisch, ob eine Seiteninstanz schon länger als eine bestimmte Verweildauer im Seitenpool verbringt, und verwirft sie bei Ablauf der Verweildauer automatisch. Die Standardverweildauer beträgt zehn Minuten.
5.3.1
Teilnehmen am Lebenszyklus einer Tapestry-Seite
Wenn eine Seite zum ersten Mal benötigt wird, wird sie geladen. Beim Ladevorgang werden die Seite und alle enthaltenen Komponenten instanziiert sowie das Template geparst. Nachdem die Seite geladen wurde, löst das Framework das Ereignis pageLoaded aus. In einer Handler-Methode, die dieses Ereignis behandelt, kann eine einmalige Initialisierung vorgenommen werden. Gemäß der Namenskonvention für die Ereignisse des Lebenszyklus von Komponenten kann diese Handler-Methode den Namen pageLoaded() tragen. Sie darf keinen Parameter und keinen Rückgabewert besitzen. In Listing 5.1 wird in der Methode pageLoaded() der Text einer Begrüßungsnachricht der Eigenschaft helloMessage zugewiesen. Damit diese Nachricht bei der Rückkehr der Seite in den Pool nicht verloren geht, wird die Eigenschaft mit @Retain annotiert. Weitere Details über diese Annotation folgen.
89
5 Entwicklung von zustandsbehafteten Anwendungen
Namenskonvention für Lebenszyklus-Methoden In einer Tapestry-Anwendung kann es eine große Menge von Ereignissen geben, die entweder durch das Framework oder durch Ihren Code ausgelöst werden. All diese Ereignisse dienen der Kommunikation zwischen Komponenten und stellen eine Erweiterung der öffentlichen API der Komponenten dar. Lebenszyklus-Ereignisse von Seiten sind dagegen eher interner Natur. Sie werden speziell für eine Seite ausgelöst und nicht durch die Komponentenhierarchie weitergereicht. Ein weiterer Unterschied gegenüber gewöhnlichen Handler-Methoden besteht darin, dass Lebenszyklus-Methoden keine Parameter und keinen Rückgabetyp besitzen. Um diesen wichtigen Unterschied hervorzuheben, wurde für sie eine andere Namenskonvention vereinbart. Der Name einer Lebenszyklus-Methode entspricht exakt dem Namen des Ereignisses, das sie behandelt. So ist beispielsweise eine Methode mit dem Namen pageLoaded() für die Behandlung des Ereignisses pageLoaded zuständig. Listing 5.1: Behandelt das Ereignis pageLoaded public class Index { @Retain private String helloMessage; void pageLoaded() { helloMessage = "Hello World!!!"; } }
Alternativ können Sie Ihre Handler-Methode mit der Annotation @PageLoaded versehen. In diesem Fall müssen Sie die Namenskonvention nicht befolgen und können Ihre Methode beliebig benennen. Die Seite in Listing 5.2 ist äquivalent zu der in Listing 5.1. Listing 5.2: Behandelt das Ereignis pageLoaded public class Index { @Retain private String helloMessage; @PageLoaded void foo() { helloMessage = "Hello World!!!"; } }
90
5.3 Lebenszyklus einer Tapestry-Seite
Sobald eine Seite geladen wurde, kann sie der aktuellen Benutzeranfrage und damit dem aktuellen Thread zugewiesen werden. Sofort nach der Zuweisung wird das Ereignis pageAttached ausgelöst, das zur anfragespezifischen Initialisierung der Seite verwendet werden kann. In Listing 5.3 wird die Locale der aktuellen Anfrage der Eigenschaft locale zugewiesen. Die Locale wird mithilfe des Dienstes Request bezogen, der mittels @Inject in eine Seite injiziert werden kann. Der Dienst Request ist das Pendant zu javax.servlet.http.HttpServletRequest aus der Servlet-API und zu javax.portlet.PortletRequest aus der Portlet-API und dient der Vereinheitlichung der beiden APIs in Tapestry. Die gespeicherte Locale ist eine anfragespezifische Information, sie ist also nur für die aktuelle Anfrage gültig. Sobald diese abgearbeitet wurde, wird der Wert der Eigenschaft bereinigt, bevor die Seite in den Pool zurückkehrt. Dies ist notwendig, damit die Seite für die nächste Anfrage, die möglicherweise aus einem anderen Land versendet wird, in ihrem Ursprungszustand vorliegt. Listing 5.3: Behandelt das Ereignis pageAttached public class Index { ... @Inject private Request request; private Locale locale; void pageAttached() { locale = request.getLocale(); } ... }
Auch für das Ereignis pageAttached existiert die Annotation @PageAttached, die anstelle der Namenskonvention eingesetzt werden kann. Ein Beispiel ist in Listing 5.4 zu sehen. Listing 5.4: Behandelt das Ereignis pageAttached public class Index { ... @Inject private Request request; private Locale locale; @PageAttached void bar() {
91
5 Entwicklung von zustandsbehafteten Anwendungen
locale = request.getLocale(); } ... }
Sobald die Anfrage an eine Seite verarbeitet wurde, wird die Seite dieser Anfrage entzogen und freigegeben. Kurz bevor die freigegebene Instanz in den Pool zurückkehrt, wird das Ereignis pageDetached ausgelöst. Dieses Ereignis kann analog zu pageAttached durch eine Methode behandelt werden, die entweder den Namen pageDetached() trägt oder mit @PageDetached annotiert ist. Das Ereignis pageDetached kann benutzt werden, um den Zustand einer Seite zu bereinigen, bevor sie in den Pool zurückkehrt. Wird in der Handler-Methode für pageDetached eine Exception geworfen, nimmt Tapestry an, dass die Bereinigung der Seiteninstanz nicht abgeschlossen werden konnte. Der Zustand dieser Instanz wird als dirty markiert, sodass sie verworfen wird und nicht in den Pool zurückkehrt. In Listing 5.1 ist das Beispiel einer Seite zu sehen, deren Instanzen niemals in den Pool zurückkehren werden. Nach einmaligem Einsatz wird die Instanz zerstört. Listing 5.5: Wird niemals in den Pool zurückkehren public class Index { void pageDetached() { throw new RuntimeException("Page is dirty"); } }
5.3.2
Wann werden pageLoaded(), pageAttached() und pageDetached() aufgerufen?
Die Methode pageLoaded() wird einmalig aufgerufen, nachdem eine Instanz einer Seite erzeugt wurde. Die beiden Methoden pageAttached() bzw. pageDetached() werden aufgerufen, wenn: ein Client eine Seite im Browser aufruft und Tapestry eine Antwort für diese An-
frage erzeugt. ein PageLink angeklickt wird. ein ActionLink angeklickt wird. ein EventLink angeklickt wird. eine Ajax-Anfrage (Asynchronous JavaScript and XML) abgeschickt wird. ein Formular abgesendet wird.
92
5.3 Lebenszyklus einer Tapestry-Seite
Das Listing 5.6 enthält das Template der Seite Lyfecycle. Die dazugehörige Klasse ist in Listing 5.7 zu sehen. Wenn Sie diese Seite zum ersten Mal aufrufen, können Sie auf der Konsole erkennen, dass Tapestry die Methoden pageLoaded(), pageAttached() und pageDetached() aufgerufen hat. Beim wiederholten Aufruf der Seite wird die Methode pageLoaded() nicht mehr aufgerufen, da eine freie Instanz dieser Seite im Pool zu finden ist. Listing 5.6: Seite mit einem ActionLink Klicke mich.
Listing 5.7: Lyfecycle.java public class Lyfecycle{ void pageLoaded(){ System.out.println("pageLoaded()"); } void pageAttached() { System.out.println("pageAttached()"); } void pageDetached() { System.out.println("pageDetached()"); } void onAction() { System.out.println("onAction()"); } }
Nun klicken Sie auf den Link. Sie sollten auf der Konsole eine Ausgabe wie in der Abbildung 5.1 sehen. Anhand der Log-Ausgaben können Sie erkennen, dass zwei Anfragen an den Server abgeschickt wurden. Durch den Klick auf den Link haben Sie eine Anfrage an die URL /hello-world/lifecyle.actionlink ausgelöst. Um das Ereignis action des ActionLink zu behandeln, hat Tapestry eine Instanz der Seite aus dem Pool geholt, um deren Methode onAction() aufzurufen. Anschließend hat Tapestry diese Instanz freigegeben. Nachdem das Ereignis behandelt wurde, muss Tapestry eine Antwort erzeugen. Da die Methode onAction() keinen Rückgabetyp enthält, wird die Seite Lyfecylce genutzt, um diese Antwort zu rendern. Dazu holt Tapestry erneut eine Instanz der Seite Lyfecylce aus dem Pool. Nachdem die Antwort erzeugt wurde, gibt Tapestry die ausgeheckte Seiteninstanz wieder frei.
93
5 Entwicklung von zustandsbehafteten Anwendungen
Abbildung 5.1: Ausgabe auf der Konsole
5.4
Verwalten des Seitenzustandes
Lassen Sie uns das Template der Seite Lyfecycle aus dem Listing 5.6 um einen Kontext für die Komponente ActionLink wie in Listing 5.8 erweitern. In der Handler-Methode wird der Kontext über den Parameter dieser Methode empfangen und der Eigenschaft pi der Seitenklasse zugewiesen (Listing 5.9). Aus dem Template heraus wird auf den Wert dieser Eigenschaft über die Expansion ${pi} zugegriffen. Nun laden Sie die Seite in Ihrem Browser, und klicken Sie auf den Link. Auf der Konsole können Sie erkennen, dass die Methode onAction() aufgerufen wurde. Folglich wurde der übermittelte Kontext der Eigenschaft pi zugewiesen. Doch warum wird die Zahl 3.14159 nicht im Browser angezeigt? Die Antwort ist einfach. Sie erinnern sich sicherlich, dass Tapestry bei einem Klick auf einen ActionLink die jeweilige Seite zwei Mal aus dem Pool auschecken muss. In einer Anwendung, die unter Last steht, wird höchstwahrscheinlich eine andere Instanz der Seite Lyfecycle zum Erzeugen der Antwort benutzt als die, die Tapestry zum Behandeln des Ereignisses action verwendet hat. Aus diesem Grund geht der übermittelte Wert verloren. Um diesen Wert zu retten, müssen Sie ihn zwischen zwei Anfragen in einem Zwischenspeicher ablegen. Dies erfolgt in Tapestry mithilfe der Annotation @Persist. Listing 5.8: Lifycycle.tml Klicke mich.
Listing 5.9: Lifecycle.java public class Index{ @Property private Double pi; void onAction(Double context) {
94
5.4 Verwalten des Seitenzustandes
System.out.println("onAction("+context+")"); } }
Die Annotation @Persist informiert Tapestry, dass der Wert einer Seiteneigenschaft zwischen zwei Anfragen gespeichert werden soll. Versehen Sie die Eigenschaft pi mit der Annotation @Persist wie in Listing 5.10, und laden Sie die Seite neu. Sie werden sehen, dass nach einem Klick auf den ActionLink der Wert des Kontextes wie erwartet im Browser angezeigt wird. Listing 5.10: Zwischenspeichern einer Seiteneigenschaft public class Index{ @Persist @Property private Double pi; ... }
Um die Eigenschaft pi zwischenzuspeichern, leistet das Framework hinter den Kulissen viel Arbeit. Beispielsweise überwacht Tapestry den Schreibzugriff auf diese Eigenschaft. Sobald der Wert von pi geändert wird, speichert Tapestry diesen in dem ausgewählten Zwischenspeicher (siehe Abschnitt 5.4.1). Kurz bevor die Seite in den Pool zurückkehrt, werden die Werte der mit @Persist annotierten Eigenschaften aus der jeweiligen Seiteninstanz verworfen. Sobald eine neue Instanz dieser Seite vom gleichen Client angefragt wird, werden die Werte aus dem Zwischenspeicher wieder in die Eigenschaften der Seite geschrieben, und die Seite wird der Benutzeranfrage zugewiesen.
5.4.1
Strategien zum Zwischenspeichern der Eigenschaften
Als Standard wählt Tapestry die HttpSession als Zwischenspeicher für die zu speichernden Eigenschaften. Falls die Session bis dahin nicht erstellt wurde, wird Tapestry dies erledigen. Da in einer HttpSession ein Attribut unter einem eindeutigen Schlüssel abgelegt wird, erzeugt Tapestry diesen Schlüssel aus einem internen Präfix, dem Namen der Seite, dem Namen der geschachtelten Komponente und dem Namen der Eigenschaft. Für die Eigenschaft pi der Seite Lifecycle würde Tapestry den Schlüssel state:Lifecycle::pi wählen. An dieser Stelle sei darauf hingewiesen, dass die Erzeugung eines Schlüssels für eine mit @Persist versehene Eigenschaft zur internen Funktionalität des Frameworks gehört. Sie sollten niemals den Wert einer Eigenschaft direkt aus der HttpSession holen, da der Algorithmus zum Berechnen des eindeutigen Schlüssels sich ändern kann. Sie würden sich unnötige Arbeit bei einem Upgrade auf eine neue Version von Tapestry machen.
95
5 Entwicklung von zustandsbehafteten Anwendungen
Neben javax.servlet.http.HttpSession unterstützt Tapestry weitere Strategien zur Zustandsbehaftung von Seiteneigenschaften: session: Speichert der Wert einer Eigenschaft innerhalb der HttpSession. flash: Wie bei session werden die Werte der Eigenschaften in der HttpSession abge-
legt. Der Unterschied ist, dass ein Wert nicht für die Lebensdauer der Session gespeichert wird. Stattdessen überlebt dieser Wert in HttpSession, bis er zum Wiederherstellen des Zustands einer Instanz der gleichen Seite herangezogen wird. Mit anderen Worten sorgt diese Strategie dafür, dass ein Wert beim erstmaligen Zugriff aus der Session entfernt wird. Typischerweise wird diese Strategie zum Speichern von Meldungen an den Benutzer verwendet, da diese normalerweise nur ein einziges Mal erscheinen sollen. Ein Beispiel ist in Listing 5.11 zu sehen. client: Der Zustand einer Seite wird beim Client abgespeichert. Tapestry seriali-
siert die Eigenschaft einer Seite und fügt sie als einen Anfrageparameter an jede URL zu dieser Seite hinzu. Diese Strategie sollte nur in seltenen Fällen verwendet werden. Da die Anzahl der Zeichen einer URL begrenzt ist, stellt diese Strategie ein mögliches Problem für die Skalierung Ihrer Anwendung dar. entity: Speichert den Primärschlüssel einer Hibernate-Entität in der HttpSession ab.
Diese Strategie ist ein Teil der Hibernate-Integration und wird später in Kapitel 14 behandelt. Zur Auswahl einer anderen Strategie für eine Eigenschaft muss der Name dieser Strategie in der Annotation @Persist angegeben werden. So wird in Listing 5.11 durch einen Klick auf ein ActionLink (Listing 5.12) in der Methode onAction() eine Nachricht erzeugt, deren Inhalt mit der Strategie flash zwischengespeichert wird. In der Klasse PersistenceConstants sind die Namen aller erhältlichen Strategien als Konstanten definiert. Listing 5.11: Flash.java (Beispiel der Flash-Strategie) public class Flash{ @Persist(PersistenceConstants.FLASH) @Property private String message; void onAction() { message = "Sie haben den Prozess gestartet."; ... } }
Listing 5.12: Flash.tml (Beispiel der Flash-Strategie) ${message}
96
5.4 Verwalten des Seitenzustandes
Zum Starten des Prozesses klicke hier.
Falls Sie keine explizite Strategie angegeben haben, wird Tapestry die Standardstrategie session verwenden. Diese Einstellung kann lokal überschrieben werden, indem der Name der gewünschten Strategie mithilfe der Annotation @Meta angegeben wird. Diese Annotation dient der Bereiststellung von Metadaten für Seiten oder Komponenten. In Listing 5.13 wird die Metainformation mit dem Schlüssel tapestry.persistence-strategy und dem Wert flash bereitgestellt. Dies bewirkt, dass Tapestry zum Speichern des Zustands der Seite FlashDefault die Strategie flash wählt, falls keine angegeben wurde. Falls eine Seite geschachtelte Komponenten besitzt, wird diese Einstellung von diesen Komponenten übernommen, wenn sie selbst keine Strategie explizit gesetzt haben. Listing 5.13: Definiert flash als Default-Strategie zum Speichern der Eigenschaften @Meta("tapestry.persistence-strategy=flash") public class FlashDefault{ @Persist @Property private String message; }
Übrigens, der Name der Metainformation tapestry.persistence-strategy ist als Konstante SymbolConstants.PERSISTENCE_STRATEGY definiert.
5.4.2
Verwerfen des Zustands einer Seite
Der Zustand einer Seite kann programmatisch verworfen werden. Dazu muss der Dienst ComponentResources injiziert werden. Dieser Dienst stellt einer Seite oder einer Komponente die vom Framework bereitgestellten Ressourcen zur Verfügung. Unter anderem wird die Methode discardPersistentFieldChanges() bereitgestellt, mit deren Hilfe in Listing 5.14 der Zustand der Seite bereinigt wird. Listing 5.14: Verwerfen des Seitenzustandes public class DiscardDemo{ @Inject private ComponentResources componentResources; void onActionFromDiscard() {
97
5 Entwicklung von zustandsbehafteten Anwendungen
componentResources.discardPersistentFieldChanges(); } }
Der Aufruf der Methode discardPersistentFieldChanges() innerhalb einer Seite sorgt dafür, dass auch der Zustand von eventuell vorhandenen geschachtelten Komponenten einer Seite verworfen wird.
5.4.3
Unterbinden des Verwerfens des Seitenzustands
In Abschnitt 5.3.1 wurde angesprochen, dass die Annotation @Retain dazu verwendet werden kann, das Verwerfen der Werte von Seiteneigenschaften vor der Rückkehr in den Seitenpool zu unterbinden. Wenn Tapestry eine mit @Retain versehene Eigenschaft erkennt, wird der Wert dieser Eigenschaft nicht bereinigt, sodass die Seite diesen Wert mit in den Pool übernimmt. Bei der nächsten Anfrage durch einen anderen Client kann die gleiche Seiteninstanz aus dem Pool geholt werden, weshalb den mit @Retain annotierten Eigenschaften keine benutzerspezifischen Daten zugewiesen werden sollten. Vielmehr wird @Retain für Daten verwendet, die lazy, also nur bei Bedarf, geladen werden.
5.5
Verwaltung des Anwendungszustandes
In vielen Anwendungen sollten nicht nur einzelne Seiten mit Zustand behaftet sein, sondern die gesamte Anwendung. Wenn Sie zwischen unterschiedlichen Seiten Daten austauschen möchten, dann ist das Zwischenspeichern einzelner Seiteneigenschaften wenig geeignet. Diese Eigenschaften sind nur innerhalb der einzelnen Seite sichtbar und können nicht von mehreren Seiten gemeinsam genutzt werden. Stellen Sie sich vor, Sie möchten einen Warenkorb für einen Online-Shop implementieren. Ein Warenkorb ist an einen einzelnen Benutzer gebunden und wird in den meisten Seiten eines Shops (Artikelauswahl, Warenkorbansicht, Kasse usw.) benötigt. In Tapestry wird so ein anwendungsweites Objekt als Session State Object (kurz SSO) bezeichnet. Listing 5.15: Beispiel eines Session-State-Objects public class Shop{ @SessionState private ShoppingCart cart; }
98
5.5 Verwaltung des Anwendungszustandes
In Listing 5.15 wird in die Seite Shop ein Session State Object vom Typ ShoppingCart mittels der Annotation @SessionState injiziert. Standardmäßig wird ein SSO in der HttpSession gespeichert. In Gegensatz zu persistierbaren Eigenschaften von Seiten wird für ein SSO kein eindeutiger Schlüssel zum Abspeichern in der HttpSession berechnet. Der Name der Klasse eines SSOs stellt diesen Schlüssel dar. Damit können Sie von jeder Klasse nur eine einzige Instanz als Session State Object abspeichern. Session State Object vs. Application State Object In Tapestry 5.0 wurde ein Session State Object als Application State Object (ASO) bezeichnet. Zum Markieren einer Seiten- oder Komponenteneigenschaft als ein ASO wurde die Annotation @ApplicationState eingesetzt. Dies führte dazu, dass viele Entwickler mit Erfahrung bezüglich Servlets und JavaServer Pages diesen Begriff fälschlicherweise für ein Analogon von Application Scope aus der JSPAPI gehalten haben. Der Begriff Application State Object bzw. Session State Object ist nicht mit dem Begriff Application Scope aus der JSP-API zu verwechseln. Ein Wert, der innerhalb eines Application Scopes abgelegt wurde, ist im javax.servlet.ServletContext abgespeichert und ist somit für alle Benutzer der Anwendung sichtbar. In Tapestry dagegen wird ein ASO bzw. SSO standardmäßig in der javax.servlet.http.HttpSession abgelegt und ist damit einem einzelnen Benutzer exklusiv zugewiesen. In Tapestry 5.1 wurde die Annotation @ApplicationState als deprecated markiert und die neue Annotation @SessionState eingeführt. Zurzeit können die beiden Annotationen als äquivalent angesehen werden, doch langfristig sollte @SessionState benutzt werden. Konsequenterweise sollten auch alle zugrunde liegenden Dienste umbenannt werden (beispielsweise ApplicationStateManager, in SessionStateManager). Um die Rückwärtskompatibilität mit bestehenden Tapestry-Anwendungen zu gewährleisten, wurde jedoch entschieden, dass keiner dieser Dienste umbenannt wird.
5.5.1
Erzeugen eines Session State Objects
In Listing 5.15 wurde ein Session State Object aus einer Session in eine Seite injiziert. Doch wann wird ein SSO erzeugt? Dies erfolgt automatisch beim ersten Zugriff auf die Seiten- oder Komponenteneigenschaft, die mit der Annotation @SessionState markiert ist. Typischerweise handelt es sich bei einem SSO um eine JavaBean, die einen öffentlichen Konstruktor ohne Argumente besitzt. Die automatische Erzeugung eines SSO ist hilfreich, da Sie Ihr SSO niemals auf null überprüfen müssen. Sie greifen einfach auf die Eigenschaften des SSO zu, ohne dass eine NullPointerExeption auftreten kann. Die Überprüfung auf die Existenz des Waren-
99
5 Entwicklung von zustandsbehafteten Anwendungen
korbs in Listing 5.16 macht sogar keinen Sinn, da eine Instanz von ShoppingCart sofort erzeugt wird, sobald die Anweisung if(cart==null) ausgeführt wird. Somit kann ein SSO niemals null sein. Listing 5.16: Erzeugung eines SSO durch eine Überprüfung auf null public class Shop{ @SessionState private ShoppingCart cart; void pageAttached() { if(cart == null){ } } }
Um die automatische Erzeugung eines SSO zu verhindern, können Sie den Parameter create der Annotation @SessionState auf false setzen. In Listing 5.17 wird das SSO ShoppingCart nicht erzeugt, sobald Sie im Code auf die Eigenschaft cart zugreifen. In diesem Fall müssen Sie die Erzeugung von ShoppingCart selbst übernehmen. Listing 5.17: Verhindern der automatischen Erzeugung eines SSO public class Shop{ @SessionState(create=false) private ShoppingCart cart; }
5.5.2
Überprüfung der Existenz eines Session State Objects
Die Überprüfung der Existenz eines SSO macht nur dann Sinn, wenn dieses nicht von Tapestry automatisch erzeugt wird. Wenn Sie die automatische Erzeugung eines SSO bevorzugen und trotzdem eine Existenzüberprüfung durchführen möchten, können Sie der Klasse eine weitere Eigenschaft hinzufügen, deren Wert das Ergebnis der Überprüfung enthält. Falls Tapestry in einer Seitenklasse eine boolesche Eigenschaft findet, deren Name sich aus dem Namen einer Eigenschaft, die ein SSO darstellt, und dem Suffix Exists zusammensetzt, wird die Überprüfung auf null durchgeführt. In Listing 5.18 wird die Überprüfung des SSO vom Typ ShoppingCart auf den null-Wert automatisch von Tapestry durchgeführt und das Ergebnis dieser Überprüfung der Eigenschaft cartExists zugewiesen, da der Name dieser Eigenschaft sich aus dem Namen der Eigenschaft cart und dem Suffix Exist zusammensetzt.
100
5.5 Verwaltung des Anwendungszustandes
Listing 5.18: Überprüfung der Existenz eines SSO public class Shop{ @SessionState private ShoppingCart cart; private boolean cartExists; }
5.5.3
Arbeiten mit dem Dienst ApplicationStateManager
Der Dienst ApplicationStateManager stellt die Funktionalität zur Verwaltung von Session State Objects bereit. Sie können diesen Dienst alternativ zur Annotation @SessionState einsetzen. ApplicationStateManager leistet dabei Folgendes: Erzeugen, Überschreiben und Löschen von SSOs. Existenzprüfung von SSOs. Auslesen eines SSO.
In Listing 5.19 wird der Dienst ApplicationStateManager in die Seite Shop injiziert und zum Auslesen des SSO ShoppingCart eingesetzt. Die Methode get() des Dienstes wird das SSO erzeugen, falls es noch nicht existiert, sodass sie niemals einen null-Wert zurückgibt. Listing 5.19: Auslesen eines SSO mit ApplicationStateManager public class Shop{ @Inject private ApplicationStateManager applicationStateManager; private ShoppingCart cart; void pageAttached() { cart = applicationStateManager.get(ShoppingCart.class); } }
Zum Auslesen eines SSO, ohne es zu erzeugen, soll die Methode getIfExists() verwendet werden. Mit der Methode set() kann ein neues SSO angelegt oder ein bestehendes ersetzt werden. Diese Methode erwartet die Klasse des SSO und die anzulegende Instanz dieser Klasse als Parameter. Durch Angabe des Wertes null wie in Listing 5.20 wird ein SSO entfernt.
101
5 Entwicklung von zustandsbehafteten Anwendungen
Listing 5.20: Löschen eines SSO public class Shop{ ... void pageAttached() { cart = applicationStateManager .getIfExists(ShoppingCart.class); if(cart != null){ applicationStateManager.set( ShoppingCart.class, null); } } }
5.5.4
Eingreifen in den Prozess der Instanziierung eines Session State Objects
Es wurde bereits erwähnt, dass ein Session State Object typischerweise eine JavaBean ist, die von Tapestry durch den Aufruf des Standard-Konstruktors erzeugt wird. Doch manchmal ist es notwendig, ein SSO während der Konstruktion mit bestimmten Werten zu belegen oder durch Aufruf einer Methode zu initialisieren. Dies kann mithilfe des Dienstes ApplicationStateManager realisiert werden, indem die erzeugte Instanz eines SSO in die Methode set() übergeben wird. Ein etwas eleganterer Ansatz ist die Implementierung einer Contribute-Methode wie in Listing 5.21, mit der Sie an der Konfiguration des Dienstes ApplicationStateManager teilnehmen können. Sie müssen den Code aus dem Listing 5.21 zu diesem Zeitpunkt nicht verstehen. Sobald Sie das Kapitel über Tapestry-IoC durchgelesen haben, blättern Sie einfach zu diesem Beispiel zurück. Sie werden erkennen, dass die Konfiguration des Dienstes ApplicationStateManager einen assoziativen Speicher darstellt, indem die Klassen der SSOs auf Instanzen von ApplicationStateContribution abgebildet werden. Ein ApplicationStateContribution definiert, wie ein SSO erzeugt werden kann und mit welcher Strategie es gespeichert wird. Die eigentliche Erzeugung erfolgt durch eine Instanz von ApplicationStateCreator. In Listing 5.21 wird eine anonyme Implementierung von ApplicationStateCreator erzeugt, in der eine neue Instanz von ShoppingCart konstruiert wird. Der erzeugte ApplicationStateCreator wird einer Instanz von ApplicationStateContribution übergeben, die die erzeugten SSOs in der Session abspeichert. Sobald ApplicationStateManager eine neue Instanz von ShoppingCart konstruieren muss, wird er in seiner Konfiguration anhand der Klasse des SSO nach einer bereitgestellten ApplicationStateContribution suchen. Falls ein Eintrag gefunden wird, wird ein SSO wie dort hinterlegt erzeugt.
102
5.6 Zusammenfassung
Listing 5.21: Explizite Erzeugung eines Session State Objects public class AppModule { ... public void contributeApplicationStateManager( MappedConfiguration config){ ApplicationStateCreator creator = new ApplicationStateCreator() { public ShoppingCart create(){ return new ShoppingCart(new Date()); } }; config.add( ShoppingCart.class, new ApplicationStateContribution("session", creator)); } }
5.6
Zusammenfassung
Im Gegensatz zu Servlets und JavaServer Pages sind Tapestry-Seiten keine Singletons, sondern werden in einem Pool verwaltet, aus dem sie geholt und einer Anfrage exklusiv zugeordnet werden. Kurz nach dem Holen werden die Eigenschaften einer Seite mit benutzerspezifischen Werten belegt. Nach der Bearbeitung der Anfrage wird der Zustand einer Seiteninstanz bereinigt, sodass sie im Ausgangszustand in den Pool zurückkehrt. Zum Zwischenspeichern von Seiteneigenschaften wird die Annotation @Persist benutzt. Tapestry bietet mehrere Strategien zum Speichern der Werte dieser Eigenschaften an: session, flash, client und entity. Für die Teilnahme am Lebenszyklus einer Seite können Lebenszyklus-Methoden implementiert werden, die entweder per Namenskonvention oder mithilfe bestimmter Annotationen identifiziert werden. Neben dem Zustand einer Seite existiert der Begriff eines Session State Objects. Ein SSO stellt einen applikationsweiten, jedoch benutzerspezifischen Zustand dar, den sich verschiedene Seiten teilen können. Zum Verwalten eines Session State Objects stellt Tapestry die Annotation @SessionState und eine Menge von Diensten bereit, sodass ein Entwickler den Zustand niemals aus der HttpSession manuell wiederherstellen muss.
103
6 Lokalisierung Bei einer sprachen- und kulturunabhängigen Gestaltung von Webseiten müssen mehrere Aspekte beachtet werden. Dazu gehören unter anderem: Übersetzung von Texten. Übersetzung von Grafiken, die Texte enthalten. Anpassung der Layouts. Beispielsweise besitzen die meisten semitischen Sprachen
wie Hebräisch oder Arabisch linksläufige Schriften, d.h., sie werden von rechts nach links geschrieben. Beachten der lokalen Zahlen- und Datumsformatierung.
Für die Realisierung dieser Aspekte unterscheidet man in der Softwareentwicklung zwischen Internationalisierung und Lokalisierung: Internationalisierung (I18N): Design und Entwicklung einer Software derart, dass
eine Lokalisierung einfach möglich wird. Lokalisierung (L10N): Der Prozess der Anpassung der Software an die sprachli-
chen und kulturellen Gegebenheiten. Zu Lokalisierung gehört beispielsweise die Übersetzung einer Software in eine neue Sprache.
6.1
Internationalisierung und Lokalisierung in Java
Um eine Anwendung sprachenunabhängig zu gestalten, müssen alle Zeichenketten, die einem Anwender präsentiert werden, im Java-Quellcode durch symbolische Namen ersetzt werden. Diese Namen werden dann mit Zeichenketten einer Landessprache in Ressourcendateien verknüpft. Die Ressourcendateien enthalten eine Menge von Schlüssel-Wert-Paaren und sind üblicherweise an der Erweiterung *.properties zu erkennen. Auch XML-Format ist möglich. Für den Zugriff auf eine Ressourcendatei ist in Java die Klasse ResourceBundle zuständig. Wie in Listing 6.1 zu sehen ist, wird ein ResourceBundle über den Aufruf der statischen Methode getBundle() erzeugt, in die der Name einer Ressourcendatei als Parameter übergeben wird. In Tapestry müssen Sie dies nicht manuell durchführen. Wie an anderen Stellen auch werden Sie sehen, dass Tapestry hier sinnvolle Konventionen einsetzt und wieder das Prinzip Convention over Configuration gilt.
6 Lokalisierung
Listing 6.1: Arbeiten mit ResourceBundle ResourceBundle bundle = ResourceBundle.getBundle("MyStrings"); System.out.println(bundle.getString("welcome"));
6.2
Anwendungsweiter Nachrichtenkatalog
In Tapestry werden die zu einer Komponente gehörenden Ressourcendateien als Nachrichtenkatalog bezeichnet. Für die Verwendung von Nachrichten, die für die gesamte Anwendung gelten, wird der sogenannte Anwendungs-Nachrichtenkatalog benutzt. Der Name der Ressourcendateien dieses Katalogs setzt sich aus dem Namen des TapestryFilters, der in web.xml vergeben wurde, und gegebenenfalls einem Locale-Suffix zusammen. Wurde dem Filter der Namen app vergeben, so könnten die Ressourcendateien des Applikations-Nachrichtenkatalog app.properties, app_de.properties (oder auch app_de_DE.properties) usw. heißen. Diese Dateien müssen im Verzeichnis WEB-INF der Anwendung abgelegt werden. Listing 6.2: WEB-INF/app.properties welcome=Welcome
Listing 6.3: WEB-INF/app_ru.properties welcome=Добро пожаловать
6.3
Komponenten-Nachrichtenkatalog
Jede Seite und jede Komponente kann ihren eigenen Nachrichtenkatalog besitzen, der von Tapestry als Erstes nach einem Wert für einen Schlüssel durchsucht wird. Falls kein Eintrag gefunden wird, sucht Tapestry im Anwendungs-Nachrichtenkatalog weiter. Damit können Seiten und Komponenten die Schlüssel-Werte-Paare des AnwendungsNachrichtenkatalogs überschreiben. Der Namen der Ressourcendateien eines Nachrichtenkatalogs setzt sich aus dem Namen der jeweiligen Seiten- oder Komponentenklasse und gegebenenfalls einem Locale-Suffix zusammen. So könnte es für die Seite MyPage Übersetzungsdateien wie MyPage.properties, MyPage_de.properties usw. geben.
6.4
Lokalisierte Templates
Es gibt viele Schriftkulturen, die eine andere Schreibrichtung als die lateinische Schrift besitzen. So wird beispielsweise in Hebräisch von rechts nach links geschrieben. Eine Lokalisierung Ihrer Anwendung für Hebräisch ist durch Übersetzung der Inhalte noch lange nicht abgeschlossen. Sie müssen zusätzlich das Layout Ihrer Anwendung so anpassen, dass alle Inhalte rechtsbündig sind. Auch eine Navigation, die Sie üblicherweise auf dem linken Seitenrand erwarten würden, müssten Sie rechtsbündig ausrichten.
106
6.5 Zugreifen auf den Nachrichtenkatalog
Wenn Tapestry ein Template einer Seite zum Erzeugen des Markups benötigt, versucht es, zunächst eine lokalisierte Version dieser Seite zu finden. Ein lokalisiertes Template enthält als Suffix ein Locale in ihrem Namen. So ist in Listing 6.6 eine hebräische Version des Templates der Seite MyPage zu sehen. An den Namen dieser Datei ist das Locale iw angefügt. Diese Version unterscheidet sich von der englischen (Listing 6.7) nur minimal: Die Schrift ist rechtsbündig. Falls keine lokalisierte Version eines Templates gefunden wird, nimmt Tapestry das Standard-Template, d. h. das Template ohne Suffix. Listing 6.4: MyPage.properties hello-world=Hello, World!
Listing 6.5: MyPage_iw.properties hello-world=\u05E2\u05D5\u05DC\u05DD\u002C\u05E9\u05DC\u05D5\u05DD
Listing 6.6: MyPage_iw.tml (hebräische Version) ${message:hello-world}
Listing 6.7: MyPage.tml ${message:hello-world}
In der Abbildung 6.1 ist das Ergebnis einer Anfrage an die Seite MyPage dargestellt, die zwei Benutzer mit unterschiedlichen Spracheinstellungen im Browser sehen würden.
6.5
Zugreifen auf den Nachrichtenkatalog
Oft muss man auf die übersetzten Nachrichten nicht nur aus dem Template heraus zugreifen, sondern auch aus einer Seiten- oder Komponentenklasse. Dafür stellt Tapestry den Dienst Messages bereit, der Ihnen das manuelle Laden eines ResourceBundles abnimmt. Sie können diesen Dienst wie in Listing 6.8 mittels @Inject injizieren und auf die Nachrichten über zwei Methoden zugreifen. Die Methode get() sucht in dem Nachrichtenkatalog nach einem Wert für den angegebenen Schlüssel. Bei der Verwendung der Methode format() wird die Nachricht als ein Format-String mit Argumenten, wie er von java.util.Formatter verlangt wird, interpretiert.
107
6 Lokalisierung
Abbildung 6.1: Hebräische und englische Versionen der Seite MyPage Anders als bei Standardmechanismen von Java zum Auslesen von lokalisierten Nachrichten, müssen Sie ein ResourceBundle nicht manuell erzeugen. Außerdem ist es nicht notwendig, den Namen der Ressourcendatei anzugeben. Tapestry verknüpft eine Seite bzw. Komponente mit dem dazugehörigen Nachrichtenkatalog über den Namen. Listing 6.8: Zugreifen auf den Nachrichtenkatalog aus der Seite MyPage heraus public class MyPage { ... @Inject private Messages messages; String getShopingCartSummary(){ if(shoppingCart.isEmpty(){ return messages.get("cart-empty"); } return messages.format("cart-size", shoppingCart.size()); } }
Listing 6.9: MyPage_de.properties cart-empty=Ihr Warenkorb ist leer. cart-size=Sie haben %d Waren in Ihrem Korb.
108
6.6 Unterstützte Sprachen
6.6
Unterstützte Sprachen
Die Suche nach einer passenden Übersetzung eines Schlüssels innerhalb eines Nachrichtenkatalogs sowie die Suche nach einer lokalisierten Version eines Templates ist in Tapestry auf die Menge der Sprachen eingeschränkt, die innerhalb des IoC-Containers konfiguriert sind. Zurzeit sind in Tapestry folgende Locales vorkonfiguriert: Listing 6.10: Unterstützte Locales en, it, es, zh_CN, pt_PT, de, ru, hr, fi_FI, sv_SE, fr_FR, da, pt_BR, ja, el
Für jede dieser Locale besitzt das Framework Übersetzungen für alle Nachrichten, die Tapestry einem Benutzer präsentiert. Darunter sind beispielsweise Texte, die von Komponenten präsentiert werden, und Validierungsnachrichten von Formularen. Falls ein Benutzer mit einer Locale, die nicht in dieser Liste vertreten ist, eine Anfrage abschickt, versucht Tapestry, die nächste passende Sprache auszuwählen. Wird beispielsweise eine Anfrage einem Benutzer mit der Spracheinstellung de_CH gestellt, wird die Sprache de ausgewählt. Falls keine passende Locale gefunden wird, werden ihm die Übersetzungen der Standardsprache präsentiert. Die Standardsprache ist die erste Locale in dieser Liste, also en. Sie können die Liste der unterstützten Sprachen überschreiben, indem Sie eine Contribute-Methode innerhalb Ihres Applikationsmoduls wie in Listing 6.11 schreiben. Kopieren Sie diesen Quellcode in Ihr Modul, und geben Sie die Liste Ihrer gewünschten Sprachen in Form einer durch Komma separierten Liste von Locales an. Listing 6.11: Überschreiben der Menge der unterstützten Locales public class AppModule { public static void contributeApplicationDefaults( MappedConfiguration configuration) { configuration.add(SymbolConstants.SUPPORTED_LOCALES, "en,de,ru,iw"); } }
6.7
Lokalisierung statischer Ressourcen
Jede Webanwendung besitzt statische Ressourcen wie Bilder, Cascading Style Sheets (CSS) und JavaScript-Bibliotheken, die in Form von Referenzen auf die jeweiligen Dateien in den HTML-Code eingebunden werden können (siehe Listing 6.12). Dabei gibt es zwei Möglichkeiten, eine Referenz anzugeben: durch relative oder absolute URLs. Bei der Angabe von absoluten Pfaden besteht das Problem, dass Sie die
109
6 Lokalisierung
Adresse einer Webseite in einem Template hart kodieren oder zur Laufszeit herausfinden müssen. Eine relative Pfadangabe ist mit der Gefahr verbunden, dass die Referenzen auf die Ressourcen bei einem Verschieben der Seiten nicht mehr funktionieren. Listing 6.12: Einbinden statischer Ressourcen Tapestry 5 Buch
Um dieses Problem umzugehen, gibt es in Tapestry sogenannte Assets. Ein Asset ist ein Java-Objekt, das eine Referenz auf eine Ressource darstellt und in Seiten oder Komponenten mittels @Inject injiziert werden kann. Die Annotation @Path dient der Angabe des Pfades eines Assets. In Listing 6.13 werden zwei Assets injiziert: das Logo der Anwendung und eine CSS-Datei. Das Präfix context informiert Tapestry, dass der Pfad images/logo.gif im Wurzelverzeichnis der Anwendung zu finden ist. Das Präfix classpath wird dagegen eingesetzt, um eine Ressource aus dem Klassenpfad (beispielsweise aus einem Java-Archiv) zu referenzieren. Falls kein Präfix angegeben wird, wird das Standardpräfix classpath verwendet, wobei der Wert innerhalb der Annotation @Path als ein relativer Pfad zu der jeweiligen Klasse interpretiert wird. Listing 6.13: Injizieren von Assets public class Index{ @Inject @Path("context:images/logo.gif") @Property private Asset logo; @Inject @Path("classpath:/css/main.css ") @Property private Asset css; @Inject @Path("js/lib.js") @Property private Asset jsLib; }
110
6.7 Lokalisierung statischer Ressourcen
Um ein Asset in ein Template einzubinden, benutzt man Expansions. Dazu muss entweder eine Getter-Methode bereitgestellt oder die jeweilige Eigenschaft mit @Property annotiert werden. Wenn Tapestry diese Expansion auswertet, wird das Framework erkennen, dass es sich um ein Asset handelt, und eine entsprechende URL generieren. Ein Beispiel ist in Listing 6.14 zu sehen. Listing 6.14: Einbinden von Assets in ein Template Tapestry 5 Buch
Falls Sie die Assets nicht in Eigenschaften Ihrer Seiten oder Komponente injizieren möchten, können Sie die beiden Annotationen @IncludeJavaScriptLibrary und @IncludeStyleSheet wie in Listing 6.15 verwenden. In diesem Fall müssen Sie die jeweiligen Ressourcen im Template nicht explizit einbinden. Tapestry wird dies für Sie übernehmen. Sollten mehrere Komponenten innerhalb derselben Seite die gleiche JavaScript-Bibliothek oder CSS-Datei mittels dieser Annotationen einfügen, wird Tapestry dafür sorgen, dass die jeweiligen Referenzen nur einmalig im erzeugten Markup auftauchen. Listing 6.15: Alternative Möglichkeit zum Einbinden von Assets @IncludeJavaScriptLibrary("js/lib.js") @IncludeStyleSheet("classpath:/css/main.css") public class Index{ ... }
Einer der großen Vorteile von Assets ist, dass sie wie die Inhalte eines Nachrichtenkatalogs lokalisiert werden können. Damit Tapestry eine passende Grafik für die Sprache des Benutzers darstellt, müssen Sie lediglich die Namenskonvention für die Benennung von Assets befolgen, die der der Ressourcendateien eines Nachrichtenkatalogs entspricht. In der Abbildung 6.2 ist zu sehen, dass die Seite WikiLogo (Listing 6.16) ein Bild enthält. Einem Benutzer mit der Spracheinstellung de wird das Wiki-Logo mit dem deutschen Untertitel präsentiert (wiki_de.png). Ein Benutzer mit der Locale ru sieht dagegen ein Logo mit einem russischen Untertitel (wiki_ru.png).
111
6 Lokalisierung
Listing 6.16: Lokalisierung der Assets public class WikiLogo { @Inject @Path("wiki.png") @Property private Asset wikiLogo; }
Abbildung 6.2: Lokalisierung von Assets
6.8
Umschalten zwischen unterstützten Sprachen einer Anwendung
Die Auswahl der Sprache einer Tapestry-Anwendung erfolgt, indem das Locale der jeweiligen Sprache in der angefragten URL kodiert wird. In jeder URL an Tapestry wird zwischen dem Kontextpfad und dem Namen der angefragten Seite nach einer String-Repräsentation einer Sprache gesucht. So würde Tapestry in den folgenden URLs die Sprache de erkennen. http://localhost:8080/app/de http://localhost:8080/app/de/mypage Falls dieses erkannte Locale in der Liste der unterstützten Sprachen enthalten ist (siehe Abschnitt 6.6), wird es mithilfe des Dienstes PersistentLocale dauerhaft gespeichert. Die Methode set() des Dienstes sorgt dafür, dass das Locale für den aktuellen Thread zwischengespeichert wird. Wenn Tapestry URLs für PageLink, ActionLink usw.
112
6.8 Umschalten zwischen unterstützten Sprachen einer Anwendung
generiert, wird das in PersistentLocale gespeicherte Locale in der URL wieder kodiert. Damit wird sichergestellt, dass eine einmal in einer URL kodierte Sprache bei der Navigation zwischen Seiten einer Anwendung von einer Seite zur nächsten übertragen wird. Falls Sie Ihren Benutzern die Möglichkeit geben wollen, die Sprache der Anwendung durch eine Aktion zu ändern, können Sie ein Locale programmatisch im Dienst PersistentLocale setzen. In Listing 6.17 ist das Template einer Seite zu sehen, in dem zwei ActionLink zur Auswahl der Sprachen Deutsch und Englisch zu sehen sind. Durch einen Klick auf einen der beiden Links der jeweiligen Handler-Methoden wird für das Ereignis action (Listing 6.18) entweder Locale.GERMAN oder Locale.ENGLISH im Dienst PersistentLocale gespeichert. Dies hat zur Folge, dass in allen Links auf die Seiten der Anwendung die gesetzte Sprache kodiert wird, sodass sie immer wieder ausgelesen werden kann. Sie können das nachvollziehen, wenn Sie nach der Auswahl einer Sprache nun auf den Link zur Startseite klicken. Sie werden sehen, dass eine der beiden Sprachen an die URL der Startseite angehängt wurde. Listing 6.17: ChooseLanguage.tml Bitte wählen Sie Ihre Sprache aus und gehen Sie anschließend zur Startseite.
Deutsch | Englisch
Gehe zur Startseite
Listing 6.18: ChooseLanguage.java public class ChooseLanguage { @Inject private PersistentLocale persistentLocale; void onActionFromDe(){ persistentLocale.set(Locale.GERMAN); } void onActionFromEn(){ persistentLocale.set(Locale.ENGLISH); } }
113
6 Lokalisierung
Zusätzlich wird das in der angefragten URL erkannte Locale im Dienst ThreadLocale gespeichert, der für jede ankommende Anfrage neu erzeugt wird. Jede Funktionalität in Tapestry, die das Locale der aktuellen Anfrage zur Internationalisierung benötigt, bezieht dieses Locale über den Dienst ThreadLocale. Auch Sie können diesen Dienst in Ihre Seiten bzw. Komponenten injizieren, um das Locale der aktuellen Anfrage herauszufinden. Wenn in einer angefragten URL kein Locale erkannt wurde oder das erkannte Locale nicht in der Liste der unterstützten Sprachen enthalten ist, wird die in den Browsereinstellungen des Benutzers ausgewählte Sprache ermittelt. Die im Browser ausgewählte Sprache wird bei jeder Anfrage in dem HTTP-Header Accept-Language mitgeliefert, sodass Tapestry mithilfe der Servlet-API das Locale des Clients ermitteln und im Dienst ThreadLocale setzen kann. PersistentLocale vs. ThreadLocale Die beiden Dienste PersistentLocale und ThreadLocale werden zur Speicherung des Locale des aktuellen Threads eingesetzt, es besteht jedoch ein wichtiger Unterschied. Der Dienst PersistentLocale dient der Speicherung des Locale in einer URL. Falls in der URL einer Anfrage kein Locale erkannt wurde, wird keine Standardsprache in diesem Dienst gesetzt. Sie sollten sich nicht auf diesen Dienst verlassen, wenn Sie ein Locale benötigen. Der Dienst ThreadLocale besitzt dagegen immer ein Locale, entweder das aus der URL oder aus dem HTTP-Header Accept-Language. Für den Zugriff auf ein Locale gibt es also mehrere Alternativen: Für den Zugriff auf das Locale einer Anfrage kann der Dienst Request injiziert werden, über dessen Methode getLocale() das Locale bezogen werden kann. Falls gesetzt, kann das gespeichert Locale mithilfe des Dienstes PersistentLocale ausgelesen werden. Dazu kann die Methode get() aufgerufen werden. Das Locale des aktuellen Thread kann beim Dienst ThreadLocale über die Methode getLocale() abgefragt werden. Mann kann aber auch eine Instanz von Locale in eine Seite injizieren lassen, indem eine Eigenschaft vom Typ java.util.Locale mit @Inject annotiert wird. Das injizierte Locale kommt aus dem Dienst ThreadLocale.
In Listing 6.19 sind alle Möglichkeiten zum Zugriff auf die Spracheinstellungen des aktuellen Benutzers zu sehen.
114
6.9 Zusammenfassung
Listing 6.19: Unterschiedliche Arten des Zugriffs auf Locale public class LocaleExample { @Inject private Request request; @Inject private Locale locale; @Inject private ThreadLocale threadLocale; @Inject private PersistentLocale persistentLocale;
void onActivate() { System.err.println("request.getLocale(): " + request.getLocale()); System.err.println("locale: " + locale); System.err.println("threadLocale: " + threadLocale.getLocale()); System.err.println("persistentLocale.get(): " + persistentLocale.get()); } }
6.9
Zusammenfassung
In Tapestry werden die Übersetzungen der darzustellenden Texte in sogenannten Nachrichtenkatalogen verwaltet. Es wird zwischen einem anwendungsweiten- und Komponenten-Nachrichtenkatalog unterschieden. Ein anwendungsweiter Nachrichtenkatalog umfasst alle Nachrichten, die in vielen Seiten oder Komponenten benötigt werden. Ein Komponenten-Nachrichtenkatalog gehört dagegen exklusiv einer Seite oder Komponente. Neben einzelnen Texten können auch ganze Templates lokalisiert werden, um beispielsweise das Layout einer Anwendung an andere Schreibrichtungen anzupassen. Weiterhin können statische Ressourcen durch Asset repräsentiert werden, die zum einen gecachet, zum anderen lokalisiert werden können. Das Zuordnen der lokalisierten Ressourcen (Nachrichtenkatalog, Template oder Asset) zur Sprache eines Benutzers erfolgt über das Suffix der jeweiligen Datei. Zum Umschalten der Sprache einer Anwendung kann das Locale der jeweiligen Sprache in die Anfrage-URL kodiert werden. Tapestry wird das Locale erkennen und die Inhalte in der gewünschten Sprache darstellen. Dazu muss die anfragte Sprache in der Liste der unterstützten Sprachen enthalten sein.
115
7
Formulare
Formulare sind der Schlüssel zu interaktiven Webseiten. Sie erlauben es dem Anwender, Daten einzugeben und an den Server zu übermitteln. Zur Verarbeitung der übermittelten Daten können beispielsweise Servlets oder JSP (JavaServer Pages) eingesetzt werden. Innerhalb einer JSP kann ein HTML-Formular erzeugt werden, das eine Menge von Eingabefeldern und eine Schaltfläche zum Absenden des Formulars enthält. Sobald die Schaltfläche des Formulars angeklickt wird, werden die Eingaben zum Verarbeiten an ein Servlet versendet. Für die Datenübermittlung ist das Protokoll HTTP (HyperText Transfer Protocol) zuständig, wobei zwischen zwei Methoden POST und GET unterschieden wird. Im Servlet muss dementsprechend die Methode doPost() und/oder doGet() implementiert werden, in der die übermittelten Werte über die Namen der Formularfelder ausgelesen werden. Das Auslesen erfolgt, indem das Objekt vom Typ HttpServletRequest nach dem Wert eines Anfrageparameters abgefragt wird. Da die Werte dieser Parameter vom Typ String sind, müssen sie zunächst in die entsprechenden Typen (Integer, Date, Boolean usw.) konvertiert werden, bevor sie in der Geschäftslogik verwendet werden können, beispielsweise zur Aktualisierung des Datenbankzustandes. Danach erfolgt eine Weiterleitung an die UrsprungsJSP oder eine andere JSP, und der Anfragezyklus wird abgeschlossen. Listing 7.1: Einfache JSP mit einem HTML-Formular Einfache JSP
Listing 7.2: Servlet zur Behandlung der Anfrage der JSP public class MyServlet extends HttpServlet{ public void doPost(HttpServletRequest request,
7 Formulare
HttpServletResponse response){ Integer number = Integer.valueOf(request.getParameter("myNumber")); ... RequestDispatcher dispatcher = getServletContext(). get RequestDispatcher("/jsp/result.jsp"); dispatcher.forward(request, response); } }
Der Tapestry-Ansatz zur Behandelung der Formulardaten übernimmt für Sie die meisten der oben genannten Schritte. Tapestry versteckt die Details von HTTP und kapselt die Servlet-API, sodass ein Entwickler niemals die Anfrageparameter nach bestimmten Namen durchsuchen muss. Des Weiteren übernimmt Tapestry die Konvertierung der empfangenen Werte in die richtigen Java-Typen und die Weiterleitung zu einer Ergebnisseite. Damit sind Sie nur für die Erzeugung eines Formulars und die Implementierung der Geschäftslogik zuständig. Eine Fülle von Tapestry-Komponenten unterstützt Sie dabei. In der Tabelle 7.1 sind einige HTML-Elemente zum Einsatz in Formularen und die entsprechenden Tapestry-Komponenten zusammengefasst. HTML-Elemente
Tapestry-Komponenten
und
und
Tabelle 7.1: Formularelemente in HTML und Tapestry
7.1
Erzeugen eines einfachen Login-Formulars
Eine der häufigsten Anwendungen eines HTML-Formulars ist die Authentifizierung eines Anwenders einer Webanwendung. Typischerweise besitzt ein Anwender eine
118
7.1 Erzeugen eines einfachen Login-Formulars
Kennung, die ihn eindeutig identifiziert, und ein Passwort, mit dem er sich authentifiziert. In diesem Abschnitt wird beispielhaft ein einfaches Formular zum Einloggen wie in der Abbildung 7.1 entwickelt.
Abbildung 7.1: Login-Formular In Listing 7.3 ist das Template einer Indexseite einer Anwendung zu sehen, in der entweder ein Link zur Login-Seite oder die Begrüßung eines authentifizierten Anwenders präsentiert wird. Diese Entscheidung wird anhand des Vorhandenseins der Variablen userName getroffen. Beim ersten Aufruf dieser Seite existiert in der aktuellen Sitzung kein Anwender, sodass Sie durch den Klick auf den Link zum Einloggen übergehen können. Listing 7.3: Index.tml Hallo, ${userName}!!! Zum Einloggen
Listing 7.4: Index.java public class Index{ @Persist @Property private String userName; }
119
7 Formulare
Das Formular zur Eingabe der Authentifizierungsdaten ist in Listing 7.5 dargestellt. Anstelle eines HTML-Formulars ist die Komponente Form mit der ID loginForm zu sehen, die für die Übermittlung der Eingabedaten zuständig ist. Wenn Sie den Quellcode der erzeugten Seite anschauen, werden Sie feststellen, dass das Formular die Daten mittels POST-Methode an die Adresse login.loginForm versendet. Anhand dieser Adresse erkennt Tapestry, dass die Anfrage von der Komponente mit der ID loginForm der Seite Login abgeschickt wurde. Zur Eingabe des Benutzernamens beinhaltet das Formular eine geschachtelte Komponente TextField, deren Wert auf die Eigenschaft userName der Seite Login verweist. Sobald das Formular abgeschickt wird, wird Tapestry hinter den Kulissen dafür sorgen, dass die Eingabe des Feldes dieser Eigenschaft zugewiesen wird. So können Sie innerhalb der Seite Login über diese Eigenschaft auf den abgeschickten Wert zugreifen. Das zweite Feld ist vom Typ PasswordField und wird der Eigenschaft password zugewiesen. Analog zum HTML-Standard unterscheidet sich dieses Feld von einem Textfeld nur dadurch, dass die Eingabe des Anwenders durch Platzhalter unkenntlich gemacht wird. Wenn das Formular durch einen Klick auf die entsprechende Schaltfläche abgeschickt wird, können Sie diese Anfrage verarbeiten, indem Sie eine Handler-Methode für eines der von der Form-Komponente ausgelösten Ereignisse schreiben. Hier können Sie beispielsweise die Eingaben validieren oder Ihre Geschäftslogik ausführen. In Listing 7.6 ist die Methode onSuccess() zur Behandelung des Ereignisses success implementiert. Innerhalb dieser Methode wird die Authentifizierung des Anwenders durchgeführt und im Erfolgsfall zur Seite Index weitergeleitet (siehe Abbildung 7.2). Falls die Authentifizierung fehlschlägt, kehrt der Anwender zur Seite Login zurück. Da die Eigenschaften userName und password der Seite mit @Persist in der Session abgespeichert werden, enthalten die jeweiligen Formularfelder bei einem erneuten Aufruf der Seite die ursprünglichen Eingaben. Listing 7.5: Login.tml Name: Passwort:
Listing 7.6: Login.java public class Login { @Property @Persist
120
7.2 Behandlung der Ereignisse der Komponente Form
private String userName; @Property @Persist private String password; @InjectPage private Index indexPage; Object onSuccess() { if ("root".equals(userName) && "secret".equals(password)) { indexPage.setUserName(userName); return indexPage; } return null; } }
Abbildung 7.2: Indexseite nach einem erfolgreichen Login
7.2
Behandlung der Ereignisse der Komponente Form
Die Komponente Form erzeugt ein -Element im generierten HTML-Code und dient als ein Container für eine Menge von enthaltenen Eingabefeldern unterschiedlicher Typen. Sie stellt eine Umgebung bereit, die die jeweiligen Felder mit den benötigten Informationen bedient. Beispielsweise werden in dieser Umgebung die Nachrichten über die Validierungsfehler aller teilnehmenden Felder gesammelt. Bei einer Anfrage löst die Komponente eine Menge von Ereignissen aus. Bevor die Komponente mit der Erzeugung des Markups beginnt, wird das Ereignis prepareForRender veröffentlicht. Anschließend folgt das Ereignis prepare, das beispielsweise zur Initialisierung von Variablen eingesetzt werden kann. Sobald das Formular abgeschickt wurde, erscheint das Ereignis prepareForSubmit. Dies ist ein richtiger Zeitpunkt, um den Zustand einer Komponente vor dem Versenden der Eingabedaten zu aktualisieren. Nachdem die Werte der Felder in die jeweiligen Eigenschaften der Seite geschrieben wurden, folgt das Ereignis validateForm. Dieses Ereignis kann zur Überprüfung der Eingabedaten verwendet werden. Möchten Sie beispielsweise überprüfen, ob der eingegebene Name in Ihrem System bereits vergeben wurde, ist die Hand-
121
7 Formulare
ler-Methode onValidateForm() die richtige Stelle. Abhängig davon, ob die Validierung erfolgreich war, wird eines der beiden Ereignisse success oder failure veröffentlicht. Abschließend löst die Komponente das Ereignis submit aus. Einer der möglichen Rückgabewerte von Handler-Methoden ist void. Mit Ausnahme der Methoden onPrepareForSubmit() und onPrepare() kann auch java.lang.Class oder java.lang.Object als Rückgabetyp definiert werden. Im Falle von java.lang.Class wird die Klasse der Seite angegeben, zu der nach der Behandlung des Ereignisses zurückgekehrt werden soll. Tapestry wird eine freie Instanz dieser Klasse aus dem Seitenpool holen und die Anfrage an sie weiterleiten. Außerdem ist es möglich, eine Instanz der Zielseite mittels @InjectPage zu injizieren (siehe Listing 7.6), um diese Instanz in Ihrer Handler-Methode zurückzugeben. In Listing 7.7 ist die Seite Login zu sehen, in der alle Ereignisse der Komponente Form behandelt werden. Experimentieren Sie mit dieser Klasse, um ein besseres Gefühl für die von Form ausgelösten Ereignisse zu bekommen. In der Methode onPrepare() wird eine Instanz von User erzeugt und der Seiteneigenschaft user zugewiesen. Die Formulareingaben werden ab sofort nicht in die Eigenschaften der Seite, sondern in die der erzeugten Instanz von User geschrieben (siehe Listing 7.8). In der Handler-Methode für das Ereignis validateForm werden der Benutzername und das Passwort überprüft. Falls die Validierung fehlschlägt, wird eine Fehlernachricht der injizierten Form hinzugefügt. Mehr über Validierung erfahren Sie in Abschnitt 7.6. Im Falle einer erfolgreichen Validierung wird die Methode onSuccess() aufgerufen, in der eine Weiterleitung zur Seite Index erfolgt. Ansonsten wird die Methode onFailure() aufgerufen. Listing 7.7: Login.java (behandelt mehrere Ereignisse der Komponente Form) public class Login { @Property @Persist private User user; @InjectComponent private Form loginForm; @InjectPage private Index indexPage; void onPrepareForRender() { System.out.println("onPrepareForRender"); } void onPrepare() { System.out.println("onPrepare"); if (user == null) { user = new User(); } }
122
7.2 Behandelung der Ereignisse der Komponente Form
void onPrepareForSubmit() { System.out.println("onPrepareForSubmit"); } void onValidateForm() { System.out.println("onValidateForm"); if (loginForm.getHasErrors()) { return; } if (!"root".equals(user.getUserName()) || !"secret".equals(user.getPassword())) { loginForm.recordError( "Benutzername oder Passwort ist falsch."); } } void onFailure() { System.out.println("onFailure"); } Object onSuccess() { System.out.println("onSuccess"); indexPage.setUser(user); return indexPage; } void onSubmit() { System.out.println("onSubmit"); } }
Listing 7.8: Login.tml Name: Passwort:
123
7 Formulare
Ähnlich wie bei den Komponenten PageLink und ActionLink kann für ein Formular ein Kontext angegeben werden. Ein Kontext ist ein Array von Objekten, die beim Versenden des Formulars in java.lang.String konvertiert und mit verschickt werden. Eine Handler-Methode kann die Werte des Kontextes auswerten, falls sie Argumente von entsprechenden Typen besitzt. Dabei findet eine Rücktransformation der Strings in die ursprünglichen Typen statt. In Listing 7.9 hat ein Formular im Parameter context einen Kontext angegeben, der in der entsprechenden Klasse (Listing 7.10) in der Methode getFormContext() erzeugt wird. Dieser Kontext besteht aus einem Integerund einem Double-Wert. Die drei Handler-Methoden sind an unterschiedlichen Teilen des Kontextes interessiert und besitzen dementsprechend eine unterschiedliche Anzahl von Argumenten. Die Methode onValidataForm() empfängt den Kontext als Ganzes, da die Anzahl der Argumente dieser Methode der Anzahl der Elemente des Kontextes entspricht. Die Methode onSuccess() empfängt nur das erste Element des Kontextes, da sie nur ein einziges Argument besitzt. Die Methode onSubmit() hat keine Argumente in ihrer Signatur und wird folglich ohne Informationen aus dem Kontext aufgerufen. Beachten Sie, dass die Anzahl der Argumente einer Handler-Methode die Anzahl der Elemente des bereitgestellten Kontextes nicht überschreiten darf. Auch die Reihenfolge der Argumente einer Methode ist entscheidend: Das i-te Argument einer Methode entspricht dem i-ten Element des Kontextes. Listing 7.9: Formular mit einem Kontext ...
Listing 7.10: Enthält Handler-Methoden mit Kontext public class FormWithContext { ... public Object[] getFormContext(){ return new Object[]{new Integer(7), new Double(3.14159)}; } void onValidateForm(Integer first, Double second){ System.out.println("onValidateForm("+first+", "+second+")"); } void onSuccess(Integer first) { System.out.println("onSuccess("+first+")"); } Object onSubmit(){
124
7.3 Mehrere Formulare auf einer Seite
System.out.println("onSubmit"); return FormWithContext.class; } }
Abschließend sei erwähnt, dass ein Kontext Objekte von beliebigen Typen enthalten darf. Falls Sie eine Instanz einer eigenen Klasse als einen Teil des Kontextes bereitstellen möchten, müssen Sie Tapestry informieren, wie die Transformation in ein java.lang.String für die Übermittlung an den Server und eine Rücktransformation stattfinden soll. Für diesen Zweck stellt Tapestry den Mechanismus Type Coercion bereit, der in Kapitel 19 beschrieben wird.
7.3
Mehrere Formulare auf einer Seite
Sobald Sie mehr als nur ein Formular auf einer Seite benötigen, werden Sie sich möglicherweise fragen, wie Sie innerhalb der Handler-Methoden unterscheiden können, von welchem der Formulare die Anfrage kommt. Falls Sie Ihr Template um ein weiteres Formular wie in Listing 7.11 erweitern, ohne die Java-Klasse der Seite anzupassen, werden die Handler-Methoden zur Behandlung der Ereignisse jeweils beider Formulare aufgerufen. Dies bedeutet, dass Sie innerhalb einer Handler-Methode zunächst nicht unterscheiden können, welches Formular die Quelle des zu behandelnden Ereignisses war. Durch Eingabe unterschiedlicher Kontexte für die beiden Formulare können Sie die benötigten Informationen für diese Unterscheidung bereitstellen. Doch dann müssten Sie in jeder Handler-Methode eines Ereignisses eine Fallunterscheidung einbauen, die Ihren Code unnötig aufbläht. Alternativ können Sie die Quelle des zu behandelnden Ereignisses in den Namen der Handler-Methode kodieren. Duplizieren Sie Ihre Handler-Methoden, und nennen Sie diese um, indem Sie an den Namen das Wort From und die ID des jeweiligen Formulars anfügen. Sie haben zwar die Anzahl der Handler-Methoden innerhalb Ihrer Klasse verdoppelt, doch Sie müssen nun nicht mehr für die lästige Fallunterscheidung sorgen. Tapestry wird erkennen, dass die jeweiligen Methoden zur exklusiven Behandlung der Ereignisse eines Formulars aufgerufen werden sollen. In Listing 7.12 werden für zwei Formulare insgesamt sechs Handler-Methoden implementiert, die zur exklusiven Behandlung von drei Ereignissen zuständig sind. Listing 7.11: MultipleForms.tml ... ...
125
7 Formulare
Listing 7.12: Behandelt exklusive Ereignisse von zwei Formularen public class MultipleForms { ... void onValidateFormFromLoginForm(){ System.out.println("onValidateFormFromLoginForm"); } void onSuccessFromLoginForm() { System.out.println("onSuccessFromLoginForm"); } void onSubmitFromLoginForm(){ System.out.println("onSubmitFromLoginForm"); } void onValidateFormFromSearchForm(){ System.out.println("onValidateFormFromSearchForm"); } void onSuccessFromSearchForm() { System.out.println("onSuccessFromSearchForm"); } void onSubmitFromSearchForm(){ System.out.println("onSubmitFromSearchForm"); } }
7.4
Überblick über Tapestrys Formularkomponenten
Die Tabelle 7.1 gibt einen Überblick über die HTML-Formularelemente und die dazugehörigen Tapestry-Komponenten. Einige dieser Komponenten wurden bereits kurz angesprochen, um Ihnen einen Einblick in die Erzeugung von Formularen mit Tapestry zu geben. In diesem Abschnitt werden alle Formularkomponenten vorgestellt und an einem durchgehenden Beispiel erläutert. Es wird ein Formular zur Registrierung von Benutzern in einem System entwickelt, das Sie in Ihrer Anwendung einsetzen können. Das Beispiel wird schrittweise ausgebaut und um neue Funktionalität erweitert.
7.4.1
Texteingabe in Text- und Passwortfelder
Felder zur Eingabe von Text gehören sicherlich zu den am meisten eingesetzten Formularelementen. Entsprechend den HTML-Standards bietet Tapestry drei Komponenten zur Eingabe von Text an: TextField, TextArea und PasswordField. Diese drei
126
7.4 Überblick über Tapestrys Formularkomponenten
Komponenten unterscheiden sich minimal in ihrer Darstellung im Browser, doch sie werden auf identische Weise eingesetzt. Die Beschreibungen in diesem Abschnitt treffen auf alle drei Komponenten zu. Die drei Komponenten besitzen einen erforderlichen Parameter, über den das Schreiben bzw. Lesen der Benutzereingaben in bzw. aus einer Eigenschaft der Seitenklasse erfolgt. Zusätzlich gibt es weitere optionale Parameter, die in den folgenden Kapiteln besprochen werden, wenn wir das Formular nach und nach erweitern. In Listing 7.13 ist ein Registrierungsformular mit fünf Feldern zu sehen, die die Benutzereingaben den Eigenschaften userName, password, password2, email und signature zuweisen. Das erste Feld ist vom Typ TextField und wird zur Angabe des Benutzernamens eingesetzt. Für die beiden Felder zur Eingabe der Passwörter wird die Komponente PasswordField benutzt. Sie unterscheidet sich von einem Textfeld lediglich dadurch, dass die Eingaben des Benutzers durch einen Platzhalter wie * unkenntlich gemacht werden. Das Feld für die Eingabe der E-Mail-Adresse ist wiederum ein TextField. Das letzte Feld gibt dem Benutzer die Möglichkeit, eine E-Mail-Signatur anzulegen. Da es sich bei einer Signatur um eine möglicherweise mehrzeilige Eingabe handelt, wird für das entsprechende Feld die Komponente TextArea verwendet. Beachten Sie, dass die Felder zur Eingabe des Benutzernamens und der E-Mail-Adresse den Parameter size benutzen, der die Anzeigebreite des Feldes angibt. Es handelt sich um einen sogenannten informellen Parameter (siehe Kapitel 11), der nicht von Tapestry spezifiziert wurde. Auf diese Weise können Sie alle in HTML spezifizierten Parameter in Tapestry-Formularkomponenten benutzen. So werden auch die Anzeigebreite und Anzeigehöhe der Komponente TextArea durch die beiden informellen Parameter cols und rows definiert. Ein weiteres interessantes Detail ist die Annotation @Width der beiden Passwort-Felder innerhalb der Seitenklasse. Diese Annotation gibt die Anzeigebreite der entsprechenden HTML-Felder an und dient somit als eine Alternative für den informellen Parameter size innerhalb des Templates. Übrigens, die Komponenten TextField, TextArea und PasswordField müssen zwingend in ein Formular eingebettet werden. Falls kein umschließendes Formular gefunden wird, wird eine Exception geworfen. Dies gilt auch für alle anderen Formularkomponenten, die in den nächsten Kapiteln vorgestellt werden. Listing 7.13: Register.tml Benutzername: Passwort:
127
7 Formulare
Passwort (Wiederholung): Email: Signatur:
Ähnlich wie in Listing 7.6 können Sie eine Handler-Methode für das Ereignis success implementieren, um die Eingaben auf der Konsole auszugeben. Listing 7.14: Register.java public class Register { @Persist @Property private String userName; @Persist @Property @Width(30) private String password; @Persist @Property @Width(30) private String password2; @Persist @Property private String email; @Persist @Property private String signature; void onSuccess() { //Hier wird der Benutzer in der Datenbank gespeichert.
128
7.4 Überblick über Tapestrys Formularkomponenten
System.out.println("userName = " + userName); System.out.println("password = " + password); System.out.println("password2 = " + password2); System.out.println("email = " + email); System.out.println("signature = " + signature); } }
Das Ergebnis der Seite Register sieht wie in der Abbildung 7.3 aus.
Abbildung 7.3: Registrierungsformular
7.4.2
Checkboxen
Checkboxen sind Formularelemente, bei denen ein Anwender aus einer vordefinierten Anzahl von Kontrollkästchen eine Auswahl treffen kann. Die einzelnen Kästchen lassen sich einzeln aktivieren bzw. deaktivieren, sodass eine Mehrfachauswahl möglich ist. Im Gegensatz zu HTML lassen sich Tapestry-Checkboxen nicht gruppieren. Wir erweitern unser Registrierungsformular um eine Checkbox, um einem Benutzer die Möglichkeit zu geben, im Zuge der Registrierung einen Newsletter zu abonnieren (siehe Abbildung 7.4).
129
7 Formulare
Abbildung 7.4: Checkbox zum Abonnieren eines Newsletters In Tapestry wird eine Checkbox durch die Komponente Checkbox (siehe Listing 7.15) erzeugt. Der Wert dieser Komponente muss einer boolesche Eigenschaft der Seitenklasse zugewiesen werden (siehe Listing 7.16). Der Grund dafür ist, dass der Wert einer Checkbox nur dann an den Server übermittelt wird, wenn der Anwender sie aktiviert hat. Dementsprechend wird der Eigenschaft subscribeNewsletter der Wert true zugewiesen, falls das Kontrollkästchen aktiviert wurde. Andernfalls hat die Eigenschaft den Wert false. Listing 7.15: Register.tml (erweitert um Checkbox) ... Newsletter abonnieren: ...
Listing 7.16: Register.java (erweitert um Checkbox) public class Register { ... @Persist @Property private boolean subscribeNewsletter; void onSuccess(){ ...
130
7.4 Überblick über Tapestrys Formularkomponenten
System.out.println("subscribeNewsletter = " + subscribeNewsletter); } }
Der Standardwert der Eigenschaft subscribeNewsletter ist entscheidend dafür, ob die Checkbox voraktiviert wird. Setzen Sie den Wert dieser Eigenschaft in der ActivateMethode auf true, und rufen Sie die Seite erneut auf. Sie werden sehen, dass das Kästchen bereits markiert ist. Durch einen Blick in den Quellcode der dargestellten Seite werden Sie feststellen, dass Tapestry das jeweilige Feld mit dem Attribut checked="checked" generiert hat.
7.4.3
Radiobuttons
Ein Radiobutton ist ein Formularelement, bei dem ein Benutzer eine Auswahl aus einer vordefinierten Menge von Optionen treffen kann. Dabei schließen sich die Optionen gegenseitig aus, sodass nur ein Element aus der Menge der möglichen Optionen ausgewählt werden kann. Ein klassischer Anwendungsfall eines Radiobuttons ist die Angabe des Geschlechts bei einer Registrierung. Auch wir erweitern unser Registrierungsformular um einen Radiobutton für die Angabe des Geschlechts des zu registrierenden Benutzers. Das Ergebnis ist in der Abbildung 7.5 zu sehen.
Abbildung 7.5: Radiobuttons zur Angabe des Geschlechts Während in HTML die Radiobuttons durch den Namen der jeweiligen Felder gruppiert werden, erfolgt die Gruppierung in Tapestry durch eine Schachtelung mehrerer Radio-Komponenten innerhalb einer RadioGroup Komponente. In Listing 7.17 werden die Literale male und female mit zwei Radios verknüpft. Einer der beiden Werte wird beim Verschicken des Formulars an den Server übermittelt und der Seiteneigenschaft gender (Listing 7.18) zugewiesen. Dies ist am Parameter value der Komponente RadioGroup zu sehen. Falls Sie den Quellcode der dargestellten Seite anschauen, können Sie sehen, dass Tapestry zwei Felder generiert hat, die den gleichen Namen besitzen, der der ID der Komponente RadioGroup entspricht. Damit wird sichergestellt, dass nur einer der Werte angeklickt werden kann.
131
7 Formulare
Listing 7.17: Register.tml (erweitert um Radiobutton) ... Geschlecht: ...
Listing 7.18: Register.java (erweitert um Radiobutton) public class Register { ... @Persist @Property private String gender; void onSuccess(){ ... System.out.println("gender = " + gender); } }
Dieses Beispiel kann etwas eleganter realisiert werden, indem die String-Werte der Radios durch Werte einer Java Enum-Deklaration ersetzt werden (siehe Listing 7.19). Anstatt die einzelnen Radio-Komponenten innerhalb einer RadioGroup per Hand aufzuschreiben, kann die Komponente Loop benutzt werden, um über alle Werte der Enum zu iterieren und die jeweiligen Radio-Komponenten zu generieren. In Listing 7.20 wird über ein Array vom Typ Gender iteriert, das von der Methode getValues() der Seitenklasse (Listing 7.21) zurückgegeben wird. Auf jedes Element der Iteration wird über die Seiteneigenschaft currentElement zugegriffen, die ebenfalls vom Typ Gender ist. Beachten Sie, dass der Typ der Eigenschaft gender von java.lang.String auf Gender geändert werden muss. Andernfalls erhalten Sie eine java.lang.ClassCastException. Des Weiteren müssen Sie für das Radio-Element das Attribut label bereitstellen. Normalerweise ist dieses Attribut optional, da Tapestry ein Label aus der ID der jeweili-
132
7.4 Überblick über Tapestrys Formularkomponenten
gen Radio-Komponente generieren kann. Da Sie aber innerhalb von Loop nur eine einzelne ID angeben können, würde Tapestry für jedes Radio das gleiche Label rendern. An dieser Stelle müssen Sie Tapestry helfen, indem sie für jedes Iterationselement in der Methode getLabel() ein Label aus dem Namen des aktuellen Gender erzeugen. Listing 7.19: Enum-Deklaration Gender.java public enum Gender { MALE, FEMALE }
Listing 7.20: Register.tml (Radiobutton mit Enum) ... Geschlecht: ...
Listing 7.21: Register.java (Radiobutton mit Enum) public class Register { ... @Persist @Property private Gender gender; @Property private Gender currentElement; public Gender[] getValues(){ return Gender.values(); }
133
7 Formulare
public String getLabel(){ return currentElement.name().toLowerCase(); } void onSuccess(){ ... System.out.println("gender = " + gender); } }
7.4.4
Auswahllisten
Mithilfe einer Auswahlliste erlauben Sie einem Benutzer, eine Auswahl sehr bequem aus einem Drop-down-Menü zu treffen (siehe Abbildung 7.6). Eine Auswahlliste wird in HTML durch das Element erzeugt. Die innerhalb einer Auswahlliste eingebetteten Elemente repräsentieren die zur Auswahl stehenden Einträge. Ein Beispiel für eine HTML-Auswahlliste ist in Listing 7.22 zu sehen. Dabei wird für jeden Listeneintrag zwischen und ein Label angegeben, das dem Anwender präsentiert wird. Der Wert, der an den Server übermittelt wird, wird mittels des Parameters value spezifiziert. Wird in der entsprechenden Combobox beispielsweise Credit Card ausgewählt und das Formular versendet, so wird an den Server der Wert cc übermittelt.
Abbildung 7.6: Auswahl der Zahlungsart Listing 7.22: Eine Auswahlliste mit HTML Zahlungsart: Credit Card Bank Transfer
134
7.4 Überblick über Tapestrys Formularkomponenten
Cash PayPal
In Tapestry kann eine Auswahlliste mithilfe der Komponente Select erzeugt werden. Wir erweitern unser Registrierungsformular um die Komponente Select, die zur Auswahl der Zahlungsart des Anwenders eingesetzt wird. Die Komponente besitzt zwei erforderliche und mehrere optionale Parameter. Der Parameter model wird zur Angabe der zur Auswahl stehenden Optionen des Drop-down-Menüs benutzt. Es gibt mehrere Möglichkeiten, diese Optionen an die Komponente Select zu übergeben. Sie können beispielsweise eine Instanz von java.util.List oder java.util.Map in einer Getter-Methode der Seitenklasse erzeugen und den Eigenschaftsnamen (also den Namen der Methode ohne Präfix get) als Wert des Parameters model angeben. So wird beispielsweise in Listing 7.24 eine Liste von Strings erzeugt. Durch die Angabe des Ausdrucks prop:options in Listing 7.23 wird die Komponente informiert, dass die Methode getOptions() aufgerufen werden soll. Listing 7.23: Register.tml (Erweitert um Auswahlliste) ... Zahlungsart: ...
Listing 7.24: Register.java (Erweitert um Auswahlliste) public class Register { ... @Persist @Property private String paymentType; public List getOptions() { return Arrays.asList("Credit Card", "Bank Transfer",
135
7 Formulare
"Cash", "PayPal"); } void onSuccess() { ... System.out.println("paymentType = " + paymentType); } }
Dieses Beispiel kann vereinfacht werden, indem die Optionen durch ein Literal repräsentiert werden. Ersetzen Sie den Ausdruck des Parameters wie im folgenden Beispiel, entfernen Sie die Methode getOptions() aus Ihrer Klasse, und aktualisieren Sie die Seite. Sie werden sehen, dass unsere Seite immer noch funktioniert. Anhand des Bindings literal erkennt Tapestry, dass der Ausdruck hinter dem Doppelpunkt als eine durch Komma separierte Liste interpretiert werden muss. Listing 7.25: Angabe einer Liste von Optionen durch Literale
Auch die Angabe einer Menge von Schlüssel-Wert-Paaren ist möglich. Listing 7.26: Angabe einer Map von Optionen durch Literale
Wie Sie sehen können, ist Tapestry sehr flexibel, was die Angabe der Parameterwerte einer Komponente angeht. Sie können eine java.util.List, java.util.Map oder auch einen einfachen String an die Komponente Select übergeben, um die Auswahloptionen zu bestimmen. Wenn Sie aber einen Blick in die Komponentenreferenz im Anhang werfen, sehen Sie, dass das Parameter model eine Instanz vom Typ org.apache.tapestry5.SelectModel erwartet. Dies mag auf den ersten Blick verwirren, doch die Erklärung ist einfach. Tapestry besitzt sogenannte Type Coercions, die Sie sich als Transformatoren vorstellen können. Sie übersetzen eine Instanz einer Klasse in eine Instanz einer anderen Klasse. So wird beispielsweise hinter den Kulissen aus einer java.util.List ein SelectModel erzeugt. Der Mechanismus Type Coercions wird im Detail in Kapitel 19 behandelt.
Auswahllisten mit Enums Anstatt die einzelnen Optionen einer Auswahlbox einer java.util.List oder einer java.util.Map per Hand hinzuzufügen, können Sie die verfügbaren Zahlungsarten als eine Java-Enum deklarieren (siehe Listing 7.27). Dann müssen Sie den Typ der Seiteneigenschaft paymenType von java.lang.String auf PaymenType und den Rückgabetyp der
136
7.4 Überblick über Tapestrys Formularkomponenten
Methode getOptions() auf SelectModel ändern (Listing 7.28). Gemäß der neuen Signatur wird in der Methode getOptions() eine Instanz von EnumSelectModel erzeugt und zurückgegeben. Der Konstruktor dieser Klasse erwartet die Enum-Klasse, für die ein SelectModel erzeugt werden soll, und eine Instanz des Dienstes org.apache.tapestry5. ioc.Messages. Den Dienst Messages können Sie in Ihre Klasse injizieren, indem Sie die entsprechende Eigenschaft mit der Annotation @Inject markieren. Das Template muss nicht verändert werden. Sobald das Formular versendet wird, wird Tapestry für den übermittelten Wert eine passende Instanz der PaymentType-Enum finden und diese der Eigenschaft paymentType zuweisen. Listing 7.27: Enum-Deklaration PaymentType.java public enum PaymentType { CREDIT_CARD, BANK_TRANSFER, CASH, PAYPAL }
Listing 7.28: Register.java public class Register { ... @Persist @Property private PaymentType paymentType; @Inject private Messages messages; public SelectModel getOptions(){ return new EnumSelectModel(PaymentType.class, messages); } void onSuccess(){ ... System.out.println("paymentType = " + paymentType); } }
Wie Sie in der Abbildung 7.6 sehen können, erzeugt die Klasse EnumSelectModel aus den Namen der Enum-Instanzen menschenlesbare Bezeichnungen für die Optionen der Auswahlliste. So wird beispielsweise für die Instanz PaymentType.CREDIT_CARD der Name »Credit Card« generiert, indem Unterstriche durch Leerzeichen ersetzt werden. Für die Anfangsbuchstaben wird Groß- und für die restlichen Buchstaben Kleinschreibung eingesetzt. Dieses Feature erspart Ihnen eine Menge Code. Sie können diese Labels sogar internationalisieren, indem Sie im Nachrichtenkatalog der Seite Übersetzungen für die Instanzen des Enums bereitstellen. Für eine deutsche Übersetzung legen Sie dazu im Paket für Seitenklassen eine Ressourcendatei an, in der Sie für jede Instanz des Enums ein Schlüssel-Wert-Paar bereitstellen. Jeder Schlüssel setzt sich
137
7 Formulare
aus dem Klassennamen des Enums ohne Paketangabe, einem Punkt und dem Namen der jeweiligen Instanz zusammen. Die Werte sind Übersetzungen, die den Benutzern präsentiert werden. Listing 7.29: Register_de.properties (Auszug) PaymentType.CREDIT_CARD=Kreditkarte PaymentType.BANK_TRANSFER=Überweisung PaymentType.CASH=Bar PaymentType.PAYPAL=PayPal
Abbildung 7.7: Übersetzte Auswahlliste
Leere Optionen innerhalb einer Auswahlliste Wie Sie später lernen werden, können Eingaben der Formularfelder erforderlich oder optional sein. Abhängig davon, ob Sie Ihre Auswahlliste als ein erforderliches oder optionales Feld des Formulars spezifiziert haben, variiert die Anzahl der Auswahloptionen. Bei einem optionalen Feld wird eine zusätzliche leere Option generiert, die vorselektiert ist. Dies gibt dem Anwender die Möglichkeit, das Feld unausgefüllt zu lassen. Im Falle eines erforderlichen Feldes wird die leere Option nicht generiert, sodass der Benutzer eine Auswahl treffen muss. Das beschriebene Verhalten ist eine Voreinstellung, die Sie mithilfe des Parameters blankOption der Komponente Select überschreiben können. Dieser Parameter erwartet eines der drei folgenden Literale: auto: Erzeugt eine leere Option, falls das Feld optional ist (Standard). always: Erzeugt eine leere Option, selbst wenn das Feld erforderlich ist. never: Erzeugt niemals eine leere Option, selbst wenn das Feld optional ist.
Neben diesen drei Literalen akzeptiert der Parameter blankOption auch eine Instanz des Enum org.apache.tapestry5.corelib.data.BlankOption. Falls Sie für die leere Option ein Label erzeugen möchten, können Sie dies durch Angabe eines Strings im Parameter blankLabel tun. Alternativ können Sie eine Über-
138
7.4 Überblick über Tapestrys Formularkomponenten
setzung im Nachrichtenkatalog der Seite bereitstellen. Der Schlüssel der jeweiligen Nachricht setzt sich nach der vordefinierten Konvention aus der ID der Komponente und dem Suffix –blanklabel zusammen. So würde man für die Komponente aus dem Listing 7.23 folgenden Eintrag im Nachrichtenkatalog der Seite anlegen: Listing 7.30: Register_de.properties (Auszug) paymentType-blanklabel=Bitte auswählen... ...
Das Ergebnis sieht dann wie in der Abbildung 7.8 aus.
Abbildung 7.8: Auswahlliste mit einer leeren Option
7.4.5
Palette
Die Komponente Palette erlaubt die Auswahl mehrerer Elemente aus einer vordefinierten Menge von Werten. Der Benutzer markiert ein oder mehrere Elemente aus einer Liste mit verfügbaren Elementen und verschiebt sie per Knopfdruck in eine Liste mit ausgewählten Elementen (siehe Abbildung 7.9). Die Auswahl der Elemente kann rückgängig gemacht werden, indem die ausgewählten Elemente alle zusammen oder einzeln zurück in die Box mit den verfügbaren Werten verschoben werden. Wir erweitern unser Registrierungsformular und geben dem Benutzer die Möglichkeit, seine Kenntnisse in Programmiersprachen anzugeben. Die Komponente Palette hat drei erforderliche Parameter model, selected und encoder. Analog zur Komponente Select gibt der Parameter model die zur Auswahl stehenden Optionen an. Der Parameter encoder wird benutzt, um die ausgewählten Strings auf der Clientseite nach dem Absenden des Formulars in serverseitige Objekte und umgekehrt zu transformieren. Nach dem Versenden des Formulars werden die ausgewählten Optionen in einer java.util.List abgelegt, die durch den Parameter selected angegeben wird.
139
7 Formulare
Abbildung 7.9: Mehrfachauswahl mit Palette Komponente Listing 7.31: Register.tml (erweitert um Palette) ... Programmiersprachen:
Wie schon bei der Select-Komponente, ist auch hier Java-Enum die einfachste Möglichkeit, ein org.apache.tapestry5.SelectModel zu erzeugen. Auch ein org.apache.tapestry5. ValueEncoder für eine Enum-Deklaration lässt sich einfach konstruieren, indem eine Instanz von EnumValueEncoder erzeugt wird (siehe Listing 7.32). Der Konstruktor von EnumValueEncoder erwartet die Klasse eines Enum, für den ein ValueEncoder erzeugt werden soll. Listing 7.32: Language.java public enum Language { C, JAVA, JAVASCRIPT, PERL, PHP, PYTHON, RUBY }
140
7.4 Überblick über Tapestrys Formularkomponenten
Listing 7.33: Register.java public class Register { ... @Inject private Messages messages; @Persist @Property private List selectedLanguages; public SelectModel getLanguagesModel(){ return new EnumSelectModel(Language.class, messages); } public ValueEncoder getLanguageEncoder(){ return new EnumValueEncoder(Language.class); } void onSuccess(){ ... for (Language language : selectedLanguages) { System.out.println(language.name()); } } }
7.4.6
Eingabe eines Datums
Für die Eingabe eines Datums bietet Tapestry die Komponente DateField an. Wir setzen diese Komponente ein, um unser Registrierungsformular um die Eingabe des Geburtsdatums des Benutzers zu erweitern. Die Komponente DateField unterscheidet sich von einem herkömmlichen Textfeld dadurch, dass die Eingabe eines Anwenders in eine Instanz von java.util.Date transformiert wird. Durch einen Klick auf das Bild links vom Textfeld wird ein Drop-down-Menü angezeigt, in dem ein Datum bequem ausgewählt werden kann (siehe Abbildung 7.10). Die Komponente DateField besitzt einen erforderlichen und mehrere optionale Parameter. Der erforderliche Parameter value verweist auf eine Seiteneigenschaft, die vom Typ java.util.Date sein muss. Die übermittelte Eingabe des Feldes wird von Tapestry mithilfe von java.text.DateFormat in ein Datumsobjekt umgewandelt und dieser Eigenschaft zugewiesen. Für das Format der gültigen Eingaben sind die Spracheinstellungen des Browsers entscheidend. Ist beispielsweise Deutsch als Standardsprache des Browsers eingestellt, so werden die Eingaben im Format dd.MM.yyyy erwartet. Dagegen werden die Eingaben für die Einstellung Englisch/USA im Format M/d/yy erwartet. Jede Eingabe, die von dem jeweiligen Format abweicht, resultiert in einem Validierungsfehler. Sie können aber das Datumsformat selbst bestimmen und der Komponente über den Parameter format übergeben.
141
7 Formulare
Abbildung 7.10: Auswahl eines Datums Listing 7.34: Register.tml (erweitert um DateField) ... Geburtstag:
Listing 7.35: Register.java (erweitert um ein Datumsfeld) public class Register { ... @Persist @Property private Date birthday; void onSuccess(){ ... System.out.println("birthday = " + birthday); } }
142
7.4 Überblick über Tapestrys Formularkomponenten
Falls Sie sicherstellen möchten, dass Ihre Benutzer kein Datum per Hand in das Feld eintippen können, können Sie die Darstellung des Textfeldes deaktivieren. Wenn Sie den Wert des Parameters hideTextField auf true setzen, wird nur noch das Bild zum Anklicken des Drop-down-Menüs angezeigt (siehe Abbildung 7.11).
Abbildung 7.11: Auswahl eines Datums ohne Anzeige des Textfelds
7.4.7
Hochladen von Dateien
Zum Hochladen von Dateien gibt es in HTML Eingabefelder vom Typ . Bei diesen Formularelementen wird rechts vom Eingabefeld eine Schaltfläche dargestellt, die zum Auswählen einer Datei auf dem lokalen System dient (siehe Abbildung 7.12). Durch einen Klick auf die Schaltfläche erscheint ein Dialog, in dem der Benutzer durch Verzeichnisse seines Rechners navigieren und eine Datei auswählen kann. Wird eine Datei ausgewählt, wird der Dialog geschlossen, und in dem Eingabefeld erscheint der absolute Pfad zu dieser Datei. Durch das Versenden des Formulars wird die ausgewählte Datei an den Server übertragen. In Tapestry wird der Datei-Upload mithilfe der Komponente Upload realisiert. Die Implementierung dieser Komponente basiert auf den Apache-Bibliotheken Commons File Upload und Commons IO. Um die Anzahl der Abhängigkeiten (JARDateien) von Anwendungen, die keinen Datei-Upload benötigen, zu minimieren, wurde diese Komponente in ein separates Unterprojekt mit dem Namen tapestryupload ausgegliedert. Um dieses Projekt zu benutzen, müssen Sie die drei JARDateien tapestry-upload-5.1.0.5.jar, commons-fileupload-1.2.jar und commons-io-1.3.jar in den Klassenpfad legen. Wir erweitern unser Registrierungsformular um die Funktionalität zum Hochladen von Dateien und geben dem Benutzer so die Möglichkeit, einen Avatar bei der Registrierung anzugeben. In Listing 7.37 ist die Komponente Upload zu sehen, die auf die Seiteneigenschaft uploadedFile verweist (Listing 7.37).
143
7 Formulare
Listing 7.36: Register.tml ... Avatar auswählen:
Abbildung 7.12: Hochladen von Dateien Die Komponente Upload erwartet, dass die Eigenschaft uploadedFile vom Typ UploadedFile ist. Die Klasse UploadedFile repräsentiert eine hochgeladene Datei, die auf Serverseite empfangen und behandelt werden kann (siehe Listing 7.37). In der Methode onSuccess() wird im Temp-Verzeichnis des Servers eine temporäre Datei erzeugt, in die der Inhalt der hochgeladenen Datei geschrieben wird. Listing 7.37: Register.java (erweitert um Datei-Upload) public class Register { ... @Property private UploadedFile uploadedFile; public void onSuccess() { ... String tempDir = System.getProperty("java.io.tmpdir"); File file = new File(tempDir + uploadedFile.getFileName()); uploadedFile.write(file); } }
144
7.4 Überblick über Tapestrys Formularkomponenten
Behandlung von Exceptions während eines Uploads Während eines Uploads kann es zu unerwarteten Fehlern kommen, die beispielsweise aus der Überschreitung der maximal erlaubten Upload-Größe resultieren. In diesem Fall wird Tapestry das Ereignis uploadException auslösen, um Sie über diesen Fehler zu informieren. Um dieses Ereignis zu behandeln, muss eine HandlerMethode implementiert werden, die einen Parameter vom Typ FileUploadException besitzt. In Listing 7.38 wird über diesen Parameter die geworfene Exception empfangen, deren Nachricht dem Benutzer präsentiert wird. Die Methode zur Behandlung des Ereignisses uploadException sollte einen Nicht-Null-Wert zurückgeben. Dieser Wert repräsentiert die Seite, die nach der Behandlung der FileUploadException dem Benutzer präsentiert werden soll. In diesem Beispiel wird eine Fehlermeldung in einer Seiteneigenschaft gespeichert und this zurückgegeben, um die Nachricht der Exception dem Benutzer auf der gleichen Seite zu präsentieren. Im Falle einer voidMethode oder der Rückgabe eines null-Wertes wird die Exception dem Benutzer über die Fehlerseite präsentiert. Listing 7.38: Register.java (Behandlung einer Exception während eines Uploads) public class Register { ... @Persist(PersistenceConstants.FLASH) @Property private String uploadErrorMessage; ... Object onUploadException(FileUploadException ex){ uploadErrorMessage = "Upload exception: " + ex.getMessage(); return this; } }
Konfiguration des Datei-Uploads Der Upload von Dateien kann durch vier Parameter konfiguriert werden, die in Form von Symbolen der IoC-Registry hinzugefügt werden (siehe Kapitel 17). Die verfügbaren Symbole sind in der Tabelle 7.2 zusammengefasst. Symbol
Standard-Wert
Beschreibung
upload.filesize-max
Keine Einschränkung (–1)
Maximal erlaubte Größe einer Datei (in Bytes).
upload.repository-location
Der Wert der Systemeigenschaft java.io.tmpdir
Pfad zum Schreiben von temporären Dateien.
Tabelle 7.2: Symbole zur Konfiguration von Datei-Uploads
145
7 Formulare
Symbol
Standard-Wert
Beschreibung
upload.repository-threshold
10 Kilobytes
Schwellenwert (in Bytes), ab dem eine hochgeladene Datei aus dem Arbeitsspeicher in das Dateisystem geschrieben wird.
upload.requestsize-max
Keine Einschränkung (–1)
Maximal erlaubte Größe (in Bytes) einer einzelnen Upload-Anfrage.
Tabelle 7.2: Symbole zur Konfiguration von Datei-Uploads (Forts.) Die Namen dieser Symbole sind in der Klasse UploadSymbols aus dem Paket org.apache. tapestry5.upload.services als Konstanten definiert. In Listing 7.39 ist ein Applikationsmodul zu sehen, in dem in der Contribute-Methode des Dienstes mit der ID ApplicationDefaults die Werte der vier Symbole überschrieben werden. Listing 7.39: Konfiguration von Datei-Uploads im Applikationsmodul public class AppModule { ... public static void contributeApplicationDefaults( MappedConfiguration configuration){ ... configuration.add(UploadSymbols.REPOSITORY_THRESHOLD, "5120"); configuration.add(UploadSymbols.REPOSITORY_LOCATION, System.getProperty("java.io.tmpdir")); configuration.add(UploadSymbols.REQUESTSIZE_MAX, "-1"); configuration.add(UploadSymbols.FILESIZE_MAX, "1048576"); } }
7.4.8
Abschicken von Formularen
In den bisherigen Beispielen wurde zum Abschicken der Formulare ein HTML-Element vom Typ submit eingesetzt. Tapestry stellt zwei eigene Komponenten bereit, die Sie in bestimmten Fällen gegenüber dem HTML-Submit bevorzugen sollten. Für die Erzeugung einer Schaltfläche ist die Komponente Submit zuständig. Mithilfe der Komponente LinkSubmit können Sie Ihre Formulare mittels Hyperlinks abschicken.
Erzeugen einer Schaltfläche Ähnlich wie alle anderen Formularkomponenten muss die Komponente Submit in eingebettet werden. Wird die Schaltfläche angeklickt, löst sie, noch vor dem Ereignis validateForm des umgebenen Formulars, das Ereignis selected aus. Zum Behandeln dieses Ereignisses kann eine Handler-Methode mit dem Namen onSelected() implementiert werden. Spätestens wenn Sie mehr als eine Schaltfläche innerhalb eines Formu-
146
7.4 Überblick über Tapestrys Formularkomponenten
lars benötigen, sollten Sie die Komponente Submit einsetzen. Wir können beispielsweise unser Registrierungsformular um eine Schaltfläche zum Abbrechen der Registrierung erweitern. Dazu benutzen wir die Komponente Submit und vergeben ihr die ID cancel. Gleichzeitig ersetzen wir das HTML-Submit zum Abschicken der Registrierung durch eine weitere Instanz der Komponente Submit und vergeben ihr die ID register. Die beiden IDs werden zur Unterscheidung der Quelle des Ereignisses selected eingesetzt. Nach der Namenskonvention für Handler-Methoden für Komponentenereignisse muss die Methode zum Behandeln des Ereignisses selected, das von der Komponente mit der ID cancel ausgelöst wurde, den Namen onSelectedFromCancel() tragen. In dieser Methode können wir den Registrierungsprozess abbrechen, indem wir zum Beispiel alle Eingaben des Benutzers löschen. Dementsprechend hört die Methode onSelectedFromRegister() auf das Ereignis der Schaltfläche mit der ID register. Beachten Sie, dass eine Registrierung innerhalb dieser Methode noch nicht möglich ist, da die Eingaben des Benutzers noch nicht validiert worden sind. Sie erinnern sich sicherlich, dass das Ereignis selected vor dem Ereignis validateForm ausgelöst wird. Listing 7.40: SubmitExample.tml ...
Listing 7.41: SubmitExample.java public class SubmitExample { @Persist @Property private String userName; @Persist @Property private String email; void onSelectedFromCancel() { //Abbruch der Registrierung this.userName = null; this.email= null; } void onSelectedFromRegister(){ System.out.println("onSelectedFromRegister()");
147
7 Formulare
// Zu früh für Registrierung } void onSuccess(){ // Registrierung erfolgt erst hier } }
Falls Sie den Namen des Ereignisses, das von der Komponente Submit ausgelöst wird, ändern möchten, können Sie dies mithilfe des Parameters event tun. Geben Sie einen Ereignisnamen, den Sie bevorzugen, an (siehe Listing 7.42), und benennen Sie Ihre Handler-Methode entsprechend um (siehe Listing 7.43). Listing 7.42: Umbenennen des Ereignisses der Komponente Submit
Listing 7.43: Behandeln des neuen Ereignisses public class SubmitExample { ... void onMySubmitEvent (){ System.out.println("onMySubmitEvent()"); } }
Versenden eines Formulars mit einem Link Neben der herkömmlichen Schaltfläche bietet Tapestry auch die Möglichkeit, ein Formular durch einen Klick auf einen Link abzuschicken. Dafür ist die Komponente LinkSubmit zuständig, die exakt die gleichen Parameter wie die Komponente Submit besitzt. Der Unterschied ist minimal: Die Komponente LinkSubmit erzeugt einen Hyperlink anstelle von . Listing 7.44: Versenden eines Formulars mit einem Link ... Submit mit Link
7.5
Labels für Formularfelder
In allen bisher besprochenen Beispielen haben wir die Labels für Textfelder erzeugt, indem wir einen Text in der Nähe des Feldes platziert haben. Ein solcher Text hat allerdings keinen Bezug zu dem HTML-Formularelement, den er beschreibt. Mithilfe des HTML-Elements kann ein solcher Bezug her-
148
7.5 Labels für Formularfelder
gestellt werden. Tapestry unterstützt HTML-Labels mit der Label-Komponente. Über den Parameter for dieser Komponente wird auf das Formularfeld verwiesen, für das ein erzeugt werden soll. Alle Formularfelder in Tapestry besitzen zudem den Parameter label. Hier wird der Text angegeben, der von für dieses Formularfeld als Bezeichner generiert werden soll. Listing 7.45 zeigt den Einsatz der LabelKomponente beim Eingabefeld für den Benutzernamen. Außerdem wird der im Parameter label des Formularfelds angegebene Wert in Validierungsnachrichten benutzt, sodass sichergestellt ist, dass die Validierungsnachrichten immer zum entsprechenden Formularfeld passen. Listing 7.45: Textfeld mit einem Label ... ...
Wenn die Labels der Formularfelder lokalisiert werden sollen, müssen im Nachrichtenkatalog zu dieser Seite entsprechende Einträge gemacht werden. Auf diese Einträge kann aus dem Template heraus mit dem message-Präfix zugegriffen werden. Listing 7.46: Internationalisierte Labels der Felder ... ...
Alternativ kann auch eine Namenskonvention für Schlüsselnamen im Nachrichtenkatalog genutzt werden: Wenn der Parameter label einer Formularkomponente weggelassen wird, sucht Tapestry im Nachrichtenkatalog nach einem Schlüssel mit dem Namen -label und verwendet den Wert als Label. In Listing 7.47 gibt es die Formularfelder userName und password, jeweils ohne label-Parameter. Tapestry sucht in dem Fall im Nachrichtenkatalog für diese Seite nach Schlüsseln mit dem Namen userName-label bzw. password-label (Listing 7.48). Listing 7.47: Auszug aus Register.tml ...
149
7 Formulare
...
Listing 7.48: Auszug aus Register_de.properties userName-label=Benutzername password-label=Passwort ...
7.6
Eingabevalidierung
Am Ende der Benutzerregistrierung steht oftmals das Speichern der gesammelten Informationen in einer Datenbank. Abhängig vom Datenbankmodell sind gewisse Eingaben des Benutzers (z. B. Benutzername, Passwort, E-Mail-Adresse usw.) zwingend erforderlich, um überhaupt einen Eintrag in der Datenbank anlegen zu können. Ohne diese Angaben kann eine Registrierung nicht abgeschlossen werden. Außerdem muss sichergestellt sein, dass die Eingaben bestimmte bestehende Einschränkungen erfüllen. Beispielsweise darf der Benutzer keinen beliebig langen Namen auswählen, wenn das entsprechende Datenbankfeld eine Längenbegrenzung besitzt. Aus diesem Grund muss nicht nur das Vorhandensein erforderlicher Werte, sondern auch ihre Gültigkeit überprüft werden. Für diesen oft mühsamen Prozess bietet Tapestry einen hervorragenden Mechanismus. Der Wert jedes einzelnen Feldes kann mithilfe von Validatoren überprüft werden, die durch Komma getrennt im Parameter validate von Formularfeldern angegeben werden. Jeder Validator hat einen Namen, über den er identifiziert wird, und ein optionales Argument. In Listing 7.49 enthält der Parameter validate drei Validatoren. Das erste Validator signalisiert, dass es sich um eine erforderliche Eingabe handelt. Es wird sichergestellt, dass das Formular erst dann das Ereignis success auslöst, wenn das Feld einen Wert besitzt. Die beiden anderen Validatoren grenzen die Eingabe auf einen numerischen Wert zwischen 10 und 99 ein. Listing 7.49: Beispiel für eine Eingabevalidierung
Anhand dieses Beispiels können Sie sehen, dass einige der Validatoren Argumente besitzen, mithilfe derer sie konfiguriert werden. Diese Argumente werden als Constraints (Einschränkungen) bezeichnet. Die Tabelle 7.3 listet alle in Tapestry verfügbaren Validatoren auf.
150
7.6 Eingabevalidierung
Name
Argumenttyp
Beschreibung
required
Stellt sicher, dass der eingegebene Wert nicht null und kein leerer String ist.
email
Stellt sicher, dass der eingegebene Wert eine gültige E-Mail-Adresse ist.
max
java.lang.Long
Stellt sicher, dass der eingegebene Wert einen bestimmten ganzzahligen Wert nicht überschreitet.
min
java.lang.Long
Stellt sicher, dass der eingegebene Wert einen bestimmten ganzzahligen Wert nicht unterschreitet.
maxlength
java.lang.Integer
Stellt sicher, dass der eingegebene String eine bestimmte Zeichenanzahl nicht überschreitet.
minlength
java.lang.Integer
Stellt sicher, dass der eingegebene String eine bestimmte Zeichenanzahl nicht unterschreitet.
regexp
java.util.regex.Pattern
Stellt sicher, dass der eingegebene Wert einem angegebenen regulären Ausdruck genügt.
Tabelle 7.3: Eingebaute Validatoren In Listing 7.50 wird unser Registrierungsformular um einige Validatoren erweitert. Das Feld für die Eingabe des Benutzernamens erfordert eine Eingabe, deren Länge zwischen drei und 50 Zeichen beträgt. Außerdem wird mithilfe eines regulären Ausdrucks überprüft, ob es sich um eine alphanumerische Eingabe handelt. Bei einem Passwort handelt es sich ebenfalls um eine erforderliche Eingabe, die eine Mindestlänge von fünf hat. Bei der Eingabe der E-Mail-Adresse wird überprüft, ob es sich um eine gültige E-Mail-Adresse handelt. Das Feld für die Eingabe des Geburtstags besitzt zwar keinen Validator-Eintrag im Template, es wird jedoch programmatisch validiert (siehe Listing 7.51). Listing 7.50: Auszug aus Register.tml (erweitert um Validatoren) : :
151
7 Formulare
: : ...
Die Validierung des Feldes birthday erfolgt mithilfe der Handler-Methode zur Behandlung des Ereignisses validate der Komponente mit der ID birthday. Diese Methode enthält als Argument eine Instanz von java.util.Date und wird aufgerufen, nachdem die herkömmlichen Validatoren ihre Arbeit erledigt haben. Falls die Eingabe die Validierung nicht besteht, kann eine ValidationException geworfen werden, deren Nachricht dem Benutzer präsentiert wird. Auf die gleiche Weise können Sie für jedes Feld eines Formulars Handler-Methoden für eine zusätzliche Validierung bereitstellen. Beachten Sie, dass die jeweiligen Felder eine eindeutige t:id besitzen müssen, da die Ereignisse sonst nicht der entsprechenden Handler-Methode zugewiesen werden können. Listing 7.51: Register.java (erweitert um Handler-Methode zur Validierung) public class Register { ... void onValidateFromBirthday(Date value) throws ValidationException{ if(value != null && value.after(new Date())){ throw new ValidationException( "Ihr Geburtstag liegt in der Zukunft."); } } }
Für die Darstellung der Validierungsnachrichten ist die Komponente Errors (Listing 7.50) verantwortlich. Das Ergebnis einer fehlgeschlagenen Validierung ist in der Abbildung 7.13 zu sehen.
152
7.6 Eingabevalidierung
Abbildung 7.13: Fehlgeschlagene Validierung Die Überprüfung der Eingaben mithilfe von Validatoren ist zwar ein bequemes Konzept, doch es hat seine Grenzen. Es wird nämlich nur die Syntax der Eingaben kontrolliert. Für die Validierung der Geschäftslogik sind aber weiterhin Sie zuständig. Nachdem Sie sicher sind, dass die Eingaben gültig sind, könnten Sie beispielsweise in der Datenbank nachschauen, ob der gewählte Benutzername bereits vergeben ist. Dazu können Sie das Ereignis validateForm des Formulars behandeln, das nach den Ereignissen validate der einzelnen Felder ausgelöst wird. In Listing 7.52 wird das im Template erzeugte Formular mithilfe der Annotation @InjectComponent in die Seite injiziert, um in der Handler-Methode den Status der Validierung abzufragen. Falls keine Fehler vorhanden sind, wird der eingegebene Benutzername auf Eindeutigkeit überprüft. Falls die Eindeutigkeit nicht gegeben ist, wird ein Fehler im Formular durch Aufruf der Methode Form.recordError(String) abgespeichert. Dieser Fehler wird ebenfalls mittels dem Anwender präsentiert. Listing 7.52: Register.java (erweitert um Validierung der Geschäftslogik) public class Register { ... @InjectComponent private Form registerForm;
153
7 Formulare
void onValidateForm(){ if(registerForm.getHasErrors()){ return; } if("root".equals(userName)){ registerForm.recordError("Der Name ist bereits vergeben."); } } }
Neben der serverseitigen Validierung können Formulare auch auf dem Client mittels JavaScript überprüft werden. Falls Sie eine clientseitige Validierung der Eingaben wünschen, belegen Sie den Parameter clientValidation der Komponente Form mit dem Wert true. Das Ergebnis einer JavaScript-Validierung ist in der Abbildung 7.14 zu sehen.
Abbildung 7.14: Clientseitige Validierung
7.6.1
Überschreiben von Validierungsmeldungen
Jeder der eingebauten Validatoren besitzt seine eigene Nachricht, die dem Benutzer im Falle einer erfolgslosen Validierung präsentiert wird. Diese Nachrichten sind in einem internen Nachrichtenkatalog von Tapestry abgelegt. Falls Sie die Meldung des Validators ändern möchten, können Sie eine Übersetzung im Nachrichtenkatalog der jeweiligen Seite anlegen. Auch hier gibt es eine Konvention. Zunächst sucht Tapestry nach einem Eintrag mit einem Schlüssel, dessen Name sich aus vier folgenden Bestandteilen zusammensetzt, die voneinander durch Unterstriche getrennt sind: 1. ID des Formulars, in dem das Feld eingebettet ist. 2. ID des Feldes. 3. Name des Validators. 4. Das Wort »message«. Wenn kein Eintrag gefunden wurde, sucht Tapestry nach einem Schlüssel ohne die ID des Formulars. Falls auch dieser Eintrag nicht gefunden wird, wird die Tapestry-
154
7.6 Eingabevalidierung
interne Nachricht benutzt. Um diese Konvention zu veranschaulichen, betrachten wir einen Auszug aus dem Nachrichtenkatalog der Seite Register. Für das Feld userName werden beide Formate eingesetzt. Wie in Listing 7.53 zu sehen ist, können Sie die Syntax von Formatstrings der Klasse java.util.Formatter innerhalb eines Nachrichtenkatalogs einer Tapestry-Seite verwenden. Listing 7.53: Auszug aus Register_de.properties ... registerForm-userName-required-message=’%s’ ist erforderlich. userName-minlengthmessage= ’%2$s’ darf die maximale Länge von '%1$d' Zeichen nicht überschreiten, ...
7.6.2
Eigene Validatoren
Die Menge der in Tapestry verfügbaren Validatoren (siehe Tabelle 7.3) kann durch eigene Implementierungen erweitert werden. Jeder Validator muss das Interface org.apache.tapestry5.Validator implementieren, das in Listing 7.54 zu sehen ist. Das Interface besitzt zwei generische Typparameter C und T. Der Parameter C definiert den Typen der Einschränkung des Validators und der Parameter T den Typ der zu validierenden Eingabe. Die Einschränkung eines Validators dient seiner Parametrisierung und wird später behandelt. Listing 7.54: Validator.java public interface Validator { Class getConstraintType(); Class getValueType(); String getMessageKey(); void validate(Field field, C constraintValue, MessageFormatter formatter, T value) throws ValidationException; boolean isRequired(); void render(Field field, C constraintValue, MessageFormatter formatter, MarkupWriter writer, FormSupport formSupport); }
Über die Methoden getConstraintType() bzw. getValueType() kann auf die Klassen der beiden Typparameter zugegriffen werden. Die Methode getMessageKey() gibt den Schlüssel der Nachricht im Nachrichtenkatalog an. Im Falle einer erfolgslosen Validie-
155
7 Formulare
rung wird diese Nachricht einem Benutzer präsentiert. Die eigentliche Validierung erfolgt in der Methode validate(). Sie besitzt folgende Parameter: Formularfeld, dessen Eingabe überprüft wird Typ der Einschränkung Dient zum Formatieren von Validierungsnachrichten Die zu validierende Eingabe
Die Methode isRequired() gibt an, ob der Validator einen null-Wert oder einen leeren String akzeptieren soll. Bei den meisten Validatoren gibt diese Methode den Wert false zurück, was dazu führt, dass der Validator im Falle einer fehlenden Eingabe überhaupt nicht aufgerufen wird. Die Methode render() gibt einem Validator die Möglichkeit, eigenes Markup zu erzeugen. Der Parameter vom Typ MarkupWriter kann zum Schreiben weiterer Attribute für das HTML-Element eines Formularfeldes benutzt werden, während der Parameter vom Typ FormSupport zur Bereitstellung der benutzerseitigen Validierung dient.
Eigene Validatoren ohne Einschränkungen Lassen Sie uns einen einfachen Validator implementieren, der nur alphanumerische Eingaben akzeptiert. Wir werden nicht alle Methoden des Interface Validator implementieren, da Tapestry eine abstrakte Superklasse für alle Validatoren bereitstellt, die einige Methoden bereits implementiert. Wie in Listing 7.55 zu sehen ist, erweitert unser Validator die abstrakte ValidatorSuperklasse AbstractValidator. Da unser Validator keine Einschränkung besitzt, wird Void als konkreter Typ des Typparameters C gewählt. Der konkrete Typ des Parameters T ist String, da wir als Eingaben Strings erwarten. Der Konstruktor von AbstractValidator erwartet drei Parameter: Typ der Einschränkung, Typ der zu validierenden Eingabe und der Name eines Schlüssels für eine Nachricht im Nachrichtenkatalog. Listing 7.55: AlphanumericValidator.java public class AlphanumericValidator extends AbstractValidator { public AlphanumericValidator() { super(null, String.class, "not-alphanumeric"); } public void validate(Field field, Void constraintValue, MessageFormatter formatter, String value) throws ValidationException { if (!value.matches("[a-zA-Z0-9]+")) { throw new ValidationException(
156
7.6 Eingabevalidierung
buildMessage(formatter, field)); } } public void render(Field field, Void constraintValue, MessageFormatter formatter, MarkupWriter writer, FormSupport formSupport) { formSupport.addValidation(field, "alphanumeric", buildMessage(formatter, field), null); } private String buildMessage(MessageFormatter formatter, Field field) { return formatter.format(field.getLabel()); } }
Da die Klasse AlphanumericValidator von AbstractValidator abgeleitet ist, müssen wir zwei Methoden implementieren. Wenn Tapestry die Eingabe eines Benutzers überprüft, wird die Methode validate() aufgerufen. Unser Validator benutzt einen regulären Ausdruck, der nur auf alphanumerische Eingaben zutrifft. Falls eine Eingabe dem Ausdruck nicht entspricht, wird eine ValidationException geworfen. Die Nachricht dieser Exception wird aus dem Validatoren-Nachrichtenkatalog ausgelesen und einem Benutzer präsentiert. Ein Beispiel für eine solche Nachricht ist in Listing 7.56 zu sehen. Beachten Sie, dass diese Nachricht den Formatparameter %s hat, der bei der Generierung der Nachricht durch das Label des Feldes ersetzt wird. Listing 7.56: ValidationStrings.properties not-alphanumeric=Bitte geben Sie für '%s' einen alphanumerischen Wert an.
Falls Sie eine clientseitige Validierung durch Ihren Validator wünschen, müssen Sie die JavaScript-Klasse Tapestry.Validator in einer beliebigen JavaScript-Datei um eine Funktion erweitern, in der Sie die Validierungslogik in JavaScript implementieren. Diese JavaScript-Datei muss dann in die Seite eingebunden werden, in der der Validator verwendet wird. In Listing 7.58 wird die JavaScript-Klasse Tapestry.Validator um die Funktion alphanumeric() erweitert, in der der gleiche reguläre Ausdruck wie auf der Serverseite zur Überprüfung der Eingaben benutzt wird. Listing 7.57: validator.js Tapestry.Validator = { alphanumeric : function(field, message){ var regexp = new RegExp('[a-zA-Z0-9]+'); field.addValidator(function(value){
157
7 Formulare
if (! regexp.test(value)){ throw message; } }); } };
Die JavaScript-Funktion alphanumeric() besitzt zwei Parameter: Das Formularfeld, dessen Eingabe validiert wird. Eine Nachricht für den Fall, dass Validierung fehlschlägt.
Optional kann ein dritter Parameter angegeben werden, durch den der Wert der Einschränkung an die Funktion übergeben wird. Damit die JavaScript-Funktion im generierten HTML-Code erscheint, muss beim serverseitigen Validator in der Methode render() der Dienst FormSupport benutzt werden. Durch den Aufruf der Methode addValidator() dieses Dienstes wird die JavaScriptFunktion in das Markup geschrieben. Diese Methode erwartet als Parameter: Das Formularfeld, dessen Eingabe validiert wird. Den Namen der JavaScript-Funktion. Eine Nachricht für den Fall, dass Validierung fehlschlägt. Den Wert der Einschränkung.
Damit unser Validator benutzt werden kann, muss er in der Contribute-Methode des Dienstes FieldValidatorSource der Konfiguration des Dienstes hinzugefügt werden (siehe Listing 7.58). Für die Validierungsnachrichten von selber geschriebenen Validatoren muss ein eigener Nachrichtenkatalog angelegt werden. In der Contribute-Methode des Dienstes ValidationMessagesSource muss dazu ein Pfad zum gewünschten Nachrichtenkatalog angegeben werden. In Listing 7.58 wird der Pfad zur Datei ValidationString.properties der Konfiguration von ValidationMessagesSource unter dem Namen MyValidationString hinzugefügt. Der Wert after:Default gibt an, dass der bereitgestellte Nachrichtenkatalog nach Tapestrys Standard-Nachrichtenkatalog nach der Validierungsmeldung durchsucht werden soll. Listing 7.58: Bereitstellen eines Validators im Applikationsmodul public class AppModule { ... public static void contributeFieldValidatorSource( MappedConfiguration configuration) { configuration.add("alphanumeric", new AlphanumericValidator()); }
158
7.6 Eingabevalidierung
public void contributeValidationMessagesSource( OrderedConfiguration configuration) { configuration.add( "MyValidationString", "de/t5book/services/impl/ValidationString", "after:Default"); } }
Eigene Validatoren mit Einschränkungen Wie bereits erwähnt, kann ein Validator über eine Einschränkung parametrisiert werden. Lassen Sie uns einen Validator entwickeln, der beispielsweise zur Überprüfung von Passwörtern benutzt werden kann. In vielen Anwendungen muss das gewünschte Passwort einem bestimmten Anspruch genügen. Ein solcher möglicher Anspruch könnte beispielsweise sein, dass das Passwort eine bestimmte Mindestanzahl von Zahlen enthält. Das Format zu Eingabe der Einschränkungen ist: Name des Validators = Einschränkung
In Listing 7.59 wird der Validator hasdigits mit dem Wert 3 parametrisiert. Dieser Wert wird an den Validator über den zweiten Parameter der validate()-Methode übergeben (siehe Listing 7.60). Listing 7.59: HasDigitValidatorExample.tml ...
In Listing 7.60 wird die Einschränkung benutzt, um die Mindestanzahl von Zahlen innerhalb einer Eingabe zu überprüfen. Falls die Anzahl der Zahlen innerhalb einer Eingabe unter diesem Minimalwert liegt, wird eine ValidationException geworfen. Beachten Sie, dass der Typ der Einschränkung durch den ersten Typparameter der Validatorklasse (hier java.lang.Integer) bestimmt wird. Listing 7.60: HasDigits.java public class HasDigits extends AbstractValidator { public HasDigits() { super(Integer.class, String.class, "has-digits");
159
7 Formulare
} public void validate(Field field, Integer constraintValue, MessageFormatter formatter, String value) throws ValidationException { int count = 0; for (int i = 0; i < value.length(); i++) { if (Character.isDigit(value.charAt(i))) { count++; } } if (count != constraintValue) { String message = formatter.format( field.getLabel(), constraintValue); throw new ValidationException(message); } } public void render(Field field, Integer constraintValue, MessageFormatter formatter, MarkupWriter writer, FormSupport formSupport) { } }
7.7
Null-Werte in Formularfeldern
Haben Sie bei der Definition eines Feldes keinen Gebrauch vom required-Validator gemacht, so ist dieses Feld automatisch optional. In diesem Fall müssen Sie damit rechnen, dass ein Benutzer keine Eingaben in diesem Feld vornimmt. Oft können fehlende optionale Eingaben ignoriert werden, doch manchmal müssen Sie sich eine geschickte Strategie zur Behandlung der nicht vorhandenen Werte einfallen lassen. Wenn Sie beispielsweise Standardwerte für optionale Eingaben vorgesehen haben, müssen Sie die eingegebenen Werte auf null überprüfen und gegebenenfalls auf den jeweiligen Standardwert setzen. Bei einer großen Anzahl von optionalen Feldern wird Ihr Code somit durch if-Anweisungen aufgebläht. Um dies zu vermeiden, kann Tapestry Ihnen beim Umgang mit null-Werten helfen. Dazu teilen Sie dem jeweiligen Feld mit, wie eine nicht vorhandene Eingabe behandelt werden soll. Dies erfolgt durch die Angabe des Namens der gewünschten Strategie über den Parameter nulls, den jede Formularkomponente besitzt. Tapestry bringt zwei Strategien mit: default und zero. Die Strategie default lässt einen null-Wert unbehandelt und wird standardmäßig von allen Formularkomponenten benutzt. Die Strategie zero ersetzt einen nullWert durch die Zahl 0 und sollte demnach für numerische Eingaben benutzt werden. Ein Beispiel ist in Listing 7.61 zu sehen.
160
7.7 Null-Werte in Formularfeldern
Listing 7.61: Textfeld mit der Zero-Strategie
Sie können Ihre eigene Strategie entwickeln, indem Sie das Interface org.apache. tapestry5.NullFieldStrategy implementieren. Das Interface spezifiziert zwei Methode zum Ersetzen von null-Werten in beide Richtungen: vom Client zum Server und umgekehrt. In Listing 7.62 wird eine anonyme Implementierung dieses Interface in der Methode getNullStrategy() zurückgegeben, die einen null-Wert durch den kleinstmöglichen Integerwert ersetzt. Die Methode replaceFromClient() liefert einen Ersatzstring für einen null-Wert. Die Methode replaceToClient() stellt den Ersatzstring für die Rückrichtung bereit, wenn der serverseitige Wert zum String konvertiert wird. Listing 7.62: NullFieldStrategyExample.java public class NullFieldStrategyExample { @Persist @Property private Integer age; public NullFieldStrategy getNullStrategy(){ return new NullFieldStrategy(){ public String replaceFromClient() { return String.valueOf(Integer.MIN_VALUE); } public Object replaceToClient() { return Integer.MIN_VALUE; } }; } void onSuccess() { System.out.println("age = " + age); } }
Innerhalb eines Templates erfolgt der Zugriff auf diese Strategie mittels prop-Präfix (Listing 7.63). Listing 7.63: Textfeld mit neuer NullFieldStrategy
161
7 Formulare
7.8
Umwandlung von Eingaben zwischen Client und Server
Die Komponenten TextField, TextArea und PasswordField bieten dem Benutzer die Möglichkeit, Eingaben vorzunehmen. Die vom Benutzer eingegebenen Strings werden an den Server gesendet, wo sie in serverseitige Objekte umgewandelt werden. In den bisherigen Beispielen wurden diese drei Komponenten mit Seiteneigenschaften vom Typ String verknüpft. Somit mussten Sie nichts für die Umwandlung zwischen client- und serverseitiger Repräsentation von Eingaben tun. Jedoch besitzen die Komponenten TextField, TextArea und PasswordField jeweils den erforderlichen Parameter translate vom Typ org.apache.tapestry5.FieldTranslator. Ein FieldTranslator ist für die Umwandlung von Benutzereingaben in serverseitige Objekte zuständig. Sie mussten nur deshalb keine Werte für diesen erforderlichen Parameter angeben, weil Tapestry für folgende Typen automatisch eine Umwandlung von benutzerseitigen Strings in serverseitige Objekte vornehmen kann: java.lang.String (triviale Transformation). java.lang.Byte, java.lang.Short, java.lang.Integer, java.lang.Long, java.lang.Float, java.lang.Double, java.lang.BigInteger, java.lang.BigDecimal.
Im Folgenden betrachten wir ein Beispiel, bei dem Sie selber für die Umwandlung zwischen benutzer- und serverseitiger Repräsentation von Benutzereingaben sorgen müssen. Wir entwickeln eine einfache Seite (Listing 7.64), in der ein Benutzer zur Eingabe einer Währung in ein Formularfeld aufgefordert wird. Das Formular enthält ein TextField, dessen Eingaben nach dem Versand der Seiteneigenschaft currency zugewiesen werden (siehe Listing 7.65). Listing 7.64: CurrencyTranslatorExample.tml Eingegebene Währung: ${currency.getSymbol()}
Bitte geben Sie eine Währung ein:
162
7.8 Umwandlung von Eingaben zwischen Client und Server
Listing 7.65: CurrencyTranslatorExample.java public class CurrencyTranslatorExample { @Property @Persist(PersistenceConstants.FLASH) private Currency currency; }
Wenn Sie die Seite CurrencyTranslatorExample in Ihrem Browser aufrufen, werden Sie eine Exception sehen, die Ihnen mitteilt, dass der erforderliche Parameter translate für das Textfeld zur Eingabe einer Währung nicht gesetzt ist. Der Grund dafür ist, dass Tapestry keinen FieldTranslator zur Umwandlung der Benutzereingaben in eine Instanz von java.util.Currency findet. Um diesen Fehler zu beheben, müssen Sie einen Translator für Currency schreiben und dem Framework bereitstellen. Für die Umwandlung zwischen client- und serverseitiger Repräsentation von Eingaben ist eine Klasse verantwortlich, die das Interface org.apache.tapestry5.Translator implementiert. Wie in Listing 7.66 zu erkennen ist, besitzt das Interface einen generischen Typparameter T, der den Zieltyp der Umwandlung definiert. Über die Methode getType() kann auf die Klasse dieses Typs zugegriffen werden. Weiterhin besitzt ein Translator einen Namen, der in der Methode getName() zurückgegeben wird. Bei einer fehlgeschlagenen Transformation muss einem Benutzer eine Fehlernachricht präsentiert werden, die in einem Nachrichtenkatalog hinterlegt ist. Der Schlüssel dieser Nachricht innerhalb des Nachrichtenkatalogs wird von der Methode getMessageKey() zurückgegeben. Die beiden Methoden toClient() bzw. parseClient() dienen der Umwandlung der Eingaben vom bzw. zum Benutzer. Ähnlich wie bei einem Validator kann die Methode render() zur Erzeugung zusätzlichen Markups durch den Translator verwendet werden. Listing 7.66: Translator.java public interface Translator { String getName(); Class getType(); String getMessageKey(); String toClient(T value); T parseClient(Field field, String clientValue, String message) throws ValidationException; void render(Field field, String message, MarkupWriter writer, FormSupport formSupport); }
163
7 Formulare
In Listing 7.67 ist eine Implementierung von Translator zu sehen, die für die Umwandlung von Strings in eine Instanz von java.util.Currency und umgekehrt zuständig ist. Um aus einem String eine Instanz von Currency zu erzeugen, wird in der Methode parseClient() der übermittelte String in die statische Methode getInstance() der Klasse Currency weitergegeben. Diese Methode liefert für einen ISO-4217-Code einer Währung eine Instanz von Currency oder wirft im Falle eines ungültigen Codes eine IllegalArgumentException. Die Exception wird abgefangen, um eine ValidationException zu werfen. Die Nachricht der ValidationException wird im Nachrichtenkatalog für Validierungsfehler unter dem Schlüssel currency-parse-exception nachgeschlagen und der Methode parseClient() über den Parameter mit dem Namen message zur Verfügung gestellt. Für die Umwandlung in die andere Richtung ist die Methode toClient() zuständig. Sie erhält über ihren Parameter eine Instanz von Currency, für die eine benutzerseitige Repräsentation erzeugt werden soll. Dazu wird die Methode toString() auf dieser Instanz aufgerufen, die beispielsweise für die Währung Euro den String »EUR« zurückgibt. Listing 7.67: CurrencyTranslator.java public class CurrencyTranslator implements Translator { public String getMessageKey() { return "currency-parse-exception"; } public String getName() { return "currency"; } public Class getType() { return Currency.class; } public Currency parseClient(Field field, String clientValue, String message) throws ValidationException { try { return Currency.getInstance(clientValue); } catch (final IllegalArgumentException e) { throw new ValidationException(message); } } public String toClient(final Currency value) { return value.toString(); }
164
7.8 Umwandlung von Eingaben zwischen Client und Server
public void render(Field field, String message, MarkupWriter writer, FormSupport formSupport){ } }
Die Registrierung des Translators erfolgt im Applikationsmodul durch das Schreiben einer Contribute-Methode für den Dienst TranslatorSource. In Listing 7.68 wird eine Instanz von CurrencyTranslator der Konfiguration von TranslatorSource hinzugefügt. Listing 7.68: Bereitstellen eines Translators im Applikationsmodul public class AppModule { ... public static void contributeTranslatorSource( Configuration configuration) { configuration.add(new CurrencyTranslator()); } }
Um den Translator einzusetzen, müssten Sie für den Parameter translate des Textfeldes zur Eingabe einer Währung den Namen des Translator angeben, der in der Methode getName() (siehe Listing 7.67) zurückgegeben wird. Doch die Angabe des Translators wie in Listing 7.69 ist in diesem Fall nicht notwendig, da Tapestry den richtigen Translator anhand des Typen T auswählt. Für jede Seiteneigenschaft vom Typ java.util.Currency wird automatisch CurrencyTranslator benutzt. Listing 7.69: CurrencyTranslatorExample.tml (erweitert um Parameter translate) ...
Wenn allerdings aufgrund des Typs der Seiteneigenschaft ein Translator nicht eindeutig identifiziert werden kann, muss der Name des Translator im Template angegeben werden. Wenn Sie beispielsweise die abstrakte Superklasse java.lang.Number anstelle einer konkreten Unterklasse wie java.lang.Integer für eine Seiteneigenschaft wählen (siehe Listing 7.70), kann Tapestry den benötigten Translator nicht eindeutig identifizieren. Tapestry besitzt Translatoren für acht Unterklassen von java.lang.Number, sodass Sie explizit angeben müssen, welcher dieser acht Translatoren benutzt werden soll.
165
7 Formulare
Listing 7.70: MyPage.java public class MyPage { @Property private Number myNumber; }
FieldTranslator vs. Translator Obwohl der Parameter translate einen Wert vom Typ FieldTranslator erwartet, haben wir in den vorigen Beispielen immer das Interface Translator implementiert. Bei einem FieldTranslator handelt es sich um einen Wrapper um Translator, der dem Translator einen Zugriff auf den gleichen Nachrichtenkatalog wie den der Validatoren gewährt. Die Nachrichten für einen Translator werden also im gleichen Nachrichtenkatalog wie für einen Validator gespeichert. Die im Applikationsmodul hinterlegten Translator sind global. Sie werden für alle Formularfelder benutzt, deren Typ dem Typ des Translators entspricht. Um Ihnen die Möglichkeit zu geben, diese globale Implementierung in bestimmten Fällen zu überschreiben, werden von Tapestry für die Komponenten TextField, TextArea und PasswordField zwei Ereignisse ausgelöst: onParseClient und onToClient. Die Ereignisse dienen der Umwandlung zwischen der client- und serverseitigen Repräsentation von Eingaben. In Listing 7.71 sind zwei Handler-Methoden für die beiden Ereignisse implementiert, in denen die Logik von CurrencyTranslator überschrieben werden kann. Die Methode onParseClientFromCurrencyField() ersetzt den Aufruf der Methode parseClient() eines Translator und onToClientFromCurrencyField() den Aufruf von toClient(). Falls eine der beiden Methoden null zurückgibt, wird die entsprechende Methode von CurrencyTranslator aufgerufen. Listing 7.71: CurrencyTranslatorExample.java (erweitert um Translate-Handler-Methoden) public class CurrencyTranslatorExample { @Property @Persist(PersistenceConstants.FLASH) private Currency currency; public Currency onParseClientFromCurrencyField(String value) throws ValidationException { return Currency.getInstance(value); } public String onToClientFromCurrencyField(Currency currency) { if (currency!= null) { return currency.toString();
166
7.9 Zusammenfassung
} return null; } }
7.9
Zusammenfassung
Bei der Entwicklung von Formularen unterstützt Tapestry die Entwickler durch eine Menge von Formularkomponenten. Ein Entwickler muss sich nicht mit HTTP und Servlet-API auseinandersetzen. Die verschickten Eingaben werden von Tapestry in die richtigen Java-Typen konvertiert und den Eigenschaften einer Seite zugewiesen. Eine Weiterleitung zu einer Ergebnisseite erfolgt wie bei der Navigation zwischen Seiten durch Rückgabe einer Seitenklasse. Die Behandlung der Formulare erfolgt in den Handler-Methoden für Ereignisse, die von der Komponente Form ausgelöst werden: prepareForRender: Wird ausgelöst, bevor die Komponente mit der Erzeugung des
Markups beginnt. prepare: Wird zur Initialisierung von Variablen eingesetzt. prepareForSubmit: Wird ausgelöst, bevor Formular abgeschickt wird. validateForm: Wird zur Überprüfung von Eingaben verwendet. success: Wird im Falle einer erfolgreichen Validierung ausgelöst. failure: Wird im Falle einer erfolgslosen Validierung ausgelöst. submit: Das letzte Ereignis der Komponente Form. Wird immer ausgelöst.
Zur Validierung von Eingaben stellt Tapestry eine Menge von Validatoren bereit. Eingaben eines Formularfeldes können durch mehrere Validatoren überprüft werden. Auch Entwicklung von eigenen Validatoren ist möglich. Überprüfung von Eingaben kann sowohl server- als auch clientseitig erfolgen. Die in Tapestry eingebauten Validierungsmeldungen können überschrieben werden. Der Prozess der Umwandlung von Benutzereingaben zwischen benutzer- und serverseitiger Repräsentation kann ebenfalls erweitert werden. Dazu können eigene Translator geschrieben und Tapestry zur Verfügung gestellt werden.
167
8 Dynamische Formulare Die Erstellung eines Formulars ist in den meisten Fällen ziemlich unkompliziert. Sie bestimmen die benötigten Eingabefelder und ihre Datentypen und entscheiden sich anschließend für eine zu benutzende Formularkomponente sowie mögliche Validierungseinschränkungen. All diese Entscheidungen treffen Sie bereits zur Entwicklungszeit. In manchen Fällen liegen Ihnen die Informationen über Anzahl oder Datentypen von Formularfeldern aber erst zur Laufzeit vor, weil diese Informationen beispielsweise in einer Datenbank hinterlegt sind. In diesem Fall wird die Erstellung von Formularen etwas komplizierter. Der Code wird weniger übersichtlich, da Sie jede Menge if-else-Anweisungen innerhalb des Formulars platzieren müssen. Außerdem wird automatisch ein Teil der Anwendungslogik in die Präsentationsschicht verlagert, was die Wartbarkeit deutlich reduziert. Bei der Erzeugung von Formularen mit einer variablen Anzahl von Eingabefeldern kommen Sie allein mit Formularkomponenten nicht aus. Es werden weitere Komponenten benötigt, die in diesem Kapitel vorgestellt werden.
8.1
Variable Anzahl von Formularfeldern
Wenn Anzahl und Datentypen von Feldern eines Formulars variieren, müssen Sie das Formular zur Laufzeit dynamisch erzeugen. Sie können nicht einfach wie gewohnt den Seiteneigenschaften Formularfelder zuordnen, da Sie gar nicht wissen, wie viele Eigenschaften die Seite besitzen soll. Gewöhnlich wird die Anzahl der Formularfelder durch eine Liste von Objekten bestimmt, sodass die Komponente Loop benötigt wird, um über diese Liste zu iterieren. In jedem Iterationszyklus wird für das aktuelle Objekt der Liste ein Formularfeld erzeugt, wobei Sie sich für eine der Formularkomponenten (TextField, DateField usw.) entscheiden müssen. Dazu platzieren Sie im Formular eine Menge von if-else-Anweisungen, wodurch das Template unnötig aufgebläht wird. Eine bessere Alternative ist die Komponente Delegate. Sie erzeugt selber keine Ausgabe, sondern delegiert die Erzeugung des Markups an eine weitere Komponente wie beispielsweise Block. Alle erzeugten Formularfelder werden mit einer einzigen Seiteneigenschaft verknüpft, die beim Abschicken des Formulars mehrmals mit Eingaben aus den Feldern aktualisiert wird. Um diese Eingaben auszulesen, wird die Komponente SubmitNotifier benötigt. Sie benachrichtigt Sie über den Versand des Formulars, indem es die Ereignisse BeginSubmit und AfterSubmit auslöst.
8 Dynamische Formulare
8.1.1
Beispielszenario
Stellen Sie sich vor, Sie hätten eine Software zur Erstellung von Geschäftsberichten einer Bank entwickelt oder nehmen eine kommerzielle Reporting-Lösung in Betrieb. Jeder Bericht hat Parameter unterschiedlicher Datentypen wie Datum, Zahl, String usw., die den Inhalt des Berichts bestimmen. Diese Parameter und Ihre Typen sind in einer Datenbank hinterlegt oder werden von der API der kommerziellen Lösung zur Verfügung gestellt. Nun haben Sie als Webentwickler die Aufgabe, eine Webanwendung zum Laden der Berichte zu schreiben. Die Applikation soll ein Formular besitzen, in dem Werte für die Parameter des Berichts eingegeben werden (siehe Abbildung 8.1). Sobald das Formular abgeschickt wird, soll ein Bericht in Form einer PDF-Datei zur Verfügung stehen.
Abbildung 8.1: Berichtsformular Nehmen wir an, dass Sie den Dienst ReportService zur Verfügung haben, der Ihnen für einen Bericht die entsprechenden Parameter bereitstellt (siehe Listing 8.1). Listing 8.1: ReportService.java public interface ReportService { List getReportParameters(String reportName); ReportParameter findReportParameter(String reportName, String name); InputStream getReportData(String reportName, List parameters); }
170
8.1 Variable Anzahl von Formularfeldern
Jeder Parameter wird durch die Klasse ReportParameter (Listing 8.2) repräsentiert. Es handelt sich um eine abstrakte Klasse mit einer generischen Typdefinition. Der Typ T gibt den Typ des Parameters an und wird von Unterklassen von ReportParameter angegeben. In Listing 8.3 ist eine Unterklasse zu sehen, deren Werte vom Typ java.util.Date sind. Nehmen wir an, es gibt noch zwei weitere Unterklassen StringParameter und NumericParameter. Listing 8.2: ReportParameter.java public abstract class ReportParameter { private String name; private T value; ... }
Listing 8.3: DateParameter.java public class DateParameter extends ReportParameter { ... }
8.1.2
Implementierung des Beispielszenarios
Die Klasse der Seite zum Anzeigen des Berichtsformulars ist in Listing 8.4 zu sehen. Wenn die Seite der aktuellen Anfrage zugewiesen wird, wird der Dienst ReportService nach den Parametern für den Bericht geschaeftsbericht befragt. Die zurückgegebenen Parameter werden der Seiteneigenschaft parameters zugewiesen. Diese Eigenschaft ist mit @Property annotiert, sodass auf sie aus dem Template heraus zugegriffen werden kann. Listing 8.4: ViewReport.java public class ViewReport { @Inject private ReportService reportService; @Property private ReportParameter currentParameter; @Property private List parameters; void pageAttached(){ parameters = reportService .getReportParameters("geschaeftsbericht"); }
171
8 Dynamische Formulare
public ValueEncoder getEncoder() { return new ValueEncoder() { public String toClient(ReportParameter clientValue) { return clientValue.getName(); } public ReportParameter toValue(String clientValue) { return reportService.findReportParameter( reportName, clientValue); } }; } public StreamResponse onSuccess() { return new StreamResponse() { public String getContentType() { return "application/pdf"; } public InputStream getStream() throws IOException { return reportService.getReportData( "geschaeftsbericht", parameters); } public void prepareResponse(final Response response) { response.setHeader("Content-disposition", "attachment;filename=Bericht.pdf"); } }; } }
Im Template dieser Seite (Listing 8.5) wird ein Formular platziert, das eine Loop-Komponente enthält. Die Komponente Loop iteriert über alle Parameter des Berichts und erzeugt für jeden ein TextField. Der aktuelle Parameter der Iteration wird der Eigenschaft currentParameter zugewiesen. Damit Tapestry die Benutzereingaben in Instanzen von ReportParameter umwandeln kann, benötigt Loop einen ValueEncoder. In Listing 8.4 wird in der Methode getEncoder() eine anonyme Implementierung von ValueEncoder erzeugt, in der der Name eines Berichtsparameters zur Umwandlung zwischen client- und serverseitiger Repräsentation von ReportParameter verwendet wird. Sobald das Formular mit den Eingaben abgeschickt wird, wird die Methode onSucces() aufgerufen, die für die Behandlung des Ereignisses succes des Formulars zuständig ist. Innerhalb dieser Methode wird eine anonyme Implementierung von StreamResponse erzeugt, in der mithilfe des Dienstes ReportService die Daten des Berichts geladen werden. Schließlich wird der Bericht in Form einer PDF-Datei angezeigt. StreamResponse wird in Kapitel 10 im Detail besprochen.
172
8.1 Variable Anzahl von Formularfeldern
Listing 8.5: ViewReport.tml
In den bisherigen Beispielen wurden die Eingaben des Benutzers noch nicht behandelt. Dazu müssen wir unsere Seite so erweitern, dass wir beim Absenden des Formulars die Gelegenheit bekommen, jeden eingegebenen Wert eines Formularfeldes auszulesen. Außerdem müssen wir eine Unterstützung für die unterschiedlichen Datentypen der Berichtparameter einbauen. Bisher haben wir für jeden Parameter eine TextField-Komponente erzeugt, für einen DateParameter würde aber eher die DateField-Komponente passen. Dazu passen wir das bisherige Beispiel an und benutzen die Komponenten Delegate und SubmitNotifier (Listing 8.6). Wie bereits erwähnt erzeugt SubmitNotifier selber kein Markup, sondern benachrichtigt lediglich über den Versand des Formulars, indem es die Ereignisse BeginSubmit und AfterSubmit auslöst. Die Komponente Delegate erzeugt ebenfalls keine Ausgabe, sondern delegiert die Erzeugung des Markups an eine weitere Komponente. Der Parameter to gibt an, welche Komponente mit der Markup-Erzeugung beauftragt wird. In unserem Beispiel wird dies von einer der Block-Komponenten mit der ID stringBlock, dateBlock oder numericBlock übernommen. Jede dieser Block-Komponenten erzeugt jeweils ein Formularfeld für die Eingabe eines Strings, eines Datums oder einer Zahl. Die Entscheidung darüber, welche der drei Block-Komponenten die Markup-Erzeugung durchführt, wird in der Methode getActiveBlock() der Seitenklasse getroffen. Wenn die Komponente Loop über die Berichtsparameter iteriert, wird der Parameter der aktuellen Iteration der Seiteneigenschaft currentParameter zugewiesen. Anhand des Typen dieser Eigenschaft wird in der Methode getActiveBlock() entschieden, welche der drei Block-Komponenten für die Erzeugung des entsprechenden Formularfeldes zuständig ist. Die Methode getActiveBlock() gibt eine Instanz von Block zurück, die ein aus dem Template repräsentiert. Ein Block aus dem Template kann in die Seitenklasse injiziert werden, indem eine Eigenschaft vom Typ Block mit @Inject annotiert wird. Dabei muss der Name dieser Eigenschaft der Client-ID des jeweiligen Block im Template entsprechen. Die Client-ID der Komponente kann auch mithilfe der @Id-Annotation explizit angegeben werden.
173
8 Dynamische Formulare
Listing 8.6: ViewReport.tml (erweitert) ${currentParameter.name}:
Wenn das Formular versendet wird, läuft die Komponente Loop erneut über die Liste der Berichtsparameter und weist den Wert der aktuellen Iteration der Seiteneigenschaft currentParameter zu. Zusätzlich löst SubmitNotifier in jeder Iteration die beiden Ereignisse BeginSubmit und AfterSubmit aus. In Listing 8.7 ist eine Handler-Methode für das Ereignis AfterSubmit implementiert, in der aus der Eigenschaft currentParameter die Eingabe des Formularfeldes der aktuellen Iteration ausgelesen wird. Dieser Wert wird durch den Aufruf der Methode setValue() in eine Instanz von ReportParameter gesetzt, sodass am Ende alle Eingaben des Benutzers ausgelesen wurden. Listing 8.7: ViewReport.java (erweitert) public class ViewReport { @Inject private ReportService reportService; @Property
174
8.1 Variable Anzahl von Formularfeldern
private ReportParameter currentParameter; @Property private List parameters; @Inject private Block stringBlock; @Inject private Block dateBlock; @Inject private Block numericBlock; void pageAttached(){ ... } public ValueEncoder getEncoder() { ... } public StreamResponse onSuccess() { ... } void onAfterSubmit() { for (ReportParameter next : parameters) { if (next.equals(currentParameter)) { next.setValue(currentParameter.getValue()); return; } } } public Block getActiveBlock(){ if(currentParameter instanceof NumericParameter){ return numericBlock; }else if(currentParameter instanceof DateParameter){ return dateBlock; } return stringBlock; } }
Falls Sie die Beispiele begleitend zum Lesen nachprogrammiert haben, können Sie nun die Seite ViewReport in Ihrem Browser aufrufen. Das Ergebnis sollte wie in der Abbildung 8.1 aussehen. Wenn Sie nun das Formular ausfüllen und abschicken, wird der Download des Berichts gestartet.
175
8 Dynamische Formulare
8.2
Erweitern der Formulare durch Benutzerinteraktionen
In einigen Fällen ist es notwendig, Formulare aus der Benutzerschnittstelle heraus um Eingabefelder erweitern zu können. Im letzten Abschnitt haben Sie gelernt, wie Sie mit einer variablen Anzahl von Formularfeldern umgehen können. In diesem Abschnitt gehen wir einen Schritt weiter und lassen Ihre Benutzer entscheiden, wie viele Felder das Formular besitzen soll. Wir benötigen also eine Komponente, die nicht nur über eine Liste von Objekten iteriert, sondern auch die Möglichkeit bietet, auf Benutzerinteraktionen zu reagieren. Tapestry stellt dafür die Komponente AjaxFormLoop bereit. Diese Komponente ist eine spezielle Art von Loop, die Formulare mithilfe von Ajax (Asynchronous JavaScript and XML) um dynamisches Verhalten erweitern kann. Sie wird zum Hinzufügen neuer bzw. Entfernen bestehender Formularfeldern verwendet.
8.2.1
Beispielszenario
Betrachten wir als Beispiel eine Seite mit dem Namen VoteAdmin zum Bearbeiten einer Umfrage, bei der die zu beantwortende Frage mehreren Antwortoptionen besitzen kann (siehe Abbildung 8.3). Die Seite VoteAdmin soll also ein Formular mit mehreren Feldern enthalten. In jedes der Formularfelder wird eine Antwortoption eingegeben. Die Anzahl der Felder wird ausschließlich durch einen Benutzer bestimmt, in dem er neue Felder hinzufügt bzw. bestehende Felder entfernt. Die Umfrage wird durch die Klasse Vote repräsentiert, die Antwortoptionen durch die Klasse Option. Die Beziehung zwischen den beiden Klassen ist in der Abbildung 8.2 dargestellt.
Abbildung 8.2: Stellt die Beziehung zwischen Vote und Option dar.
8.2.2
Implementierung des Beispielszenarios
Die Klasse der Seite VoteAdmin ist in Listing 8.8 zu sehen. Neben der Eigenschaft vote zum Speichern des Aktivierungskontextes besitzt die Seite die Eigenschaft voteService, in die der Dienst VoteService mittels @Inject injiziert wird. Dieser Dienst wird in diesem Beispiel zum Speichern bzw. Aktualisieren einer Vote-Instanz benutzt. Die Implementierungsdetails des Dienstes sind für dieses Beispiel irrelevant.
176
8.2 Erweitern der Formulare durch Benutzerinteraktionen
Abbildung 8.3: Seite VotingAdmin Listing 8.8: VoteAdmin.java public class VoteAdmin { @Property @Persist private Vote vote; @Property private Option currentOption; @Inject private VoteService voteService; void onActivate() { vote = voteService.findVote(); } Object onAddRowFromOptions() { Option option = new Option(); this.vote.getOptions().add(option); this.voteService.saveOrUpdateVote(this.vote); return option; } void onRemoveRowFromOptions(final Option option) {
177
8 Dynamische Formulare
this.vote.getOptions().remove(option); this.voteService.saveOrUpdateVote(this.vote); } void onSuccess() { this.voteService.saveOrUpdateVote(this.vote); } }
Listing 8.9: VoteAdmin.tml Frage der Abstimmung: Mögliche Antwortoptionen: | Option entfernen Neue Option hinzufügen
Da die Anzahl der Optionen zur Entwicklungszeit unbekannt ist, wird ein dynamisches Formular benötigt, das einem Benutzer die Möglichkeit bietet, weitere Optionen zur Laufzeit hinzuzufügen. In Listing 8.9 ist das Template der Seite aus der Abbildung 8.3 zu sehen. Das Template enthält ein Formular, in das die Komponente AjaxFormLoop eingebettet ist. Die Komponente AjaxFormLoop ist eine Erweiterung von Loop, die mithilfe von Ajax neue Zeilen hinzufügen bzw. bestehende Zeilen entfernen kann. Um das Hinzufügen einer neuen Zeile auszulösen, wird die Komponente AddRowLink benutzt. In unserem Beispiel erzeugt den Link mit dem Text »Neue Option hinzufügen« (siehe Abbildung 8.3). Sobald ein Benutzer auf diesen Link klickt, löst AddRowLink das Ereignis addRow aus, das durch eine HandlerMethode wie onAddRowFromOptions() behandelt werden kann (siehe Listing 8.8). Eine Handler-Methode für das Ereignis addRow muss ein Objekt zurückgeben, das dann der Liste der Objekte, über die AjaxFormLoop iteriert, hinzugefügt wird. So wird in Listing 8.8 eine leere Antwortoption für die Frage des Votings erzeugt, dem Voting hinzugefügt, mithilfe des Dienstes VoteService abgespeichert und abschließend zurück-
178
8.3 Teilformulare
gegeben. Im Browser erscheint ein neues Eingabefeld, begleitet von einer clientseitigen Animation. Dieses Feld kann zum Hinzufügen einer neuen Antwort zur Umfrage verwendet werden. Die bestehenden Eingabefelder können mithilfe der Komponente RemoveRowLink entfernt werden. In Listing 8.9 wird neben jedem Textfeld ein platziert, das zum Entfernen des jeweiligen Feldes benutzt wird. Durch einen Klick auf einen der Links mit dem Text »Option entfernen« wird das Ereignis removeRow ausgelöst (siehe Abbildung 8.3). Der Kontext dieses Ereignisses ist das zu entfernende Objekt, das der Handler-Methode über ihren Parameter übergeben wird. So wird die Methode onRemoveRowFromOptions() (Listing 8.8) zum Entfernen einer Instanz von Option eingesetzt. Nachdem das Ereignis serverseitig behandelt wurde, wird auf dem Client das entsprechende Feld entfernt. Die Komponente AjaxFormLoop besitzt einen erforderlichen Parameter encoder, der in Listing 8.9 aus Gründen der Übersichtlichkeit nicht belegt wurde. Sie haben bereits gelernt, wie ein ValueEncoder implementiert werden kann. Implementieren Sie Ihren ValueEncoder für Option in der Seite VoteAdmin, und geben Sie diese lokale Implementierung über den Parameter encoder an die Komponente AjaxFormLoop weiter. Alternativ können Sie einen ValueEncoder mit globaler Sichtbarkeit innerhalb des Applikationsmoduls AppModule implementieren, sodass die Angabe eines Wertes für den Parameter encoder optional wird. Die Beispiele zu diesem Kapitel auf der beiliegenden CD setzen Hibernate zur Implementierung des Dienstes VoteService ein. Wie Sie später lernen werden, erzeugt Tapestry entsprechende ValueEncoder für Ihre Hibernate-Entitäten automatisch.
8.3
Teilformulare
Manche Formulare besitzen Eingabefelder, die abhängig von bestimmten Bedingungen entweder optional oder erforderlich sind. Beispielsweise kann bei einer Bestellung in einem Online-Shop neben der Rechnungsadresse auch eine Lieferadresse angegeben werden. Solange die Lieferadresse mit der Rechnungsadresse übereinstimmt, sind die Felder zur Eingabe der Lieferadresse optional. Falls ein Benutzer die Ware an eine andere Adresse als die Rechnungsadresse geliefert haben möchte, werden diese Felder auf einmal erforderlich. Oft werden diese optionalen Felder erst dann angezeigt, wenn der Benutzer sie benötigt (siehe Abbildung 8.4). Ein solches Teilformular kann in Tapestry mit der Komponente FormFragment umgesetzt werden. In Listing 8.10 enthält das Formular neben den Eingabefeldern für die Rechnungsadresse auch die Komponente FormFragment, in der die Felder zur Eingabe der Lieferadresse enthalten sind. Wenn diese Seite aufgerufen wird, sind zunächst nur die Felder zur Eingabe der Rechnungsadresse sichtbar. Die Komponente FormFragment entscheidet anhand des Wertes des Parameters visible, ob ihr Inhalt dargestellt werden soll. In diesem Beispiel greift FormFragment auf die Eigenschaft shipToAnotherAddress der Seitenklasse (Listing 8.11) zu, um diese Entscheidung zu treffen. Der Standardwert der Eigenschaft shipToAnotherAddress ist false, sodass FormFragment kein Markup erzeugt.
179
8 Dynamische Formulare
Abbildung 8.4: Formular mit einem Teilformular Wird das Formular abgeschickt, dann werden nur die ersten drei Felder während der Validierung überprüft. Die Felder innerhalb von werden bei der Validierung ignoriert, selbst wenn sie mit validate="required" als erforderlich gekennzeichnet sind. Falls der Benutzer auf die Checkbox mit dem Label »An eine andere Adresse liefern?« klickt, wird der zweite Teil des Formulars angezeigt, in dem die Felder zur Eingabe der Lieferadresse durch eine clientseitige Animation in Erscheinung treten. Nun sind auch die neuen Felder des Formulars erforderlich, sodass beispielsweise eine fehlende Eingabe bei der Straße in der Lieferadresse eine Validierungsnachricht auslöst. Das clientseitige Einblenden der optionalen Lieferadresse übernimmt hier ein sogenanntes Mixin, das über den Parameter mixin der Checkbox-Komponente angegeben wird. Mixins behandeln wir später. An dieser Stelle genügt es zu wissen, dass es sich um kleine Bausteine handelt, die bestehende Komponenten mit neuer Funktionalität versehen können, ohne dass der Quellcode der Komponenten modifiziert werden muss. In diesem Beispiel sorgt das Mixin TriggerFragment dafür, dass die Felder zur Eingabe der Lieferadresse bei einem Klick auf die Checkbox angezeigt werden. Dieses Mixin kann auch mit der Radio-Komponente benutzt werden. Listing 8.10: FormFragmentExample.tml Rechnungsadrese Strasse: Stadt: PLZ: An eine andere Adresse liefern? Lieferadresse Strasse: Stadt: PLZ:
Listing 8.11: FormFragmentExample.java public class FormFragmentExample { @Property private Address billingAdress; @Property private Address shippingAdress; @Property private boolean shipToAnotherAddress; void onPrepare() { billingAdress = new Address(); shippingAdress = new Address();
181
8 Dynamische Formulare
} void onSuccess(){ ... } }
8.4
Zusammenfassung
In diesem Kapitel haben Sie gelernt, wie Sie Formulare mit einer dynamischen Anzahl von Eingabefeldern erzeugen können. Die Anzahl der Eingabefelder kann dabei entweder als Konfiguration in einer Datenbank vorliegen oder während einer Interaktion mit einem Benutzer bestimmt werden. Außerdem können Formulare Eingabefelder haben, die abhängig von bestimmten Bedingungen entweder optional oder erforderlich sind. Dazu stellt Tapestry eine Reihe von Komponenten bereit: SubmitNotifier in Kombination mit Loop wird zur Erzeugung von Formularen mit
einer dynamischen Anzahl von Eingabefeldern eingesetzt. AjaxFormLoop zusammen mit AddRowLink und RemoveRowLink erlaubt die Erweiterung
von Formularen um Eingabefelder aus der Benutzerschnittstelle heraus. FormFragment umschließt ein Teilformular, dessen Sichtbarkeit an eine Bedingung geknüpft ist. Diese Bedingung kann durch das Mixin TriggerFragment umgeschaltet
werden.
182
9 Arbeiten mit JavaBeans Darstellung und Bearbeitung von Domainobjekten stellen wohl die häufigsten Anforderungen an webbasierte Benutzerschnittstellen dar. Tapestry unterscheidet sich von den anderen Frameworks unter anderem dadurch, dass für die häufigsten Anforderungen elegante und mächtige Werkzeuge bereitgestellt werden. Falls Ihre Domainobjekte der JavaBean-Konvention entsprechen, kann Tapestry viel Arbeit für Sie übernehmen und Ihnen damit eine Menge Code ersparen. In diesem Kapitel lernen Sie, wie Benutzerschnittstellen zur Darstellung und Bearbeitung von JavaBeans mit wenig Aufwand erzeugt werden können.
9.1
Formulare für JavaBeans
In den letzten Kapiteln haben Sie gesehen, wie einfach Formulare in Tapestry erzeugt werden können. Selbst wenn die Anzahl und Art der Benutzereingaben zur Entwicklungszeit unbekannt ist, können Formulare elegant und mit wenig Aufwand entwickelt werden. In diesem Kapitel wird erläutert, wie mithilfe der Komponente BeanEditForm Formulare für JavaBeans größtenteils automatisch generiert werden können.
9.1.1
Don’t Repeat Yourself
In Kapitel 7 wurde ein Login- und ein Registrierungsformular entwickelt, indem im Template einer Seite die Form-Komponente und darin geschachtelt mehrere Formularfelder definiert wurden. Jedem der Formularfelder wurde eine Seiteneigenschaft bzw. eine Eigenschaft der Klasse User zugewiesen, in die die Eingaben des Benutzers nach dem Absenden des Formulars geschrieben wurden. Dabei wurden für unterschiedliche Eigenschaften der Klasse User unterschiedliche Eingabefelder von entsprechenden Typen ausgewählt ( für boolesche Werte, für Datum, für kurze Texte usw.). Der Nachteil dieser Vorgehensweise ist, dass sie einen Verstoß gegen das DRY-Prinzip (Don’t Repeat Yourself) darstellt. Während der Modellierung der Klasse User haben wir uns für eine Menge von Eigenschaften von bestimmten Typen entschieden. Bei der Zuweisung dieser Eigenschaften an die Formularfelder des Formulars tauchen ihre Namen erneut im Template der Seite auf. Dies stellt eine Redundanz dar, die im Falle eines Refactorings der Klasse User eine Anpassung des Templates nach sich zieht. Auch bei der Auswahl der Art des Formularfeldes für eine Eigenschaft erfolgt eine Wiederholung. Beispielsweise wird bei der Wahl von für die Eingabe des Geburtstags des Anwenders die
9 Arbeiten mit JavaBeans
Information wiederholt, dass es sich um ein Datum handelt. Wie kann aber diese Redundanz beseitigt werden? Die Komponente BeanEditForm ist für die Generierung von Formularen für JavaBeans zuständig. Mithilfe dieser Komponente kann ein Registrierungsformular mit einer einzigen Zeile erzeugt werden. Das Ergebnis des Codes in Listing 9.1 ist in der Abbildung 9.1 zu sehen. Listing 9.1: Formular mit BeanEditForm
Abbildung 9.1: Das von BeanEditForm generierte Formular Wie Sie in der Abbildung 9.1 erkennen können, hat BeanEditForm für jede Eigenschaft von User ein Formularfeld generiert. Hinter den Kulissen bewältigt Tapestry viel Arbeit, um dem Entwickler das Leben zu erleichtern. Dabei schaut sich das Framework die Klasse des zu editierenden Objektes an und identifiziert die darin enthaltenen Eigenschaften nach der SUN JavaBean-Konvention. Laut dieser Konvention muss jede Eigenschaft eine Getter-Methode für den lesenden und eine Setter-Methode für den schreibenden Zugriff besitzen. Ist dies für eine Eigenschaft erfüllt, generiert Tapestry für sie ein Formularfeld vom entsprechenden Typ. Intern ist in Tapestry eine Zuordnung von unterschiedlichen Typen zu Formular-Komponenten definiert. Diese Zuordnung ist in der Tabelle 9.1 zusammengefasst.
184
9.1 Formulare für JavaBeans
Typ
Formularkomponente
java.lang.String
java.lang.Number
Enum
java.lang.Boolean
java.util.Date
Tabelle 9.1: Unterstützte Typen von BeanEditForm Gemäß dieser Zuordnung generiert BeanEditForm beispielsweise für eine Eigenschaft vom Typ java.util.Date die Komponente . Für Enums wird benutzt. Außerdem werden aus den Namen der Eigenschaften Labels für die Formularfelder generiert, indem aus der Camel Case Notation ein lesbarer String erzeugt wird. So wird beispielsweise für die Eigenschaft userName das Label »User Name« erzeugt (siehe Abbildung 9.1). Die Komponente BeanEditForm ist sehr flexibel. Sie lässt sich auf viele Arten anpassen: Eigenschaften der JavaBeans können versteckt werden, sodass sie nicht im Formu-
lar auftauchen. Die Reihenfolge der generierten Formularfelder kann verändert werden. Die Darstellung der Felder für Eigenschaften der JavaBeans kann überschrieben
werden. Labels der Formularfelder können internationalisiert werden. Text der Schaltfläche kann angepasst werden. Metadaten, die aus einer JavaBean extrahiert wurden, können modifiziert werden.
Beispielsweise können virtuelle Eigenschaften hinzugefügt werden.
9.1.2
Generieren von Formularen mit BeanEditForm
Die Komponente BeanEditForm besitzt einen erforderlichen und mehrere optionale Parameter. Der erforderliche Parameter object wird zur Angabe des zu editierenden Objekts eingesetzt. In Listing 9.1 wird dieser Parameter an die Eigenschaft user der Seitenklasse gebunden. In diesem Beispiel greift BeanEditForm auf die Methode getUser() zu (Listing 9.2), um ein Formular für die Klasse User zu generieren. Das Formular wird mit den aktuellen Werten der Eigenschaften der zurückgegebenen Instanz von User vorbelegt.
185
9 Arbeiten mit JavaBeans
Listing 9.2: Register.java public class Register { ... public User getUser() { return user; } ... }
Wird in der Methode getUser() null zurückgegeben, so erzeugt BeanEditForm automatisch eine neue Instanz von User. Dabei schaut sich das Framework die zu konstruierende Klasse an und wählt den Konstruktor mit der größten Anzahl von Argumenten. Sie werden später sehen, dass dieser Ansatz für die Erzeugung von Diensten sehr gut geeignet ist. Doch im Falle von einfachen JavaBeans kann es zu unerwartetem Verhalten führen, wenn Ihre zu erzeugende Klasse mehr als einen Konstruktor besitzt. Aus diesem Grund müssen Sie Tapestry sagen, welcher Konstruktor benutzt werden soll. Dies kann mithilfe der Annotation @Inject wie in Listing 9.3 gemacht werden. Es wird empfohlen, immer den Standardkonstruktor mit @Inject zu annotieren. Listing 9.3: Auswahl des Konstruktors public class User { ... @Inject public User() { super(); } public User(String userName, String password) { super(); this.userName = userName; this.password = password; } ... }
Die Komponente BeanEditForm schachtelt die Komponente Form, um das Formular für die zu editierende JavaBean zu generieren. Sie haben bereits gelernt, dass die Komponente Form eine Menge von Ereignissen zu unterschiedlichen Zeitpunkten auslöst. Durch das Konzept Event Bubbling (siehe Kapitel 3) werden zunächst die HandlerMethoden zur Behandlung dieser Ereignisse in dem Container von Form, hier also in der Klasse von BeanEditForm, gesucht. Falls keine Handler-Methode für ein Ereignis gefunden wurde, geht Tapestry eine Ebene in der Komponentenhierarchie nach oben und sucht nach entsprechenden Handler-Methoden weiter. Die Komponente BeanEditForm behandelt nur das Ereignis prepare der geschachtelten Komponente Form. Nach der Behandlung wird das Ereignis wieder ausgelöst. Damit können Sie in Ihrer
186
9.1 Formulare für JavaBeans
Seite eine Handler-Methode für dieses Ereignis schreiben. Alle anderen Ereignisse von Form werden in BeanEditForm nicht behandelt und durch das Prinzip Event Bubbling an Ihre Seite verschickt. Damit können Sie beim Einsatz von BeanEditForm alle Ereignisse der Komponente Form benutzen, wie wir es in Kapitel 7 getan haben. In Listing 9.4 wird ein User in der Handler-Methode für das Ereignis prepare erzeugt und der Seiteneigenschaft user zugewiesen. Für diese Instanz wird ein Formular generiert, in dem die Eingaben vorgenommen werden. Nach dem Versenden des Formulars werden die Eingaben den Eigenschaften von User zugewiesen. In der HandlerMethode onSuccess() wird die Instanz von User in der Datenbank angelegt. Listing 9.4: BeanEditForm und Event Bubbling public class Register { ... @Inject private UserDao userDao; @Persist private User user; void onPrepare(){ user = new User(); } void onSuccess(){ userDao.save(user); } public User getUser() { return user; } ... }
9.1.3
Verstecken bestimmter Eigenschaften von JavaBeans
Einige Eigenschaften einer JavaBean sind nur für den internen Gebrauch gedacht und sollten vom Anwender weder gesehen noch verändert werden können. Es macht beispielsweise keinen Sinn, für die Eigenschaft id der Klasse User ein Formularfeld zu erzeugen. Diese Eigenschaft repräsentiert den Primärschlüssel und wird in der zugrunde liegenden Datenbank durch eine Sequenz oder ein Autoinkrement vergeben. Ein Anwender sollte diesen Primärschlüssel nicht bestimmen dürfen, sodass die Eigenschaft id versteckt werden sollte. Dies kann mithilfe des Parameters exclude gemacht werden (siehe Listing 9.5). Der Parameter erwartet eine durch Komma separierte Liste von Eigenschaftennamen, die ausgeschlossen werden sollen. Das Ergebnis können Sie in der Abbildung 9.2 sehen.
187
9 Arbeiten mit JavaBeans
Listing 9.5: Verstecken der Eigenschaften einer JavaBean
Abbildung 9.2: Das von BeanEditForm generierte Formular In Kapitel 9.3 wird die Komponente Grid vorgestellt, die eine tabellarische Ansicht für eine Menge von JavaBeans generiert. Die beiden Komponenten BeanEditForm und Grid arbeiten auf eine ähnliche Weise und setzen die gleiche Funktionalität zur Extraktion von Metadaten einer Klasse ein. Für eine konsistente Darstellung der Klasse User auf der Benutzerschnittstelle sollte die Eigenschaft id auf eine andere Weise als in Listing 9.5 versteckt werden. Falls diese Eigenschaft mit der Annotation @NonVisual annotiert ist (siehe Listing 9.6), wird sie bei der Extraktion der Metadaten aus User nicht berücksichtigt. Folglich wird kein Formularfeld für id generiert, sodass der Parameter exclude aus dem Template entfernt werden kann. Listing 9.6: Verstecken einer Eigenschaf mittels @NonVisual public class User { @NonVisual private Long id; private String userName; ... }
188
9.1 Formulare für JavaBeans
Anstatt einzelne Eigenschaften auszuschließen, können Sie auch die Menge der eingeschlossenen Eigenschaften mithilfe des Parameters include bestimmen. In Listing 9.7 wird ein Formular mit zwei Feldern generiert, das nur die Eigenschaften userName und password beinhaltet. Alle anderen Eigenschaften werden ausgelassen. Listing 9.7: Einschließen von Eigenschaften einer JavaBean
9.1.4
Angepasste Formularfelder
Bei einem Formularfeld für ein Passwort sollte die Eingabe durch einen Platzhalter wie * ersetzt werden. Aus diesem Grund sollte anstelle von ein erzeugt werden. Doch Tapestry kann nicht wissen, dass es sich bei der Eigenschaft password der Klasse User um sensible Daten handelt. Da diese Eigenschaft vom Typ String ist, generiert die Komponente BeanEditForm ein . Dieses Verhalten kann überschrieben werden, indem innerhalb von BeanEditForm ein benutzt wird. Listing 9.8: Überschreiben der Darstellung einer Eigenschaft
Ein ist eine Tapestry-Komponente, die es ermöglicht, einen Teil des Templates an eine andere Komponente als Parameter zu übergeben. In Listing 9.8 wird eingesetzt, um BeanEditForm zu informieren, wie die Eigenschaft password dargestellt werden soll, nämlich als Passwortfeld. Der Parameter name von dient der Identifikation der Eigenschaft, für die die Standarddarstellung überschrieben werden soll. In Tapestry 5.1 wurde der Namensraum tapestry:parameter eingeführt, dem üblicherweise das Präfix p zugewiesen wird. Dieser Namensraum wird dazu verwendet, ein mit einer alternativen Syntax zu definieren. In Listing 9.9 wird die Darstellung der Eigenschaft password überschrieben, indem das Präfix des Namensraumes
189
9 Arbeiten mit JavaBeans
tapestry:parameter und ein Element mit dem Namen password benutzt werden. Diese Syntax ist äquivalent zu der im Listing 9.8.
Listing 9.9: Namensraum tapestry:parameter im Einsatz
Alternativ kann die Darstellung einer Eigenschaft auch in der JavaBean-Klasse definiert werden. Dazu muss die jeweilige Eigenschaft mit der Annotation @DataType versehen werden. Diese Annotation akzeptiert einen der Werte aus der Tabelle 9.2. In Listing 9.10 wird festgelegt, dass für die Eigenschaft password die Komponente PasswordField generiert werden soll. Würden Sie die Annotation @DataType entfernen, so würde BeanEditForm wieder die Komponente TextField generieren. Da eine Eigenschaft vom Typ java.util.String standardmäßig mit text verknüpft ist (siehe Tabelle 9.2), ist die Angabe von @DataType("text") nicht notwendig. Beachten Sie, dass die Namen longtext und password mit keinem Typen verknüpft sind. Damit müssen Sie @DataType("longtext") bzw. @DataType("password") explizit angeben, falls für eine Eigenschaft TextArea bzw. PasswordField generiert werden soll. Listing 9.10: Bestimmen der Darstellung einer Eigenschaft public class User { ... @DataType("password") private String password; ... } Wert von @DataType
Typ der Eigenschaft
Formularkomponente
boolean
java.lang.Boolean
date
java.util.Date
enum
java.lang.Enum
longtext
–
Tabelle 9.2: Zuordnung der Werte von @DataType zu Typen von Eigenschaften
190
9.1 Formulare für JavaBeans
Wert von @DataType
Typ der Eigenschaft
Formularkomponente
number
java.lang.Number
password
–
text
java.lang.String
Tabelle 9.2: Zuordnung der Werte von @DataType zu Typen von Eigenschaften (Forts.)
9.1.5
Reihenfolge der Formularfelder
Die Reihenfolge der Formularfelder, die von BeanEditForm generiert werden, wird durch die Reihenfolge der Getter-Methoden der Eigenschaften innerhalb der Klasse des zu editierenden Objektes bestimmt. So taucht in der Benutzerschnittstelle das Formularfeld für die Eigenschaft userName vor dem Formularfeld für die Eigenschaft password auf, da die Methode getUserName() vor der Methode getPassword() im Java-Code definiert ist. Wenn Sie die Reihenfolge der beiden Methoden in der Klasse User ändern, werden Sie feststellen, dass sich auch die Reihenfolge der entsprechenden Formularfelder geändert hat. Viele moderne IDEs wie Eclipse bieten die Funktionalität zur automatischen Sortierung von Klasseneigenschaften an. Dies kann dazu führen, dass die Reihenfolge der Formularfelder unbeabsichtigt geändert wird. Um dies zu vermeiden, kann die jeweilige JavaBean mit der Annotation @ReorderProperties versehen werden (siehe Listing 9.11). Diese Annotation erwartet eine durch Komma separierte Liste von Eigenschaftennamen. Diese Liste repräsentiert die Reihenfolge der Eigenschaften in der Benutzerschnittstelle. Listing 9.11: Ändern der Reihenfolge der Felder mit @ReorderProperties @ReorderProperties("password,userName") public class User { ... }
Alternativ kann der Parameter reorder benutzt werden, um die Reihenfolge der Felder zu beeinflussen. In Listing 9.12 wird die Reihenfolge der beiden Felder für die Eigenschaften password und userName bestimmt. Alle Eigenschaften, die nicht in dieser Liste auftauchen, werden am Ende angefügt. Listing 9.12: Ändern der Reihenfolge der Felder ...
191
9 Arbeiten mit JavaBeans
9.1.6
Virtuelle Eigenschaften
In unserem Registrierungsformular (Kapitel 7) wurden zwei Formularfelder für die Eingabe des Passworts benutzt, damit der Anwender während des Registrierungsprozesses sein Passwort zweimal angeben muss. Diese beiden Eingaben wurden auf Gleichheit überprüft, um Tippfehler zu vermeiden. Da die Klasse User nur eine Eigenschaft besitzt, die für die Speicherung des Passworts vorgesehen ist, generiert BeanEditForm nur ein Formularfeld für das Passwort. Glücklicherweise können mithilfe des Parameters add virtuelle Eigenschaften den Metadaten einer JavaBean hinzugefügt werden. In Listing 9.13 wird eine virtuelle Eigenschaft password2 hinzugefügt, die mithilfe von als ein Passwortfeld dargestellt wird. Beachten Sie, dass die Eingaben des neuen Passwortfeldes nicht in eine Eigenschaft der Klasse User geschrieben werden. Stattdessen ist dieses Formularfeld der Seiteneigenschaft password2 (Listing 9.14) zugewiesen. Listing 9.13: Hinzufügen virtueller Eigenschaften
Listing 9.14: Register.java public class Register { @Property private String password2; ... }
9.1.7
Programmatisches Ändern von Metadaten einer JavaBean
Mithilfe der Parameter add, exclude, include und reorder lassen sich die Metadaten der zu editierender JavaBean aus dem Template heraus modifizieren. Die zugrunde lie-
192
9.1 Formulare für JavaBeans
gende Funktionalität ist aber nicht in der Komponente BeanEditForm implementiert, sondern in einer internen Implementierung des Interface org.apache.tapestry5.beaneditor.BeanModel. Dieses Interface repräsentiert die Metadaten einer JavaBean und stellt Operationen zu deren Modifikation bereit. Standardmäßig erzeugt BeanEditForm eine Instanz von BeanModel für die zu editierende JavaBean. Alle Einstellungen, die Sie innerhalb eines Templates vornehmen, werden von der Komponente an BeanModel weitergeleitet. Alternativ kann eine Instanz von BeanModel an die Komponente BeanEditForm über den Parameter model übergeben werden. Für die Erzeugung von BeanModel ist der Dienst BeanModelSource zuständig, der in eine Seitenklasse mittels @Inject injiziert werden kann. In Listing 9.15 wird ein BeanModel durch den Aufruf der Methode createEditModel() des Dienstes BeanModelSource erzeugt. Diese Methode erwartet zwei Parameter: die Klasse der JavaBean, für die ein BeanModel erzeugt werden soll, und eine Instanz von Messages. Der Dienst Messages wird zur Internationalisierung der Labels der Formularfelder benötigt und kann ebenfalls mittels @Inject in die Seitenklasse injiziert werden. Nachdem BeanModel erzeugt wurde, werden drei Modifikationen der Metadaten vorgenommen: Die Eigenschaft id wird ausgeschlossen. Eine künstliche Eigenschaft mit dem Namen password2 wird hinzugefügt. Die Reihenfolge für die Eigenschaften userName, email, password und password2 wird
bestimmt. Die restlichen Eigenschaften werden dahinter angefügt. Listing 9.15: Erzeugen einer Instanz von BeanModel public class Register { ... @Inject private BeanModelSource beanModelSource; @Inject private Messages messages; public BeanModel getModel(){ BeanModel model = beanModelSource.createEditModel( User.class, messages); model.exclude("id"); model.add("password2", null); model.reorder("userName","email","password","password2"); return model; } }
193
9 Arbeiten mit JavaBeans
Im Template (Listing 9.16) wird das erzeugte BeanModel an durch den Parameter model übergeben. Dadurch wird die Komponente keine eigene Instanz von BeanModel erzeugen, sondern die Methode getModel() der enthaltenen Seite aufrufen. Listing 9.16: Bereitstellen eines BeanModels ...
9.1.8
Eingabenvalidierung
Da unter Einsatz von BeanEditForm das Formular zum Bearbeiten einer JavaBean automatisch generiert wird, ist die Klasse dieser JavaBean die beste Stelle zur Definition der Informationen über die Validierung von Eigenschaften. Um eine Eigenschaft mit Validierungsinformationen zu versehen, muss diese Eigenschaft oder eine Ihrer Zugriffsmethoden mit @Validate annotiert werden (siehe Listing 9.17). Listing 9.17: User.java public class User { @Validate("required") private String userName; @Validate("required,minlength=5,regexp=[a-zA-Z]+") private String password; ... }
Die Annotation @Validate erwartet eine durch Komma separierte Liste von Validatoren, die zur Überprüfung des Wertes einer annotierten Eigenschaft eingesetzt werden sollen. Dabei sind alle in Kapitel 7 vorgestellten Validatoren erlaubt. In Kapitel 7 haben Sie gelernt, dass die Übersetzungen der Validierungsnachrichten im Nachrichtenkatalog einer Seite oder Komponente überschrieben werden können. Für jede unterstützte Sprache kann eine Übersetzung bereitgestellt werden. Nun stellen Sie sich vor, Sie möchten neben den Validierungsnachrichten auch die Validatoren selbst lokalisieren. Nehmen wir an, Sie hätten folgende Anforderung: Englischsprachige Benutzer dürfen alphanumerische Passwörter mit einer Min-
destlänge von fünf Zeichen auswählen. Deutschsprachige Benutzer dürfen nur numerische Passwörter mit einer Mindest-
länge von sieben Zeichen auswählen.
194
9.1 Formulare für JavaBeans
Um diese Anforderung zu erfüllen, werden drei Validatoren benötigt: required, minlength und regexp. Beachten Sie, dass die beiden Validatoren minlength und regexp in Listing 9.18 ohne Einschränkungen angegeben werden, obwohl diese eigentlich benötigt werden. Listing 9.18: User.java public class User { ... @Validate("required,minlength,regexp") private String password; ... }
Da die Einschränkungen der Validatoren minlength und regexp lokalisiert werden sollen, können sie nicht in der Klasse User zusammen mit Validatoren angegeben werden. Stattdessen werden die Einschränkungen in den Übersetzungsdateien des Nachrichtenkatalogs der jeweiligen Seite bereitgestellt. Dabei schlägt Tapestry die Einschränkung eines Validators in einem Nachrichtenkatalog unter einem Schlüssel nach, der sich auf eine ähnliche Weise wie die Schlüssel der Validierungsnachrichten zusammensetzt. Der Schlüssel zum Nachschlagen einer Einschränkung im Nachrichtenkatalog besteht aus den folgenden drei Bestandteilen, die voneinander durch Unterstriche getrennt sind: 1. ID des Formulars, in dem das Feld eingebettet ist. 2. ID des Feldes. 3. Name des Validators. Falls kein Eintrag gefunden wird, sucht Tapestry nach einem Eintrag mit dem Schlüssel ohne die ID des Formulars. In Listing 9.19 werden die Einschränkungen und Validierungsnachrichten für die englischsprachigen Benutzer festgelegt. Die Einschränkungen für deutschsprachige Benutzer sind in Listing 9.20 zu sehen. Listing 9.19: Register_en.properties password-minlength=5 password-minlength-message=Minimum length of a password is '%d' password-regexp=[a-zA-Z0-9]+ password-regexp-message=Provided password does not match '%s'
Listing 9.20: Register_de.properties password-minlength=7 password-minlength-message=Minimale Länge eines Passworts ist '%d' password-regexp=[0-9]+ password-regexp-message=Das eingegebene Passwort stimmt nicht mit '%s' überein
195
9 Arbeiten mit JavaBeans
Ähnlich wie bei der Komponente Form können Sie die Validierung der Geschäftslogik in einer Handler-Methode für das Ereignis validateForm durchführen. In Listing 9.21 wird das Ereignis validateForm behandelt, um den eingegebenen Benutzernamen auf Eindeutigkeit zu überprüfen. Listing 9.21: Valdidierung durch Handler-Methoden public class Register { ... @InjectComponent private BeanEditForm registerForm; void onValidateForm(){ if(registerForm.getHasErrors()){ return; } if("root".equals(userName)){ registerForm.recordError("Der Name ist bereits vergeben."); } } }
9.2
Darstellen von JavaBeans
Wenn Sie Ihre JavaBeans nicht editieren, sondern nur anzeigen möchten, können Sie die Komponente BeanDisplay anwenden. Es handelt sich um eine Read Only Version von BeanEditForm, die auf die gleiche Weise verwendet wird. Die beiden Komponenten sind verwandt. Der einzige große Unterschied ist, dass BeanDisplay kein Formular zum Editieren der Eigenschaft einer JavaBean darstellt. In Listing 9.22 wird die Komponente BeanDisplay benutzt, um die Eigenschaften einer Instanz von User darzustellen (siehe Abbildung 9.3). Wie Sie erkennen können, erzeugt die Komponente aus den Namen der Eigenschaften von User lesbare Labels, wie es auch BeanEditForm tut. Des Weiteren werden die Werte der Eigenschaften entsprechend ihren Typen formatiert. So wird die Eigenschaft birthday wie ein Datum formatiert. Listing 9.22: Darstellen einer JavaBean
196
9.3 Darstellen mehrerer JavaBeans
Abbildung 9.3: Die von BeanDisplay erzeugte Darstellung einer Instanz der Klasse User Außerdem kann BeanDisplay auf die gleiche Weise wie BeanEditForm angepasst werden. Die in Kapitel 9.1 vorgestellte Funktionalität kann auch auf BeanDisplay angewendet werden. In Listing 9.23 wird beispielsweise die Darstellung der Eigenschaft password überschrieben. Das Ergebnis können Sie in der Abbildung 9.4 sehen. Listing 9.23: Überschreiben der Darstellung einer Eigenschaft *****
9.3
Darstellen mehrerer JavaBeans
Während die Komponente BeanDisplay für die Darstellung der Eigenschaften einer einzelnen JavaBean zuständig ist, erzeugt die Komponente Grid eine tabellarische Ansicht einer Menge von JavaBeans. Lassen Sie uns die Startseite des Online-Shops aus dem Kapitel 4 wie in Listing 9.24 überarbeiten, um den Einsatz der Grid-Komponente zu demonstrieren. Das Ergebnis sieht dann wie in der Abbildung 9.5 aus.
197
9 Arbeiten mit JavaBeans
Abbildung 9.4: Überschreiben der Darstellung einer Eigenschaft Listing 9.24: Grid im Einsatz
Abbildung 9.5: Komponente Grid im Einsatz
198
9.3 Darstellen mehrerer JavaBeans
Die Komponente Grid besitzt einen erforderlichen und mehrere optionale Parameter. Der erforderliche Parameter source dient der Angabe der JavaBeans, die dargestellt werden sollen. In Listing 9.25 wird diese Menge durch eine Liste von Book repräsentiert, die von der Methode getBooks() zurückgegeben wird. Durch source="books" wird Grid informiert, dass diese Methode aufgerufen werden soll. Wenn Sie einen Blick in die Komponentenreferenz werfen, werden Sie erkennen, dass der Parameter source eine Instanz von GridDataSource erwartet. Dieses Interface modelliert eine Quelle für Grid. Wir haben aber in der Methode getBooks() (Listing 9.25) eine List zurückgegeben. An dieser Stelle kommt das Konzept Type Coercion zum Einsatz, das eine Umwandlung einer Instanz von List in eine Instanz von GridDataSource durchführt. Dieses Konzept wird später in Kapitel 19 behandelt. Listing 9.25: Index.java public class Index { @Inject private BookService bookService; public List getBooks() { return bookService.findAllBooks(); } }
Wie Sie in der Abbildung 9.5 sehen können, repräsentiert jede Zeile von Grid ein Book. Jede Spalte entspricht dagegen einer Eigenschaft der Klasse Book. Ähnlich wie bei BeanEditForm leistet Tapestry hinter den Kulissen richtig viel Arbeit. Die Werte der Tabellenzellen sind automatisch entsprechend ihrer Typen formatiert. So ist beispielsweise das Erscheinungsjahr eines Buches (Eigenschaft publicationDate) als ein Datum formatiert. Aus den Namen der Eigenschaften von Book werden sinnvolle Spaltennamen erzeugt, wobei aus der Camel Case Notation lesbare Strings erzeugt wurden. Des Weiteren ist die Tabelle sortierbar. Falls die Menge der JavaBeans leer oder null ist, wird anstelle einer Tabelle die Nachricht »There is no data to display« dargestellt. Um diese Nachricht zu überschreiben, kann ein definiert werden. Ein Block ist eine Komponente, die einen Teil des Templates markiert, der standardmäßig nicht dargestellt wird und an eine weitere Komponente zur Darstellung übergeben werden kann. In Listing 9.26 wird zum Erzeugen einer alternativen Nachricht über eine leere Menge von Book eingesetzt. Die ID des Blocks wird über den Parameter empty an die Komponente Grid übergeben. Listing 9.26: Überschreiben der Nachricht über fehlende Daten Keine Bücher gefunden
199
9 Arbeiten mit JavaBeans
9.3.1
Paging
Falls mehr als 25 Bücher angezeigt werden sollen, erzeugt Grid automatisch eine Seitennavigation oberhalb der Tabelle und stellt pro Seite nur 25 Bücher dar. Die Anzahl der Bücher, die pro Seite dargestellt werden sollen, kann mittels des Parameters rowsPerPage geändert werden. In Listing 9.27 werden pro Seite nur noch zehn Bücher angezeigt. Auch die Position der Seitennavigation kann angepasst werden. Geben Sie dazu beim Parameter pagerPosition einen der folgenden Werte an: top: Die Seitennavigation wird oberhalb der Tabelle (Standardwert) dargestellt. bottom: Die Seitennavigation wird unterhalb der Tabelle dargestellt. both: Die Seitennavigation wird oberhalb und unterhalb der Tabelle dargestellt. none: Es wird keine Seitennavigation angezeigt, auch wenn die Gesamtanzahl der
JavaBeans die pro Seite zulässige Anzahl übersteigt. Listing 9.27: Ändern der Einstellungen für die Seitennavigation
Abbildung 9.6: Seitennavigation Alternativ kann auch eine der Instanzen der Enum GridPagerPosition aus dem Paket org.apache.tapestry5.corelib.data angegeben werden. In Listing 9.28 wird die Instanz BOTH in der Methode getPagerPosition() zurückgegeben, auf die Grid aus dem Template heraus mithilfe des Präfixes prop zugreifen kann (Listing 9.29).
200
9.3 Darstellen mehrerer JavaBeans
Listing 9.28: Index.java public class Index { ... public GridPagerPosition getPagerPosition() { return GridPagerPosition.BOTH; } }
Listing 9.29: Zugriff auf eine Instanz von GridPagerPosition
9.3.2
Zugriff auf die Werte der aktuellen Iteration
Für den Zugriff auf das Buch in der aktuellen Iteration kann der Parameter row benutzt werden. Dieser Parameter erwartet den Namen einer Seiteneigenschaft, in die der aktuelle Wert geschrieben wird. Zusätzlich stellt Grid die beiden Parameter rowIndex und columnIndex bereit, die der Identifikation des Index der aktuellen Zeile und Spalte dienen. Diese beiden Parameter erwarten jeweils eine Eigenschaft vom Typ int. Listing 9.30: Index.tml ...
Listing 9.31: Index.java public class Index { @Property private int rowIndex; @Property private int columnIndex; @Property private Book currentBook; ... }
201
9 Arbeiten mit JavaBeans
9.3.3
Überschreiben der Darstellung von Spalten
Ähnlich wie bei der Komponente BeanEditForm kann auch die Standarddarstellung der Spalten von Grid überschrieben werden. Dazu wird der bereits vorgestellte Namensraum tapestry:parameter benutzt. Der einzige Unterschied zu BeanEditForm ist, dass an den Namen einer Eigenschaft einer der Suffixe cell oder header angefügt werden muss. In Listing 9.32 wird die Darstellung der Spalte für die Eigenschaft title überschrieben. Der Parameter mit dem Namen titleHeader ist für das Überschreiben des Spaltennamens der Eigenschaft title zuständig. Analog wird innerhalb des Parameters mit dem Namen titleCell die Darstellung für die Zellen dieser Spalte bestimmt. In jeder Zeile wird ein Link zur Seite ViewBook erzeugt. Listing 9.32: Überschreiben der Darstellung der Spalten Buchtitel ${currentBook.title}
Abbildung 9.7: Überschreiben der Darstellung der Spalten
202
9.4 Datentypen von Eigenschaften
9.4
Datentypen von Eigenschaften
In den vorigen Abschnitten haben Sie gelernt, dass die Komponenten BeanEditForm, BeanDisplay und Grid die Eigenschaften von JavaBeans abhängig vom Typ der jeweiligen Eigenschaft darstellen. Wie ein bestimmter Typ dargestellt wird, ist in den internen Seiten PropertyEditBlocks und PropertyDisplayBlocks definiert. Die beiden Seiten enthalten mehrere Blöcke, in denen jeweils die Darstellung für einen Typ festgelegt ist. Jeder dieser Blöcke ist mit einem Typ über einen symbolischen Namen, den sogenannten Data Type, verknüpft. Ein Data Type ist ein Literal, das einem Typ zugewiesen wird (siehe Tabelle 9.2). Wenn die Komponente BeanEditForm ein Formularfeld für eine Eigenschaft einer JavaBean generiert, ermittelt die Komponente den Data Type dieser Eigenschaft. Wie Sie bereits gelernt haben, kann ein Data Type für eine Eigenschaft mithilfe der Annotation @DataType festgelegt werden (siehe Abschnitt 9.1.4). Falls eine Eigenschaft einer JavaBean mit dieser Annotation versehen ist, wird der Wert dieser Annotation als Data Type benutzt. Ansonsten löst Tapestry den Data Type anhand des Typen der Eigenschaft auf. Der ermittelte Data Type wird zur Identifikation eines Blocks in der Seite PropertyEditBlocks verwendet. Der identifizierte Block übernimmt dann die Darstellung der Eigenschaft. Die Komponenten BeanDisplay und Grid arbeiten bis auf einen kleinen Unterschied auf die gleiche Weise wie BeanEditForm: Es wird die Seite PropertyDisplayBlocks nach einem Block durchsucht. Falls für eine Eigenschaft kein Block gefunden wird, kann diese Eigenschaft weder editiert noch dargestellt werden. Wenn Sie beispielsweise den Typ der Eigenschaft birthday der Klasse User (Abbildung 9.1) von java.util.Date auf java.util.Calendar ändern, wird in der Tapestry-Version 5.1 BeanEditForm das entsprechende Formularfeld nicht mehr darstellen. Ab der Version 5.2 kann BeanEditForm Formularfelder auch für Eigenschaften vom Typ java.util.Calendar generieren (siehe TAP5-7899).
9.4.1
Neue Datentypen zum Editieren von Eigenschaften
Nun lassen Sie uns die Komponente BeanEditForm so erweitern, dass für eine Eigenschaft vom Typ java.util.Calendar die Komponente DateField generiert wird. Dazu schreiben wir die Seite AppPropertyEditBlocks (Listing 9.33), die neben der Seite PropertyEditBlocks von Tapestry nach Blöcken durchsucht wird. In der Seite wird mithilfe der Annotation @Component eine Instanz von DateField erzeugt. Außerdem wird in die Seite der Dienst PropertyEditContext injiziert. Dieser Dienst stellt Funktionalität bereit, die zum Erzeugen von Formularfeldern für Eigenschaften von JavaBeans benötigt wird. So kann beispielsweise mithilfe dieses Dienstes auf den Wert der jeweiligen Eigenschaften zugegriffen werden. Bitte beachten Sie, dass es sich um einen
9
https://issues.apache.org/jira/browse/TAP5-789
203
9 Arbeiten mit JavaBeans
Umgebungsdienst handelt, was an der Annotation @Environmental zu sehen ist. Umgebungsdienste werden in Kapitel 11 behandelt. Listing 9.33: AppPropertyEditBlocks.java public class AppPropertyEditBlocks { @Property @Environmental private PropertyEditContext context; @Component(parameters = { "value=prop:context.propertyValue", "label=prop:context.label", "clientId=prop:context.propertyid", "validate=prop:dateFieldValidator" }) private DateField dateField; public FieldValidator getDateFieldValidator() { return context.getValidator(dateField); } }
Im Template der Seite AppPropertyEditBlocks wird ein Block erzeugt, in dem die in der Seitenklasse erzeugte Komponente DateField enthalten ist (siehe Listing 9.34). Dem Block wird die eindeutige ID calendarBlock zugewiesen. Diese ID wird von Tapestry zum Nachschlagen des Blocks verwendet. Listing 9.34: AppPropertyEditBlocks.tml
Nun müssen wir die Seite AppPropertyEditBlocks Tapestry zur Verfügung stellen, sodass auch sie nach Blöcken durchsucht wird. Dazu muss zunächst ein neuer Data Type erzeugt werden, der mit der Klasse java.util.Calendar verknüpft wird. In Listing 9.35 wird in der Contribute-Methode für den Dienst DefaultDataTypeAnalyzer der Data Type calendar mit der Klasse java.util.Calendar verknüpft. Listing 9.35: AppModule.java public class AppModule { public static void contributeDefaultDataTypeAnalyzer( MappedConfiguration configuration) { configuration.add(Calendar.class, "calendar"); }
204
9.4 Datentypen von Eigenschaften
public static void contributeBeanBlockSource( Configuration configuration) { BeanBlockContribution editBlock = new BeanBlockContribution( "calendar", "AppPropertyEditBlocks" "calendarBlock", true); configuration.add(displayBlock); } }
In der Contribute-Methode für den Dienst BeanBlockSource wird der neue Data Type calendar mit dem Block mit der ID calendarBlock aus der Seite AppPropertyEditBlocks verknüpft. Dies erfolgt, indem eine Instanz von BeanBlockContribution der Konfiguration des Dienstes BeanBlockSource hinzugefügt wird. Der Konstruktor der Klasse BeanBlockContribution besitzt vier Parameter: Den Namen des Data Types, für den ein Block erzeugt wird. Den Namen der Seite, die das Markup für den Data Type erzeugt. Die ID eines Block innerhalb des Seitentemplates, in dem das Markup zu finden ist. Einen booleschen Wert, der angibt, ob die jeweilige Eigenschaft einer JavaBean ak-
tualisiert werden darf. Die Methode contributeBeanBlockSource() (Listing 9.35) bewirkt, dass Tapestry bei einer Eigenschaft vom Typ java.util.Calendar die Seite AppPropertyEditBlocks nach dem Block mit der ID calendarBlock befragt. Dieser Block generiert dann ein DateField.
9.4.2
Neue Datentypen zum Darstellen von Eigenschaften
Das Erweitern der Komponenten BeanDisplay und Grid um neue Datentypen erfolgt ähnlich wie bei der Komponente BeanEditForm. Dazu wird eine Seite benötigt, in der das Markup zur Darstellung einer Eigenschaft einer JavaBean hinterlegt wird (siehe Listing 9.36). Der einzige Unterschied ist, dass diese Seite einen Block für den lesenden Zugriff auf die Eigenschaften von JavaBeans bereitstellt. Deshalb wird der Umgebungsdienst PropertyOutputContext und nicht PropertyEditContext in die Seite injiziert. Listing 9.36: AppPropertyDisplayBlocks.java public class AppPropertyDisplayBlocks { @Environmental private PropertyOutputContext context; @Inject private Locale locale;
205
9 Arbeiten mit JavaBeans
public Date getCalendarDate() { Calendar calendar = (Calendar) context.getPropertyValue(); return calendar.getTime(); } public DateFormat getDateFormat() { return DateFormat.getDateInstance(DateFormat.MEDIUM, locale); } }
Im Template der Seite AppPropertyDisplayBlocks wird ein Block mit der ID calendarBlock erzeugt, um das Datum des Kalenders mithilfe der Komponente Output zu formatieren (siehe Listing 9.37). Beachten Sie, dass das Element kein Markup erzeugt. Dieses stellt nur das Wurzelelement des Templates dar. Listing 9.37: AppPropertyDisplayBlocks.tml
Ähnlich wie für die Seite AppPropertyEditBlocks muss auch für die Seite AppPropertyDisplayBlocks eine Instanz von BeanBlockContribution der Konfiguration des Dienstes BeanBlockSource hinzugefügt werden (siehe Listing 9.38). Beachten Sie, dass in diesem Fall der vierte Konstruktorparameter von BeanBlockContribution mit dem Wert false belegt werden muss. Listing 9.38: AppModule.java public class AppModule { ... public static void contributeBeanBlockSource( Configuration configuration) { ... BeanBlockContribution displayBlock = new BeanBlockContribution( "calendar", "AppPropertyDisplayBlocks", "calendarBlock", false); configuration.add(displayBlock); } }
206
9.5 Zusammenfassung
9.5
Zusammenfassung
Ein wichtiges Ziel von Tapestry ist die Steigerung der Produktivität der Entwickler. Die in diesem Kapitel vorgestellten Komponenten erlauben Ihnen, innerhalb kürzester Zeit Benutzerschnittstellen zur Darstellung und Bearbeitung von JavaBeans zu erzeugen. Durch das Prinzip Don’t Repeat Yourself ermöglicht Tapestry, dass Wiederholungen im Quellcode auf ein Minimum reduziert werden. In diesem Kapitel haben Sie drei mächtige Komponenten kennengelernt: BeanEditForm erzeugt ein Formular für eine JavaBean anhand der Metadaten, die
aus der Klasse dieser JavaBean ausgelesen werden. BeanDisplay stellt die Eigenschaften einer JavaBean dar. Grid erzeugt eine tabellarische Ansicht einer Menge von JavaBeans. Die erzeugte
Tabelle ist sortierbar und kann eine Seitennavigation besitzen. Alle drei vorgestellten Komponenten bringen unter anderem folgende Features mit: Aus den Namen der Eigenschaften von JavaBeans werden lesbare Labels für For-
mularfelder bzw. Spaltennamen erzeugt. Die Eigenschaften der JavaBeans werden entsprechend ihrer Typen formatiert. Die Reihenfolge der generierten Formularfelder bzw. Spalten wird durch die Rei-
henfolge der Getter-Methoden von Eigenschaften bestimmt und kann überschrieben werden. Die Darstellung der Eigenschaften kann überschrieben werden.
207
10
Multimedia-Inhalte
In Kapitel 3 wurden die unterschiedlichen Rückgabetypen von Handler-Methoden besprochen, die nach Behandlung eines Ereignisses zur Weiterleitung zu einer Seite oder einer externen URL benutzt werden. Manchmal ist es aber notwendig, auf eine Anfrage mit einem Multimedia-Datenstrom (z. B. Datenstrom eines Bildes, einer Audio-Datei, einer einfachen PDF-Datei usw.) zu antworten. Als zusätzlich möglichen Rückgabetyp für eine Handler-Methode stellt Tapestry das Interface org.apache.tapestry5.StreamResponse bereit. In diesem Kapitel lernen Sie, wie Sie auf eine Benutzeranfrage mit einer dynamisch erzeugten PDF-Datei bzw. einem JFreeChart-Diagramm unter Zuhilfenahme einer StreamResponse antworten können.
10.1
Senden eines einfachen Textes als Bytestrom
Tapestry beinhaltet eine Implementierung des Interface StreamResponse, die zum Versenden eines einfachen Textes an den Client in Form eines Datenstroms benutzt wird. In Listing 10.1 ist das Template einer einfachen Seite zu sehen, die einen ActionLink enthält. Die dazugehörige Seitenklasse (siehe Listing 10.2) implementiert eine Handler-Methode zur Behandlung des Ereignisses action. Listing 10.1: TextStreamPage.tml Erzeuge Response
Listing 10.2: TextStreamPage.java public class TextStreamPage { public StreamResponse onAction() { String text = "Success!"; return new TextStreamResponse("text/html", text); } }
10 Multimedia-Inhalte
Sobald der Link von einem Benutzer angeklickt wird, wird die Methode onAction() aufgerufen, in der eine Instanz von TextStreamResponse erzeugt wird. Der Konstruktor dieser Klasse erwartet zwei Parameter: den MIME-Typ der Daten, die an den Client gesendet werden, und die Daten selbst. Der MIME-Typ wird auch als Internet-MediaType bezeichnet und besteht aus der Angabe des Medientyps und dessen Subtypen (siehe dazu RFC 204510 und RFC 204611). Da es sich bei den Daten in Listing 10.2 um HTML handelt, wird als Medientyp text und als Subtyp html gewählt. Rufen Sie die Seite TextStreamPage in Ihrem Browser auf, und klicken Sie auf den Link. Sie sollten sehen, dass Sie als Ergebnis die Zeichenkette »Success!« wie in der Abbildung 10.1 erhalten. Nun schauen Sie sich den Quellcode der Ergebnisseite an. Er sollte mit dem HTML-Code übereinstimmen, den Sie in der Methode onAction() als zweiten Parameter an den Konstruktor von TextStreamResponse übergeben haben.
Abbildung 10.1: Ergebnisseite
10.2
Anbieten einer Download-Funktionalität
Das Interface StreamResponse kann auch dazu verwendet werden, um Benutzern eine Download-Funktionalität bereitzustellen. Lassen Sie uns ein Szenario implementieren, bei dem Sie einem Benutzer eine PDF-Datei zum Download anbieten, die Sie dynamisch innerhalb einer Seite erzeugen. Der Download wird gestartet, sobald der Benutzer auf einen ActionLink wie in Listing 10.3 klickt. In Listing 10.4 wird in der Handler-Methode des Ereignisses action eine anonyme Implementierung von StreamResponse erzeugt. Da Sie eine PDF-Datei zum Download anbieten, ist application als Medientyp und pdf als Subtyp zu wählen. Dieser MIMETyp wird in der Methode getContentType() als String zurückgegeben. In der Methode
10 RFC 2045: http://www.ietf.org/rfc/rfc2045.txt 11 RFC 2046: http://www.ietf.org/rfc/rfc2046.txt
210
10.2 Anbieten einer Download-Funktionalität
getStream() wird ein java.io.InputStream erzeugt, der an den Client geschickt wird. In diesem Beispiel wird die Bibliothek iText12 zur Erzeugung einer PDF-Datei eingesetzt. Die Details über die iText-Bibliothek sind in diesem Buch uninteressant.
Bevor der Datenstrom an den Client verschickt wird, ruft Tapestry die Methode prepareResponse() auf der zurückgegebenen Instanz von StreamResponse auf. Diese Methode kann beispielsweise zum Setzen der HTTP-Response-Header benutzt werden. In diesem Beispiel wird der Header Content-disposition gesetzt, um den Namen der Download-Datei zu bestimmen. Wir vergeben der Datei den Namen »Tapestry.pdf«. Listing 10.3: PdfPage.tml Download
Listing 10.4: PdfPage.java public class PdfPage { public StreamResponse onAction() { return new StreamResponse() { public String getContentType() { return "application/pdf"; } public InputStream getStream() throws IOException { return createPdf(); } public void prepareResponse(final Response response) { response.setHeader("Content-disposition", "attachment;filename=Tapestry.pdf"); } }; } public InputStream createPdf() { Document document = new Document(); ByteArrayOutputStream stream = new ByteArrayOutputStream(); try { PdfWriter.getInstance(document, stream); document.open(); document.add(new Paragraph( "Erzeugung einer PDF-Datei mit Tapestry und iText.")); URL url = getClass().getResource("tapestry_logo.png"); 12 http://www.lowagie.com/iText/
211
10 Multimedia-Inhalte
document.add(Image.getInstance(url)); } catch (final Exception e) { throw new RuntimeException(e); } document.close(); return new ByteArrayInputStream(stream.toByteArray()); } }
Abbildung 10.2: Download einer dynamisch erzeugten PDF-Datei Sobald ein Benutzer auf den Link klickt, startet der Download, und die dynamisch erzeugte PDF-Datei kann mit einem Programm zur Anzeige von PDF-Dateien geöffnet werden. Das Ergebnis sollte dann wie in der Abbildung 10.2 aussehen.
10.3
Anzeigen eines Diagramms
Sie können StreamResponse auch einsetzen, um dem Benutzer Diagramme in Form von Grafiken anzuzeigen. Die am meisten verbreitete Bibliothek zur Erzeugung von Diagrammen in Java ist wohl JFreeChart13. Lassen Sie uns eine Seite erzeugen, in der wir dynamisch ein Tortendiagramm mit JFreeChart erzeugen (siehe Abbildung 10.3). Zur Anzeige des Diagramms setzen wir im Template das HTML-Element ein, dessen Attribut src mithilfe einer Expansion auf die Methode getChartSrc() der Seitenklasse (siehe Listing 10.6) zugreift.
13 http://www.jfree.org/jfreechart/
212
10.3 Anzeigen eines Diagramms
Abbildung 10.3: Kuchendiagramm mit Tapestry und JFreeChart Listing 10.5: ChartPage.tml
Die Methode getChartSrc() der Seite ChartPage erzeugt einen Link durch den Aufruf der Methode createEventLink() auf dem Dienst ComponentResources und gibt diesen zurück. Die URI des zurückgegebenen Links wird im Template in den Parameter src des Elementes geschrieben und stellt somit den Pfad zur Grafik des Diagramms dar. Sobald die Seite mit dem Diagramm aufgerufen wird, wird die URL des Links angefragt. Dadurch wird das Ereignis chart ausgelöst, das durch die Methode onChart() behandelt wird. In der Methode onChart() wird eine anonyme Implementierung von StreamResponse erzeugt. Da das Diagramm als eine JPEG-Grafik dargestellt wird, müssen der Medientyp image und der Subtyp jpeg gewählt werden. Der Rest ist für dieses Buch ziemlich uninteressant: In der Methode getStream() wird eine Instanz von JFreeChart erzeugt, die in Form eines InputStream an den Client geschickt und dem Benutzer als ein Tortendiagramm präsentiert wird. Das Ergebnis können Sie in der Abbildung 10.3 sehen.
213
10 Multimedia-Inhalte
Listing 10.6: ChartPage.java public class ChartPage { @Inject private ComponentResources componentResources; public Link getChartSrc() { return this.componentResources.createEventLink("chart"); } StreamResponse onChart() { return new StreamResponse() { public String getContentType() { return "image/jpeg"; } public InputStream getStream() throws IOException { JFreeChart chart = createChart(); ByteArrayOutputStream stream = new ByteArrayOutputStream(); ChartUtilities.writeChartAsJPEG( stream, chart, 400, 300); return new ByteArrayInputStream(stream.toByteArray()); } public void prepareResponse(final Response response) { } }; } private JFreeChart createChart() { DefaultPieDataset dataset = new DefaultPieDataset(); dataset.setValue("Java", new Double(43.2)); dataset.setValue("Visual Basic", new Double(10.0)); dataset.setValue("PHP", new Double(32.5)); dataset.setValue("Perl", new Double(1.0)); dataset.setValue("C/C++", new Double(17.5)); PiePlot3D plot = new PiePlot3D(dataset); plot.setForegroundAlpha(0.5f); plot.setCircular(true); plot.setStartAngle(290); plot.setDepthFactor(0.15); return new JFreeChart(plot); } }
214
10.4 Zusammenfassung
10.4
Zusammenfassung
In diesem Kapitel haben Sie das Interface StreamResponse als einen alternativen Rückgabetyp von Handler-Methoden kennengelernt. Eine Handler-Methode kann eine Instanz von StreamResponse zurückgeben, um eine Anfrage mit einem Datenstrom zu beantworten. Anhand der MIME-Typen text, application und image wurde beispielhaft gezeigt, wie Multimedia-Daten mit Tapestry an den Client geschickt werden können.
215
Teil II Tapestry für Fortgeschrittene
11
Entwicklung wiederverwendbarer Komponenten
In den vorigen Kapiteln dieses Buches haben Sie bereits einen ersten Eindruck bekommen, wie einfach Komponenten in Tapestry erzeugt werden können. In diesem Kapitel widmen wir uns diesem Thema im Detail.
11.1
Bestandteile einer Komponente
Es wurde bereits erwähnt, dass der Unterschied zwischen Seiten und Komponenten minimal ist. Deshalb ist es sehr einfach, eine Seite in eine wiederverwendbare Komponente zu verwandeln. Ähnlich wie eine Seite besteht eine Komponente aus einer Java-Klasse und einem optionalen Template. Die beiden Bestandteile einer Komponente sind im Unterpaket components des Wurzelpakets der Applikation anzulegen.
11.2
Parameter von Komponenten
Eine Komponente kann eine beliebige Anzahl von Parametern besitzen, die dazu benutzt werden, die Komponente zu konfigurieren. Jeder Parameter hat einen Namen, einen Typ und einen Wert. Des Weiteren kann ein Parameter entweder erforderlich oder optional sein. Die Parameter einer Komponente werden durch Felder der Komponentenklasse repräsentiert, die mit der Annotation @Parameter markiert sind. In Listing 11.1 ist der Ausschnitt der Komponente ForEach zu sehen, die über ein Intervall iteriert, das mit den beiden Parametern start und end angegeben wird. Der Parameter start ist optional und ist mit dem Standardwert 1 vorbelegt. Der Parameter end ist dagegen erforderlich. Listing 11.1: Komponente mit zwei Parametern public class ForEach { @Parameter private int start = 1; @Parameter(required=true) private int end; ... }
11 Entwicklung wiederverwendbarer Komponenten
In Listing 11.2 wird die Komponente ForEach zwei Mal eingesetzt. Die erste der beiden Instanzen läuft von 1 bis 25, die zweite von 10 bis 25. Listing 11.2: ForEachDemo.tml
Standardmäßig entspricht der Name des Parameters dem Namen der jeweiligen Eigenschaft der Komponentenklasse. Alternativ kann der Name des Parameters in der Annotation @Parameter angegeben werden, indem der gewünschte Name in das Attribut name übergeben wird (siehe Listing 11.3). Allerdings hat diese Alternative einen kleinen Nachteil: Der Java-Compiler kann zur Entwicklungszeit nicht erkennen, ob ein Name eines Parameters innerhalb einer Komponente mehrfach vergeben wurde. Diese Überprüfung wird von Tapestry erst zur Laufzeit durchgeführt. Bei der Definition der Parameternamen über die Namen der Eigenschaften der Komponentenklasse wird der Compiler sofort erkennen, dass die Eindeutigkeit der Parameternamen verletzt wurde. Schließlich ist es nicht möglich, in einer Java-Klasse zwei Felder mit dem gleichen Namen zu erzeugen. Listing 11.3: Vergeben von Parameternamen public class ForEach { @Parameter(name="start") private int from = 1; ... }
Die Annotation @Parameter hat noch weitere Attribute, die in der Tabelle 11.1 zusammengefasst sind. Attributname Typ
Standardwert
Beschreibung
allowNull
boolean
true
Legt fest, ob ein Parameter den Wert null akzeptiert. Ist dieses Attribut auf true gesetzt und hat der jeweilige Parameter einen Wert, so darf dieser Wert nicht null sein.
autoconnect
boolean
false
Wird zur Erzeugung der Standard-Bindings benutzt (siehe Abschnitt ).
Tabelle 11.1: Parameter der Annotation @Parameter
220
11.3 Bidirektionale Parameter
Attributname Typ
Standardwert
Beschreibung
cache
boolean
true
Falls true, wird der Wert des Parameters während der Markup-Erzeugung gecachet. Beim wiederholten Zugriff auf den Parameter wird der Wert aus dem Cache ausgegeben. Falls false, wird der Parameter bei jedem Zugriff auf das Feld erneut eingelesen.
defaultPrefix
String
prop
Standardpräfix für das Binding.
name
String
leerer String
Name des Parameters
principal
Boolean
false
Markiert einen Parameter als ein Primärparameter. Falls true, wird der Parameter vor allen anderen Parametern initialisiert. Dies ist dann von Nutzen, falls das Standard-Binding eines Sekundärparameters von einem Primärparameter abhängig ist.
required
boolean
false
Falls true, ist der Parameter erforderlich, ansonsten optional.
value
String
leerer String
Standardwert des Parameters in Form eines Standardausdruckes.
Tabelle 11.1: Parameter der Annotation @Parameter (Forts.)
11.3
Bidirektionale Parameter
Die Parameter einer Komponente stellen eine Verbindung zwischen einer Eigenschaft dieser Komponente und einer Eigenschaft ihres Containers (Seite oder Komponente) dar. Aus diesem Grund können Sie nicht nur einen statischen Wert für einen Parameter angeben, sondern auch den Namen einer Eigenschaft des Containers, die dann von der Komponente ausgelesen wird. Diese Verbindung wird als Binding bezeichnet und ist bidirektional. Das bedeutet, dass eine Komponente nicht nur den Wert der Eigenschaft des Containers auslesen, sondern auch modifizieren kann. In Listing 11.4 wird die Komponente ForEach um den Parameter value erweitert, der in der Methode initializeValue() mit dem Wert des Parameters start belegt wird. Aufgrund der Annotation @SetupRender wird diese Methode zum Initialisieren der Komponente vor der Markup-Erzeugung aufgerufen. Mehr dazu in Abschnitt 11.7. Listing 11.4: Bidirektionale Parameter public class ForEach { @Parameter private int start = 1;
221
11 Entwicklung wiederverwendbarer Komponenten
@Parameter(required=true) private int end @Parameter private int value @SetupRender void initializeValue() { value = start; } ... }
In Listing 11.5 verbindet der Parameter value die Eigenschaft value der Komponente ForEach mit der Eigenschaft currentValue der Seite ForEachDemo. Durch die Änderung des Wertes der Eigenschaft value in der Komponente wird die Eigenschaft currentValue der Seite mit modifiziert. Listing 11.5: ForEachDemo.tml
11.4
Standard-Bindings der Parameter
Parameter, die nicht erforderlich sind, werden als optional bezeichnet. Sie müssen bei der Benutzung der Komponente nicht mit einem Wert belegt werden. In vielen Fällen ist es sinnvoll, einen Standardwert für einen optionalen Parameter anzugeben. Die einfachste Methode, einen Standardwert für einen Parameter festzulegen, ist die Vorinitialisierung des jeweiligen Feldes durch eine Zuweisung. Des Weiteren kann der Parameter value der Annotation @Parameter dazu benutzt werden, einen Standardwert anzugeben. Als Wert dieses Attributes wird ein sogenannter Binding-Ausdruck erwartet. Ein Binding-Ausdruck ist mit einer Expansion vergleichbar, wobei ${ und } nicht benötigt werden. Genau wie bei Expansions kann bei einem Binding-Ausdruck ein Präfix angegeben werden, der Tapestry darüber informiert, wie der Ausdruck zu interpretieren ist. In Listing 11.6 wird für den Parameter start der Binding-Ausdruck angegeben, der zum Holen des Standardwertes des Parameters benutzt wird. Da der Parameter value der Annotation @Parameter vom Typ java.lang.String ist, kann Tapestry nicht unter-
222
11.4 Standard-Bindings der Parameter
scheiden, ob es sich bei dem Wert »defaultValueForStartParameter« um ein Literal, eine Eigenschaft der Komponente, ein Asset oder etwas anderes handelt. Durch die Angabe des Parameters defaultPrefix der Annotation @Parameter teilen wir Tapestry mit, dass es sich um ein Property-Binding handelt. Damit erkennt Tapestry, dass die Methode getDefaultValueForStartParameter() aufgerufen werden soll, wenn ein Standardwert für den Parameter start der Komponente berechnet wird. Listing 11.6: Bestimmen des Standardwertes eines Parameters public class ForEach { @Parameter(value="defaultValueForStartParameter", defaultPrefix=BindingConstants.PROP) private int start; ... public int getDefaultValueForStartParameter(){ return 1; } }
Alle Präfixe, die in Expansions innerhalb der Templates eingesetzt werden, können auch für Standard-Bindings von Komponentenparametern benutzt werden. Alle verfügbaren Präfixe sind in der Klasse BindingConstants als Konstanten definiert. In Listing 11.7 wird beispielsweise für den Parameter message der Komponente OutputMessage der String »Hello, world!« als Standardwert festgelegt. Damit Tapestry diesen Wert auch als String interpretiert, wird BindingConstants.LITERAL als Standardpräfix benötigt. Listing 11.7: Angeben eines Literal-Bindings public class OutputMessage { @Parameter(value="Hello, world!", defaultPrefix=BindingConstants.LITERAL) private String message; boolean beginRender(MarkupWriter writer) { writer.writeRaw(message); return false; } }
Falls innerhalb der Annotation @Parameter eines Komponentenparameters kein Standardpräfix festgelegt wurde, wird der Standardpräfix BindingConstants.PROP benutzt.
223
11 Entwicklung wiederverwendbarer Komponenten
11.4.1
Standard-Binding-Methoden
Eine weitere Möglichkeit zum Bereitstellen der Standardwerte ist die Implementierung der sogenannten Standard-Binding-Methode für den jeweiligen Parameter. Wenn Tapestry einen Standardwert für einen Parameter berechnet, wird es in der Komponente nach einer Methode suchen, deren Signatur der folgenden Konvention entspricht. Namenskonvention für Standard-Binding-Methoden Der Name einer Standard-Binding-Methode setzt sich aus dem Präfix default und dem Namen des jeweiligen Parameters zusammen. Ferner darf die Methode keine Argumente besitzen. Der Rückgabewert einer Standard-Binding-Methode ist eine Instanz von Binding.
Zur Erzeugung einer Instanz von Binding wird der Dienst BindingSource benutzt. Die Methode newBinding() dieses Dienstes kann ein Binding anhand von vier Argumenten erzeugen: 1. Lesbare Beschreibung des Bindings. 2. Instanz des Dienstes ComponentResources. 3. Name des Standardpräfixes des Bindings. 4. Binding-Ausdruck. Die beiden Dienste BindingSource und ComponentResources können mittels der Annotation @Inject in die Komponente injiziert werden. In Listing 11.8 wird der Standardwert des Parameters start nicht in der Annotation @Parameter festgelegt. Stattdessen sucht Tapestry nach einer Standard-Binding-Methode innerhalb der Komponente ForEach nach der Methode defaultStart(). In dieser Methode wird der Dienst BindingSource zur Konstruktion eines Bindings für den Ausdruck defaultValueForStartParameter benutzt. Listing 11.8: Standard-Binding-Methode im Einsatz public class ForEach { @Parameter private int start; ... @Inject private ComponentResources resources; @Inject private BindingSource bindingSource;
224
11.4 Standard-Bindings der Parameter
Binding defaultStart() { return bindingSource.newBinding( "Default binding for parameter 'start'", resources, BindingConstants.PROP, "defaultValueForStartParameter"); } public int getDefaultValueForStartParameter() { return 1; } }
In Listing 11.9 ist zu sehen, wie ein Literal-Binding innerhalb einer Standard-BindingMethode erzeugt werden kann. Listing 11.9: Erzeugen eines Literal-Bindings in einer Standard-Binding-Methode public class OutputMessage { @Parameter private String message; @Inject private ComponentResources resources; @Inject private BindingSource bindingSource; boolean beginRender(MarkupWriter writer) { writer.writeRaw(message); return false; } Binding defaultMessage() { return bindingSource.newBinding( "Default binding for parameter 'message'", resources, BindingConstants.LITERAL, "Hello, world!"); } }
11.4.2
Generierung von Standard-Binding-Methoden
Die Implementierung von Standard-Binding-Methoden kann bei einer großen Zahl von Parametern den Code einer Komponente unnötig aufblähen. Wie an vielen anderen Stellen in Tapestry, kann das Framework die Implementierung dieser Methoden für den Entwickler übernehmen. Falls der Container (Seite oder Komponente) einer
225
11 Entwicklung wiederverwendbarer Komponenten
Komponente eine Eigenschaft enthält, die der ID der Komponente entspricht, kann der Parameter autoconnect der Annotation @Parameter genutzt werden, um eine Standard-Binding-Methode von Tapestry zur Laufzeit generieren zu lassen. In Listing 11.10 wird die Komponente OutputMessage vereinfacht, indem der Parameter autoconnect der Annotation @Parameter auf true gesetzt und die Standard-BindingMethode entfernt wird. Listing 11.10: Generierung einer Standard-Binding-Methode public class OutputMessage { @Parameter(autoconnect = true) private String message; ... }
In Listing 11.11 ist das Template der Seite OutputMessageDemo zu sehen, in dem der Komponente OutputMessage die ID helloMessage vergeben wird. Da die Seitenklasse (Listing 11.12) auch eine Eigenschaft helloMessage besitzt, erfolgt eine automatische Verknüpfung dieser Eigenschaft mit der Eigenschaft message der Komponente OutputMessage. Listing 11.11: OutputMessageDemo.tml
Listing 11.12: OutputMessageDemo.java public class OutputMessageDemo { public String getHelloMessage(){ return "Hello again!"; } }
11.5
Vererbung von Bindings
Falls Sie eine Komponente entwickelt haben, die weitere Komponenten beinhaltet, können Sie standardmäßig aus einer Seite heraus nicht auf die Parameter der inneren Komponenten zugreifen. Falls Sie aber die eingebetteten Komponenten gleich über die äußere Komponente mitkonfigurieren wollen, können Sie Parameter »veröffentlichen«. Dabei kann eine innere Komponente ihre Parameter an die äußere Komponente
226
11.5 Vererbung von Bindings
weitereichen, sodass Letztere um diese Parameter erweitert wird. In Listing 11.13 ist die Komponente Inner zu sehen, die den Parameter value besitzt und in die Komponente Outer (Listing 11.14) eingebettet wird. Listing 11.13: Inner.java public class Inner { @Parameter private String value; ... }
Die Komponente Inner wird in Outer mithilfe der Annotation @Component eingebettet, wobei der Parameter publishParameters dazu benutzt wird, den Parameter value von Inner als Parameter von Outer zu definieren. Listing 11.14: Outer.java public class Outer { @Component(publishParameters="value") private Inner inner; }
Obwohl die Komponente Outer keine eigenen Parameter besitzt, wird in der Seite OuterDemo (Listing 11.15) der Parameter value benutzt, als ob dieser zu Outer gehöre. Listing 11.15: OuterDemo.tml
Wenn innerhalb einer Komponente mehrere Komponenten eingebettet werden, deren Parameter nach außen gereicht werden sollen, müssen Sie darauf achten, dass keine Namenskollision entsteht. Wenn Sie beispielsweise wie in Listing 11.16 die Komponente Inner mehrmals in Outer einbetten, bekommen Sie eine Exception zu sehen, die Ihnen mitteilt, dass der Parameter value nicht mehrmals veröffentlicht werden kann. Schließlich müssen die Parameter von Komponenten eindeutig sein. Listing 11.16: Namenskollision bei veröffentlichten Parametern public class Outer { @Component(publishParameters="value") private Inner inner;
227
11 Entwicklung wiederverwendbarer Komponenten
@Component(publishParameters="value") private Inner inner2; }
Um dieses Problem zu lösen, können Sie Parameter auch vererben. Dazu müssen Sie in der Komponente Outer einen Parameter einführen, den Sie an die beiden inneren Komponenten vererben. In Listing 11.17 wird der Parameter text der Komponente Outer an die beiden inneren Instanzen der Komponente Inner mithilfe des Präfixes inherit vererbt. Listing 11.17: Vererben der Parameter public class Outer { @Parameter private String text; @Component(parameters="value=inherit:text") private Inner inner; @Component(parameters="value=inherit:text") private Inner inner2; }
Beim Einsatz der Komponente Outer muss dann der Parameter text wie in Listing 11.18 angegeben werden. Listing 11.18: OuterDemo.tml
Für die Parametervererbung benötigen Sie einen Parameter in der äußeren Komponente, der lediglich als eine Art Verbindung dient und ansonsten keinerlei Bedeutung hat. Sie sollten in Fällen, wo keine Namenskollision auftritt, die Parameterveröffentlichung bevorzugen.
11.6
Eigene Binding-Präfixe
In Kapitel 2 wurden unterschiedliche Binding-Typen vorgestellt, die eine Verbindung zwischen Komponenten und Ihren Containern darstellen. Mittels Bindings kann eine Komponente auf die Ressourcen Ihres Containers, z. B. Eigenschaften, Nachrichtenkatalog usw., zugreifen. Für die Unterscheidung des Typs der Ressourcen ist das Präfix des Bindings zuständig. In diesem Kapitel wird erläutert, wie Sie eigene Präfixe entwickeln können.
228
11.6 Eigene Binding-Präfixe
Wir betrachten die Erzeugung eines Kontexts für einen PageLink. Erinnern Sie sich, wie Sie einen Kontext, der aus mehreren Objekten besteht, für ein PageLink erzeugt haben? Sie haben in der Seiten- oder Komponentenklasse eine Getter-Methode implementiert, in der Sie ein Array mit mehreren Objekten zurückgegeben haben (siehe Listing 11.19). Aus dem Template (Listing 11.20) heraus haben Sie auf diese GetterMethode mittels eines Property-Bindings zugegriffen. Listing 11.19: Kontext mit mehreren Bestandteilen public class Index { @Property private User user; @Property private Book book; public Object[] getContext(){ return new Object[] { user.getId(), book.getId() }; } }
Listing 11.20: Referenziert den Kontext aus der Seitenklasse Klick mich
Nun entwickeln wir ein Binding, mit dem Sie einen Kontext aus dem Template heraus erzeugen können. In Listing 11.21 wird der Ausdruck hinter dem Präfix list als eine durch Komma separierte Liste von Property-Bindings interpretiert. So wird in diesem Beispiel der Kontext erzeugt, indem die beiden Bindings prop:user.id und prop:book.id ausgewertet werden. Die Methode getContext() kann nun aus der Seitenklasse entfernt werden. Listing 11.21: Erzeugung eines Kontexts mit List-Binding Klick mich
229
11 Entwicklung wiederverwendbarer Komponenten
Ein Binding wird in Tapestry durch das Interface org.apache.tapestry5.Bindig repräsentiert. Dieses Interface definiert fünf Methoden, die in Listing 11.22 implementiert werden. Die Klasse ListBinding erhält über ihren Konstruktor eine Liste von PropertyBindings, die im Template durch Komma separiert angegeben wurden. Die Methode get() wird vom Framework aufgerufen, wenn die Auswertung des Ausdrucks stattfinden soll. Im Falle eines Property-Bindings wird beispielsweise der Wert einer Eigenschaft ausgelesen. Unser ListBinding iteriert über eine Menge von Property-Bindings und ruft für jedes Binding die Methode get() auf. Die zurückgegebenen Werte der einzelnen Property-Bindings werden einer Liste hinzugefügt, die zurückgegeben wird. Die Klasse der von der Methode get() zurückgegebenen Instanzen muss von der Methode getBindingType() zurückgegeben werden. Die Methode isInvariant() gibt an, ob das Binding geändert werden kann. Im Falle von Property-Bindings gibt sie also an, ob der Wert der ausgelesenen Eigenschaft aktualisiert werden kann. Wir geben in dieser Methode den Wert true zurück, um zu signalisieren, dass Binding nicht veränderbar ist. Dementsprechend wird in der Methode set() eine TapestryException mit der entsprechenden Nachricht geworfen. Die Methode getAnnotation() erbt das Interface Binding von seinem Superinterface org.apache.tapestry5.ioc.AnnotationProvider. Diese Methode wird zur Suche nach einer java.lang.annotation.Annotation zum gegebenen Typen aufgerufen. Die meisten Binding-Implementierungen können in dieser Methode null zurückgeben. Listing 11.22: ListBinding.java public class ListBinding implements Binding { private final List bindings; public ListBinding(List bindings) { this.bindings = bindings; } public Object get() { List values = new ArrayList(bindings.size()); for (Binding next : bindings) { values.add(next.get()); } return values; } public Class getBindingType() { return List.class; } public boolean isInvariant() { return true; }
230
11.6 Eigene Binding-Präfixe
public void set(Object value) { throw new TapestryException( String.format("Binding %s is read-only", this), null, null); } public T getAnnotation( Class annotationClass) { return null; } }
Damit Tapestry eine Instanz von Binding erzeugen kann, wird eine Implementierung des Interface BindingFactory benötigt. In Listing 11.23 ist eine Implementierung dieses Interface zu sehen, die für die Erzeugung von ListBinding zuständig ist. Bei der Methode newBinding handelt es sich um eine Factory-Methode, in der mithilfe des Dienstes BindingSource eine Instanz von Binding erzeugt und zurückgegeben wird. Die Methode besitzt sechs Parameter: 1. Eine lesbare Beschreibung des Bindings. 2. Eine Instanz von ComponentResources der Komponente, in der das Binding definiert ist. 3. Eine Instanz von ComponentResources der Komponente, deren Parameter gebunden wird. 4. Den Namen des Standardpräfixes des Bindings. 5. Den Binding-Ausdruck. 6. Die Stelle im Template, an der das Binding definiert ist. Wenn list:user.id,book.id als Binding-Ausdruck angegeben wurde, erhält die Klasse ListBindingFactory den Wert user.id,book.id. Dieser wird an den Kommas aufgeteilt, um die einzelnen Property-Bindings zu extrahieren. Aus jedem der extrahierten Ausdrücke wird eine Instanz von Binding erzeugt und einer Liste hinzugefügt. Diese Liste wird als Parameter bei der Erzeugung von ListBinding verwendet. Listing 11.23: ListBindingFactory.java public class ListBindingFactory implements BindingFactory { private final BindingSource bindingSource; public ListBindingFactory(BindingSource source) { this.bindingSource = source; } public Binding newBinding(String description, ComponentResources container, ComponentResources component,
231
11 Entwicklung wiederverwendbarer Komponenten
String expression, Location location) { List bindings = new ArrayList(); String[] items = expression.split(","); for (String item : items) { Binding next = bindingSource.newBinding( description, container, component, BindingConstants.PROP, item, location); bindings.add(next); } return new ListBinding(bindings); } }
Abschließend muss eine Instanz von ListBindingFactory der Konfiguration des Dienstes BindingSource hinzugefügt werden. Dazu wird im IoC-Modul eine ContributeMethode wie in Listing 11.24 benötigt. Beachten Sie, dass die Klasse ListBindingFactory eine Instanz von BindingSource benötigt und somit eine zyklische Abhängigkeit entsteht. Auf den ersten Blick scheint dies unmöglich, da BindingSource erst dann erzeugt werden kann, wenn seine Konfiguration vorliegt. Sie werden später lernen, dass Tapestry-IoC einen Proxy von BindingSource an die Methode contributeBindingSource() übergibt. Dadurch können zyklische Abhängigkeiten aufgelöst werden. Listing 11.24: AppModule.java public class AppModule { ... public static void contributeBindingSource( MappedConfiguration configuration, BindingSource bindingSource) { configuration.add("list", new ListBindingFactory(bindingSource)); } }
11.7
Markup-Erzeugung einer Komponente
Gewöhnlich wird das Markup einer Komponente im Template festgelegt. Doch manchmal ist es sehr praktisch, das Erscheinungsbild einer Komponente auch im Java-Code zu bestimmen. Der Erzeugungsprozess besteht aus einer Menge von so genannten Render-Phasen, über die die jeweilige Komponente vom Framework informiert wird. In jeder Phase der Erzeugung wird ein Ereignis ausgelöst, das von der Komponente durch eine Render-Phase-Methode behandelt werden kann.
232
11.7 Markup-Erzeugung einer Komponente
In der Abbildung 11.1 sind die Zustandsübergänge aller Render-Phasen einer Komponente zu sehen. Für jede Phase existiert eine Annotation mit dem gleichen Namen, die zur Markierung der Methode benutzt wird, in der das gleichnamige Ereignis behandelt werden soll. Die grau gefärbten Phasen werden ausschließlich von Tapestry behandelt, sodass eine Komponente keine Ereignisse über diese Phase erhält. In Listing 11.25 ist eine einfache Komponente implementiert, die das Element zur Darstellung des Tapestry-Logos erzeugt. Listing 11.25: TapestryLogo.java public class TapestryLogo { @Inject @Path("tapestry.gif") private Asset logo; @BeginRender boolean renderImage(MarkupWriter writer) { writer.element("img", "src", logo); writer.end(); return false; } }
Der Rückgabewert einer Render-Phase-Methode ist entscheidend dafür, ob der Übergang in die nächste Phase stattfinden soll. Die Methode renderImage() gibt den Wert false zurück und beendet damit die Markup-Erzeugung der Komponente. Die nachfolgenden Zustandsübergänge werden nicht mehr stattfinden. Außerdem kann eine Render-Phase-Methode den Rückgabetyp void besitzen, was dem Rückgabewert true entspricht. In Listing 11.26 wird die Komponente ForEach um eine Render-Phase-Methode für die Phase AfterRender erweitert. In dieser Phase erfolgt die Entscheidung, ob eine weitere Iteration benötigt wird. Dazu wird der Wert der aktuellen Iteration erhöht und mit dem Wert des Parameters end verglichen. Falls der Wert von end noch nicht überschritten wurde, wird in der Methode next() false zurückgegeben, wodurch ein Übergang in die Phase BeginRender stattfindet. Dies bewirkt, dass die Komponente ForEach ein weiteres Mal ihr Markup erzeugt. Erreicht die Eigenschaft value einen Wert, der höher als der des Parameters end ist, wird die Markup-Erzeugung der Komponente ForEach beendet, indem durch die Rückgabe von true ein Übergang zur Phase CleanupRender stattfindet.
233
11 Entwicklung wiederverwendbarer Komponenten
Start
SetupRender true BeginRender true BeforeRenderTemplate true RenderTemplate
BeforeRenderBody false
false
false
false
true false
RenderBody
false
AfterRenderBody true AfterRenderTemplate true AfterRender true CleanupRender true Stop
Abbildung 11.1: Render-Phasen von Tapestry-Komponenten Listing 11.26: ForEach.java public class ForEach { @Parameter private int start = 1; @Parameter(required=true) private int end
234
false
false
11.7 Markup-Erzeugung einer Komponente
@Parameter private int value @SetupRender void initializeValue() { value = start; } @AfterRender boolean next() { int newValue = value + 1; if (newValue