In my last post I explained how to get the JSR 303 reference implementation up and running and showed some of the very basic features of the Bean Validation API.
Today I want to delve a bit more into the details of the JSR by showing how to create custom constraint annotations and how to make use of the concept of validation groups.
Creating custom constraint annotations
Though the JSR defines a whole bunch of standard constraint annotations such as @NotNull, @Size, @Min or @AssertTrue (btw. raise your voice in the JSR feedback forum, if you want to have more standard annotations included), there will always be validation requirements, for which these standard annotations won't suffice.
Being aware of that problem, the specification authors laid out the API in an expansible manner, that allows users to define their custom constraint annotations.
To try that out, lets create a constraint annotation, that ensures, that the licensePlate field of the Car entity from my previous post always contains an upper-case String. First we have to define the annotation itself. If you've never designed an annotation before, this may look a bit scary, but actually it's not very hard:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package org.gm.beanvalidation.constraints;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import org.gm.beanvalidation.validators.UpperCaseValidator;
@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = UpperCaseValidator.class)
@Documented
public @interface UpperCase {
String message() default "{validator.uppercase}";
Class<?>[] groups() default {};
}
|
An annotation type is defined using the @interface keyword. All attributes of an annotation type are declared in a method-like manner. The specification of the Bean Validation API demands, that any constraint annotation defines
- an attribute "message" that returns the default key for creating error messages in case the constraint is violated
- an attribute "groups" that allows the specification of validation groups, to which this constraint belongs (see section "Using validation groups" for further details). This must default to an empty array.
In addition we annotate the annotation type with a couple of so-called meta annotations:
- @Target({ METHOD, FIELD, ANNOTATION_TYPE }): Says, that methods, fields and annotation declarations may be annotated with @UpperCase (but not type declarations e.g.)
- @Retention(RUNTIME): Specifies, that annotations of this type will be available at runtime by the means of reflection
- @Constraint(validatedBy = UpperCaseValidator.class): Specifies the validator to be used to validate elements annotated with @UpperCase
- Documented: Says, that the use of @UpperCase will be contained in the JavaDoc of elements annotated with it
Implementing the constraint validator
Next, we need to implement a constraint validator, that is able to validate elements with the @UpperCase annotation. To do so, we implement the interface ConstraintValidator as shown below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package org.gm.beanvalidation.validators;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.gm.beanvalidation.constraints.UpperCase;
public class UpperCaseValidator implements ConstraintValidator<UpperCase, String> {
public void initialize(UpperCase constraintAnnotation) {
//nothing to do
}
public boolean isValid(String object,
ConstraintValidatorContext constraintContext) {
if (object == null)
return true;
return object.equals(object.toUpperCase());
}
}
|
The ConstraintValidator interface specifies two type parameters, which we set in our implementation. The first specifies the annotation type to be validated by a ConstraintValidator (in our example UpperCase), the second the type of elements, which the validator can handle (here String).
The implementation of the validator is straightforward. The initialize() method gives us access to any attributes of the annotation (such as the min/max fields in case of the Size annotatation), but as @UpperCase doesn't define any attributes, we have nothing to do here.
What's interesting for us, is the isValid() method. On line 20 we implement the logic, that determines, whether a given object is valid according to the @UpperCase annotation or not.
Note, that the specification recommends, that null values should be declared to be valid. If null is not a valid value for an element, it should be annotated with @NotNull explicitely. We could use the passed-in ConstraintValidatorContext to raise any custom validation errors, but as we are fine with the default behavior, we can ignore that parameter.
Specifying the error message
Finally we need to specify the error message, that shall be used, in case the @UpperCase constraint is violated. To do so, we create a file named ValidationMessages.properties under src/main/resources with the following content:
1
|
validator.uppercase=String must be upper-case.
|
If a validation error occurs, the validation runtime will use the default value, that we specified for the message attribute of the @UpperCase annotation to look up the error message in this file.
Now that our first custom constraint is completed, we can use it in the Car class to specify that the licensePlate field shall only contain upper case Strings:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package org.gm.beanvalidation;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.gm.beanvalidation.constraints.UpperCase;
public class Car {
@NotNull
private String manufacturer;
@NotNull
@Size(min = 2, max = 14)
@UpperCase
private String licensePlate;
public Car(String manufacturer, String licencePlate) {
super();
this.manufacturer = manufacturer;
this.licensePlate = licencePlate;
}
//getters and setters ...
|
Let's demonstrate in a little test that the @UpperCase constraint is properly validated:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
package org.gm.beanvalidation;
import static org.junit.Assert.*;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import org.junit.BeforeClass;
import org.junit.Test;
public class CarTest {
private static Validator validator;
@BeforeClass
public static void setUp() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
@Test
public void testLicensePlateNotUpperCase() {
Car car = new Car("Audi", "dd-ab-123");
Set<ConstraintViolation<Car>> constraintViolations =
validator.validate(car);
assertEquals(1, constraintViolations.size());
assertEquals(
"String must be upper-case.",
constraintViolations.iterator().next().getInterpolatedMessage());
}
@Test
public void testCarIsValid() {
Car car = new Car("Audi", "DD-AB-123");
Set<ConstraintViolation<Car>> constraintViolations =
validator.validate(car);
assertEquals(0, constraintViolations.size());
}
}
|
Compound constraints
Looking at the license plate field of the Car class, we see three constraint annotations already. In complexer scenarios, where even more constraints could be applied to one element, this might become a bit confusing easily.
Furthermore, if we had a licensePlate field in another class, we would have to copy all constraint declarations to the other class as well, violating the DRY principle.
Luckily, the Bean Validation API comes to the rescue by allowing the creation of compound constraints. So let's compose a new constraint annotation @ValidLicensePlate, that comprises the constraints @NotNull, @Size and @UpperCase:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
package org.gm.beanvalidation.constraints;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.gm.beanvalidation.validators.ValidLicensePlateValidator;
@NotNull
@Size(min = 2, max = 14)
@UpperCase
@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = ValidLicensePlateValidator.class)
@Documented
public @interface ValidLicensePlate {
String message() default "{validator.validlicenseplate}";
Class<?>[] groups() default {};
}
|
To do so, we just annotate the constraint declaration with its comprising constraints (btw. that's exactly why we allowed annotation types as target for the @UpperCase annotation). The validator class for @ValidLicensePlate doesn't add any additional validation logic, so we skip it for now.
Using the new compound constraint at the licensePlate field now is fully equivalent to the previous version, where we declared the three constraints directly at the field itself:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package org.gm.beanvalidation;
import org.gm.beanvalidation.constraints.ValidLicensePlate;
public class Car {
// ...
@ValidLicensePlate
private String licensePlate;
// ...
}
|
Using validation groups
Now, let's imagine another custom constraint annotation, @NotStolen, which could be annotated at the licensePlate field, too. Upon the validation of @NotStolen an external service shall be called, that checks whether a given license plate is stolen or not.
Calling this service would be rather expensive (in terms of processing time, but possibly also in terms of money, as we might be charged for each call by the service provider), so we are interested in calling the service not more than neccessary.
Especially we don't want the service to be called, if a Car instance is not even valid with respect to the @NotNull constraint at the manufacturer field or the @ValidLicensePlate constraint, e.g. due to an input form not completely filled.
To meet this requirement, we can leverage the concept of validation groups. Each constraint at the fields of an object to be validated can be part of one or more such validation group, and instead of validating all constraints at once, we validate one group after the other, stopping, if one group couldn't be validated successfully.
Validation groups are defined by creating simple marker interfaces. Let's see, how this might look in our Car class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
package org.gm.beanvalidation;
import javax.validation.constraints.NotNull;
import org.gm.beanvalidation.constraints.NotStolen;
import org.gm.beanvalidation.constraints.ValidLicensePlate;
public class Car {
@NotNull
private String manufacturer;
@ValidLicensePlate
@NotStolen(groups = Extended.class)
private String licensePlate;
/**
* Used as validation group.
*/
public interface Extended {
}
// ...
}
|
The @NotStolen constraint is part of the Extended validation group. On the contrary, the @NotNull and the @ValidLicensePlate constraints belong to the Default group (which is specified by the JSR 303 API), as no special validation group is specified for them.
We can use the validation group concept now in a sample class, that checks the validity of a given Car instance:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
package org.gm.beanvalidation;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.groups.Default;
import org.gm.beanvalidation.Car.Extended;
public class CarValidityChecker {
public boolean checkCarValidity(Car car) {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<Car>> constraintViolations = validator
.validate(car, Default.class);
if (!constraintViolations.isEmpty())
return false;
constraintViolations = validator.validate(car, Extended.class);
return constraintViolations.isEmpty();
}
}
|
At first, only the constraints of the Default group are validated. If they are violated, processing stops here. Only if all constraints of the Default group are met, the Extended group is validated, issuing the expensive external service call required for the validation of the @NotStolen constraint.
Conclusion
That concludes my exploration of the JSR 303 (Bean Validation) API for now. I showed how to create custom constraint annotations, how to make use of constraint composition and finally how to leverage the concept of validation groups.
Of course that's not all to be said about the Bean Validation API. Features that I didn't mention during my journey through the JSR's API include class-level constraints, the proposed validation of method parameters and return values and the external declaration of constraints by the means of an XML descriptor.
JSR 303 remains in Public Review status for another couple of days. So you still have the chance to give feedback to the specification's authors. In my opinion the JSR looks really promising and I hope to see it soon in Final status, neatly integrated with the JSF 2.0 and JPA 2.0 APIs.