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).

[code]

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

[/code]

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

[code language=”java”]

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

[/code]

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

[code language=”xml”]

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

[/code]

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

[code language=”xml”]
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>4.3.0.Final</version>
</dependency>
[/code]

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

[code language=”java”]

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;
}
}

[/code]

Im HomePage-Markup ersetzen wir alles durch folgendes Formular.

[code language=”html”]
<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>
[/code]

und den passenden Java-Code hierzu:

[code language=”java”]

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);

}

[/code]

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.

[code language=”java”]

@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;

[/code]

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.

[code language=”java”]

@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;

[/code]

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).

[code]

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

[/code]

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.

[code language=”java”]
//hibernate validator specific
@Range(message = "{zip.muenchen}",min = 80805, max=80805)
private Integer zip;

[/code]

Folgendes Textfeld.

[code language=”java”]

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

[/code]

und folgende Text-Property.

[code]

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

[/code]

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.

[code language=”java”]

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()));

[/code]

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

[code language=”java”]
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

[/code]

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:

[code language=”java”]
"customer.address.street" getCustomer().getAddress().getStreet()
[/code]

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.

[code language=”java”]
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"))));
[/code]

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.

[code language=”java”]

public interface EffectiveTrainer {
String getName();

String getEmail();

String getPhone();
}

[/code]

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

Die Constraints definieren wir jetzt so um:

[code language=”java”]

@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;

[/code]

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.

[code language=”java”]
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"))));

[/code]

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

Effective Trainings & Consulting - Martin Dilger



Hat Ihnen dieser Blog-Eintrag gefallen? Ich stelle in diesem Blog Informationen über Tools, Frameworks und Werkzeuge zur Verfügung, die mich produktiver machen. Vielleicht kann ich auch Ihnen helfen, produktiver zu werden.


Ich unterstütze Sie als freier Mitarbeiter bei der Entwicklung von Software-Projekten, Agiler Arbeit sowie Schulungen / Fortbildungen.


Jeden Tag ein bisschen produktiver - ab heute