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." });  
  }

}

No comments: