JSR 303 Beans Validation - Using Groups (HOWTO)
Hi all,
The next version of Hibernate Validator (currently 4.0.0 Alpha3) will be the Bean Validation (JSR-303) RI (Hibernate Validator 4.0.0-A3 actually implements the CR1 of the specification), as usually with specifications that interest me I read it and look for interesting changes - in this case changes from the legacy Hibernate Validator. We can easily identify the strong roots of Hibernate Validator in the new specification but it has some new ideas. In this entry I want to focus on one of them - the constraints grouping. But first a basic example.
Basic Validation Example
The concept of the new API is not very different from the legacy Hibernate Validator (I wrote about it in the past - here), basically we have two parts to the API: (1) the bean constraints and (2) the validator. The code fragment bellow illustrates how bean constraints are assigned to properties:
package com.jroller.eyallupu;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
public class Book {
@NotNull
private String title;
@NotNull
private String author;
@Min(value=100)
private int numOfPages;
@NotNull
private String isbn;
... ... ...
}
This is almost identical to the legacy validated beans, the only change is the package name for the constraints (marked in blue). However the invocation of the validator itself is different, instead of the ClassValidator we have to use the ValidatorFactory and the Validator:
Book book = new book();
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set> violations = validator.validate(book);
for (ConstraintViolation violation : violations) {
System.out.format("%s: %s%n",violation.getPropertyPath(), violation.getMessage());
}
// The output of the execution was: (see the bean constraint in the code fragment above)
isbn: may not be null
numOfPages: must be greater than or equal to 100
author: may not be null
title: may not be null
In the code above I obtain a validator using a factory which was created by the buildDefaultValidatorFactory() static method on the Validation class. This validator is then used to validate the book bean.
Groups
JSR 303 adds the concept of constraints grouping, using groups we can configure which constraint should be validated per validator invocation, for example we might decide that our Book bean above has a life cycle - while the book is in the draft phase it must has an author and a title but the numOfPages and isbn properties are optional and we would not like to force their constraints yet. However when the book is in the printing phase of its life cycle all of the properties must obey their constraints. This behavior can be controlled using validation groups.
Technically speaking a validation group is an interface and each constraint defines the groups is belongs to using the 'groups' attribute on the constraint (by default all constraints belong to the javax.validation.groups.Default group). Assuming we have defined two interfaces (groups) Draft and Printing our Book bean can be modified to look as illustrated below:
package com.jroller.eyallupu;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
public class Book {
@NotNull(groups=Draft.class)
private String title;
@NotNull(groups=Draft.class)
private String author;
@Min(value=100, groups=Printing.class)
private int numOfPages;
@NotNull(groups=Printing.class)
private String ISBN;
... ... ...
}
// Validate for the printing phase
Set> violations = validator.validate(book, Printing.class);
for (ConstraintViolation violation : violations) {
System.out.format("%s: %s%n",violation.getPropertyPath(), violation.getMessage());
}
// Validate both the printing and the Draft phases
Set> violations = validator.validate(book, Draft.class, Printing.class);
for (ConstraintViolation violation : violations) {
System.out.format("%s: %s%n",violation.getPropertyPath(), violation.getMessage());
}
I assigned each constraint to a group so I can validate the Book bean based on its lifecycle phase. In the second part of the example above I validate the book according to the Printing phase of its lifecycle, and then I validate it for both Draft and Printing phase. Notice that when asking for a specific group to be validated the Default group will not be validated - to validate constraints which are members of the Default group we must add Default class to the list of groups on the validate() method.
On first look it seems reasonable - if a book is in the printing phase it must qualify for both the Draft and the Printing constraints so I will the validator to validate both groups, however a better way for doing that is by using groups inheritance.
Groups and Inheritance
A group can extend another group, and since groups are interfaces a group can extend more than one group. When the validator evaluates a specific group's constraints it also evaluates all of its super groups (interfaces) constraints. So actually a better way to configure the Book bean validation groups would have been as illustrated bellow:
public interface BookLifeCycle extends Default {}
public interface Draft extends BookLifeCycle{}
public interface Printing extends Draft{}
When looking at it bottom up we see that the Printing group (interface) extends the Draft group - which means that any constraint applied to the Draft group is also applied to the Printing group. The Draft group extends the BookLikeCycle interface (we will see soon what is it good for) which in his turn extends the Default interface. By building an inheritance chain starting at Default we can be sure that we can continue using "un grouped" constraints (as in the first example on this entry) and they will be evaluated with any group extending the BookLifeCycle group.
It is important to notice the following:
- "a given validation constraint will not be processed more than once per validation" (on the specification - section 3.5) - it means that if a constraint belongs to more than one group and more than one of these groups are evaluated on a specific invocation of the validate() method the constraint will be evaluated only once. However we can't tell as a part of each group the specific constraint will be validated (see 2)
- "Unless ordered by group sequences, groups can be validated in no particular order" (same section) - which means that a validation routine can evaluate more than one group on the same pass over a bean properties, and that the evaluation will be done for all of the groups (regardless whether an error occurred in any of the groups). This is not the case when using GruopSequence (see below).
Validating a Bean based on its Life Cycle Phase
A nice pattern, or structure, is when the bean announces its own lifecycle phase - for that reason I added the BookLifeCycle interface, by adding to the bean a reference to its own life cycle phase we can use it to validate the bean:
public class Book {
private Class lifeCyclePhase;
public Class getLifeCyclePhase() {
return lifeCyclePhase;
}
public void setLifeCyclePhase(Class lifeCyclePhase) {
this.lifeCyclePhase = lifeCyclePhase;
}
}
// Later in the code someone sets the appropriate lifecycle phase to the bean, now we can use it to validate the
// bean properties. I assume it is not null.
Set> violations = validator.validate(book, book.getLifeCyclePhase());
This could be done without the BookLifeCycle interface (using the Draft interface) but it looks better that way.
GroupSequence
To control the order in which groups are validated the @GroupSequence annotation can be used. This annotation doesn't only forces the evaluation order but it also means that if during the evaluation of a group we have errors the following groups will not(!) be evaluated. Since each constraint is evaluated only once per invocation of the validate() method the GroupSequence ensures that it will be evaluated as part of the first group in the sequence it belongs to (opposite to none-ordered validation on which we can't tell when the constraint will be evaluated). For example I can define a new interface BookOrderedValidation which will look as the following:
package com.jroller.eyallupu;
import javax.validation.GroupSequence;
import javax.validation.groups.Default;
@GroupSequence({Default.class, Draft.class, Printing.class})
public interface BookOrderedValidation {}
When validating the BookOrderedValidation group the validator ensures that the groups will be validated in the following order: Default, Draft, Printing and that if we have one or more validation violations in a specific group the validation will not proceed to the following groups. Another thing to notice is that I add a new interface (BookOrderedValidation) rather than annotate the Draft or Printing groups with @GroupSequence. The reason is that if I annotate these interface I will create a cycle and an exception would be thrown by the validator.
Multiple Instances of the Same Constraints
Sometimes we would like to have multiple instance of the same constraint on a property. The specification mentions a classic example: the Pattern constraint - we would like a string property to either be in pattern A or B. To support this requirement each constraint has an inner annotation named istList which can be used to apply multiple instances of the same constraint on a property:
@Pattern.List( {@Pattern(regexp="A.*"), @Pattern(regexp="B.*")} )
private String title;
In the example above the 'title' property must be in one of the two patterns: "A.*" or "B.*", this is a simple example and we could have done it with a single pattern as well but the following example might be much more interesting.
Different Constraint Configurations per Group
Using the inner List annotation one can also apply different constraint configurations for each validation group. Suppose that when the book is in Draft phase it must have at least 5 pages (cover, copyrights, etc.) but when it is in the Printing phase it must have at least 100 pages (so I can charge more...). The following configuration supports it:
@Min.List( { @Min(value = 100, groups = Printing.class), @Min(value = 5, groups = Draft.class) })
private int numOfPages;
However, when using this pattern it is important to remember that if the Printing group extends the Draft group (and groups sequence is not used) then when validating the Printing group the validator will also validate the Draft group. It means that if the 'numOfPages' property value is invalid for both constraints (for example 0) we will get two constraints violations.
Summary
I think it is easy to see that the groups feature (which in my opinion is one of the important new features) creates another way to configure our constraints. This can be done in applications' specific domain grouping (like in the book lifecycle) but it can also be done from the architecture point of view. An example is an application framework which creates a layered based validation: these rules should be applied before entering the server, these should be applied right before the bean is being persistent, and so on.
The specification also defines implicit groups and setting the default group for a bean. I hoped I would have the time to write on these as well but I have to rush to LAX to catch a flight.
Please notice: I had recover this blog post from my old blog at http://www.jroller.com/eyallupu since jroller.com is no longer available. As such the styling might be a bit wobbly ... If something seems 'too broken' please contact me and I'll adjust
Please notice: I had recover this blog post from my old blog at http://www.jroller.com/eyallupu since jroller.com is no longer available. As such the styling might be a bit wobbly ... If something seems 'too broken' please contact me and I'll adjust
Comments