Tuesday, 25 May 2010

Creating a custom JSR 303 constraint annotation with Spring 3

Spring 3.0 has support for JSR303 annotation-driven validation, which makes it easy to build declarative validation into Java Beans in Spring applications. Out of the box, JSR303 supports annotations such as @NotNull and @Size etc which allow you to perform basic validation checks. However, most applications will need to do more sophisticated "business-logic" validation such as checking whether an email already exists in a database. I recently needed to do just that in a Spring MVC application that needed a "Forgot Password" function to let users receive an email with their password. The Spring MVC command object was very simple:
public class SendPasswordReminderCommand implements Serializable {
 
 private static final long serialVersionUID = 42L;

 private String email;

 public String getEmail() {
  return email;
 }

 public void setEmail(String email) {
  this.email = email;
 }

      // equals, hashcode, toString, constructors omitted
}
Annotating this class with JSR 303 annotations to check the format of the email or ensure that the email is not null is easy enough (using @NotNull, @Length from the core javax.validation.constraints standard package and @Email from the org.hibernate.validator.constraints package). Ideally, you would also be able to check the database and ensure that the email exists in the database and inform the user if not as part of the validation. To do this, having a custom annotation called @EmailExistsConstraint would be ideal - but you need to make your own!
public class SendPasswordReminderCommand implements Serializable {
 
 private static final long serialVersionUID = 42L;
 
 @NotNull
 @Length(min=1)
 @Email
 @EmailExistsConstraint
 private String email;

 public String getEmail() {
  return email;
 }

 public void setEmail(String email) {
  this.email = email;
 }
}
Creating the custom constraint requires you to create two classes - an annotation interface and associated constraint validator. The annotation is straightforward.

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)
@Documented
@Constraint(validatedBy=EmailExistsConstraintValidator.class)

public @interface EmailExistsConstraint {
 String message() default "Email doesn't exists";
 Class[] groups() default {};
 Class[] payload() default {};
}

The constraint validator is where you perform the database check. As this is a Spring and Hibernate application, the constraint validator uses a Hibernate DAO as a "collaborator" to perform the check.
public class EmailExistsConstraintValidator implements ConstraintValidator {

 private Log log = LogFactory.getLog(EmailExistsConstraintValidator.class);
 
 @Autowired
 private EmployeeFinder employeeFinder;
 
 @Override
 public void initialize(EmailExistsConstraint constraint) {
  
 }

 @Override
 public boolean isValid(Object target, ConstraintValidatorContext context) {
  
  try {
   Collection employees = employeeFinder.findEmployeeByEmail((String) target);
   if (employees.size() > 0) {
    return true;
   }
  } catch (Exception e) {
   log.error(e);
  }
  return false;
 }

 public void setEmployeeFinder(EmployeeFinder employeeFinder) {
  this.employeeFinder = employeeFinder;
 }
}

This is a fairly straightforward constraint validator implementation. If the database contains a record matching the email address, the isValid() method returns true and false if not. All that Spring needs to use JSR303 is to register the Spring validator implementation in the application context. It then automatically detects any custom constraints and will validate classes using your annotations without any further configuration.

However, there is still one problem with this. As it stands, the Validator will validate all of the annotations in one pass. This is a problem, because the database check will occur even if the email fails all of the other checks which is wasteful and potentially confusing to the user, who would see multiple validation messages. Thankfully, the JSR303 specification contains the concept of groups, which can be used to lump annotations together and process them in a specified order. The example below illustrates how groups can be used to split the basic format checks from the database check.
@GroupSequence(value={SendPasswordReminderCommand.class, FormatChecks.class,BusinessLogicChecks.class})
public class SendPasswordReminderCommand implements Serializable {
 
 private static final long serialVersionUID = 42L;
 
 @NotNull(groups=FormatChecks.class)
 @Length(min=1, groups=FormatChecks.class)
 @Email(groups=FormatChecks.class)
 @EmailExistsConstraint(groups=BusinessLogicChecks.class)
 private String email;

 public String getEmail() {
  return email;
 }

 public void setEmail(String email) {
  this.email = email;
 }
}
Firstly, each annotation has a group parameter that specifies a class (in this example, FormatChecks.class and BusinessLogicChecks.class). These classes are marker interfaces that are simply used to identify groups. To specify the sequence in which annotation groups are processed, the @GroupSequence annotation is used. An array of classes is passed to the values parameter that starts by convention with the class being validated, followed by the group marker interfaces in the order that you wish to process them. The marker interfaces are very straightforward. One of the interfaces must extend the javax.validation.groups.Default marker interface, which as you may guess is the default group if no group is specified.
public interface FormatChecks extends Default {

}

public interface BusinessLogicChecks {

}
You can define as many groups as you want. As you can see, creating custom constraint annotations is pretty straightforward with Spring 3.0. JSR 303 is well thought out and the ability to group constraints is very useful for ensuring that expensive business logic checks are not performed without reason, when validation has already failed for basic data format reasons.