6. Defining a Custom Validation Annotation

One other integrity constraint we have to consider is about the allowed values of the year property, which must be in the interval [1459, nextYear()] where nextYear() is a function invocation expression. We may have the idea to use @Min and @Max to specify the interval constraint, but this is not possible because the @Max annotation (as well as any other annotation) does not allow expressions, but only data literals. So, while we can express the interval's lower bound with @Min( value=1459), we need another solution for expressing the upper bound.

Fortunately, the Bean Validation API allows to define custom validation annotations with custom code performing the constraint checks. This means that we are free to express any kind of validation logic in this way. Creating and using a custom validation annotation requires the following steps:

  1. Create the annotation interface UpToNextYear with the following code:

    @Target( {ElementType.FIELD, ElementType.METHOD})
    @Retention( RetentionPolicy.RUNTIME)
    @Constraint( validatedBy = UpToNextYearImpl.class)
    public @interface UpToNextYear {
      String message() default 
          "The value of year must be between 1459 and next year!";
      Class<?>[] groups() default {};
      Class<? extends Payload>[] payload() default {};
    }

    The interface needs to define three methods, message (returns the default key or error message if the constraint is violated), groups (allows the specification of validation groups, to which this constraint belongs) and payload (used by clients of the Bean Validation API to assign custom payload objects to a constraint - this attribute is not used by the API itself). Notice the @Target annotation, which defines the element types that can be annotated (fields/properties and methods in our case). The @Constraint annotation allows to specify the implementation class that will perform the validation, i.e. UpToNextYearImpl in our case.

  2. Create an implementation class with the validation code:

    public class UpToNextYearImpl implements 
        ConstraintValidator< UpToNextYear, Integer> {
      private Calendar calendar;
    
      @Override
      public void initialize( UpToNextYear arg0) {
        this.calendar = Calendar.getInstance();
        calendar.setTime( new Date());
      }
      @Override
      public boolean isValid( Integer year, 
          ConstraintValidatorContext context) {
        if (year == null || 
            year > this.calendar.get( Calendar.YEAR) + 1) {
          return false;
        }
        return true;
      }
    }

    The implementation class implements the ConstraintValidator interface, which requires two type parameters: the annotation interface defined before (i.e. UpToNextYear), and the type of elements the validator can handle (i.e. Integer, so implicitly also the compatible primitive type int). The initialize method allows initializing variables required for performing the validation check. The isValid method is responsible for performing the validation: it must return true if the validation succeeds, and false otherwise. The first parameter of the isValid method represents the value to be validated and its type must be compatible with the type defined by the second type parameter of the ConstraintValidator (Integer in our case).

  3. Annotate the property or method concerned:

    @Entity
    @Table( name = "books")
    public class Book {
      // ...
    
      @Min( value = 1459)
      @UpToNextYear
      private Integer year;
    
      //...
    }