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:
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 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.
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 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
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:
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 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.
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 |
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.
Very helpful. Thank you 🙂