Enterprise Java Development@TOPIC@
Revision: v2013-09-23Built on: 2014-03-07 00:03 EST
Copyright © 2014 jim stafford (jcstaff@apl.jhu.edu)
Abstract
This presentation provides information for developers to leverage the Java Validation API within their application and wire into their JPA persistence context.
Explore capabilities of Validation API
Introduce the JPA persistence lifecycle
Demonstrate Validation API integration with JPA lifecycle
Integrate validation into Maven builds
Independent of other APIs (e.g., persistence, web-tier)
Available server-side and client-side
Provide validation metadata thru annotations and XML descriptors
Applied to FIELDs, METHODs, TYPEs, etc.
import javax.validation.constraints.*;
public class Person {
private int id;
@NotNull
@Size(min=1,max=12)
@Pattern(regexp="^[a-zA-Z\\ \\-]+$", message="invalid characters in name")
private String firstName;
@NotNull
@Size(min=1,max=20)
@Pattern(regexp="^[a-zA-Z\\ \\-]+$", message="invalid characters in name")
private String lastName;
@Past
private Date birthDate;
@Size(min=7,max=50)
@Pattern(regexp="^.+@.+\\..+$")
private String email;
Obtain instance of Validator from ValidationFactory
private ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
private Validator val = vf.getValidator();
Validate bean instances
Person p = new Person()
.setFirstName("Bob2")
.setLastName("Smith")
.setEmail("bob2");
Set<ConstraintViolation<Person>> violations = val.validate(p);
for (ConstraintViolation<Person> v : violations) {
log.info(v.getPropertyPath() + ":" + v.getInvalidValue() + " " + v.getMessage());
}
assertEquals("unexpected number of violations", 3, violations.size());
-email:bob2 size must be between 7 and 50 -email:bob2 must match "^.+@.+\..+$" -firstName:Bob2 invalid first name
Logical grouping of constraints
Used to define which constraints and activated when
Can be used to define sequences -- with short-circuit
package ejava.jpa.example.validation;
import javax.validation.groups.Default;
public interface Drivers extends Default {}
package ejava.jpa.example.validation;
import javax.validation.groups.Default;
public interface POCs extends Default {}
public class Person {
...
@NotNull(groups={Drivers.class, POCs.class})
@Past(groups=Drivers.class)
private Date birthDate;
@NotNull(groups=POCs.class)
@Size(min=7,max=50)
@Pattern(regexp="^.+@.+\\..+$")
private String email;
Validate named groups of constraints
Person p = new Person()
.setFirstName("Bob")
.setLastName("Smith")
.setEmail("bob.smith@gmail.com")
.setBirthDate(new Date(System.currentTimeMillis()+100000));
Set<ConstraintViolation<Person>> validPerson = val.validate(p, Default.class);
Set<ConstraintViolation<Person>> validDriver = val.validate(p, Drivers.class);
Set<ConstraintViolation<Person>> validPOC = val.validate(p, POCs.class);
assertTrue("not validPerson", validPerson.isEmpty());
assertFalse("validDriver", validDriver.isEmpty());
assertTrue("not validPOC", validPOC.isEmpty());
Drivers group fails because birthDate in future
POC does not yet have a birthDate constraint
package ejava.jpa.example.validation;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
import javax.validation.Constraint;
import javax.validation.Payload;
/**
* Defines a constraint annotation for expressing a minimum age.
*/
@Documented
@Constraint(validatedBy={MinAgeValidator.class})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
public @interface MinAge {
String message() default "too young";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default{};
int age() default 0;
}
@Constraint defines validator class
@Target defines what can be assigned to
FIELD - attributes
METHOD - getters
TYPE - classes
ANNOTATION_TYPE - constraints composing other constraints
PARAMETER - no support required by spec
CONSTRUCTOR - no support required by spec
Several reserved properties
message() - used to create error message
groups() - defines which groups constraint member of. Defaults to Default group
payload() - defines association for constraint
@MinAge(age=16, payload=Severity.Critical.class) private Date birthDate;
@NotNull(payload=Severity.Warning.class) private Date voterRegistrationDate;
names starting with "valid" - reserved/not allowed
Define use-specific properties (i.e., age)
Optionally define annotation for multiple annotations
public @interface MinAge {
...
/**
* Defines an array of annotations so that more than one can be applied.
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
public @interface List {
MinAge[] value();
}
}
@Documented adds this spec to API spec of what it annotates
package ejava.jpa.example.validation;
...
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class MinAgeValidator implements ConstraintValidator<MinAge, Date>{
int minAge;
@Override
public void initialize(MinAge constraint) {
this.minAge = constraint.age();
}
@Override
public boolean isValid(Date date, ConstraintValidatorContext ctx) {
if (date==null) { return true; }
//get today's date
Calendar latestBirthDate = new GregorianCalendar();
latestBirthDate.add(Calendar.YEAR, -1*minAge);
//get calendate date of object
Calendar birthDate = new GregorianCalendar();
birthDate.setTime(date);
if (birthDate.after(latestBirthDate)) {
String errorMsg = String.format("%d is younger than minimum %d",
getAge(birthDate),
minAge);
ctx.buildConstraintViolationWithTemplate(errorMsg)
.addConstraintViolation();
return false;
} else {
return true;
}
}
private int getAge(Calendar birth) {
...
...
}
public class Person {
...
@NotNull(groups={Drivers.class, POCs.class})
@MinAge.List({
@MinAge(age=18, groups=POCs.class),
@MinAge(age=16, groups=Drivers.class)
})
private Date birthDate;
...
Calendar fifteen = new GregorianCalendar();
fifteen.add(Calendar.YEAR, -16);
fifteen.add(Calendar.DAY_OF_YEAR, 2);
Person p = new Person()
.setFirstName("Bob")
.setLastName("Smith")
.setBirthDate(fifteen.getTime());
Set<ConstraintViolation<Person>> violations = val.validate(p, Drivers.class);
for (ConstraintViolation<Person> v : violations) {
log.debug(v.getPropertyPath() + ":" + v.getInvalidValue() + " " + v.getMessage());
}
...
assertFalse("valid driver", violations.isEmpty());
Bob is too young to be a valid driver
-birthDate:Wed Jun 11 01:06:33 EDT 1997, 15 is younger than minimum 16 -birthDate:Wed Jun 11 01:06:33 EDT 1997, too young
public class Person {
...
@NotNull
@Size(min=1,max=12)
@Pattern(regexp="^[a-zA-Z\\ \\-]+$", message="invalid characters in name")
private String firstName;
@NotNull
@Size(min=1,max=20)
@Pattern(regexp="^[a-zA-Z\\ \\-]+$", message="invalid characters in name")
private String lastName;
Multiple constraints makeup complete definition
Verbose and tedious to define multiple times
public class Person {
...
@ValidName(min=1, max=12, regexp="^[a-zA-Z\\ \\-]+$", message="invalid first name")
private String firstName;
...
@ValidName(min=1, max=20, regexp="^[a-zA-Z\\ \\-]+$", message="invalid last name")
private String lastName;
...
/**
* Defines a validation composition
*/
@NotNull
@Size
@Pattern(regexp="")
@Documented
@Constraint(validatedBy={})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
public @interface ValidName {
String message() default "invalid name";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default{};
@OverridesAttribute(constraint=Size.class, name="min") int min() default 0;
@OverridesAttribute(constraint=Size.class, name="max") int max() default Integer.MAX_VALUE;
@OverridesAttribute(constraint=Pattern.class, name="regexp") String regexp() default ".*";
/**
* Defines an array of annotations so that more than one can be applied.
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
public @interface List {
ValidName[] value();
}
}
Annotate a composite constraint annotation with the building blocks
Define attribute overrides as appropriate
Person p = new Person()
.setFirstName("Bob")
.setLastName("Smithhhhhhhhhhhhhhhhhh$%$%$$$$$$$$$$$$$$$$");
Set<ConstraintViolation<Person>> violations = val.validate(p);
for (ConstraintViolation<Person> v : violations) {
log.debug(v.getPropertyPath() + ":" + v.getInvalidValue() + " " + v.getMessage());
}
-lastName:Smithhhhhhhhhhhhhhhhhh$%$%$$$$$$$$$$$$$$$$ size must be between 1 and 20 -lastName:Smithhhhhhhhhhhhhhhhhh$%$%$$$$$$$$$$$$$$$$ must match "^[a-zA-Z\ \-]+$"
Bean failed two composed constraints
Define a sequence of validation groups to be tested in order and short-circuit upon failure
public interface DBChecks {} //checks whether data will fit within DB
public interface DataChecks {} //more detailed content checks
A set of Constraint Groups defined based on complexity/cost
@GroupSequence({Default.class, DBChecks.class, DataChecks.class})
public interface ValidationSequence {}
A Group Sequence defines the order
@Column(name="STREET", length=32, nullable=false)
@NotNull(message="street not supplied")
@Size(max=32, message="street name too large", groups=DBChecks.class)
@Pattern(regexp="^[0-9A-Za-z\\ ]+$", groups=DataChecks.class,
message="street must be numbers and letters")
private String street;
Constraints are assigned to Constraint Groups
We cause an error in the data size and content
Address a = new Address()
.setStreet("1600$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$")
.setCity("Washington")
.setState("DC")
.setZip("20500");
Set<ConstraintViolation<Address>> violations = val.validate(a, ValidationSequence.class);
...
//we should only get violations from the DB group
assertEquals("unexpected number of violations", 1, violations.size());
Notice Default passed, DBChecks failed, and DataChecks not attempted
-street:1600$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ street name too large
Look across properties of type
...
@Documented
@Constraint(validatedBy={CityStateOrZipValidator.class})
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
public @interface CityStateOrZip {
String message() default "must have city and state or zip code";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default{};
}
public class CityStateOrZipValidator implements ConstraintValidator<CityStateOrZip, Address1>{
@Override
public void initialize(CityStateOrZip constraintAnnotation) {}
@Override
public boolean isValid(Address1 address, ConstraintValidatorContext context) {
if (address==null) { return true; }
return (address.getCity()!=null && address.getState()!=null) ||
address.getZip()!=null;
}
}
Traverse validation across relationships
public class Purchase {
@Valid
private Set<PurchaseItem> items;
Define Constraints external to Bean Class
Constraints need not be defined within bean class
public class Book {
private int id;
//@NotNull(message="title is required")
//@Size(max=32, message="title too long")
private String title;
//@Min(value=1, message="pages are required")
private int pages;
<validation-config xmlns="http://jboss.org/xml/ns/javax/validation/configuration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/configuration">
<default-provider>org.hibernate.validator.HibernateValidator</default-provider>
<constraint-mapping>META-INF/book-constraints.xml</constraint-mapping>
</validation-config>
<constraint-mappings
xmlns="http://jboss.org/xml/ns/javax/validation/mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jbss.org/xml/ns/javax/validation/mapping
http://jboss.org/xml/ns/javax/validation/mapping/validation-mapping-1.0.xsd">
<bean class="ejava.jpa.example.validation.Book">
<field name="title">
<constraint annotation="javax.validation.constraints.NotNull">
<message>title is required</message>
</constraint>
<constraint annotation="javax.validation.constraints.Size">
<message>title is required</message>
<element name="max">32</element>
</constraint>
</field>
<field name="pages">
<constraint annotation="javax.validation.constraints.Min">
<message>pages are required</message>
<element name="value">1</element>
</constraint>
</field>
</bean>
</constraint-mappings>
Provide access to persistence lifecycle events
Persistence lifecycle events provided to class methods
@Entity
@Table(name="ORMLISTEN_PERSON")
public class Person {
@Id @GeneratedValue
private long id;
private String name;
@OneToOne(mappedBy="person", optional=true, cascade=CascadeType.ALL)
private Residence residence;
@PrePersist public void prePersist() {
}
@PostPersist public void postPersist() {
}
@PostLoad public void postLoad() {
}
@PreUpdate public void preUpdate() {
}
@PostUpdate public void postUpdate() {
}
@PreRemove public void preRemove() {
}
@PostRemove public void postRemove() {
}
}
Could be used to log or validate instance at certain stages of lifecycle
JPA prohibits calling EntityManager within callback methods
Persistence lifecycle events provided to external class
@Entity
@Table(name="ORMLISTEN_PERSON")
@EntityListeners(Listener.class)
public class Person {
public class Listener {
@PrePersist public void prePersist(Object entity) {
}
@PostPersist public void postPersist(Object entity) {
}
@PostLoad public void postLoad(Object entity) {
}
@PreUpdate public void preUpdate(Object entity) {
}
@PostUpdate public void postUpdate(Object entity) {
}
@PreRemove public void preRemove(Object entity) {
}
@PostRemove public void postRemove(Object entity) {
}
}
Same Listener class may listen to multiple entity types
Good for when Listener has specific purpose that is type-agnostic
JPA and Validation API annotations can be mixed together
@Entity
@Table(name="VALIDATION_PERSON")
public class Person {
@Id @GeneratedValue
private int id;
@Column(name="FIRST_NAME", length=12, nullable=false)
@ValidName(min=1, max=12, regexp="^[a-zA-Z\\ \\-]+$", message="invalid first name")
private String firstName;
@Column(name="LAST_NAME", length=20, nullable=false)
@ValidName(min=1, max=20, regexp="^[a-zA-Z\\ \\-]+$", message="invalid last name")
private String lastName;
@Temporal(TemporalType.DATE)
@NotNull(groups={Drivers.class, POCs.class})
@Past(groups=Drivers.class)
@MinAge.List({
@MinAge(age=18, groups=POCs.class),
@MinAge(age=16, groups=Drivers.class)
})
private Date birthDate;
@Column(name="EMAIL", length=50)
@NotNull(groups=POCs.class)
@Size(min=7,max=50)
@Pattern(regexp="^.+@.+\\..+$")
private String email;
...
<?xml version="1.0" encoding="UTF-8"?>
<persistence ...
<persistence-unit name="jpa-validation-example-test">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<validation-mode>AUTO</validation-mode>
<properties>
<property name="javax.persistence.validation.group.pre-persist"
value="ejava.jpa.example.validation.Drivers"/>
<property name="javax.persistence.validation.group.pre-update"
value="ejava.jpa.example.validation.Drivers"/>
</properties>
</persistence-unit>
</persistence>
validation-mode
AUTO - enable if validator present in classpath
CALLBACK - turn on and report error if no validator found in classpath
NONE - turn off
Can also be set using javax.persistence.validation.mode property
validation groups
javax.persistence.validation.group.pre-persist - defines groups to call during @PrePersist phase
javax.persistence.validation.group.pre-update - defines groups to call during @PreUpdate phase
javax.persistence.validation.group.pre-remove - defines groups to call during @PreRemove phase
default behavior
Default group for @PrePersist and @PreUpdate
Nothing for @PreRemove
Person p = new Person()
.setFirstName("Bob")
.setLastName("Smith")
.setBirthDate(new Date());
try {
em.persist(p);
} catch (ConstraintViolationException ex) {
log.info("caught expected exception:" + ex);
}
-caught expected exception:javax.validation.ConstraintViolationException:
Validation failed for classes [ejava.jpa.example.validation.Person]
during persist time for groups [ejava.jpa.example.validation.Drivers, ]
List of constraint violations:[
ConstraintViolationImpl{
interpolatedMessage='too young',
propertyPath=birthDate,
rootBeanClass=class ejava.jpa.example.validation.Person,
messageTemplate='too young'}
ConstraintViolationImpl{
interpolatedMessage='0 is younger than minimum 16',
propertyPath=birthDate,
rootBeanClass=class ejava.jpa.example.validation.Person,
messageTemplate='0 is younger than minimum 16'}
]Validation is not automatically cascaded (@Valid) across relationships
Validation will occur for related entity during its appropriate lifecycle phases
Groups could be divided into client, service, DAO-insert, DAO-update, DAO-delete
Permits errors to be automatically detected without transaction going into rollback state
Figure 4.1. API Dependency
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
<scope>compile</scope>
</dependency>
Figure 4.2. Implementation Dependency (includes API dependency)
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.0.1.Final</version>
<scope>test</scope>
</dependency>
Figure 4.3. Hibernate Implementation Dependency for @Pattern constraints
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>2.2.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>javax.el</artifactId>
<version>2.2.4</version>
<scope>test</scope>
</dependency>
Figure 4.4. Add validator to schema generation pluginManagement
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>hibernate3-maven-plugin</artifactId>
<version>3.0</version>
<extensions>true</extensions>
<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>3.6.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>4.3.1.Final</version>
</dependency>
</dependencies>
</plugin>