Sunday, February 1, 2009

Bookmark and Share

In my last post I explained how to get the JSR 303 reference implementation up and running and showed some of the very basic features of the Bean Validation API.

Today I want to delve a bit more into the details of the JSR by showing how to create custom constraint annotations and how to make use of the concept of validation groups.

Creating custom constraint annotations

Though the JSR defines a whole bunch of standard constraint annotations such as @NotNull, @Size, @Min or @AssertTrue (btw. raise your voice in the JSR feedback forum, if you want to have more standard annotations included), there will always be validation requirements, for which these standard annotations won't suffice.

Being aware of that problem, the specification authors laid out the API in an expansible manner, that allows users to define their custom constraint annotations.

To try that out, lets create a constraint annotation, that ensures, that the licensePlate field of the Car entity from my previous post always contains an upper-case String. First we have to define the annotation itself. If you've never designed an annotation before, this may look a bit scary, but actually it's not very hard:

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 org.gm.beanvalidation.constraints;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

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

import javax.validation.Constraint;

import org.gm.beanvalidation.validators.UpperCaseValidator;

@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = UpperCaseValidator.class)
@Documented
public @interface UpperCase {

    String message() default "{validator.uppercase}";

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

}

An annotation type is defined using the @interface keyword. All attributes of an annotation type are declared in a method-like manner. The specification of the Bean Validation API demands, that any constraint annotation defines

  • an attribute "message" that returns the default key for creating error messages in case the constraint is violated
  • an attribute "groups" that allows the specification of validation groups, to which this constraint belongs (see section "Using validation groups" for further details). This must default to an empty array.

In addition we annotate the annotation type with a couple of so-called meta annotations:

  • @Target({ METHOD, FIELD, ANNOTATION_TYPE }): Says, that methods, fields and annotation declarations may be annotated with @UpperCase (but not type declarations e.g.)
  • @Retention(RUNTIME): Specifies, that annotations of this type will be available at runtime by the means of reflection
  • @Constraint(validatedBy = UpperCaseValidator.class): Specifies the validator to be used to validate elements annotated with @UpperCase
  • Documented: Says, that the use of @UpperCase will be contained in the JavaDoc of elements annotated with it

Implementing the constraint validator

Next, we need to implement a constraint validator, that is able to validate elements with the @UpperCase annotation. To do so, we implement the interface ConstraintValidator as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package org.gm.beanvalidation.validators;

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

import org.gm.beanvalidation.constraints.UpperCase;

public class UpperCaseValidator implements ConstraintValidator<UpperCase, String> {

    public void initialize(UpperCase constraintAnnotation) {
        //nothing to do
    }

    public boolean isValid(String object,
        ConstraintValidatorContext constraintContext) {

        if (object == null)
            return true;

        return object.equals(object.toUpperCase());
    }

}

The ConstraintValidator interface specifies two type parameters, which we set in our implementation. The first specifies the annotation type to be validated by a ConstraintValidator (in our example UpperCase), the second the type of elements, which the validator can handle (here String).

The implementation of the validator is straightforward. The initialize() method gives us access to any attributes of the annotation (such as the min/max fields in case of the Size annotatation), but as @UpperCase doesn't define any attributes, we have nothing to do here.

What's interesting for us, is the isValid() method. On line 20 we implement the logic, that determines, whether a given object is valid according to the @UpperCase annotation or not.

Note, that the specification recommends, that null values should be declared to be valid. If null is not a valid value for an element, it should be annotated with @NotNull explicitely. We could use the passed-in ConstraintValidatorContext to raise any custom validation errors, but as we are fine with the default behavior, we can ignore that parameter.

Specifying the error message

Finally we need to specify the error message, that shall be used, in case the @UpperCase constraint is violated. To do so, we create a file named ValidationMessages.properties under src/main/resources with the following content:

1
validator.uppercase=String must be upper-case.

If a validation error occurs, the validation runtime will use the default value, that we specified for the message attribute of the @UpperCase annotation to look up the error message in this file.

Now that our first custom constraint is completed, we can use it in the Car class to specify that the licensePlate field shall only contain upper case Strings:

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
package org.gm.beanvalidation;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.gm.beanvalidation.constraints.UpperCase;

public class Car {

    @NotNull
    private String manufacturer;

    @NotNull
    @Size(min = 2, max = 14)
    @UpperCase
    private String licensePlate;

    public Car(String manufacturer, String licencePlate) {

        super();
        this.manufacturer = manufacturer;
        this.licensePlate = licencePlate;
    }

    //getters and setters ...

Let's demonstrate in a little test that the @UpperCase constraint is properly validated:

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
package org.gm.beanvalidation;

import static org.junit.Assert.*;

import java.util.Set;

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

import org.junit.BeforeClass;
import org.junit.Test;

public class CarTest {

    private static Validator validator;

    @BeforeClass
    public static void setUp() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

    @Test
    public void testLicensePlateNotUpperCase() {

        Car car = new Car("Audi", "dd-ab-123");

        Set<ConstraintViolation<Car>> constraintViolations =
            validator.validate(car);
        assertEquals(1, constraintViolations.size());
        assertEquals(
            "String must be upper-case.", 
            constraintViolations.iterator().next().getInterpolatedMessage());
    }

    @Test
    public void testCarIsValid() {

        Car car = new Car("Audi", "DD-AB-123");

        Set<ConstraintViolation<Car>> constraintViolations = 
            validator.validate(car);
        assertEquals(0, constraintViolations.size());
    }
}

Compound constraints

Looking at the license plate field of the Car class, we see three constraint annotations already. In complexer scenarios, where even more constraints could be applied to one element, this might become a bit confusing easily.

Furthermore, if we had a licensePlate field in another class, we would have to copy all constraint declarations to the other class as well, violating the DRY principle.

Luckily, the Bean Validation API comes to the rescue by allowing the creation of compound constraints. So let's compose a new constraint annotation @ValidLicensePlate, that comprises the constraints @NotNull, @Size and @UpperCase:

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 org.gm.beanvalidation.constraints;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

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

import javax.validation.Constraint;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.gm.beanvalidation.validators.ValidLicensePlateValidator;

@NotNull
@Size(min = 2, max = 14)
@UpperCase
@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = ValidLicensePlateValidator.class)
@Documented
public @interface ValidLicensePlate {

    String message() default "{validator.validlicenseplate}";

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

}

To do so, we just annotate the constraint declaration with its comprising constraints (btw. that's exactly why we allowed annotation types as target for the @UpperCase annotation). The validator class for @ValidLicensePlate doesn't add any additional validation logic, so we skip it for now.

Using the new compound constraint at the licensePlate field now is fully equivalent to the previous version, where we declared the three constraints directly at the field itself:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.gm.beanvalidation;

import org.gm.beanvalidation.constraints.ValidLicensePlate;

public class Car {

    // ...

    @ValidLicensePlate
    private String licensePlate;

    // ...

}

Using validation groups

Now, let's imagine another custom constraint annotation, @NotStolen, which could be annotated at the licensePlate field, too. Upon the validation of @NotStolen an external service shall be called, that checks whether a given license plate is stolen or not.

Calling this service would be rather expensive (in terms of processing time, but possibly also in terms of money, as we might be charged for each call by the service provider), so we are interested in calling the service not more than neccessary.

Especially we don't want the service to be called, if a Car instance is not even valid with respect to the @NotNull constraint at the manufacturer field or the @ValidLicensePlate constraint, e.g. due to an input form not completely filled.

To meet this requirement, we can leverage the concept of validation groups. Each constraint at the fields of an object to be validated can be part of one or more such validation group, and instead of validating all constraints at once, we validate one group after the other, stopping, if one group couldn't be validated successfully.

Validation groups are defined by creating simple marker interfaces. Let's see, how this might look in our Car class:

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
package org.gm.beanvalidation;

import javax.validation.constraints.NotNull;

import org.gm.beanvalidation.constraints.NotStolen;
import org.gm.beanvalidation.constraints.ValidLicensePlate;

public class Car {

    @NotNull
    private String manufacturer;

    @ValidLicensePlate
    @NotStolen(groups = Extended.class)
    private String licensePlate;

    /**
     * Used as validation group.
     */
    public interface Extended {
    }

    // ...

}

The @NotStolen constraint is part of the Extended validation group. On the contrary, the @NotNull and the @ValidLicensePlate constraints belong to the Default group (which is specified by the JSR 303 API), as no special validation group is specified for them.

We can use the validation group concept now in a sample class, that checks the validity of a given Car instance:

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
package org.gm.beanvalidation;

import java.util.Set;

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

import org.gm.beanvalidation.Car.Extended;

public class CarValidityChecker {

    public boolean checkCarValidity(Car car) {

        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        Set<ConstraintViolation<Car>> constraintViolations = validator
                .validate(car, Default.class);

        if (!constraintViolations.isEmpty())
            return false;

        constraintViolations = validator.validate(car, Extended.class);

        return constraintViolations.isEmpty();
    }

}

At first, only the constraints of the Default group are validated. If they are violated, processing stops here. Only if all constraints of the Default group are met, the Extended group is validated, issuing the expensive external service call required for the validation of the @NotStolen constraint.

Conclusion

That concludes my exploration of the JSR 303 (Bean Validation) API for now. I showed how to create custom constraint annotations, how to make use of constraint composition and finally how to leverage the concept of validation groups.

Of course that's not all to be said about the Bean Validation API. Features that I didn't mention during my journey through the JSR's API include class-level constraints, the proposed validation of method parameters and return values and the external declaration of constraints by the means of an XML descriptor.

JSR 303 remains in Public Review status for another couple of days. So you still have the chance to give feedback to the specification's authors. In my opinion the JSR looks really promising and I hope to see it soon in Final status, neatly integrated with the JSF 2.0 and JPA 2.0 APIs.

13 comments:

Abdel Olakara said...

Hi, where will this be used?

JodaStephen said...

Its worth noting that Oval - http://oval.sourceforge.net - is another similar annotation based validation framework.

Gunnar Morling said...

@Abdel,

JSR 303 doesn't make any restrictions, where the BV API might be used:

"The validation API developed by this JSR is not intended for use in any one tier or programming model. It is specifically not tied to either the web tier or the persistence tier, and is available for both server-side application programming, as well as rich client Swing application developers."

That said, in the specification appendix integration possibilities with JPA (object validation might occur before inserting/updating records in(to) the DB) and JSF are considered.

@JodaStephen

This looks interesting. Are their any plans to support the JSR 303 API with OVal?

Emmanuel Bernard said...

Nice post.

For the last example, I would have done it differently and made use of the @GroupSequence feature to sequence groups

public class CarValidityChecker {
ValidatorFactory factory; //injected somehow

/** completely validate an object
* runs extended constaints only if default pass
*/
@GroupSequence(Default.class, Extended.class)
public static interface Complete {}

public boolean checkCarValidity(Car car) {
Validator validator = factory.getValidator();

Set<ConstraintViolation<Car>> constraintViolations = validator
.validate(car, Complete.class);
return constraintViolations.isEmpty();
}

}

Emmanuel Bernard said...

Oval's lead, Sebastian is in the expert group so I expect Oval to embrace Bean Validation :)

Gunnar Morling said...

Hi Emmanuel,

thanks for pointing out the GroupSequence approach. It makes the given scenario definitely a bit easier.

Regards,

Gunnar

Nickname unavailable said...

Thank you, these posts (part 1 and 2) have helped me to understand JSR 303. Painless and easy to follow!

Can't wait to start using this in my Seam project ...

Gunnar Morling said...

Great that you like the posts. I'm also contributing to the reference guide of Hibernate Validator, so you might be interested in reading that, too.

Seam looks really promising, and I am looking forward to JSR 299 as well.

Dennis said...

Thanks for the nice explanation ;)

Mustafa AL-MATARY said...

Thanks for a good explanation

I sorry this is the first time to use JSR bean validation. i just need evaluate some of my study. i am quite confused where to save the Custom Constraint file (the interface) and the other validator class. could you please mention the names of the files and where to be stored..

thanks

Gunnar Morling said...

Mustafa, you can basically name these classes/files as you like. They must be stored in the source directory of your project, e.g. somewhere under src/main/java in case of Maven project. You might also find the Hibernate Validator reference guide helpful.

Anonymous said...

I have create my Custom Annotation to validate FileName and provided @validFileName as public Response getFile(@ValidFileName @QueryParam("fileName") String fileName);

Is there any location where we need to register @ValidFileName interface?

Abirami R said...

thank you...its helpful for understand