Pages

Sunday, April 11, 2010

JSR 303 - Overview, part 3

Table of contents:
Introduction

The JSR 303 API

The JSR 303 API is made of the following functional subsets:
  • The bootstrap SPI
  • The validator API (which itself is made up the client API and the validation routine)
  • Important helper classes and interfaces used during the validation process such as the ConstraintViolation class and the message interpolators.
  • The constraint metadata request API

The API a JSR 303 will be most interested in is probably the validation API, which is the API used to trigger the validation process.

The bootstrap process: how to obtain a Validator

During the bootstrap process the service provider which implements the JSR 303 is discovered, configured and initialized. I won't cover the details of the bootstrapping API and if you're interested, you can check the specification. From an user standpoint, this API is important because it's the entry point to the validation API. After discovering and configuring all the available providers, you'll use the Validation class to obtain an instance of a ValidatorFactory.

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

The validator API

The validator API defines a series of interfaces which let an user programmatically trigger the validation process on a bean of a given class. The Validator interfaces is the following:

public interface Validator {
  <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups);
  <T> Set<ConstraintViolation<T>> validateProperty(T object, String propertyName, Class<?>... groups);
  <T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType, String propertyName, Object value, Class<?>... groups);
  BeanDescriptor getConstraintsForClass(Class<?> clazz);
}

The first three methods of this interfaces trigger the validation process on an object of type T whose logic was described in Part 2. While the first method triggers the complete validation process, the other two methods can be used to validate just a property. The third is a convenience methods which trigger the validation of a bean's property if its value were the object passed as parameter. This way you'll be able to validate a property beforehand and, most importantly, without triggering any side effect of the property change or in the case the property were read only.

No matter which validate method you choose it will return a set of contraint violations for the type T. A constraint violation is a type which is part of the JSR 303 API whose purpose is describing the errors found during the validation process. Such a type will be described later on.

The last method of the Validator interface is used to retrieve a BeanDescriptor, which is a structure which describes the constraints applied to a specific class' instances. You usually won't use this method in your daily life with a JSR 303 implementation unless you are building sophisticated logic which has to rely to such information at runtime.

Also, you won't be using the validator API directly when using API which can automatically validate beans. Such APIs are, for example, JPA and JSF with CDI beans and those are the use cases I'll be focusing on.

Defining a constraint

To define a constraint the first thing you need to do is defining the constraint annotation you're going to use. For example, let's define an annotation to validate a String instance representing an email address.

package es.reacts.validators;

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;

/**
 *
 * @author http://thegreyblog.blogspot.com/
 */
@Documented
@Constraint(validatedBy = EmailValidator.class)
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EmailAddress {

    String message() default "{email.invalid}";

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

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

It's important to note the following:
  • The @Constraint annotation is used to declare that the following annotation will be used as a constraint annotation. The validatedBy attribute of the @Constraint annotation is used to declare the class of the associated constraint validator, in this case the class EmailValidator.
  • The standard @Target annotation is used to indicate which kind of element type this annotation will be used with. A JSR 303 compliant implementation will let you define and use constraint for the FIELD, METHOD and TYPE element types. In this example an email address is represented by a String instance so that TYPE is not specified in the constraint definition. If we were to define a class to represent an email address, we could define a constraint and a validator to annotate our class.
  • The required RUNTIME @Retention policy is necessary so that the JSR 303 implementation be able to read validation metadata at runtime.
  • The message() element is used to return a message in case of a validation error. The JSR 303 supports a flexible mechanism to define messages and this example uses the standard "{...}" notation to internationalize our messages in a properties resource bundle.

Defining a constraint validator

Constraint validators are instances of the ConstraintValidator<T, V> class, being T the type of the constraint and V the type of the validated objects. In this case, T is EmailAddress and V is String so that the skeleton of our validator will be:

package es.reacts.validators;

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

/**
 *
 * @author http://thegreyblog.blogspot.com/
 */
public class EmailValidator implements ConstraintValidator<EmailAddress, String> {

    public void initialize(EmailAddress constraintAnnotation) {
    
    }

    public boolean isValid(String value, ConstraintValidatorContext context) {

    }
}

The initialize method is used to initialize the validator during the validation process: if you're defining attributes in your constraint, the initialize method body will scan the constraint passed as a parameter and save it in its state. The isValid method will validate the current object. Since our @EmailAddress constraint contains no attributes, the validator will only need to implement the validation logic in the isValid method and, for the sake of the current example, we'll use a standard regular expression to check if the current String instance represents a valid email address:

package es.reacts.validators;

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

/**
 *
 * @author http://thegreyblog.blogspot.com/
 */
public class EmailValidator implements ConstraintValidator<EmailAddress, String> {

  private static final Pattern emailPattern = Pattern.compile("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?");

    public void initialize(EmailAddress constraintAnnotation) {
    
    }

    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }

        Matcher match = emailPattern.matcher(value.toLowerCase());

        return match.matches();
    }
}

Since Pattern instances are thread-safe, our EmailValidator will take advantage of it by caching the regular expression used to validate email addresses. The isValid method simply checks that the String instance is not null (in which case the email address is considered to be valid, as suggested by the JSR 303 spec), and uses a Matcher instance to match it against our pattern.

Although a bit primitive, this is a sound constraint validator and you can take advantage of it immediately by annotating the fields of your beans. In my case, for example, I'm using it in a JPA entity and in a CDI bean which backs a JSF page in which users are submitting their email address. Without additional code, I'm taking advantage of JSF, JPA and JSR-303 to enforce a validation policy on both the web channel and the persistence layer of my Java EE 6 application.









No comments:

Post a Comment