Core Java

Custom JSR 303 Bean Validation constraints for the JSR 310 New Date/Time API

With JSR 310 Java 8 finally brought us a decent date and time API. For those of you that are still using Java 7 – like I am at my current project – there is an excellent backport available, see www.threeten.org for more details. However, I’m not going to go into any details about using the new API since there are already a ton of blog posts out there about the topic. What I am going to show you in this post is how you can use the Date/Time API in conjunction with the JSR 303 Bean Validation API by writing your own custom annotations.

If you’re using both bean validation and the new date/time API you’ll probably want to use them in conjunction. The API and an implementation like Hibernate Validator only provide a handful of constraints, e.g. NotEmpty or @Pattern. However, as of yet there are no out-of-the-box constraints for JSR 310. Fortunately it is very easy to create your own constraints. As an example I will demonstrate how you can write your own @Past annotation for validating java.time.LocalDate fields.

For testing purposes we’ll start off with a very simple class that holds a date and a dateTime. These fields are supposed to represent dates in the past. Therefore they are annotated with the @Past anootation:

ClassWithPastDates

package it.jdev.example.jsr310.validator;

import java.time.LocalDate;
import java.time.LocalDateTime;

public class ClassWithPastDates {

    @Past
    private LocalDate date;

    @Past
    private LocalDateTime dateTime;
    
    public LocalDate getDate() {
        return date;
    }
    
    public void setDate(LocalDate date) {
        this.date = date;
    }
    public LocalDateTime getDateTime() {
        return dateTime;
    }
    
    public void setDateTime(LocalDateTime dateTime) {
        this.dateTime = dateTime;
    }

}

Next, we’ll write a very basic unit test for the @Past constraint that demonstrates our intentions: obviously besides dates that lie in the past, we’ll also want a null reference to be valid but dates in the future to be invalid, and even today should count as invalid.

PastTest

package it.jdev.example.jsr310.validator;

import static org.junit.Assert.assertEquals;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

import org.junit.Before;
import org.junit.Test;

public class PastTest {
    
    private ClassWithPastDates classUnderTest;

    @Before
    public void setup() {
        classUnderTest = new ClassWithPastDates();
    }

    @Test
    public void thatNullIsValid() {
        Set<ConstraintViolation<ClassWithPastDates>> violations = validateClass(classUnderTest);
        assertEquals(violations.size(), 0);
    }

    @Test
    public void thatYesterdayIsValid() throws Exception {
        classUnderTest.setDate(LocalDate.now().minusDays(1));
        classUnderTest.setDateTime(LocalDateTime.now().minusDays(1));
        Set<ConstraintViolation<ClassWithPastDates>> violations = validateClass(classUnderTest);
        assertEquals(violations.size(), 0);
    }

    @Test
    public void thatTodayIsInvalid() throws Exception {
        classUnderTest.setDate(LocalDate.now());
        classUnderTest.setDateTime(LocalDateTime.now());
        Set<ConstraintViolation<ClassWithPastDates>> violations = validateClass(classUnderTest);
        assertEquals(violations.size(), 2);
    }

    @Test
    public void thatTomorrowIsInvalid() throws Exception {
        classUnderTest.setDate(LocalDate.now().plusDays(1));
        classUnderTest.setDateTime(LocalDateTime.now().plusDays(1));
        Set<ConstraintViolation<ClassWithPastDates>> violations = validateClass(classUnderTest);
        assertEquals(violations.size(), 2);
    }

    private Set<ConstraintViolation<ClassWithPastDates>> validateClass(ClassWithPastDates myClass) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        Set<ConstraintViolation<ClassWithPastDates>> violations = validator.validate(myClass);
        return violations;
    }

}

Now that we’ve got the basic test set up, we can implement the constraint itself. This consists of two steps. First we’ll have to write the annotation, and then we’ll have to implement a ConstraintValidator. To start with the annotation:

@interface Past

package it.jdev.example.jsr310.validator;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PastValidator.class)
@Documented
public @interface Past {

    String message() default "it.jdev.example.jsr310.validator.Past.message";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

As you can see, the @Past annotation is not very spectacular. The main thing to notice is the @Constraint annotations where we specify which class will be used to perform the actual validation.

PastValidator

package it.jdev.example.jsr310.validator;

import java.time.LocalDate;
import java.time.temporal.Temporal;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class PastValidator implements ConstraintValidator<Past, Temporal> {

    @Override
    public void initialize(Past constraintAnnotation) {
    }

    @Override
    public boolean isValid(Temporal value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }
        LocalDate ld = LocalDate.from(value);
        if (ld.isBefore(LocalDate.now())) {
            return true;
        }
        return false;
    }

}

The PastValidator is where all the magic happens. By implementing the ConstraintValidator interface we’re obliged to provide two methods but for our example only the isValid() method is of use, this is where we’ll perform the actual validation.

Note that we’ve used the java.time.temporal.Temporal as the type because it is the interface that both the LocalDate and LocalDateTime classes have in common. This allows us to use the same @Past for both LocalDate and LocalDateTime fields.

And that really is all there is to it. With this very basic example I’ve shown how easy it is to create your own custom JSR 303 bean validation constraint.

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button