November 1, 2009

The Promising Standardized JSR 303 Bean Validation and Example of Usage

For too long have the Java world lived without a standardized bean validation specification and soon will the final approval hopefully be approved (2 Nov 2009). What does this imply?
  1. Better Domain Driven Design, by putting the shallow domain validation in the POJO where it belongs.
  2. Hopefully we can finally get rid of Value Objects Pattern which is a dark heritage from the EJB 2.1 age and instead use domain POJO beans through out our architecture. There is of course exception to this rule, e.g. in service layer where the exposed model is totally different than the domain model or in a UI layer with heavy usage of line charts, there an OO model does not fit into a point orientation representation.
What is still to be proven is how the specification will fit into other framework, such as JPA, Apache Wicket or Spring Remoting. But one can rest ashore if one is using the standard validation model, the odds increase dramatically. And presumably will big framework such as Spring Framework and Apache Wicket adjust there framework to the standard.

Here follows a simple example of the usage of Bean Validation.

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>
<groupId>se.msc.examples</groupId>
<artifactId>validation-domain</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>Validation :: Domain</name>
<url>http://www.msc.se/examples/validation</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<repositories>
<repository>
<id>repository.jboss.org</id>
<url>http://repository.jboss.org/maven2</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

<dependencies>
<!-- common logging library -->
<!-- version is depended on hibernate-annotations v3.4.0.GA -->
<!--
http://repo1.maven.org/maven2/org/hibernate/hibernate-annotations/3.4.0.GA/hibernate-annotations-3.4.0.GA.pom
-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.4.2</version>
</dependency>
<!-- sun bean validation api -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.0.0.GA</version>
</dependency>
<!-- hibernate bean validation impl -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>4.0.0.GA</version>
</dependency>
<!-- test support -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<defaultGoal>install</defaultGoal>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>

</project>

package se.msc.examples.validation.domain;

import java.io.Serializable;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private Long personId;
private String firstName;
@NotNull
@Size(min=1)
private String surname;
@NotNull
@Pattern(regexp=".+@.+\\.[a-z]+")
private String mail;

public Long getPersonId() {
return personId;
}

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

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

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

}

package se.msc.examples.validation.domain;

import static org.junit.Assert.*;

import java.util.Set;

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

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class PersonTest {
  private Validator validator;
  private Person person;

  @BeforeClass
  	public static void oneTimeSetUp() throws Exception {  
  }

  @AfterClass
  	public static void oneTimeTearDown() throws Exception {
  }

  @Before
  public void setUp() throws Exception {
    validator = Validation.buildDefaultValidatorFactory().getValidator();
    person = new Person();
    person.setFirstName("Magnus K");
    person.setSurname("Karlsson");
    person.setMail("magnus.k.karlsson@domain.se");  
  }

  @After
  public void tearDown() throws Exception {
  }

  private <T> void debugPrint(Set<ConstraintViolation<T>> violations) {
    for (ConstraintViolation<T> violation : violations) {
      String propertyPath = violation.getPropertyPath().toString();
      String message = violation.getMessage();
      System.out.println("invalid value for: '" + propertyPath + "': " + message);
    }  
  }

  @Test
  public void testValidate_OK() throws Exception {
    Set<ConstraintViolation<Person>> violations = validator.validate(person);
    assertTrue(violations.size() == 0);
    debugPrint(violations);
  }

  @Test
  public void testValidate_FAIL_MAIL() throws Exception {
    person.setMail("magnus.k.karlsson@domain");
    Set<ConstraintViolation<Person>> violations = validator.validate(person);
    assertTrue(violations.size() == 1);
    debugPrint(violations);
  }

  @Test
  public void testValidate_FAIL_SURNAME() throws Exception {
    person.setSurname(null);
    Set<ConstraintViolation<Person>> violations1 = validator.validate(person);
    assertTrue(violations1.size() == 1);
    debugPrint(violations1);

    person.setSurname("");
    Set<ConstraintViolation<Person>> violations2 = validator.validate(person);
    assertTrue(violations2.size() == 1);
    debugPrint(violations2);  
  }

}

The resource bundle message ValidationMessages.properties
javax.validation.constraints.Null.message=must be null
javax.validation.constraints.NotNull.message=must not be null
javax.validation.constraints.AssertTrue.message=must be true
javax.validation.constraints.AssertFalse.message=must be false
javax.validation.constraints.Min.message=must be greater than or equal to {value}
javax.validation.constraints.Max.message=must be less than or equal to {value}
javax.validation.constraints.Size.message=size must be between {min} and {max}
javax.validation.constraints.Digits.message=numeric value out of bounds (<{integer} digits>.<{fraction} digits> expected)
javax.validation.constraints.Past.message=must be a past date
javax.validation.constraints.Future.message=must be a future date
javax.validation.constraints.Pattern.message=must match the following regular expression: {regexp}


References:
JSR 303: Bean Validation
http://jcp.org/en/jsr/detail?id=303

Article comparing JSR 303 Reference Implementation And Spring 2.5 Validation
http://blog.jteam.nl/2009/08/04/bean-validation-integrating-jsr-303-with-spring/

JSR 303 Reference Material
http://people.redhat.com/~ebernard/validation/

JSR 303 Specification Leads Emmanuel Bernard Blog
http://in.relation.to/Bloggers/Emmanuel

No comments: