Friday, May 14, 2010

Bookmark and Share

Amongst others the Bean Validation API defines two constraint annotations related to time: @Past and @Future. With the help of these constraints one can validate that a given element is a date either in the past or in the future.

As per the BV specification these constraints are allowed for the types java.util.Date and java.util.Calendar. But what if you are working with an alternative date/time library such as the Joda Time API? Does that mean you can't use the @Past/@Future constraints?

Luckily not, as the Bean Validation API defines a mechanism for adding new validators to existing constraints. Basically all you have to do is to implement a validator for each type to be supported and register it within a constraint mapping file.

Note that for the remainder of this post I'll focus on the @Past constraint. Doing the same for @Future is left as an exercise for the reader.

Providing a Validator

So let's start with implementing a validator. The Joda Time API provides a whole bunch of types replacing the JDK date and time types. A good introduction to these types can be found in Joda's quickstart guide.

All Joda types representing exact points on the time-line implement the interface ReadableInstant. Providing an @Past validator for that interface will allow the @Past constraint to be used for widely used ReadableInstant implementations such as DateTime or DateMidnight.

Implementing the validator is straight-forward. Obeying the contract defined by @Past the given date is simply compared to a new DateTime instance which represents the current instant in the default time zone:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PastValidatorForReadableInstant implements
        ConstraintValidator<Past, ReadableInstant> {

    public void initialize(Past constraintAnnotation) {}

    public boolean isValid(ReadableInstant value,
            ConstraintValidatorContext constraintValidatorContext) {

        if(value == null) {
            return true;
        }

        return value.isBefore(new DateTime());
    }
}

Similar validators could also be written for other Joda types which don't implement ReadableInstant (such as LocalDate) but as this is basically the same, it is out of the scope of this post.

Registering the Validator

Having implemented the validator we need to register it within a constraint mapping file:

1
2
3
4
5
6
7
8
9
10
11
12
13
<constraint-mappings
    xmlns="http://jboss.org/xml/ns/javax/validation/mapping"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation=
        "http://jboss.org/xml/ns/javax/validation/mapping validation-mapping-1.0.xsd">

    <constraint-definition annotation="javax.validation.constraints.Past">
        <validated-by include-existing-validators="true">
             <value>de.gmorling.moapa.joda_bv_integration.PastValidatorForReadableInstant</value>
        </validated-by>
    </constraint-definition>

</constraint-mappings>

Using the validated-by element we add our new validator to the validators for the @Past constraint. By setting include-existing-validators to true, we ensure that the @Past constraint still can be used at the JDK date types.

As demanded by the Bean Validation API we then register the constraint mapping file within the central configuration file validation.xml:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<validation-config
    xmlns="http://jboss.org/xml/ns/javax/validation/configuration"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/configuration validation-configuration-1.0.xsd">

    <constraint-mapping>META-INF/validation/custom-constraints.xml</constraint-mapping>

</validation-config>

Trying it out

Now it's time to test how that all works out. To do so, we define an examplary domain class Customer which has an attribute birthday of the Joda type DateMidnight:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Customer {

    private final String name;

    private final DateMidnight birthday;

    public Customer(String name, DateMidnight birthday) {

        this.name = name;
        this.birthday = birthday;
    }

    @NotNull
    public String getName() {
        return name;
    }

    @NotNull
    @Past
    public DateMidnight getBirthday() {
        return birthday;
    }
}

A simple test finally shows that creating a customer with a future birthday causes a constraint violation, while a customer with a birthday in the past doesn't:

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
public class CustomerTest {

    private static Validator validator;

    @BeforeClass
    public static void setUpValidatorAndDates() {

        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        validator = validatorFactory.getValidator();
    }

    @Test
    public void customerWithFutureBirthdayCausesConstraintViolation() {

        Customer customer = new Customer("Bob", new DateMidnight(2020, 11, 3));
        Set<ConstraintViolation<Customer>> constraintViolations = validator.validate(customer);

        assertEquals(1, constraintViolations.size());
        assertEquals("must be in the past", constraintViolations.iterator().next().getMessage());
    }

    @Test
    public void customerWithPastBirthdayCausesNoConstraintViolation() {

        Customer customer = new Customer("Bob", new DateMidnight(1960, 11, 3));
        Set<ConstraintViolation<Customer>> constraintViolations = validator.validate(customer);

        assertTrue(constraintViolations.isEmpty());
    }

}

The complete source code in form of a Maven project can be found in my Git repository.

So just try it out and don't hesistate to post any feedback. It is also planned to add support for the Joda types in an ucoming version of Hibernate Validator.

2 comments:

Thomas Eichberger said...

Helpful post!

Daniel Serodio said...

Hibernate Validator 4.2 support @Past and @Future validation agains Joda Time natively.