Custom parametrized validation annotation

In the previous post you could learn how to create a basic custom constraint annotation compatible with the Bean Validation standard. This demo will extend the former post by explaining how to create constraints which are more flexible due to parameters defined for particular use cases. If you’re totally unfamiliar with the topic, I refer you to the aforementioned post to grasp the essentials. Otherwise, just keep reading.

Advertisement

1. Constrain annotation with parameter

A parameter is declared as a regular member of an annotation. The return type of the member method defines the type that the parameter accepts. The list of available parameter types is restricted by the Java language specification to: primitives, enumerations, String, Class, other annotations, and arrays of all mentioned types.

Our sample constraint is going to be used with Java 8 LocalDate to verify acceptable days of a week for a particular field. The days which are valid will be passed to the corresponding validator using an array parameter called days.

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DaysOfWeekValidator.class)
public @interface DaysOfWeek {
    String message() default "{com.dolszewski.blog.DaysOfWeek.message}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    DayOfWeek[] days();
}

2. Message property

We defined the key of the message which appears by default if the error occurs. The message is interpolated and the declared attribute can be included as a part of the text. To do so, you should enclose the name of the attribute using curly brackets.

com.dolszewski.blog.DaysOfWeek.message=Only {days} are valid options

3. Validator implementation

The validator is similar to the basic one from the previous post. From the declaration we know that the class validates LocalDate fields annotated with our brand new constraint annotation.

public class DaysOfWeekValidator implements ConstraintValidator<DaysOfWeek, LocalDate> {

    private Set<DayOfWeek> validDays;

    public void initialize(DaysOfWeek constraint) {
        validDays = Arrays.stream(constraint.days()).collect(toSet());
    }

    public boolean isValid(LocalDate localDate, ConstraintValidatorContext context) {
        return localDate != null && validDays.contains(localDate.getDayOfWeek());
    }

}

In the initialize() method we extract the value passed to the days parameter from the annotation instance and store it in the private field of the validator. The field is afterwards used in the isValid() method for the verification process. Since we do a simple lookup for existence in the collection of valid values provided by the days parameter we converted the array into the set for the better performance. Unfortunately, we can’t use Set or any other collection as the annotation member so we are limited only to arrays.

4. Sample Java Bean

Let’s use our custom annotation in a sample data class for demonstration purpose. The first field accepts only weekend dates while the second only working dates.

import static java.time.DayOfWeek.*;

public class DateRequest {

    @DaysOfWeek(days = {SATURDAY, SUNDAY})
    private LocalDate weekendDay;
    @DaysOfWeek(days = {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY})
    private LocalDate workingDay;

    // constructor and getters

}

5. Unit testing

Our example from the previous post required an integration test because the validator utilized the repository bean injected by the Spring context. Our new DaysOfWeekValidator contains simple logic without any external dependencies hence a simple unit test is sufficient.

public class DateRequestTest {

    private Validator validator;

    @Before
    public void setUp() throws Exception {
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        validator = validatorFactory.getValidator();
    }

    @Test
    public void shouldMarkAllDatesInvalid() throws Exception {
        // given
        DateRequest request = new DateRequest(LocalDate.of(2016, 9, 20), LocalDate.of(2016, 9, 18));
        // when
        Set<ConstraintViolation<DateRequest>> violations = validator.validate(request);
        // then
        assertEquals(2, violations.size());
    }

}

In the test setup we acquire a validator instance, which is accessible through a factory class called (unsurprisingly) ValidatorFactory. Next, we create an instance of the DataRequest class and set its field with values that violate constraints. We expect that both violations will be found.

6. Additional remarks

Your annotation constraint can declare predefined values for parameters which can always be overridden by passing the new value just like for regular attributes. It can save developers some typing if some value is more common.

DayOfWeek[] days() default {DayOfWeek.MONDAY};

It’s worth mentioning that all validators aren’t thread-safe. If your implementation relies on some code which shouldn’t be accessed concurrently, it’s your responsibility to take care of required exclusive access.

What is more, the instance of a validator class can be reused for other validations, hence any validator should be stateless.

7. In brief

As you can see, parametrized constrain annotations aren’t very complex. An annotation can specify several attributes with optional default values. These attributes are stored by the initialize() method in class members for future usage in the verification method. Every validator should be stateless and, if required, thread-safe. The sample code for this demo can be found in the GitHub repository. As always, don’t hesitate to leave a comment or contact me with possible questions.

Facebooktwittergoogle_plusredditlinkedinmail
Advertisement