Archiv der Kategorie: Wicket

Ich bin der Autor des Monats im Java Magazin 04/2013 – vielen Dank!

Das Java Magazin hat mich für die Ausgabe 04/2013 zum Autor des Monats gekürt.Vielen Dank für diese große Ehre.

Martin Dilger - Autor des Monats

Übrigens – wahrscheinlich ist dem Einen oder Anderen aufgefallen, dass in letzter Zeit sehr wenig hier auf dem Blog passiert. Das liegt daran, dass ich mit Hochdruck an dem Buch arbeite, dass in „naher“ Zukunft erscheinen wird.

Wicket 6 und JSR-303 – Beanvalidation

Hallo zusammen,

Wicket 6.4.0 ist released und es versteckt sich eine wirkliche Perle hier. Mit WICKET-4883 hat Igor Vaynberg eine Implementierung für BeanValidation (JSR-303) eingefügt. Das bedeutet, Wicket unterstützt BeanValidation nun „Out-of-the-Box“.

Das lasse ich mir natürlich nicht nehmen.

Zunächst erzeugen wir uns wie immer ein Maven-Wicket-Archetype für die Version 6.3.0 (Die Snapshot-Repositories für den 6.4.0-SNAPSHOT Archetype sind leider nicht aktuell).


mvn archetype:generate -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart -DarchetypeVersion=6.4.0 -DgroupId=de.effectivetrainings -DartifactId=jsr303-beanvalidation -DarchetypeRepository=https://repository.apache.org/ -DinteractiveMode=false

Zunächst registrieren wir die BeanValidationConfguration in der init-Methode der Application.


/**
* @see org.apache.wicket.Application#init()
*/
@Override
public void init()
{
super.init();
new BeanValidationConfiguration().configure(this);
// add your configuration here
}

In Wicket-Experimental ist ein neues Modul entstanden. Dieses Modul deklarieren wir ebenfalls als Dependency in der pom.


<groupId>org.apache.wicket<groupId>
<artifactId>wicket-bean-validation</artifactId>
<version>0.5</version>

Zusätzlich brauchen wir eine Implementierung der Bean-Validation-API. Typischerweise Hibernate Validator.

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>4.3.0.Final</version>
</dependency>

Jetzt fehlt noch ein schön annotiertes Model-Objekt. Wir nehmen hierfür einen….


public class EffectiveTrainer implements Serializable {

@NotNull
private String name;

@Pattern(regexp = "^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*((\\.[A-Za-z]{2,}){1}$)")
@NotNull
private String email;

@Pattern(regexp = "[0-9]+")
private String phone;

@Past
@NotNull
private Date birthDay;

public EffectiveTrainer(String name, String email, String phone, Date birthDay) {
this.name = name;
this.email = email;
this.phone = phone;
this.birthDay = birthDay;
}

public String getName() {
return name;
}

public String getEmail() {
return email;
}

public String getPhone() {
return phone;
}

public Date getBirthDay() {
return birthDay;
}
}

Im HomePage-Markup ersetzen wir alles durch folgendes Formular.

<body>
<div wicket:id="feedback"/>
<form wicket:id="form">
<input type="text" wicket:id="name"/> <br/>
<input type="text" wicket:id="email"/> <br/>
<input type="text" wicket:id="birthDay"/> <br/>
<input type="text" wicket:id="phone"/> <br/>
<input type="text" wicket:id="zip"/> <br/>
<input type="text" wicket:id="globalZip"/><br/>
<input type="submit"/>
</form>
</body>

und den passenden Java-Code hierzu:


public HomePage(final PageParameters parameters) {
super(parameters);

IModel effectiveTrainerModel = Model.of(new EffectiveTrainer());
add(new FeedbackPanel("feedback"));
Form form = new Form("form", effectiveTrainerModel);

form.add(new TextField("name", new PropertyModel(effectiveTrainerModel, "name"))
.add(new PropertyValidator()));
form.add(new TextField("email", new PropertyModel(effectiveTrainerModel, "email"))
.add(new PropertyValidator()));
form.add(new TextField("phone", new PropertyModel(effectiveTrainerModel, "phone"))
.add(new PropertyValidator()));
form.add(new TextField("date", new PropertyModel(effectiveTrainerModel, "birthDay"))
.add(new PropertyValidator()));

add(form);

}

Starten wir die Anwendung und schicken das Formular testweise ab ergibt sich folgendes Bild.

Bean Validation

Bean Validation in Action

Jede FormComponent bekommt einen neuen PropertyValidator spendiert. Der PropertyValidator liest die entsprechenden Properties und deren Constraints aus.

Sehr schön, das Prinzip funktioniert. Aber die Fehlermeldungen sind nicht ideal. Wie lässt sich hier etwas machen?

Feedback-Messages

Das erste was einem wahrscheinlich einfällt ist die Fehlermeldungen direkt an den Constraints zu definieren.


@NotNull(message = "Bitte geben Sie Ihren Namen ein.")
private String name;

@Pattern(message = "Die E-Mailadresse ist nicht gültig",
regexp = "^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*((\\.[A-Za-z]{2,}){1}$)")
@NotNull(message = "Bitte geben Sie Ihre E-Mailadresse ein")
private String email;

@Pattern(message = "Ihre Telefonnummer sollte aus Ziffern bestehen",regexp = "[0-9]+")
private String phone;

@Past(message = "Sie können nicht in der Zukunft Geburtstag haben")
@NotNull(message = "Bitte geben Sie Ihr Geburtsdatum ein.")
private Date birthDay;

Und das funktioniert auch.

bean-validation - angepasste meldungen

bean-validation – angepasste meldungen

Ideal ist das aber nicht, denn meiner Ansicht nach ist die Wicket-Komponente (also die UI) verantwortlich sein zu definieren, wann in welchem Kontext welche Fehlermeldung angezeigt wird. Das Domain-Objekt „EffectiveTrainer“ ist so nicht Kontextübergreifend wiederverwendbar. Das muss besser gehen, oder?

Dynamische FeedbackMessages

Natürlich geht das besser. Wir definieren die Attribute am EffectiveTrainer um.


@NotNull
private String name;

@Pattern(message = "{email.invalid}",
regexp = "^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*((\\.[A-Za-z]{2,}){1}$)")
@NotNull
private String email;

@Pattern(message = "{phone.invalid}",regexp = "[0-9]+")
private String phone;

@Past(message = "{birthday.invalid}")
@NotNull
private Date birthDay;

Die JSR-303 Spezifikation definiert, dass Fehlermeldungen mit Platzhaltern befüllt sein dürfen. Platzhalter sind definiert als { value }. Wir verwenden die Platzhalter einfach als Keys in die Property-Datei der jeweiligen Komponente.

Hierfür definieren wir jetzt nur noch diese Property-Datei (HomePage.properties).


email.Required=Bitte geben Sie Ihre E-Mailadresse ein.
email.invalid = Ihre E-Mailadresse ist ungueltig
name.Required=Bitte geben Sie Ihren Namen ein
phone.invalid=Ihre Telefonnummer sollte aus Ziffern bestehen
birthday.invalid=Ihr Geburtsdatum ist ungültig
birthday.Required=Bitte geben Sie Ihr Geburtsdatum ein

Wieso aber email.Required? Wir haben überhaupt keine message für @NotNull definiert? Igor Vaynberg hat uns hier einen Gefallen getan:). Ein Formkomponente, deren zugeordnetes Attribut mit @NotNull annotiert ist wird standardmässig auf Required gesetzt. Das bedeutet, aber hier greifen die Wicket-Standards – also #attributName.Required.

Ergänzen wir das Beispiel noch um eine Postleitzahl. Ich biete Wicket-Trainings deutschlandweit an, aber ein Effective-Trainer kann nur aus München kommen…

Definieren wir also zusätzlich folgende Property.

//hibernate validator specific
@Range(message = "{zip.muenchen}",min = 80805, max=80805)
private Integer zip;

Folgendes Textfeld.


form.add(new TextField("zip", new PropertyModel(effectiveTrainerModel, "zip"))
.add(new PropertyValidator()));

und folgende Text-Property.


zip.muenchen=Ein Effective-Trainer kann nur aus Muenchen-Schwabing kommen

Probleme

Warum hat es wohl so lange gedauert, bis die erste Implementierung direkt im Wicket-Code zur Verfügung steht?

Wicket arbeitet extrem viel mit PropertyModels. PropertyModels lösen Expressions (diese Punkt-separierten Strings) zur Laufzeit auf.

Das bedeutet, zur Laufzeit müssen wir wissen, welche Typen wir haben und wie Constraints auf diesen konfiguriert sind. Das ist etwas komplizierter. Ich habe auch vor ca. 2 Jahren schon eine JSR-303-BeanValidation Implementierung für Wicket gemacht. Lange nicht so schick wie diese hier, aber hat funktioniert.

Versuchen wir doch mal eine Vereinfachung. Lösen wir die ganzen PropertyModels im Formular auf und ersetzen diese durch ein CompoundPropertyModel. Standard so weit.


IModel effectiveTrainerModel = Model.of(new EffectiveTrainer());
add(new FeedbackPanel("feedback"));
Form form = new Form("form", new CompoundPropertyModel(effectiveTrainerModel));

form.add(new TextField("name")
.add(new PropertyValidator()));
form.add(new TextField("email")
.add(new PropertyValidator()));
form.add(new TextField("phone")
.add(new PropertyValidator()));
form.add(new TextField("date")
.add(new PropertyValidator()));
form.add(new TextField("zip")
.add(new PropertyValidator()));

Sieht tatsächlich besser aus. Funktionierts? Leider nein.

java.lang.IllegalStateException: Could not resolve Property from component: [TextField [Component id = name]]. Either specify the Property in the constructor or use a model that works in combination with a IPropertyResolver to resolve the Property automatically

Das Model der Komponente muss vom Typ IPropertyReflectionAwareModel sein, damit BeanValidation funktioniert. Im Fall CompoundPropertyModel haben wir gar keine speziellen Models.
Das Problem ist, wir kommen so leider nicht ganz einfach an das zu validierende Feld und noch schwieriger, die zugehörige Klasse ran.
Normlerweise durchläuft Wicket standardmässig für jede Expression die Getter-Chain:

"customer.address.street" getCustomer().getAddress().getStreet()

Die Typen der einzelnen Hierarchien sind hierbei egal. Nicht jedoch, wenn es um BeanValidation geht, denn ein JSR303-Validator will den Typ wissen.

Wenn wir mit dem CompoundPropertyModel arbeiten möchten, brauchen wir als einen kleinen Workaround.
Wir teilen Wicket bzw. dem Validator einfach selbst mit, um welchen Typ es sich handelt.

 form.add(new TextField("name")
                .add(new PropertyValidator(new Property(EffectiveTrainer.class,"name"))));
        form.add(new TextField("email")
                .add(new PropertyValidator(new Property(EffectiveTrainer.class,"email"))));
        form.add(new TextField("phone")
                .add(new PropertyValidator(new Property(EffectiveTrainer.class,"phone"))));
        form.add(new TextField("birthDay")
                .add(new PropertyValidator(new Property(EffectiveTrainer.class,"birthDay"))));
        form.add(new TextField("zip")
                .add(new PropertyValidator(new Property(EffectiveTrainer.class,"zip"))));

Validation Groups

Über Gruppen kann noch ein wenig genauer definiert werden, wann eine Property wie validiert wird. Ich bin kein großer Fan hiervon, denn wenn man so etwas wie Gruppen braucht, stellt sich die Frage, ob das Domain-Objekt richtig geschnitten ist. Nichtsdestotrotz, wofür könnte man das brauchen?

Eine Group ist technisch nichts anderes als ein Interface. Definieren wir also ein Interface für unseren EffectiveTrainer.


public interface EffectiveTrainer {
String getName();

String getEmail();

String getPhone();
}

Und benennen die Implementierung so um, dass es Sinn macht in MuenchensEffectiveTrainer.

Die Constraints definieren wir jetzt so um:


@Range.List(value = {
@Range(message = "{zip.muenchen}",min = 80805, max=80805, groups = EffectiveTrainer.class),
@Range(message = "{zip.global}",min = 0, max=Integer.MAX_VALUE)
})
private Integer zip;

Wir haben einen Default-Constraint, der quasi immer dann greift, wenn keine Group definiert ist (alle positiven Zahlen ausser 0 sind erlaubt!).

Im Fall von Münchens Trainer brauchts schon eine Postleitzahl in Schwabing.

Der Einfachheit halber definieren wir einfach nochmal ein zweites Textfeld, das auf ebenfalls auf die Zip-Property im Trainer-Objekt geht.

form.add(new TextField("zip")
.add(new PropertyValidator(new Property(MuenchensEffectiveTrainer.class, "zip"), EffectiveTrainer.class)));

IModel globalZipModel = compoundModel.bind("zip");
form.add(new TextField("globalZip", globalZipModel)
.add(new PropertyValidator(new Property(MuenchensEffectiveTrainer.class, "zip"))));

Im ersten Fall definieren wir im PropertyValidator, dass dieser nur die Group „EffectiveTrainer.class“ validieren soll. Im zweiten Fall geben wir gar nichts an, das bedeutet, die Default Group greift.

Gruppen können unterschiedlich validiert werden

Gruppen können unterschiedlich validiert werden

Wie gesagt, Gruppen sollten die Ausnahme sein. Man kann eine Property auf unterschiedliche Weisen validieren, aber der Code ist schwerer zu verstehen.

Über dieses und viele weitere Themen spreche ich auch in meinem Wicket-Workshop
Kontaktieren Sie mich zum Thema Wicket, ich freue mich, wenn ich Sie unterstützen kann.

Effective Trainings Wicket Workshop

Links

JSR-303 Spezifikation

Safe-Model (von Carl Eric Menzel)
(geht auch in die richtige Richtung)

Mein Wicket Workshop

Wicket und dynamisch generiertes HTML5-Cache-Manifest

Hallo,

ein interessantes Experiment, inwiefern lässt sich Wicket und HTML5-Offline-Cache verheiraten?

Scheinbar zunächst schwierig, aber es funktioniert.

Was wir zunächst brauchen ist ein dynamisch generiertes Cache-Manifest.
Nehmen wir an, wir haben folgenden Use-Case:

Unsere Seiten sollen so stark wie möglich Offline gecached werden. Welche Ressourcen genau soll aber dynamisch generiert werden.
Beispielsweise eine bestimmte Seite soll nur bis zu einem bestimmten Zeitpunkt gecached werden, abhängig von den Daten in unserer Anwendung.

Folgendes Markup definiert ein Html5-Cache Manifest:

<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org" wicket:id="html">
</html>

Wichtig hier ist der HTML5-Doctype und das Attribut „manifest“ im Html-Tag.
Hier haben wir schon die erste Schwierigkeit, wie kommen wir in Wicket an das Html-Tag?

Das schöne ist, für Wicket ist das Html-Tag auch nur ein gewöhnliches Tag und dadurch können wir diesem genau wie jedem anderen Tag eine „wicket:id“ vergeben.

So sieht das Markup für eine sehr einfache WebPage aus:

<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org" wicket:id="html">
<body>
<img wicket:id="image">
<div wicket:id="htmlCacheDemo">
</body>
</html>

Was aber weisen wir dem html-Tag zu? Ein einfacher WebMarkupContainer?
Würde funktionieren, hat aber einen gravierenden Nachteil, vor allem wenn man mit WebPage-Hierarchien arbeitet.
Wicket erwartet, dass die Komponentenhierarchie stimmt. Das würde für diesen Fall bedeuten, dass alle Komponenten auf der Page nicht der Page selbst, sondern dem äußersten WebMarkupContainer (eben dem für das Html-Tag) zugewiesen werden müssen.

Ist das Ganze in einer abstrakten Oberklasse definiert und versteckt muss das ein Entwickler wissen..

Das geht besser.

Wir verwenden hierfür einen TransparentWebMarkupContainer. Ein „transparenter“ Container ist deswegen transparent, weil er in der Hierarchie nicht sichtbar ist. Kann der Container eine Komponente nicht auflösen schaut er einfach in seinem Parent, was in diesem Fall die Page ist. Somit können alle Entwickler genauso arbeiten wie erwartet, indem sie die Komponenten der Page zuweisen und nicht mehr dem Container.

Ich glaube eine gute Idee ist, das Auflösen des Cache-Manifests in guter alter Wicket-Manier zu machen. Wicket erwartet beispielsweise, dass das Markup zu einer Seite genau den Namen der Seitenklasse hat, nur mit der Endung „html“.

Verwenden wir diesen Mechanismus doch auf für manifest-Dateien. Auf diese Weise kann jede Page in Ihrem Package ihre eigene Manifest-Datei definieren.

Das zu implementieren war nicht ganz einfach.

Zunächst definieren wir uns ein einfaches Manifest mit Namen „Homepage.manifest“ im Package der HomePage-Klasse im vorgegebenen Format:

CACHE MANIFEST
#cacheEntries
NETWORK:
#networkEntries
FALLBACK:
#fallbackEntries

Cache-Entries sind einfach eine Liste an Resourcen die im Offline-Cache gecached werden sollen, also beispielsweise /my-image.png.

Network-Entries sind das genaue Gegenteil – alle Ressourcen die zwingend jedesmal neu geladen werden sollen können hier aufgeführt werden. Üblicherweise steht hier nur ein „*“. Das bedeutet, alle Ressourcen die nicht explizit gecached sind sollen extern geholt werden.

Fallback-Entries sind praktisch, hier erlaubt es der Cache-Mechanismus Fallbacks für Ressourcen anzugeben. Das Format hierfür ist beispielsweise „/images/online.png /images/offline.png“.
Der Browser wird versuchen die Ressource „online.png“ zu laden, schlägt dies fehl wird „offline.png“ angezeigt.

Jetzt müssen wir dafür sorgen, dass die Manifest-Dateien auch für jede Seite geladen werden können.

Hierfür brauchen wir eine PackageResource. Eine PackageResource erlaubt es uns, Dateien aus dem Classpath zu laden.

public class Html5CacheResource extends PackageResource {

    private static final Logger LOG = LoggerFactory.getLogger(Html5CacheResource.class);

    public Html5CacheResource(Class<? extends Page> scope, Locale locale, String style, String variation) {
        super(scope, scope.getSimpleName() + ".manifest", locale, style, variation);
        setTextEncoding("utf-8");
        setCachingEnabled(false);
    }

    @Override
    protected byte[] processResponse(Attributes attributes, byte[] original) {
        try {
            String manifest = IOUtils.toString(new ByteArrayInputStream(original));
            return IOUtils.toByteArray(new StringReader(generateManifest(manifest)));
        } catch (Exception e) {
            LOG.warn("Cannot create cache manifest");
        }
        return original;
    }

    @Override
    protected void setResponseHeaders(ResourceResponse data, Attributes attributes) {
        super.setResponseHeaders(data, attributes);
        data.getHeaders().addHeader("Pragma", "no-cache");
        data.getHeaders().addHeader("Cache-Control",
                "no-cache, max-age=0, must-revalidate, no-store");
    }

    /**
     * override this to process your manifest
     *
     * @param template
     * @return
     */
    protected String generateManifest(String template) {
        return template;
    }
}

Die PackageResource erhält die Page, auf der sie verwendet werden soll als Parameter im Konstruktor.

Class<? extends Page> scope

Sie baut sich daraus außerdem den Ressourcen-Namen dynamisch zusammen.

scope.getSimpleName() + ".manifest"

In der Methode „setResponseHeaders“ wird ausserdem sichergestellt, dass das Manifest keinesfalls
gecached wird. Hier bin ich mir nicht ganz sicher, warum das gebraucht wird aber ohne scheint es Probleme zu geben, vor allem mit Firefox in der Version 17. Ich habe ausserdem versucht, PackageResource#setCacheable(false) zu verwenden – leider ohne Effekt.

Interessant ist die Methode „processResources“ die in der Klasse PackageResource definiert ist und mir als Entwickler die Möglichkeit gibt, die geladene Ressource weiter zu verarbeiten.

 @Override
    protected byte[] processResponse(Attributes attributes, byte[] original) {
        try {
            String manifest = IOUtils.toString(new ByteArrayInputStream(original));
            return IOUtils.toByteArray(new StringReader(generateManifest(manifest)));
        } catch (Exception e) {
            LOG.warn(&quot;Cannot create cache manifest&quot;);
        }
        return original;
    }

Wir generieren aus dem übergebenen byte[] einen String (das byte[] ist die geladenene unmodifizierte HomePage.manifest Datei).
Das Ganze geht durch eine von mir definierte Methode „generateManifest“ und direkt wieder zurück in ein byte[] das wir für die weitere Verarbeitung zurückgeben.

Die Methode „generateManifest“ ist zum Überschreiben gedacht, das werden wir gleich noch sehen.

Cache Manifest

Wir definieren uns ausserdem folgende Klassen, die ein einfaches hinzufügen von weiteren Cache-Einträgen erlauben ohne mit Strings arbeiten zu müssen.


public class CacheManifest {

private Map<CacheManifestKeys, List<String>> manifestEntries = new HashMap<CacheManifestKeys, List<String>>();

private static final String CACHE_PLACEHOLDER = "#cacheEntries";
private static final String NETWORK_PLACEHOLDER = "#networkEntries";
private static final String FALLBACK_PLACEHOLDER = "#fallbackEntries";

public CacheManifest(){
manifestEntries.put(CacheManifestKeys.CACHE, new ArrayList<String>());
manifestEntries.put(CacheManifestKeys.FALLBACK, new ArrayList<String>());
manifestEntries.put(CacheManifestKeys.NETWORK, new ArrayList<String>());
}

public void addEntry(CacheManifestEntry entry){
if(entry.isEnabled()){
manifestEntries.get(entry.getKey()).add(entry.getValue());
}
}

public String get(String manifestTemplate){
manifestTemplate = manifestTemplate.replace(CACHE_PLACEHOLDER,joinString(CacheManifestKeys.CACHE));
manifestTemplate = manifestTemplate.replace(NETWORK_PLACEHOLDER,joinString(CacheManifestKeys.NETWORK));
manifestTemplate = manifestTemplate.replace(FALLBACK_PLACEHOLDER,joinString(CacheManifestKeys.FALLBACK));
return manifestTemplate;
}

private String joinString(CacheManifestKeys cacheKey) {
if(manifestEntries.get(cacheKey).isEmpty()){
//return just a comment or * in case of network
return CacheManifestKeys.NETWORK.equals(cacheKey) ? "*": "#";
}
return StringUtils.join(manifestEntries.get(cacheKey).iterator(), '\n');
}

}

Folgende Klasse für einen Eintrag im Manifest.


public class CacheManifestEntry {

private CacheManifestKeys key;
private String value;
private String fallback;

public CacheManifestEntry(CacheManifestKeys key, String value, String fallback) {
this.key = key;
this.value = value;
this.fallback = fallback != null ? fallback : "";
}

public CacheManifestEntry(CacheManifestKeys key, String value){
this(key, value, null);
}

public boolean isEnabled() {
return true;
}

public CacheManifestKeys getKey() {
return key;
}

public String getValue() {
if(CacheManifestKeys.FALLBACK.equals(key)){
return value + " "+ fallback;
} else {
return value;
}
}

}

Interessant ist auch die Methode „isEnabled“. Einträge können ein/ausgeblendet werden.

und dieses Enum für die Keys.


public enum CacheManifestKeys {

CACHE("CACHE"),NETWORK("NETWOR"),FALLBACK("FALLBACK");
private String key;

private CacheManifestKeys(String key){
this.key = key;
}

public String getKey(){
return key;
}
}

Im Manifest-Template, das wir uns zuvor definiert haben ist beispielsweise diese Kommentar definiert: #cacheEntries

Diese Kommentare werden einfach als Platzhalter verwendet, an deren Stelle die dynamisch generierten Einträge generiert werden.

Der Html5 Cache WebmarkupContainer

Im folgenden sieht man den Code für den bereits zuvor angesprochenen transparenten WebMarkupContainer.

public class Html5CacheManifestMarkupContainer<T> extends TransparentWebMarkupContainer implements IResourceListener {

private ResourceReference resource;

private IModel<? extends CacheManifest> cacheManifestModel;

public Html5CacheManifestMarkupContainer(String id, IModel<CacheManifest> cacheManifestModel) {
super(id);
this.cacheManifestModel = cacheManifestModel;
}

public Html5CacheManifestMarkupContainer(String id, IModel<T> model, IModel<? extends CacheManifest> cacheManifestModel){
super(id);
setDefaultModel(model);
}

@Override
protected void onInitialize() {
super.onInitialize();
final Html5CacheResource cacheResource = new Html5CacheResource(getPage().getClass(), getLocale(),getStyle(),getVariation()){
@Override
protected String generateManifest(String template) {
return cacheManifestModel.getObject().get(template);
}
};
resource = new ResourceReference(getPage().getClass().getSimpleName() + ".manifest"){

@Override
public IResource getResource() {
return cacheResource;
}
};

}

@Override
protected void onConfigure() {
super.onConfigure();
add(AttributeAppender.append("manifest",urlFor(resource, getPage().getPageParameters()).toString()));
}

@Override
public void onResourceRequested() {
resource.getResource().respond(new IResource.Attributes(getRequest(),getResponse(),getPage().getPageParameters()));
}
}

Zunächst implementiert der WebmarkupContainer das Interface IResourceListener. Hiermit erlauben wir, Ressourcen auf dieser Seite über eine URL aufzurufen. Das sehen wir gleich.

In der onInitialize-Methode erzeugen wir eine neue Html5CacheResource.


final Html5CacheResource cacheResource = new Html5CacheResource(getPage().getClass(), getLocale(),getStyle(),getVariation()){
@Override
protected String generateManifest(String template) {
    return cacheManifestModel.getObject().get(template);
}
};
resource = new ResourceReference(getPage().getClass().getSimpleName() + ".manifest"){

@Override
public IResource getResource() {
    return cacheResource;
}
};

Sehr interessant ist die Methode „onConfigure“, denn hier erzeugen wir den eigentlichen Eintrag für das Html-Tag mit der korrekten URL.


@Override
protected void onConfigure() {
   super.onConfigure();
   add(AttributeAppender.append("manifest",urlFor(resource,    getPage().getPageParameters()).toString()));
}

Was Wicket jetzt daraus generiert ist das hier:

</pre>
<html xmlns:wicket="http://wicket.apache.org" wicket:id="html" manifest="./wicket/resource/org.apache.wicket.Application/HomePage.manifest">

Ein bisschen Sicherheit muss sein

Leider (oder zum Glück) verbietet Wicket standardmässig den Zugriff auf *.manifest Dateien. Hierfür müssen wir den PackageResourceGuard ein wenig erweitern. Wir definieren einfach folgende Klasse.


public class Html5CacheAwarePackageResourceGuard extends SecurePackageResourceGuard {

public Html5CacheAwarePackageResourceGuard() {
addPattern("+*.manifest");
}
}

und hängen diesen anschließend in der Application-Klasse ein.


@Override
public void init()
{
super.init();
mountPage(HomePage.MOUNT_PATH, HomePage.class);
mountPage(Another.MOUNT_PATH, Another.class);
getResourceSettings().setPackageResourceGuard(new Html5CacheAwarePackageResourceGuard());
}

Zuletzt definieren wir noch in der web.xml den richtigen Content-Type.


<mime-mapping>
<extension>manifest<extension>
<mime-type>text/cache-manifest</mime-type>
</mime-mapping>

Der Cache in Aktion

Betrachten wir das Ganze in Aktion.

Der zuvor definierte Html5CacheManifestMarkupContainer erwartet im Konstruktor ein Model vom Type CacheManifest.

Wir definieren uns hierfür einfach folgendes Model.


public class CacheManifestModel extends AbstractReadOnlyModel<CacheManifest> {

@Override
public CacheManifest getObject() {
CacheManifest manifest = new CacheManifest();
manifest.addEntry(new CacheManifestEntry(CacheManifestKeys.CACHE,"/start.html"));
manifest.addEntry(new CacheManifestEntry(CacheManifestKeys.CACHE, "http://svn.apache.org/repos/asf/wicket/sandbox/dashorst/animation/logo-top.png"));
manifest.addEntry(new CacheManifestEntry(CacheManifestKeys.FALLBACK,"/online.png","/offline.png"));
return manifest;
}
}

Fährt man den Jetty hoch und geht einmal auf die Seite sieht man folgendes (hier ist noch eine kleine Html5-Cache Test Konsole auf Basis von JQuery integriert, aber für diesen Blogpost nicht wirklich interessant).

html5 online application example with wicket

html 5 online application

Fährt man den Jetty jetzt herunter, ohne den Browser zu schliessen geschieht folgendes.

Wicket Html5 Offline Application

Wicket Html5 Offline Application

Der Fallback Mechanismus greift, da das Image nicht mehr geladen werden kann. Der Mechanismus funktioniert also.

Ich finde die Lösung für einen ersten Proof-of-Concept recht interessant und scheint auch zu funktionieren (wenn auch nicht wirklich im Firefox).

Kommentare und Ergänzungen sind wie immer herzlich Willkommen.

Alle Sourcen finden Sich wie immer in meinem <a href=“https://github.com/dilgerma/wicket-6.0-Playground.git“>Wicket-6-Playground-Repo</a&gt;.

Über dieses und viele weitere Themen spreche ich übrigens auch in meinem Wicket-Workshop

Effective Trainings Wicket Workshop

Effective Trainings Wicket Workshop

Wicket 6 – Spaß mit IResource?

Hallo zusammen,

öfter höre ich in meinen Workshops und Vorträgen die Frage, wozu man eine IResource braucht.Da muss ich nicht lange überlegen.

Nehmen wir als Beispiel folgendes Requirement – wir möchten ein QR-Code dynamisch innerhalb unserer Anwendung generieren und zur Anzeige bringen.

Funktioniert über ein Servlet – prinzipiell – hiermit verlassen wir aber die Wicket-Welt und begehen einen Technologiebruch, der nicht notwendig ist. Es geht einfacher, und zwar mit IResource.

Zunächst erzeugen wir uns einen neuen Maven-Archetype.


mvn archetype:generate -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart -DarchetypeVersion=6.3.0 -DgroupId=de.effectivetrainings -DartifactId=wicket-6-resources -DarchetypeRepository=https://repository.apache.org/ -DinteractiveMode=false

In der pom.xml konfigurieren wir uns noch eine Abhängigkeit auf IText, eine recht nette Bibliothek zum generieren von Barcodes aller Art.


com.itextpdf
itextpdf
5.2.0

Wir schreiben uns zunächst einen einfachen Barcode-Generator. Das ist nichts anderes als die IText Standard-API und deswegen gehe ich nicht näher darauf ein.


/**
* @author martin@effectivetrainings.de
*         Date: 29.11.12
*         Time: 21:42
*/
public class BarCodeGenerator {

public BufferedImage generate(String code)  {
BarcodeQRCode qrCode = new BarcodeQRCode(code,300,300, new HashMap<EncodeHintType, Object>());
java.awt.Image image = qrCode.createAwtImage(Color.BLACK,Color.WHITE);
return toBufferedImage(image);
}

public byte[] asRawBytes(String code){
ByteArrayOutputStream bout = new ByteArrayOutputStream();
try {
ImageIO.write(generate(code), "png",bout);
return bout.toByteArray();
} catch (Exception e) {
//here you should throw some kind of exception
return new byte[0];
}
}

private BufferedImage toBufferedImage(java.awt.Image image){
BufferedImage bi = new BufferedImage(image.getWidth(null),image.getHeight(null),BufferedImage.TYPE_INT_RGB);
Graphics bg = bi.getGraphics();
bg.drawImage(image, 0, 0, null);
bg.dispose();
return bi;

}

Zum Beweis, dass das Ganze funktioniert schreiben wir uns einen sehr einfachen Test. Achtung, dieser Test schreibt eine Testdatei in euer /tmp Verzeichnis. Das ist kein echter Unit-Test, sondern nur ein Smoke-Test um zu prüfen, ob die Generierung tut.

public class BarCodeGeneratorTest {

@Test
public void testGenerateQRCode() throws Exception{
byte[] raw = new BarCodeGenerator().asRawBytes("http://www.effectivetrainings.de");
File outputfile = new File("/tmp/wicket-6-resource-example.png");
FileOutputStream fout = new FileOutputStream(outputfile);
fout.write(raw);

}
}

Der Test macht genau, was wir erwarten würden und generiert in /tmp/ folgende Datei, die mit
jedem aktuellen Smartphone gescannt werden kann.
Ok, damit ist schon viel geschafft, wie aber bringen wir dieses „Bild“ jetzt in unserer Webanwendung zur Anzeige.

Der langweilige  Weg wäre, hierfür ein Servlet zu schreiben und dieses Bild direkt in die Response zu rendern. Viel besser ist, das Ganze in eine IResource zu verpacken.

Da dies ein Standard-Use-Case ist, bringt Wicket hier natürlich schon etwas fertiges mit. Wir erzeugen uns einfach folgene Klasse:


public class QRResource extends DynamicImageResource {
@Override
protected byte[] getImageData(Attributes attributes) {
return new byte[0];
}
}

Die Klasse DynamicImageResource macht genau das, was wir brauchen und bietet schon eine Methode „getImageData“ die zufälligerweise bereits ein byte-array erwartet. Die Implementierung sieht dann so aus:


public class QRResource extends DynamicImageResource {

private BarCodeGenerator generator;

public QRResource(){
generator = new BarCodeGenerator();
}

@Override
protected byte[] getImageData(Attributes attributes) {
return generator.asRawBytes("woher kriegen wir den Code?");
}

public static ResourceReference asReference(){
    return new ResourceReference("qr"){

        @Override
        public IResource getResource() {
                return new QRResource();
        }
    };
  }
}

Die Frage ist nur, woher wir jetzt den Code bekommen, den wir eigentlich in das Bild kodieren möchten. Jetzt endlich kommt Wicket-6 so richtig in Fahrt, denn seid 1.5 kann man Resourcen mounten. Was bedeutet mounten? Die Resource bekommt eine feste URL. Außerdem kann ich Requests an diese Resource schicken kann – Und Requests beinhalten Parameter.

Die Resource mounten machen analog beispielsweise WebPages  in der Application-Klasse.


/**
* @see org.apache.wicket.Application#init()
*/
@Override
public void init()
{
super.init();
mountResource("/qr/${code}", QRResource.asReference());

// add your configuration here
}

Die mountResource-Methode erwartet nicht direkt eine Resource sondern eine ResourceReference, deren Erzeugung wir gleich mit in die QRResource und die Methode „asReference“ verpackt haben.

Das Schöne ist tatsächlich, dass wir mit dem dynamischen Pfad alles haben, was wir brauchen um unsere Webanwendung um dynamische QR-Codes zu erweitern.

Was noch fehlt ist das Auslesen des Parameters in der Resource, noch fehlt uns der eigentliche Text, den wir in den QRCode kodieren möchten.

Die Methode getImageData bekommt als Parameter eine Klasse vom Type „Attributes“. Attributes bietet folgende Methoden.

Wunderschön, wir sehen, wir bekommen direkt Zugriff auf die allseits beliebten PageParameter.

Bauen wir das Ganze in unsere Anwendung ein. Wir löschen einfach alles, was wir nicht brauchen und enden mit einer leeren Page und einem minimalistischen Markup.

public class HomePage extends WebPage {
private static final long serialVersionUID = 1L;

public HomePage(final PageParameters parameters) {
super(parameters);
}
}


<img alt="" src="/qr/effectivetrainings.de" />

Wir haben nicht mal eine „wicket:id“:). Starten wir das Ganze mit der beigefügten Start-Klasse sieht man folgendes.

effective trainings url

Alle Sourcen finden Sich wie immer in meinem Wicket-6-Playground-Repo.

War dieser Blogeintrag für Sie interessant? Evtl. kann ich noch mehr für Sie tun.

Effective Trainings Wicket Workshop

Wicket 6.2, WebSockets und JQuery-Visualize – Die richtige Atmosphäre schaffen

Im letzten Artikel zum Thema Wicket 6 ging es um JQuery und Ajax. Heute geht es um die Native WebSockets Integration – Rock´n´Roll!

Zunächst erzeugen wir uns wieder einen Wicket Maven Archetype.

Das Projekt erzeugen


<code>mvn archetype:generate -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart -DarchetypeVersion=6.2.0 -DgroupId=de.effectivetrainings -DartifactId=wicket-6-websockets -DarchetypeRepository=<a href="https://repository.apache.org/">https://repository.apache.org/</a> -DinteractiveMode=false</code>

Wieso braucht man überhaupt so etwas wie WebSockets und warum sind sie hier?

WebSockets setzen direkt auf TCP/IP auf und ermöglichen eine bidirektionale Kommunikation zwischen Client und Server – toll… und?

Auf gut deutsch gesagt, wir haben die Möglichkeit, Nachrichten vom Server zum Client zu schicken und zwar ohne, dass der Client (Browser) ständig Pollen (Beim Server anfragen) muss.

Was wäre ein einfacher Use-Case?

Beispielsweise kann man sich vorstellen, wir haben eine Webanwendung, die von unserem Kunden verwendet wird, um Daten zu visualisieren, beispielsweise eingegangene Bestellungen in unserem Online-Shop.

Ohne WebSockets müsste man entweder jedesmal die Seite neu laden, um an die aktuellen Daten zu kommen oder via Ajax (Magie..) ständig beim Server anfragen ob denn nicht zufällig neue Daten vorhanden sind. Beides funktioniert, hat sich etabliert, wurde schon millionenfach implementiert und funktioniert – schick ist eben aber was anderes.

Man kann sich eine WebSocket-Implementierung einfach so vorstellen, dass ein Client eine Anfrage an den Server schickt, diese Anfrage wird aber nicht sofort beantwortet, sondern der Server wartet damit, bis tatsächlich eine Antwort Sinn macht (neue Daten vorhanden sind, die den Client interessieren). Sobald die Sinnhaftigkeit geklärt ist schickt der Server eine Nachricht an alle interessierten Clients und diese können die Visualisierung aktualisieren. Macht Sinn? Definitiv.

Wicket und WebSockets

Wir sind uns denke ich alle einig, dass Wicket das beste Web-Framework ist, das derzeit am Markt verfügbar ist (keine Diskussion!). Wicket 6 hat hier nochmal einen richtigen Schub an schönen Features gebracht – u.a. eben die Native-WebSocket-Integration.

Beispiel

Wir haben uns ja bereits einen wunderschönen Archetype generiert. Wir haben uns auch schon einen brauchbaren Use-Case ausgedacht – Visualisierung von Bestellungen am Client. Implementieren wir das Ganze.

WebSockets aktivieren

Um WebSockets in einer Wicket-Anwendung zu aktivieren verwendet Wicket selbstverständlich ein Behavior..

Bauen wir uns zunächst eine sehr einfaches Formular, mit dem wir Bestellungen simulieren können. Im Archetype gibt es ja bereits die Klasse HomePage. Diese passen wir einfach folgendermaßen an (da es sich hierbei um Wicket Standard Bordmittel handelt, gehe ich darauf nicht genauer ein, es soll ja schließlich um WebSockets gehen).

Wir brauchen ausserdem einige einfache Domainklassen –  Food und Order (wir bauen einen Lieferservice für Essen – nein nicht Pizza – dafür gibts schon genug Beispiele).


public enum Food {
PIZZA, LEBERKAS, BURGER, SALAT, SPIEGELEI
}

public class Order implements Serializable {

//some meaningless random order id
private String orderId = String.valueOf(Math.random());
private String name;
private String street;
private String zip;
private String city;
private Food food;

public Order(){}

public String getOrderId() {
return orderId;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getStreet() {
return street;
}

public void setStreet(String street) {
this.street = street;
}

public String getZip() {
return zip;
}

public void setZip(String zip) {
this.zip = zip;
}

public String getCity() {
return city;
}

public void setCity(String city) {
this.city = city;
}

public Food getFood() {
return food;
}

public void setFood(Food food) {
this.food = food;
}

@Override
public String toString() {
return "Order{" +
"name='" + name + '\'' +
", street='" + street + '\'' +
", zip='" + zip + '\'' +
", city='" + city + '\'' +
", food=" + food +
'}';
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

Order order = (Order) o;

if (city != null ? !city.equals(order.city) : order.city != null) return false;
if (food != order.food) return false;
if (name != null ? !name.equals(order.name) : order.name != null) return false;
if (orderId != null ? !orderId.equals(order.orderId) : order.orderId != null) return false;
if (street != null ? !street.equals(order.street) : order.street != null) return false;
if (zip != null ? !zip.equals(order.zip) : order.zip != null) return false;

return true;
}

@Override
public int hashCode() {
int result = orderId != null ? orderId.hashCode() : 0;
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + (street != null ? street.hashCode() : 0);
result = 31 * result + (zip != null ? zip.hashCode() : 0);
result = 31 * result + (city != null ? city.hashCode() : 0);
result = 31 * result + (food != null ? food.hashCode() : 0);
return result;
}
}

Über die Sinnhaftigkeit dieser Domainklassen würde sich vortrefflich streiten lassen, für das Beispiel ist das aber ausreichend.

Hier jetzt die einfache Wicket Implementierung des Formulars.


public class HomePage extends WebPage {
private static final long serialVersionUID = 1L;

public HomePage(final PageParameters parameters) {
super(parameters);
Form orderForm = new Form("form", new             CompoundPropertyModel(new Order())){
@Override
protected void onSubmit() {
super.onSubmit();
System.out.println(getModelObject());
}
};
orderForm.add(new TextField("name"));
orderForm.add(new TextField("street"));
orderForm.add(new TextField("zip"));
orderForm.add(new TextField("city"));
orderForm.add(new DropDownChoice("food", Arrays.asList(Food.values())));
add(orderForm);
}
}

und das ensprechende Markup dazu.

<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
<head>
<meta charset="utf-8"/>
<title>Apache Wicket Quickstart</title>
<link href='http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:regular,bold' rel='stylesheet'
type='text/css'/>
<link rel="stylesheet" href="style.css" type="text/css" media="screen" title="Stylesheet"/>
<style>
fieldset {
width: 350px;
float: left;
text-align: right;
}
</style>
</head>
<body>
<form style="margin: 25px" wicket:id="form">
<h2>Wo gibts das beste Mittagessen?</h2>
<fieldset>
<label>Name:</label> <input type="text" wicket:id="name"/><br/>
<label>Strasse:</label> <input type="text" wicket:id="street"/><br/>
<label>PLZ: </label><input type="text" wicket:id="zip"/><br/>
<label>Stadt:</label> <input type="text" wicket:id="city"/><br/>
<label>Was möchtest Du essen?</label> <select wicket:id="food"/><br/>
<input type="submit"/>
</fieldset>
</form>
</body>
</html>

Das Ganze ergibt ausgeführt das, schick nich?

Ziel ist es, unsere eingegangenen Bestellungen zu visualisieren. Das halten wir möglichst einfach (schön wäre es jetzt, wenn wir eine lokale DB hochziehen würden, hier schön die Bestellungen via Hibernate persistieren etc..). Das sparen wir uns, wir nehmen die einfachste Datenbank, die man sich vorstellen kann – ein gutes altes Singleton mit einer gekapselten HashMap (ist es hier gerade jemandem kalt den Rücken hinunter gelaufen?).


public class DB {

private static DB instance = new DB();

private Map<String, List<Order>> orders;

private DB() {
 this.orders = Collections.synchronizedMap(new HashMap<String, List<Order>>());
 }

public void store(Order order) {
 List<Order> orders = this.orders.get(order.getFood().name());
 if(orders == null){
 this.orders.put(order.getFood().name(),new ArrayList<Order>());
 }
 this.orders.get(order.getFood().name()).add(order);

}

/*
 * do not copy that....
 * */
 public Map<String, Object> countOrdersByFood() {
 Map<String, Object> foodCount = new HashMap<String, Object>();
 for(Map.Entry<String, List<Order>> order : this.orders.entrySet()){
 foodCount.put(order.getKey(),order.getValue().size());
 }
 return foodCount;
 }

public static final DB get() {
 return instance;
 }
 }

Bitte nicht kopieren, hässlicher gehts kaum… Das interessante an unserer „DB“ ist die Methode „countOrdersByFood“ die eine Map zurückgibt, die die Anzahl von Bestellungen für bestimmte Gerichte beinhaltet. Genau diese Methode verwenden wir später für unsere Visualisierung. Dass der Rückgabewert eine Map vom Typ <String,Object> ist, hat mit der Convenience-Klasse JsonUtils zu tun, die standardmässig mit Wicket ausgeliefert wird.

Das Einzige was wir jetzt noch machen müssen ist in der onSubmit des Formulars folgender Call.


protected void onSubmit() {
 super.onSubmit();
 DB.get().store(getModelObject());
 }

Damit ist unsere Infrastruktur aufgesetzt. Jetzt gehts an die Visualisierung. Hierfür verwenden wir das hervorragende JQuery-Visualize-Plugin (bietet sich ja an, da JQuery nativ schon über Wicket verfügbar ist)

Integration des Visualize Plugins

Wir brauchen das entsprechende Plugin und natürlich die native JQuery Bibliothek.
Hierfür bauen wir uns ein kleines HeaderItem, das beides rendert und dafür sorgt, dass die Bibliotheken zur Laufzeit verfügbar sind.


public class JQueryVisualizePlugin extends JavaScriptUrlReferenceHeaderItem {

private static final String VISUALIZE_PLUGIN_URL = "https://raw.github.com/filamentgroup/jQuery-Visualize/master/js/visualize.jQuery.js";

public JQueryVisualizePlugin() {
 super(VISUALIZE_PLUGIN_URL, "jquery-visualize", true, "utf-8","");
 }

@Override
 public Iterable<?> getRenderTokens() {
 return Arrays.asList("jquery-visualize");
 }

@Override
 public Iterable<? extends HeaderItem> getDependencies() {
 List<HeaderItem> deps = new ArrayList<HeaderItem>();
 deps.add(JavaScriptHeaderItem.forReference(WicketEventJQueryResourceReference.get()));
 return deps;
 }
 }

Eine Erklärung für diese Implementierung findet sich im letzten Artikel.

Jetzt brauchen wir nur noch eine Page, um unseren OrderReport zu rendern.


public class OrderReportPage extends WebPage {

@Override
 public void renderHead(HtmlHeaderContainer container) {
 super.renderHead(container);
 container.getHeaderResponse().render(new JQueryVisualizePlugin());
 }
 }

und das entsprechende Markup dazu.


<html xmlns:wicket="http://wicket.apache.org">
 <head>
 <meta charset="utf-8"/>
 <title>OrderReport</title>
 </head>
 <body>
 <table>
 <caption>Essensbestellungen nach Art</caption>
 <thead>
 <tr>
 <td></td>
 <th scope="col">Pizza</th>
 <th scope="col">Leberkas</th>
 <th scope="col">Burger</th>
 <th scope="col">Salat</th>
 <th scope="col">Spiegelei</th>
 </tr>
 </thead>
 <tbody>
 <tr>
 <td>1</td>
 <td>2</td>
 <td>3</td>
 <td>4</td>
 <td>5</td>
 <td>6</td>
 </tr>

</tbody>
 </table>
 </body>
 </html>

Das JQuery-Visualize Plugin erwartet eine ganz  bestimmte Table-Struktur, um damit zu arbeiten. Wir möchten die Anzahl Bestellungen visualisieren, also definieren wir pro Menüart eine Spalte die initial mit 1-6 befüllt sind.

Beispiele für das JQuery Visualize Plugin finden sich hier.

Fangen wir an. Um die Visualisierung zu testen brauchen wir folgendes JavaScript, das wir initial einfach in der Page rendern.


$('table').visualize();

Das machen wir einfach genauso direkt in der Page.


@Override
 public void renderHead(HtmlHeaderContainer container) {
 super.renderHead(container);
 container.getHeaderResponse().render(new JQueryVisualizePlugin());
 container.getHeaderResponse().render(new OnDomReadyHeaderItem("$('table').visualize()"));
 }

Das Skript soll erst dann ausgeführt werden, wenn der DOM komplett aufgebaut ist, deswegen verwenden wir ein OnDomReadyHeaderItem. Was hier gerendert wird ist das.


<script language="javascript">
<pre id="line1">/*<![CDATA[*/
Wicket.Event.add(window, "domready", function(event) {
$('table').visualize();
;});</pre>
</script>

Schauen wir uns das Ganze im Browser an, sehen wir folgendes:

Na wenn das mal nicht einfach war.

Aktuell sind unsere Werte hardkodiert, das ist natürlich eher uninteressant. Was wir möchten ist, jedesmal, wenn auf dem Server eine Bestellung eingeht, soll ein Client notifiziert werden, so dass der Chart aktualisiert werden kann – LiveCharting also!

Client Notifications mit WebSockets

Endlich wird es Zeit, WebSockets zu aktivieren. Das geht ganz einfach. Der Ort, auf dem wir an Server-Events interessiert sind ist die OrderReportPage.

Um WebSockets jetzt endlich verwenden zu können, müssen wir dafür sorgen, dass die Bibliothek aus den Wicket-Extensions mitgeladen wird (kommt natürlich nicht mit dem Core mit). Hierfür deklarieren wir folgende Abhängigkeit in der Projekt-Pom.


<dependency>
 <groupId>org.apache.wicket</groupId>
 <artifactId>wicket-native-websocket-jetty</artifactId>
 <version>0.3</version>
 </dependency>

Es gibt verschiedene Implementierungen für WebSockets (verschiedene Jetty-Versionen und Tomcat). Wir verwenden hier die Jetty-Version in der Version 0.3 (derzeit die aktuellste). Das allein reicht aber leider noch nicht.

Zusätzlich dürfen wir nicht den Standard-Wicket-Filter verwenden, sondern eine spezielle Implementierung für WebSockets. Also auf in die web.xml und folgende Filter-Deklaration verwenden.


<filter>
 <filter-name>wicket.wicket-6-websockets</filter-name>
 <filter-class>org.apache.wicket.protocol.http.Jetty7WebSocketFilter</filter-class>
 <init-param>
 <param-name>applicationClassName</param-name>
 <param-value>de.effectivetrainings.WicketApplication</param-value>
 </init-param>
 </filter>

Das Ganze funktioniert natürlich nur, wenn wir auch wirklich mit dem Jetty7 arbeiten. Startet man jetzt, bekommt man die vielsagende Fehlermeldung:

java.lang.IllegalStateException: Websockets not supported on blocking connectors

Warum nur wird es Einem hier so schwer gemacht?

Was fehlt ist eine Anpassung in der Start-Klasse.
Das Problem ist diese Zeile in der Jetty-Konfiguration.


SocketConnector connector = new SocketConnector();

Für WebSockets machen aber Blocked-Sockets keinen Sinn, also nehmen wir einfach einen Nicht-Blockierenden?


SelectChannelConnector connector = new SelectChannelConnector();

Um WebSockets auf der OrderReportPage zu aktivieren verwenden wir das WebSocketBehavior mit den zugehörigen Methoden onConnect, onClose, onMessage für Textnachrichten und onMessage für Binaries.


add(new WebSocketBehavior(){
 @Override
 protected void onConnect(ConnectedMessage message) {
 super.onConnect(message);
 }

@Override
 protected void onClose(ClosedMessage message) {
 super.onClose(message);
 }

@Override
 protected void onMessage(WebSocketRequestHandler handler, TextMessage message) {
 super.onMessage(handler, message);
 handler.push("Hallo Client!!");
 }

@Override
 protected void onMessage(WebSocketRequestHandler handler, BinaryMessage binaryMessage) {
 super.onMessage(handler, binaryMessage);
 }
 });

Sobald sich ein Client per WebSocket verbindet liefert er unserer Applikation bestimmte Daten, die wir benötigen, um eine Nachricht an den Client zu schicken, hierfür ist die onConnect-Methode im Behavior zuständig.

Was der Client uns liefert ist

  • applicationName
  • sessionId
  • pageId

Diese Informationen bekommen wir über die ConnectedMessage, die als Parameter übergeben wird.

Warum brauchen wir genau diese Parameter? Die Applikation identifiziert die Applikation, weiter nicht interessant. Die SessionId identifiziert den Client eindeutig, das brauchen wir, da ja beliebig viele Clients verbunden sein können. Die PageId ist interessant, hierzu muss man wissen, was intern passiert.

Kommt ein WebSocketRequest vom Client beim Server an, wird (analag eines AjaxRequests) die Seite aus dem DiskPageStore anhand der übergebenen PageId geladen und einmal komplett durch den Komponentenbaum dieser Seite iteriert (mittels dem Standard Event Mechanismus) und ein
WebSocketConnectedPayload, WebSocketClosedPayload oder ein WebSocketPayload übergeben. Theoretisch kann also jede Komponente einzeln auf WebSocketEvents reagieren, indem die onEvent(..) Methode überschrieben wid und auf das ComponentEvent reagiert wird.
Um jetzt aber tatsächlich effektiv mit WebSockets arbeiten zu können müssen wir ein bisschen tricksen.

Eine Connection wird eindeutig über Applicationname, SessionId und PageId identifizeirt. Wir brauchen eine Art Registry um die Connections zu verwalten.

Hier ein sehr einfacher und simpler Ansatz, der für unsere Zwecke aber völlig ausreichend ist.
Wir nehmen einfach an, die Session-ID identifiziert eindeutig unsere Connections, das funktioniert wunderbar, so lange wir nur eine Seite in der Anwendung haben, die Server-Nachrichten empfängt, bei mehreren Seiten müssen wir uns was besseres ausdenken.


public class ConnectionRegistry {

private Map<String, List<ClientConnection>> connections;

public ConnectionRegistry(){
 this.connections = Collections.synchronizedMap(new HashMap<String, List<ClientConnection>>());
 }

public void clientConnects(String applicationName, String sessionId, Integer pageId){
 List<ClientConnection> clientConnections = this.connections.get(sessionId);
 if(clientConnections == null){
 connections.put(sessionId, new ArrayList<ClientConnection>());
 }
 this.connections.get(sessionId).add(new ClientConnection(applicationName,sessionId,pageId));
 }

public void clientDisconnects(String applicationName, String sessionId, Integer pageId){
 List<ClientConnection> connections = this.connections.get(sessionId);
 if(connections != null){
 connections.remove(new ClientConnection(applicationName,sessionId,pageId));
 }
 }

public List<ClientConnection> getConnectionsBySessionId(String sessionId){
 List<ClientConnection> connections =  this.connections.get(sessionId);
 return connections != null ? connections : new ArrayList<ClientConnection>();
 }

}

public class ClientConnection implements Serializable {

private String applicationName;
 private String sessionId;
 private Integer pageId;

public ClientConnection(String applicationName, String sessionId, Integer pageId) {
 this.applicationName = applicationName;
 this.sessionId = sessionId;
 this.pageId = pageId;
 }

public String getApplicationName() {
 return applicationName;
 }

public String getSessionId() {
 return sessionId;
 }

public Integer getPageId() {
 return pageId;
 }

@Override
 public boolean equals(Object o) {
 if (this == o) return true;
 if (o == null || getClass() != o.getClass()) return false;

ClientConnection that = (ClientConnection) o;

if (applicationName != null ? !applicationName.equals(that.applicationName) : that.applicationName != null)
 return false;
 if (pageId != null ? !pageId.equals(that.pageId) : that.pageId != null) return false;
 if (sessionId != null ? !sessionId.equals(that.sessionId) : that.sessionId != null) return false;

return true;
 }

@Override
 public int hashCode() {
 int result = applicationName != null ? applicationName.hashCode() : 0;
 result = 31 * result + (sessionId != null ? sessionId.hashCode() : 0);
 result = 31 * result + (pageId != null ? pageId.hashCode() : 0);
 return result;
 }
 }

Die Registry stellen wir über die Applikation bereit.


public class WicketApplication extends WebApplication
 {
 private ConnectionRegistry registry;

/**
 * @see org.apache.wicket.Application#getHomePage()
 */
 @Override
 public Class<? extends WebPage> getHomePage()
 {
 return HomePage.class;
 }

/**
 * @see org.apache.wicket.Application#init()
 */
 @Override
 public void init()
 {
 super.init();
 registry = new ConnectionRegistry();
 mountPage("/report", OrderReportPage.class);
 }

public ConnectionRegistry getRegistry(){
 return registry;
 }

public static WicketApplication get(){
 return (WicketApplication) Application.get();
 }
 }

Sobald ein Client eine WebSocket-Verbindung öffnet oder schliesst, reagieren wir im WebSocketBehavior.


@Override
 protected void onConnect(ConnectedMessage message) {
 super.onConnect(message);
 WicketApplication.get().getRegistry().
 clientConnects(message.getApplication().getName(),
 message.getSessionId(), message.getPageId());
 }

@Override
 protected void onClose(ClosedMessage message) {
 super.onClose(message);
 WicketApplication.get().getRegistry().
 clientDisconnects(message.getApplication().getName(),
 message.getSessionId(), message.getPageId());
 }

Das Schöne ist, wir bekommen jetzt überall Zugriff auf die offenen Connections per Client.


WicketApplication.get().getRegistry().getConnectionsBySessionId(Session.get().getId())

Jetzt wird es Zeit, die WebSockets einzusetzen, hierzu spendieren wir unserem Formular noch einen AjaxSubmitButton und folgenden Code in der onSubmit() – Methode.


orderForm.add(new AjaxSubmitLink("submit") {
 @Override
 protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
 SimpleWebSocketConnectionRegistry registry = new SimpleWebSocketConnectionRegistry() ;
 for(ClientConnection clientConnection :
 WicketApplication.get().getRegistry().getConnectionsBySessionId(Session.get().getId())) {
 IWebSocketConnection connection = registry.getConnection(Application.get(), clientConnection.getSessionId(), clientConnection.getPageId());
 if (connection != null) {
 WebSocketRequestHandler webSocketHandler = new WebSocketRequestHandler(this, connection);
 webSocketHandler.push("My WebSocket message");
 }
 }

}
 });

Über die SimpleWebSocketConnectionRegistry bekommt man Zugriff auf die derzeit offenen Connections. Wir erzeugen einen WebSocketRequestHandler, und dieser bietet funktioniert ziemlich analog dem AjaxRequestTarget. Ich habe also Methode wie appendJavaScript, prependJavaScript, add etc.

Über die Methode push(…) können wir eine einfache TextMessage an den Client schicken. Dieser muss nur noch auf Messages reagieren.

ClientSide Code

Folgnder Code verwendet die definieren Wicket-Callbacks für den Client-Code.


$(document).ready(function() {

Wicket.Event.subscribe("/websocket/open", function(jqEvent) {
 alert("connection opened");
 });

Wicket.Event.subscribe("/websocket/message", function(jqEvent, message) {
 alert("message received " + message);
 });

});

Mit Wicket.Event.subscribe könnten wir uns für die WebSocket-Messages registrieren. Messages werden standardmässig über den Kanal „“/websocket/message“ verschickt. Diesen Client rendern wir mit der OrderReportPage, die ja standardmässig auf die Nachrichten von der Bestellseite reagieren soll.

Hierfür definieren wir uns eine neue Resource.


public class WebSocketClientResourceReference extends PackageResourceReference {
 public WebSocketClientResourceReference() {
 super(HomePage.class, "orderreport-client.js");
 }

@Override
 public Iterable<? extends HeaderItem> getDependencies() {
 List<HeaderItem> headerItems = new ArrayList<HeaderItem>();
 headerItems.add(JavaScriptHeaderItem.forReference(WicketWebSocketJQueryResourceReference.get()));
 return headerItems;
 }
 }

Diese Referenz rendern wir einfach mit der OrderReportPage.


@Override
 public void renderHead(HtmlHeaderContainer container) {
 super.renderHead(container);
 container.getHeaderResponse().render(new JQueryVisualizePlugin());
 container.getHeaderResponse().render(new OnDomReadyHeaderItem("$('table').visualize();"));
 container.getHeaderResponse().render(JavaScriptHeaderItem.forReference(new WebSocketClientResourceReference()));
 }

Senden wir die Form jetzt testweise ab, öffnet sich automatisch die OrderReportPage im Browser und wir sehen folgendes:

Schon mal gar nicht schlecht. Damit sind wir fast am Ziel. Zuletzt müssen wir die OrderReportPage noch ein wenig tunen. Bisher ist unsere Tabelle mit der Bestellübersicht nur hardkodiert.

Wir haben jetzt zwei Möglichkeit, entweder wir machen aus der Tabelle eine WicketKomponente und fügen diese nach jedem Request mit der add(..)-Methode hinzu, damit diese neu gezeichnet wird, oder wir bauen das ganze manuell mit JQuery.

Problem beim ersten Ansatz ist, dass wir über den WebSocketRequestHandler zwar die PageInstanz bekommen, aber nicht direkt Zugriff auf die Wicket-Komponenten auf der Seite. Man müsste also über Getter- oder ähnliches die interne Struktur der Seite nach aussen exposen. Nicht schön, bauen wir das Ganze also einfacher mit JQuery.

In der onSubmit-Methode schicken wir keine einfache Textnachricht, sondern wir verschicken einfach JSON.


WebSocketRequestHandler webSocketHandler = new WebSocketRequestHandler(this, connection);
 try {
 webSocketHandler.push(JsonUtils.asArray(DB.get().countOrdersByFood()).toString());
 } catch (JSONException e) {
 e.printStackTrace();
 }

Hierfür definieren wir folgende Html Struktur in der OrderReport Page.


<table style="visibility:hidden">
 <caption>Essensbestellungen nach Art</caption>
 <thead>
 <tr>
 <th scope="col">Pizza</th>
 <th scope="col">Leberkas</th>
 <th scope="col">Burger</th>
 <th scope="col">Salat</th>
 <th scope="col">Spiegelei</th>
 </tr>
 </thead>
 <tbody>
 <tr>
 <td id="PIZZA">0</td>
 <td id="LEBERKAS">0</td>
 <td id="BURGER">0</td>
 <td id="SALAT">0</td>
 <td id="SPIEGELEI">0</td>
 </tr>

</tbody>
 </table>

und folgendes Skript im JavaScript-Client.


Wicket.Event.subscribe("/websocket/message", function(jqEvent, message) {
 var json = JSON.parse(message);
 for(i in json){
 $('#'+json[i].name).html(json[i].value);
 }
 $('.visualize').trigger('visualizeRefresh');
 })

Und ab jetzt triggert jede Bestellung ein Refresh der Ansicht auf allen aktuell geöffneten OrderReport Pages.

Fazit

Insgesamt muss ich sagen, es ist schön, dass WebSockets mittlerweile funktionieren, aber die Integration scheint mir noch ein wenig holprig. Gerade wenn es um die Integration mit verschiedenen Seiten geht (meiner Ansicht nach DER Use-Case ist das ganze ein wenig kompliziert).

Oder habe ich etwas übersehen? Geht es evtl einfacher als in diesem Artikel vorgestellt? Über Hinweise wäre ich dankbar, ansonsten wünsche ich viel Spaß mit Wicket und WebSockets.

Der Source-Code für dieses Beispiel befindet sich hier im Unterordner /wicket-6-websockets.

War dieser Blogeintrag für Sie interessant? Evtl. kann ich noch mehr für Sie tun.

Trainings & Know-How aus der Praxis zu

  • Apache Wicket 1.4.x, 1.5.x, 1.6.x
  • GIT – Best Practices, Einsatz, Methoden
  • Spring
  • Java
  • Scrum & Kanban
  • Agiles Arbeiten
Consulting & Softwareentwicklung

  • Wicket Professional
  • Requirements Engineering
  • Qualitätssicherung
  • Software-Entwicklung
  • Architektur
  • Scrum & Kanban

Links

Die Ankündigung von WebSockets

WIKI

Demo Applikation von Martin Grigorov mit Scala und Actors

Wicket 6.2 und JQuery – Dream Team in Action

In meinen letzten beiden Artikeln zum Thema Wicket 6 habe ich mich mit Wicket 6 und JQuery und Formularvalidierung beschäftigt. Heute möchte ich das Thema JQuery ein wenig auskosten. Wie wahrscheinlich jeder mittlerweile weiß, ist die Javascript-Backing-Library von Wicket mittlerweile JQuery. Betrachten wir doch mal, was damit jetzt alles möglich ist.

Zunächst bauen wir uns einen schönen Maven-Archetype.


mvn archetype:generate -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart -DarchetypeVersion=6.2.0 -DgroupId=de.effectivetrainings -DartifactId=wicket-6-jquery -DarchetypeRepository=https://repository.apache.org/ -DinteractiveMode=false

Anschließend räumen wir die ganzen Klassen und Templates ein wenig auf, damit wir mit einem sauberen Stand starten können.


public class HomePage extends WebPage {
}

<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
<head>
<meta charset="utf-8" />
<title>Apache Wicket Quickstart</title>
</head>
<body>

</body>
</html>

Damit wir auch was schönes zu sehen bekommen laden wir zusätzlich  jquery-ui in der Version 1.9.1. Hierfür implementieren wir folgendes in der Klasse HomePage. Wir ziehen uns die Bibliothek einfach direkt von der JQuery UI Homepage. (Das wäre aber nichts, was man in einem produktiven System machen würde..)


@Override
public void renderHead(HtmlHeaderContainer container) {
super.renderHead(container);
IHeaderResponse response = container.getHeaderResponse();
response.render(JavaScriptHeaderItem.forUrl("http://code.jquery.com/ui/1.9.1/jquery-ui.js","jquery-ui"));
}

Starten wir das Ganze sieht man zwar noch nichts (bisher haben wir ja keinen Content angelegt), im Sourcecode der Seite sieht man aber folgendes.

<script type="text/javascript" id="jquery-ui" src="http://code.jquery.com/ui/1.9.1/jquery-ui.js"></script>

Hm, eine Besonderheit haben wir aber. JQuery-UI benötigt zwingend JQuery, das bedeutet, das aktuelle Setup ist invalide.
Wie aber deklariert man eine derartige Abhängigkeit?

Mit Wicket 1.5.x wäre das etwas kompliziert, mit Wicket 6 total einfach. Ziehen wir die JQuery-UI-Resource in eine eigene Klasse.


public class JQueryUIResourceReference extends JavaScriptUrlReferenceHeaderItem {

private static final String URL= "http://code.jquery.com/ui/1.9.1/jquery-ui.js";

public JQueryUIResourceReference() {
super(URL, "jquery-ui-1.9.1", true, "utf-8", "");
}

/**
* jedesmal wenn die jquery-ui resource reference gerendert wird muss auch
* die jquery-bibliothek gerendert werden.
* @return
*/
@Override
public Iterable<? extends HeaderItem> getDependencies() {
List<HeaderItem> deps = new ArrayList<HeaderItem>();
deps.add(JavaScriptHeaderItem.forReference(JQueryResourceReference.get()));
return deps;
}
}

Schön ist die Methode „getDepdendencies“
Diese Methode liefert eine Liste von HeaderItems, die von der aktuellen Resource verwendet wird. Wicket stellt sicher, dass jede Abhängigkeit in der richtigen Reihenfolge gerendert wird.

Folgende Deklaration in der Page stellt sicher, dass beide Resourcen gerendert werden.


@Override
public void renderHead(HtmlHeaderContainer container) {
super.renderHead(container);
IHeaderResponse response = container.getHeaderResponse();
response.render(new JQueryUIResourceReference());
}

Bauen wir die üblichen Tabs

Folgendes Markup ist die Basis-Struktur für die JQuery-UI-Tabs.


<div id="tabs">
<ul>
<li><a href="#fragment-1"><span>Tab 1</span></a></li>
<li><a href="#fragment-2"><span>Tab 2</span></a></li>
<li><a href="#fragment-3"><span>Tab 3</span></a></li>
</ul>
<div id="fragment-1">
erster
</div>
<div id="fragment-2">
zweiter
</div>
<div id="fragment-3">
dritter
</div>
</div>

Bauen wir uns ein einfaches eigenes Panel.

public class TabbedPanel extends Panel {
public TabbedPanel(String id) {
super(id);
}

@Override
public void renderHead(HtmlHeaderContainer container) {
super.renderHead(container);
container.getHeaderResponse().render(new JQueryUIResourceReference());
container.getHeaderResponse().render(
JavaScriptHeaderItem.forReference(
new PackageResourceReference(TabbedPanel.class, "tabbed-script.js")));
}
}

Das entsprechende Markup sieht so aus

<html xmlns:wicket="http://wicket.apache.org">
<wicket:panel>
<div id="tabs">
<ul>
<li><a href="#fragment-1"><span>Tab 1</span></a></li>
<li><a href="#fragment-2"><span>Tab 2</span></a></li>
<li><a href="#fragment-3"><span>Tab 3</span></a></li>
</ul>
<div id="fragment-1">
erster
</div>
<div id="fragment-2">
zweiter
</div>
<div id="fragment-3">
dritter
</div>
</div>
</wicket:panel>
</html>

Das einzig besondere ist die renderHead()-Methode im TabbedPanel. Denn die Reihenfolge, in der Resourcen gerendert werden ist Depth-First.
Alle Komponenten rendern ihr Resourcen und erst dann kommt die Page. Da die JQuery- und JQuery-UI Referenz bisher in der Page deklariert ist,
rendert die TabbedPanel Komponente ihr Javascript und erst dann werden die JQuery-Referenzen gerendert. Autsch… das geht so nicht, ein einfacher
Workaround hierzu ist, die JQuery-Referenz einfach nochmals zu deklarieren. Wicket ist so schlau und rendert die Referenz nur einmal, und zwar an der richtigen Stelle.

Bingo!
War dieser Blogeintrag für Sie interessant? Braucht Ihr Team Unterstützung? Kontaktieren Sie mich, ich bin hier um zu helfen.

Trainings & Know-How aus der Praxis zu

  • Apache Wicket 1.4.x, 1.5.x, 1.6.x
  • GIT – Best Practices, Einsatz, Methoden
  • Spring
  • Java
  • Scrum & Kanban
  • Agiles Arbeiten
Consulting & Softwareentwicklung

  • Requirements Engineering
  • Qualitätssicherung
  • Software-Entwicklung
  • Architektur
  • Scrum & Kanban

Apache Wicket – Readable URLs mit Mount-Pathes

Heute bin ich in meinem aktuellen Projekt vor einem interessanten Problem gestanden. Nehmen wir an, wir haben ein Webprojekt, basierend auf Apache Wicket.

Ein Archetype lässt sich für die Version 6 so generieren:

mvn archetype:generate -DgroupId=de.effectivetrainings -DartifactId=wicket-6-mountpathes -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart -Dversion=1.0-SNAPSHOT

Definieren wir uns eine neue WebPage.

/**
* @author Martin Dilger
*/
public class MyPage extends WebPage {
public MyPage(PageParameters parameters){
super(parameters);
add(new Label(parameters.get("category").toString()));
}
}

Dazu das passende Html


MyPage rendert :</pre>
<div wicket:id="category"></div>
<pre>

Direktsprung auf die Seite

Nehmen wir an, wir möchten per Direkteinsprung auf die MyPage springen. Ohne weiteres würde das über diesen Link
funktionieren.

http://localhost:8080/wicket/bookmarkable/de.effectivetrainings.MyPage

Nicht besonders schick, oder?

Package Mounts

        @Override
	public void init()
	{
		super.init();
                mountPackage("pages",MyPage.class);
	}

Damit sind alle Pages im Package der Klasse MyPage über folgenden Link aufrufbar

http://localhost:8080/pages/MyPage
http://localhost:8080/pages/HomePage

Der Mount-Pfad ist hierbei einfach der Klassenname

Parameter

Eine Seite lässt sich auf mit Parametern rendern (man liest oft Restful.. hat aber nicht viel miteinander zu tun).

/**
	 * @see org.apache.wicket.Application#init()
	 */
	@Override
	public void init()
	{
		super.init();
        mountPackage("pages",MyPage.class);
        mountPage("/pages/parameter/${category}", MyPage.class);
	}

Wir machen die Page MyPage zugreifbar unter /pages/parameter/sport zum Beispiel, der Parameter sport wird hiermit als GET-Parameter über die übergebenen PageParameter zur Verfügung gestellt. Der Zugriff funktioniert einfach so:

parameters.get("category").toString()

Man kann auch mehrere Parameter übergeben.

@Override
	public void init()
	{
		super.init();
        mountPackage("pages",MyPage.class);
        mountPage("/pages/parameter/${category}/${weekday}", MyPage.class);
	}

Ein Aufruf könnte so aussehen:

http://localhost:8080/pages/parameter/sport/montag
http://localhost:8080/pages/parameter/sport/montag/

Ein Beispiel für dieses Mounting wäre eine Online-TV Zeitschrift, die alle Sportsendungen am Montag anzeigen soll.
Um alle Sportsendungen anzuzeigen gilt weiterhin dieser Link, der auch funktioniert.

http://localhost:8080/pages/parameter/sport

Was aber, wenn wir alle Sendungen am Montag anzeigen möchten?
Ein Aufruf wäre

http://localhost:8080/pages/parameter/montag

Optionale Parameter

Wie soll Wicket unterscheiden, ob der Parameter montag jetzt die Kategorie oder der Wochentag ist?

Man könnte die Seite so aufrufen

http://localhost:8080/pages/parameter//montag/

Nein.. keine besonders gute Idee.

Optionale Parameter als Rettung!

mountPage("/pages/parameter/#{category}/${weekday}", MyPage.class);

Einfach ein # statt des $ verwenden, und schon sind diese Parameter optional und werden auch so behandelt.


War dieser Blogeintrag für Sie interessant? Evtl. kann ich noch mehr für Sie tun.

Trainings & Know-How aus der Praxis zu

  • Apache Wicket 1.4.x, 1.5.x, 1.6.x
  • GIT – Best Practices, Einsatz, Methoden
  • Spring
  • Java
  • Scrum & Kanban
  • Agiles Arbeiten
Consulting & Softwareentwicklung

  • Requirements Engineering
  • Qualitätssicherung
  • Software-Entwicklung
  • Architektur
  • Scrum & Kanban

Wicket 6.0 – Teil 2 – Validierung von Forms

Und weiter gehts, im letzten Teil haben wir uns grundsätzlich über die neue Backing-Library Funktionalität von Wicket 6 unterhalten und die überfällige JQuery-Integration.

Heute schauen wir mal, ob und wenn ja welche Neuigkeiten Wicket 6 für die Validierung mitbringt.

Hierfür bauen wir eine sehr einfache Form, über die man ein Kunde seine E-Mailadresse ändern kann.




<meta charset="utf-8" />
Apache Wicket Quickstart
	<link title="Stylesheet" href="style.css" rel="stylesheet" media="screen" type="text/css" /></pre>
<form>
<input type="text" />
<input type="submit" /></form>
<pre>


Der Java-Code sieht so aus:


public class HomePage extends WebPage {
private static final long serialVersionUID = 1L;

public HomePage(final PageParameters parameters) {
Form form = new Form("form", new CompoundPropertyModel(new Email()));
form.add(new TextField("mail"));
form.add(new AjaxSubmitLink("submit") {
@Override
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
System.out.println(form.getDefaultModelObject());
target.add(HomePage.this);
}

@Override
protected void onError(AjaxRequestTarget target, Form<?> form) {
target.add(HomePage.this);
}
});
add(form);
}
}

Zusätzlich bauen wir uns noch eine einfache Email-Klasse:


/**
* @author martin.dilger
*/
public class Email implements Serializable {

private String mail;

public Email(){}

public Email(String email){
this.mail = email;
}

public String getMail(){
return mail;
}

@Override
public String toString() {
return mail;
}
}

Die Form wird über einen einfache Ajax-Submitlink abgeschickt. Bisher nichts neues, der Code sieht mit Wicket1.5.x  genauso aus.

Schickt man die Form ab, passiert erst mal nichts. Was uns noch fehlt ist die ganze Logik zum Validieren der E-Mailadresse.

Hierfür extrahieren wir die Erzeugung des Email-Textfeldes in eine eigene Factory-Methode:


private TextField emailField() {
TextField mail = new TextField("mail");

Das Model der Form ist bereits ein CompoundPropertyModel vom Typ Email, es reicht also, wenn das Textfeld mit der ID „mail“ instantiiert wird.

Da wir für die Validierung eine Instanz unserer Email benötigen, braucht das Textfeld zunächst mal einen Konverter (ansonsten könnten wir nur auf einem String arbeiten).


TextField mail = new TextField("mail"){
@Override
public  IConverter getConverter(Class type) {
return (IConverter)new AbstractConverter(){
@Override
protected Class getTargetType() {
return Email.class;
}

@Override
public Email convertToObject(String s, Locale locale) {
return new Email(s);
}

@Override
public String convertToString(Email value, Locale locale) {
return value.getMail();
}
};
}
};

Auch das unterscheidet sich in keiner Weise von Wicket 1.5.x.

Was wir jetzt aber machen können ist, einen Validator an unsere Komponente zu hängen (ja ich weiß, es gibt einen fertigen EmailAddressValidator..)


mail.add(new IValidator() {
@Override
public void validate(IValidatable validatable) {
Email email = validatable.getValue();
if (!email.isValid()) {
ValidationError validationError = new ValidationError().addKey("email.error");
validationError.setVariable("email", email.getMail());
validatable.error(validationError);
}
}
});

Die Klasse AbstractValidator ist mittlerweile deprecated, wir arbeiten also direkt auf IValidator.

Zunächst erzeugen wir eine neue ValidationError-Instanz. ValidationError selbst ist ziemlich mächtig, beispielsweise lädt dieser selbstständig Fehlermeldungen über den registrieren Fehler-Key.


new ValidationError().addKey("email.error");

Leider, leider gibt es in der aktuellen Version (Wicket-6.0-beta1) einen Bug, der eine NullPointerException nach sich zieht, wenn hier ein Propertykey angegeben wird, der nicht vorhanden ist. Dieser Bug ist aber mittlerweile gefixt und sollte mit der Wicket-6.0-beta2 ausgerollt werden (die sollte eigentlich heute abend erscheinen).

Zusätzlich hat man die Möglichkeit, Variablen zu registrieren.

validationError.setVariable("email", email.getMail());

Man kann dann beispielsweise eine solche Fehlermeldung in einer Propertydatei definieren:

email.error=Ungueltige Emailadresse : ${email}

Der Platzhalter email würde durch die Variable ersetzt werden, was in diesem Fall die eingegebene Emailadresse ist.

FeedbackMessages

Eine interessante Neuigkeit hat sich mit Wicket-6 aber doch noch fast unbemerkt eingeschlichen.
Feedback-Messages (beispielsweise nach einem Konvertierungs- oder Validierungsfehler) wurden bisher immer in der Session gespeichert. Das hat sich grundlegend geändert, denn in Wicket 6 merkt sich jede Komponente selbst, welche Fehler für sie vorhangen sind.

Wie funktioniert das im Code?

public FeedbackMessages getFeedbackMessages()
	{
		FeedbackMessages messages = getMetaData(FEEDBACK_KEY);
		if (messages == null)
		{
			messages = new FeedbackMessages();
			setMetaData(FEEDBACK_KEY, messages);
		}
		return messages;
	}

Hierfür hält jede Komponente eine Instanz der Klasse FeedbackMessages.

Wie kam man bisher an alle registrierten FeedbackMessages?


FeedbackMessages messages = Session.get().getFeedbackMessages()

Das funktioniert immer noch, würde aber in Wicket-6 nur die Meldungen zurückliefern, die explizit in der Session gespeichert wurden (was beispielsweise dann Sinn macht, wenn Fehlermeldungen über Page-Grenzen hinweg erhalten bleiben sollen).

Wie aber kommt man jetzt an alle Fehlermeldungen, die beispielsweise beim Abschicken einer Form erzeugt wurden?

Hierfür gibt es einen neuen Mechanismus, den FeedbackMessageCollector.

Schauen wir uns das Ganze im Beispiel an:

Wir haben ja bereits ein FeedbackPanel auf der einfachen Seite eingebaut, das einen Fehler anzeigt, wenn eine falsche Emailadresse eingegeben wird.

Wieso funktioniert das? Das zeigt ein einfacher Blick in die neue Implementierung des FeedbackPanels, genauer sogar in der Klasse FeedbackMessagesModel.

messages = new FeedbackCollector(pageResolvingComponent.getPage()).collect(filter);

Was passiert intern? Der Collector sucht rekursiv in der übergebenen Komponente (hier die Page) und allen Kind-Komponenten
(und optional auch der Session) nach Meldungen, die registriert sind und zeigt diese an.

Mir gefällt dieser Ansatz ganz gut, da jetzt die Zuständigkeit für das Verwalten von FeedbackMessages von der Session
(die eigentlich mit dieser Thematik überhaupt nichts zu tun haben sollte) in die Komponenten gewandert ist
(wo sie auch hingehört).

Vielleicht noch ein letztes Wort zum Cleanup von FeedbackMessages.

Am Ende jedes Requests werden FeedbackMessages weggeräumt. Die geschieht in der detachFeedback-Methode der Klasse
Component:

private void detachFeedback()
	{
		FeedbackMessages feedback = getMetaData(FEEDBACK_KEY);
		if (feedback != null)
		{
			final int removed = feedback.clear(getApplication().getApplicationSettings()
				.getFeedbackMessageCleanupFilter());

			if (feedback.isEmpty())
			{
				setMetaData(FEEDBACK_KEY, null);
			}
			else
			{
				feedback.detach();
			}
		}
	}

Recht interessant ist hierbei noch folgende Zeile:

feedback.clear(getApplication().getApplicationSettings()
				.getFeedbackMessageCleanupFilter())

Der FeedbackMessage-Cleanup-Filter entfernt per Default alle Meldungen, die bereits gerendert wurden.
Das passiert per Default auch in der Session.

Es funktioniert aber weiterhin problemlos, eine FeedbackMessage auf Seite A zu registrieren:

Session.get().error("Fehler!");
setResponsePage(SeiteB.class);

und diesen Fehler auf Seite B anzuzeigen, da in diesem Fall die FeedbackMessages noch nicht gerendert wurde und somit
auch nicht am Ende des Requests in der detach()-Phase weggeräumt werden.

Im nächsten Artikel schauen wir uns dann ein wenig Ajax-Neuheiten genauer an.

Die Sourcen zu allen Artikeln kann man sich übrigens hier klonen : git@github.com:dilgerma/wicket-6.0-Playground.git

@Override
public final List<FeedbackMessage> getObject()
{
if (messages == null)
{
// Get filtered messages from page where component lives
messages = new FeedbackCollector(pageResolvingComponent.getPage()).collect(filter);

// Sort the list before returning it
if (sortingComparator != null)
{
Collections.sort(messages, sortingComparator);
}

// Let subclass do any extra processing it wants to on the messages.
// It may want to do something special, such as removing a given
// message under some special condition or perhaps eliminate
// duplicate messages. It could even add a message under certain
// conditions.
messages = processMessages(messages);
}
return messages;
}

Der nächste Teil der Wicket-6 Serie ist hier zu finden.


War dieser Blogeintrag für Sie interessant? Evtl. kann ich noch mehr für Sie tun.

Trainings & Know-How aus der Praxis zu

  • Apache Wicket 1.4.x, 1.5.x, 1.6.x
  • GIT – Best Practices, Einsatz, Methoden
  • Spring
  • Java
  • Scrum & Kanban
  • Agiles Arbeiten
Consulting & Softwareentwicklung

  • Requirements Engineering
  • Qualitätssicherung
  • Software-Entwicklung
  • Architektur
  • Scrum & Kanban

Wicket 6.0 – Teil 1 : JQuery Backing Library

Hallo,

in den nächsten Tagen werde ich versuchen, einige neue Wicket Features die mit der Version 6 kommen etwas genauer zu beleuchten, hauptsächlich mache ich das, um mir selber ein Bild zu machen.

Schlussendlich definiere ich folgende Abhängigkeit in meiner pom.

6.0.0-beta1
<!-- WICKET DEPENDENCIES -->

org.apache.wicket
wicket
${wicket.version}
pom

org.apache.wicket
wicket-extensions
${wicket.version}

org.apache.wicket
wicket-spring
${wicket.version}

org.apache.wicket
wicket-guice
${wicket.version}

Allgemein

Die Roadmap (leicht veraltet ) findet sich hier.

Der Migrationsguide (bisher eher ein wenig mau) findet sich hier.

JQuery und Wicket 6 – Die Backing Library

Wicket 6 setzt endlich auf JQuery als Javascript-Bibliothek, meiner Ansicht nach genau die richtige Entscheidung.

Schön ist zu sehen, dass trotzdem nicht nativ auf JQuery aufgebaut wird, sondern im Prinzip bleibt die Wicket-Javascript-API im großen und ganzen erhalten und JQuery wird quasi „versteckt“ intern verwendet. So ist es theoretisch auch möglich, andere Javascript-Bibliotheken wie beispielsweise Dojo, Qooxdoo oder Prototype zu verwenden (aber warum sollte man das tun?:)).

JavaScriptLibrarySettings

Die Deklaration der JQuery – und Wicket-Ajax-Bibliotheken findet sich in der Klasse JavaScriptLibrarySettings.


public class JavaScriptLibrarySettings implements IJavaScriptLibrarySettings
{
private ResourceReference jQueryReference = JQueryResourceReference.get();

private ResourceReference wicketEventReference = WicketEventJQueryResourceReference.get();

private ResourceReference wicketAjaxReference = WicketAjaxJQueryResourceReference.get();

private ResourceReference wicketAjaxDebugReference = WicketAjaxDebugJQueryResourceReference.get();

}

Die JavascriptBibliotheken werden in der Klasse Application angebunden.

/**
* @return Application's JavaScriptLibrary settings
* @since 6.0
*/
public final IJavaScriptLibrarySettings getJavaScriptLibrarySettings()
{
checkSettingsAvailable();
if (javaScriptLibrarySettings == null)
{
javaScriptLibrarySettings = new JavaScriptLibrarySettings();
}
return javaScriptLibrarySettings;
}

/**
*
* @param javaScriptLibrarySettings
*/
public final void setJavaScriptLibrarySettings(
final IJavaScriptLibrarySettings javaScriptLibrarySettings)
{
this.javaScriptLibrarySettings = javaScriptLibrarySettings;
}

Über die Library-Settings werden automatisch die JQuery-Bibliotheken geladen (aktuell in der Version 1.7.2). Es kann aber auch
sehr einfach eine eigene Bibliothek über den Setter eingebunden weren.

Interessant ist, die JQuery-Referenz wird nur dann gerendert, wenn sie wirklich gebraucht wird, beispielsweise wenn ein
AjaxLink auf der Seite eingebunden wird.

Die Referenz im Markup sieht dann ungefähr so aus:

<script type="<a>text/javascript</a>" src="<a href=">// <![CDATA[
./wicket/resource/org.apache.wicket.resource.JQueryResourceReference/jquery/jquery-ver-1336836682000.js</a>">
// ]]></script>

Das Einbinden geschieht in der Klasse AbstractDefaultAjaxBehavior, über die quasi die komplette Ajax-Funktionalität von Wicket realisiert ist.

In der Methode render Head findet man folgende Zeile:


CoreLibrariesContributor.contributeAjax(component.getApplication(), response);

Diese Zeile sorgt dafür, dass sobald dieses Behavior irgendwo auf der Seite eingebunden ist, die JQuery-Referenz gerendert wird (wenn sie denn nicht schon gerendert wurde).

Verwendung von JQuery

Wie wird jetzt aber JQuery verwendet?

Nehmen wir als Beispiel diese einfache Loginform, die ich in einem meiner Wicket-Workshops verwende.

Die Implementierung ist überhaupt nichts besonderes, sondern eine einfache Loginform, über die sich die User einloggen können (über die DropDown kann zusätzlich die Rolle des Users gewählt werden, beispielsweise Administrator).

Der Button ist (obwohl nicht notwendig) als Ajax Submit Link realisiert.


protected AjaxSubmitLink submit(){
return new AjaxSubmitLink("submit"){
@Override
protected void onSubmit(AjaxRequestTarget target, Form form) {
super.onSubmit();
ShopSession.get().logIn((User)form.getModelObject());
continueToOriginalDestination();
}

@Override
protected void onError(AjaxRequestTarget target, Form form) {
target.add(form);
}
};
}

Interessant ist der Javascript-Code der generiert wird.


/*<![CDATA[*/
Wicket.Event.add(window, "domready", function(event) {
Wicket.Ajax.ajax({"f":"form1c","u":"./login?4-2.IBehaviorListener.0-form-submit","e":"click","c":"submit1d","sc":":submit"});;
;});

Der Button selbst sieht so aus

<input id="submit1d<" type="submit" />

Ein Ajaxlink in Wicket 1.5.x sieht im Javascript so aus:


var wcall=wicketAjaxGet('uebersicht?2-1.IBehaviorListener.0-content~panel-content-details',function() { }.bind(this),function() { }.bind(this), function() {return Wicket.$('id22f') != null;}.bind(this));return !wcall;

Was das genau bedeutet ist beispielsweise schon hier beschrieben.

Das war ein wenig grundsätzliches zur neuen Wicket-Ajax-Bibliothek. Wer sich hier etwas genauer informieren möchte, dem lege ich die Javascript-Dateien ans Herz.

Wicket Ajax JQuery

Wicket Event

Im nächsten Teil wird es dann hoffentlich ein wenig praktischer.

Die Sourcen zu allen Artikeln kann man sich übrigens hier klonen : git@github.com:dilgerma/wicket-6.0-Playground.git


War dieser Blogeintrag für Sie interessant? Evtl. kann ich noch mehr für Sie tun.

Trainings & Know-How aus der Praxis zu

  • Apache Wicket 1.4.x, 1.5.x, 1.6.x
  • GIT – Best Practices, Einsatz, Methoden
  • Spring
  • Java
  • Scrum & Kanban
  • Agiles Arbeiten
Consulting & Softwareentwicklung

  • Requirements Engineering
  • Qualitätssicherung
  • Software-Entwicklung
  • Architektur
  • Scrum & Kanban