November 1, 2009

Integrate JSR 303 Bean Validation And ApacheWicket

In my previous blog I was writing about the new promising standard JSR 303 Bean Validation. In this article I will write about JSR 303 Bean Validation and Apache Wicket. Let start with recapitulate the domain object Person and see how we used the Bean Validation annotation to annotate our property constraints.
package se.msc.examples.validation.domain;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

@Entity
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long personId;
@NotNull
@Size(min = 1, max = 250)
@Column(length = 250)
private String givenName;
@NotNull
@Size(min = 1)
@Column(length = 250)
private String surname;
@NotNull
@Pattern(regexp = ".+@.+\\.[a-z]+")
@Column(length = 250)
private String mail;

public Long getPersonId() {
return personId;
}

protected void setPersonId(Long personId) {
this.personId = personId;
}

public String getGivenName() {
return givenName;
}

public void setGivenName(String givenName) {
this.givenName = givenName;
}

public String getSurname() {
return surname;
}

public void setSurname(String surname) {
this.surname = surname;
}

public String getMail() {
return mail;
}

public void setMail(String mail) {
this.mail = mail;
}

}

This time I also have added sun's JPA annotation, because this is a domain object and sooner we want the domain objects to be persisted. We also the see that there is an overlap between the Bean Validation annotation and the JPA annotation. My approach to these is to use an open constraint approach for persistence and a narrowing/stricter definition with the Bean Validation for the domain layer. This way enables us to have a more flexible database, DBA would probably argue that this leads to bad data quality, but since we are building in validation directly in our domain object and writing small and easy to run unit test, to test these validation, there are no risk against bad data quality slipping through to the persistence layer. Actually we are protecting bad data to even reach the persistence layer long before, first in the presentation layer, but also we will use the same Bean Validation in the service layer. And all validation using the same code, the Bean Validation annotation, no more inventing the wheel over and over again in the different layers.

So how do we use the Bean Validation with the Apache Wicket? Well, there is no out-of-box integration, so I have written a few helper classes that will do that for us. These helper classes are actually worth using already how ever you will be using Bean Validation or not in your next project, because Apache Wicket is really to low-tech, like Swing, and when writing Apache Wicket classes from ground up requires you to write quite lengthy masses of code, this is not good. So lets start with our helper classes for writing org.apache.wicket.markup.html.form.Form.

package se.msc.examples;

import org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.model.CompoundPropertyModel;
import org.apache.wicket.model.LoadableDetachableModel;

public class FormBuilder<T> extends Form<T> {
private static final long serialVersionUID = 1L;
public static final String SUBMIT = "submit";

public FormBuilder(final String id, final T modelObject) {
super(id, new CompoundPropertyModel<T>(new LoadableDetachableModel<T>(
modelObject) {
private static final long serialVersionUID = 1L;

protected T load() {
return modelObject;
}
}));
}

public void addTextField(final String propertyName) {
TextField<T> textField = new TextField<T>(propertyName);
textField.add(new BeanValidator<T>(getModelObject(), propertyName));
add(textField);
}

public void addSubmitButton(final FormButtonListener<T> listener) {
final FormBuilder<T> parent = this;
add(new Button(SUBMIT) {
private static final long serialVersionUID = 1L;

public void onSubmit() {
parent.execSubmit(listener);
}
});
}

public Form<T> create() {
return this;
}

public void execSubmit(final FormButtonListener<T> listener) {
listener.onSubmit(getModelObject());
}

}





package se.msc.examples;

import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;

import org.apache.log4j.Logger;
import org.apache.wicket.validation.INullAcceptingValidator;
import org.apache.wicket.validation.IValidatable;
import org.apache.wicket.validation.ValidationError;

public class BeanValidator<T> implements INullAcceptingValidator<T> {
private static final long serialVersionUID = 1L;
private static final Logger log = Logger.getLogger(BeanValidator.class);
private final Class<T> beanClass;
private final String propertyName;

@SuppressWarnings("unchecked")
public BeanValidator(final T beanObject, final String propertyName) {
this.beanClass = (Class<T>) beanObject.getClass();
this.propertyName = propertyName;
}

@Override
public void validate(IValidatable<T> validatable) {
log.info("validate... " + validatable.getValue());
ReflectionUtil<T> util = new ReflectionUtil<T>();
T beanObject = util.createInstance(beanClass);
util.setPropertyValue(beanObject, propertyName, validatable.getValue());
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<T>> violations = validator.validateProperty(beanObject, propertyName);
for (ConstraintViolation<T> violation : violations) {
String propertyPath = violation.getPropertyPath().toString();
String message = violation.getMessage();
log.error("invalid value for: '" + propertyPath + "': " + message);

ValidationError validationError = new ValidationError();
validationError.setMessage(message);
validatable.error(validationError);
}  
}

@Override
public String toString() {
return "[BeanValidator beanObject='" + beanClass + "']";
}

}


Lets now see how we use it in our Apache Wicket controller class.

package se.msc.examples;

import org.apache.log4j.Logger;
import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.panel.FeedbackPanel;

import se.msc.examples.validation.domain.Person;

public class PersonEditPage extends WebPage implements FormButtonListener<Person> {
private static final long serialVersionUID = 1L;
private static final Logger log = Logger.getLogger(PersonEditPage.class);
protected static final String FEEDBACK_PANEL = "feedbackPanel";
protected static final String PERSON_FORM = "personForm";
protected static final String GIVEN_NAME = "givenName";

public PersonEditPage(final PageParameters parameters) {
log.info("PersonPage...");
add(new FeedbackPanel(FEEDBACK_PANEL));
add(createForm(new Person()));
}

private Form<Person> createForm(Person person) {
FormBuilder<Person> builder = new FormBuilder<Person>(PERSON_FORM, person);
builder.addTextField(GIVEN_NAME);
builder.addSubmitButton(this);
return builder.create();
}

@Override
public void onSubmit(Person modelObject) {
log.info("onSubmit " + modelObject);
info("Successfully created '" + modelObject.getGivenName() + "'.");
}

}


And the accompanying html file

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.4-strict.dtd"
xml:lang="en" lang="en">
<head>
<title>Person Edit Page</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>

<div id="feedbackPanel" wicket:id="feedbackPanel">Feedback Panel</div>
<form id="personForm" wicket:id="personForm">
<fieldset style="width: 30em">
<legend>Person Edit</legend>
<table border="1" style="width: 100%">
<tr>
<td align="right">Given Name:</td>
<td><input id="givenName" wicket:id="givenName" type="text" style="width: 98%" /></td>
</tr>
<tr>
<td colspan="2" align="right">
<button id="submit" wicket:id="submit" type="submit">Submit</button>
</td>
</tr>
</table>
</fieldset>
</form>

</body>
</html>

Now lets write a simple test case to test our Apache Wicket validation.

package se.msc.examples;



import junit.framework.TestCase;



import org.apache.wicket.markup.html.form.Form;

import org.apache.wicket.markup.html.panel.FeedbackPanel;

import org.apache.wicket.util.tester.FormTester;

import org.apache.wicket.util.tester.WicketTester;



public class PersonEditPageTest extends TestCase {

private WicketTester tester;



@Override

public void setUp() throws Exception {

tester = new WicketTester(new WicketApplication());

tester.startPage(PersonEditPage.class);

tester.assertRenderedPage(PersonEditPage.class);

}



public void testLayout() throws Exception {

tester.assertComponent(PersonEditPage.FEEDBACK_PANEL,

FeedbackPanel.class);

tester.assertComponent(PersonEditPage.PERSON_FORM, Form.class);

}



public void testCreate_OK() throws Exception {

FormTester form = tester.newFormTester(PersonEditPage.PERSON_FORM);

form.setValue(PersonEditPage.GIVEN_NAME, "Magnus");

form.submit(FormBuilder.SUBMIT);



tester.assertRenderedPage(PersonEditPage.class);

tester.assertNoErrorMessage();

}



public void testCreate_GIVENNAME_FAIL() throws Exception {

FormTester form = tester.newFormTester(PersonEditPage.PERSON_FORM);

form.setValue(PersonEditPage.GIVEN_NAME, "");

form.submit(FormBuilder.SUBMIT);



tester.assertRenderedPage(PersonEditPage.class);

tester.assertErrorMessages(new String[] { "Field givenName is required." });



form = tester.newFormTester(PersonEditPage.PERSON_FORM);

form.setValue(PersonEditPage.GIVEN_NAME, " ");

form.submit(FormBuilder.SUBMIT);



tester.assertRenderedPage(PersonEditPage.class);

tester.assertErrorMessages(new String[] { "Field givenName is required." });  

}

}

5 comments:

JanM said...

Hi Magnus
Yes, form handling can be a bit verbose in Wicket. I think it would be interesting to try your approach. Could you add the code for ReflectionUtil or point me in the right direction?
Thanks, Jan

Magnus K Karlsson said...

The idea of FormBuilder came from a great framework that never took of - spring rich client framework. If you want more good OO design decision of how to make Apache Wicket code less verbose I recommend a visit to springframework.org.

package se.msc.examples;

public interface FormButtonListener<T> {

public void onSubmit(T modelObject);
}

The code of ReflectionUtil is not a beauty, but does the job. It is inspered from Spring Framework earlier version of ReflectionUtils. The today version is much to completed for simple task, so might want to rewrite this class, before going to production.


package se.msc.examples;

import java.lang.reflect.Method;

public class ReflectionUtil<T> {

public T createInstance(Class<T> beanClass) {
try {
return beanClass.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}

public void setPropertyValue(T beanObject, String propertyName,
Object propertyValue) {
String s = propertyName.substring(0, 1);
String t = propertyName.substring(1, propertyName.length());
String str = "set" + s.toUpperCase() + t;
try {
Method propertyField = beanObject.getClass().getMethod(str, propertyValue.getClass());
propertyField.invoke(beanObject, propertyValue);
} catch (Exception e) {
e.printStackTrace();
}
}

}

itsolusenz said...

Excellent help! Thank you for this great tutorial!!
website development

Anonymous said...

Why bother? use JSF instead, it has built-in support to JSR-303.

w3c said...

Nice information, I really appreciate the way you presented.Thanks for sharing..

http://www.w3cvalidation.net/