Enterprise Java Development@TOPIC@
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) {
logger.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) {
logger.info("{}:{} {}", 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) {
logger.info("{}:{} {}", 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;
}
}
Address1 a1 = new Address1()
.setStreet("1600")
.setCity("Washington");
Set<ConstraintViolation<Address1>> violations = val.validate(a1,PreCheck.class);
...
assertEquals("unexpected violation", 1, violations.size());
-:1600 Washington, null null, must have city and state or zip code
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>