Until now, it was not possible or easy to reuse constraints to make more complex constraints.
The new specification draft introduces the notion of constraint composition. Composition is useful for three main things:
- reuse more primitive constraints to build new constraints and avoid duplication
- define fine grained error reports for a given constraint
- expose how a constraint is composed and describe its primitive blocks
The last point is particularly interesting. Constraints implementations are black boxes answering yes or no to the question: Is this value valid or not?
.
When constraints need to be applied outside the Java world or applied on a different metadata model, black boxes do no good. There is no way to know that @OrderNumber actually apply some restriction on the length of the number as well as apply some CRC verification.
Composition helps to solve the problem by giving access to the primitive constituencies of a constraint. Let's first see how to define a composed constraint.
Defining a composed constraint
To define the list of constraints composing a main constraint, simply annotate the main constraint annotation with the composing constraint annotations.
@Numerical @Size(min=5, max=5) @ConstraintValidator(FrenchZipcodeValidator.class) @Documented @Target({ANNOTATION_TYPE, METHOD, FIELD}) @Retention(RUNTIME) public @interface FrenchZipCode { String message() default "Wrong zipcode"; String[] groups() default {}; }
When @FrenchZipCode is placed on a property, its value is validated against @Numerical, @Size(min=5, max=5) and the constraint implementation FrenchZipcodeValidator: all the composing constraints are validated as well as the logic of the main constraint. Note that a composing constraint can itself be composed of constraints.
Each failing constraint will generate an individual error report which is useful when you want do display fine grained reports to your user. But this might be quite confusing and a single error report is more appropriate in some situations. You can force Bean Validation to raise a single error report (the composed constraint error report) if any of its composing constraint fails by using the @ReportAsSingleInvalidConstraint annotation.
@Numerical @Size(min=5, max=5) @ReportAsSingleInvalidConstraint @ConstraintValidator(FrenchZipcodeValidator.class) @Documented @Target({ANNOTATION_TYPE, METHOD, FIELD}) @Retention(RUNTIME) public @interface FrenchZipCode { String message() default "Wrong zipcode"; String[] groups() default {}; }
In the past two examples, none of the composing annotation parameters can be adjusted at declaration time. This is fine if the zip code is always of size 5. But what happens if the size can be adjusted depending on the property? The spec offers a way is a way to override
a parameter from the composing annotation based on a parameter from the composied annotation using the @OverridesParameter annotation.
@Numerical @Size //arbitrary parameter values @ConstraintValidator(FrenchZipcodeValidator.class) @Documented @Target({ANNOTATION_TYPE, METHOD, FIELD}) @Retention(RUNTIME) public @interface FrenchZipCode { String message() default "Wrong zipcode"; String[] groups() default {}; @OverridesParameters( { @OverridesParameter(constraint=Size.class, parameter="min") @OverridesParameter(constraint=Size.class, parameter="max") } ) int size() default 5; @OverridesParameter(constraint=Size.class, parameter="message") String sizeMessage() default "{error.zipcode.size}"; @OverridesParameter(constraint=Numerical.class, parameter="message") String numericalMessage() default "{error.zipcode.numerical}"; }
The following declaration
@FrenchZipcode(size=9, sizeMessage="Zipcode should be of size {value}")
is equivalent to the following definition / declaration combination
@Numerical @Size(min=9, max=9, message="Zipcode should be of size {value}") @ConstraintValidator(FrenchZipcodeValidator.class) @Documented @Target({ANNOTATION_TYPE, METHOD, FIELD}) @Retention(RUNTIME) public @interface FrenchZipCode { String message() default "Wrong zipcode"; String[] groups() default {}; }
Let's now see how a tool would use this extra information.
Exploring composed constraints with the metadata API
Using the metadata API, you can explore the list of constraints on a given object or property. Each constraint is described by a ConstraintDescriptor. It lists all the composing constraints and provides a ConstraintDescriptor object for each. The ConstraintDescriptor honors overridden parameters (ie using @OverridesParameter): the annotation and the parameter values returned contain the overridden value.
ElementDescriptor ed = addressValidator.getConstraintsForProperty("zipcode"); for ( processConstraintDescriptor cd : ed.getConstraintDescriptors() ) { processConstraintDescriptor(cd); //check all constraints on zip code } public void processConstraintDescriptor(processConstraintDescriptor cd) { //Size.class is understood by the tool if ( cd.getAnnotation().getAnnotationType().equals( Size.class ) ) { Size m = (Size) cd.getAnnotation(); column.setLength( m.max() ); //read and use the metadata } for (ConstraintDescriptor composingCd : cd.getComposingConstraints() ) { processConstraintDescriptor(cd); //check composing constraints recursively } }
When using the following declaration
@FrenchZipCode(size=10) public String zipCode;
the tool will set the zipCode column length to 10.
While the tool does not know what @FrenchZipCode and @Numerical means, it knows how to make use of @Max. Javascript generation libraries or persistence tools typically understand a core subset of constraints. If a complex constraint is composed of one or several of these core subset constraints, it can be partially understood and processed by Java Persistence for example.
This is one of the reasons why it is strongly recommended to build complex constraints on top of more primitive ones. The Bean Validation specification will come with a core of constraints tools will be able to rely upon.
Let us know what you think in our forum.