Wednesday, January 21, 2015

Create Your Own Java Bean Validator with a Custom Element

Abstract
The purpose of this article is to demonstrate how to do two things.  The first is how to create your own Java bean validator as specified by JSR 349: Bean Validation 1.1.  The second is adding your own custom element to this validator so as to effect or define the validator's behavior.

System Requirements
This example was developed and run using the following system setup. If yours is different, it may work but no guarantees.
  • JDK 1.7.0_11
  • Eclipse Luna
  • Maven 3.2.1 (Embedded with Eclipse)
Maven Dependencies
The following dependencies are needed in your pom.xml to work with bean validation:

javax.validation
validation-api
1.1.0.Final


org.hibernate
hibernate-validator
5.1.2.Final


com.fasterxml
classmate
1.1.0

What is Bean Validation
JSR 349: Bean Validation 1.1 is the Java standard allowing you to develop reusable constraints and validators to ensure your application's data meets its business requirements. This may sound complicated, but it's actually very simple. Suppose your application is required to get a user's email address before successfully registering them for a new account. Traditionally, a developer would need to manually code this business requirement. Using JSR 349, the same business requirement can be satisfied using a reusable @NotNull constraint from the bean validation framework. Listing 1 demonstrates this.

Listing 1: Validate user's email address is not null
public class User {
@NotNull
protected String email;
// class continues...
}

As you can see from listing 1 the @NotNull constraint and the JSR 349 Bean Validation framework are responsible for validating the email address.  Additionally, the @NotNull constraint has no specific ties to email address meaning it can be reused to validate a users first name, last name, date of birth, or whatever data your business requirements say the user must provide.

The bean validation framework is very powerful in that it's also extensible. @NotNull is a very common constraint and as such is provided by the framework. However your application will no doubt have very specific business requirements which the built-in constraints cannot handle. Let's look at an example of such a requirement and how we can build our own constraints and validators to handle it.

Specific Business Requirement
Suppose your application needs to interact with the computer's file system. The user must select a directory but in order to guarantee your application does not inadvertently modify any existing files, it's required that the directory the user selects must be completely empty. This is a good example of data validation that is very specific to your application and which the bean validation framework does not handle out of the box. Remember though the bean validation framework is extensible. It's possible to create a constraint and validator to satisfy this business requirement.  Let's first look at creating the constraint.

Constraint
A bean validation constraint is a simple annotation. Listing 2 shows a @DirectoryIsEmpty validation constraint annotation we'll use to satisfy our application's business requirement.

Listing 2: DirectoryIsEmpty bean validation constraint
package org.ferris.validation.constraints;

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;
import org.ferris.validation.validator.DirectoryIsEmptyValidator;

@Target({ 
   ElementType.TYPE, ElementType.ANNOTATION_TYPE
 , ElementType.METHOD, ElementType.FIELD
 , ElementType.PARAMETER, ElementType.CONSTRUCTOR })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = { DirectoryIsEmptyValidator.class })
@Documented
public @interface DirectoryIsEmpty {
    String message() default "{org.ferris.validation.constraints.DirectoryIsEmpty.message}";

    Class[] groups() default { };

    Class[] payload() default { };
    
    String[] excludes() default {};
}

Let's take a look at this constraint in detail.

#12 The @Target annotation defines what elements the @DirectoryIsEmpty can be put on.  In general, the annotation can either be put at the class-level, property-level, or parameter-level.  At the class-level, the validation occurs on the class as a whole. This is a way to validated properties which go together, like password and verifiedPassword properties need to be the same. At the property-level, the annotation can either go on the property or on the getter method for the property. At the parameter-level, the annotation goes on a constructor parameter or a method parameter. Typically the @Valid annotation is used at this level so the bean validation framework can validate properties before executing the constructor or method.

#17 The @Constraint annotation tells the bean validation framework which class is responsible for validating this constraint.  Here it defines the DirectoryIsEmptyValidator.class.

#19 @interface DirectoryIsEmpty is the standard way to define an annotation.

#20 The message element is required on bean validation constraint annotations. This element is used by the framework to determine what error message should be displayed if validation fails. You can hard code a plain text message, or, as is seen here, enter a resource bundle key as "{key_value}". By convention, the key value is the fully-qualified-class-name + ".message". The bean validation framework will look in the class path for a ValidationMessages.properties file.  This file is located at the root of the class path.

#22 The groups element is required on bean validation constraint annotations. This is used to group validation constraints together. For example, a field may have four validation constraints divided into three different groups. The @GroupSequence annotation can be used to define the sequence of the validation constraints.  This is helpful if a constraint validation needs to happen in a certain order.

#24 The payload element is required on bean validation constraint annotations, however it is not directly used by the bean validation framework itself. If a payload is present and a constraint violation occurs, the bean validation framework will put the payload into the ConstraintViolation#ConstraintDescriptor object for later use. If a payload is not present, the bean validation framework puts nothing into the object. A payload is a way to pass additional information about the constraint violation to the UI layer (or to any code) responsible for handling the violation. For example, a severity can be assigned to the payload to change the alerting level handling the violation.

#26 The excludes element is custom to this annotation. Just like any annotation, you are able to create your own custom elements in order to pass additional information to the validator to customize the validation. In this example, the excludes element is a String[] and it is used to define the names of directories which are excluded from the constraint validation. For example, suppose you have @DirectoryIsEmpty(excludes={"foo", "bar"}). At validation, the directory will NOT be checked if the name of the directory is either "foo" or "bar".

A constraint annotation is only the first part to creating your own bean validator. As seen on line #17, a class to do the validation is needed. In this example it is DirectoryIsEmptyValidator.class.  Let's take a look at this class next.

Validator
A validator class is the next piece you need to creating your own bean validation after creating the validation constraint annotation.  The annotation is used on whatever class, property, or parameter you want to validate. The validator class is responsible for doing the actual validation. Given our @DirectoryIsEmpty validation constraint annotation, listing 3 shows its validator

Listing 3: DirectoryIsEmptyValidator validator
package org.ferris.validation.validator;

import java.io.File;
import java.util.Arrays;
import java.util.List;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.ferris.validation.constraints.DirectoryIsEmpty;

public class DirectoryIsEmptyValidator 
implements ConstraintValidator< DirectoryIsEmpty, File > {

 private List< String > excludes;
 
 @Override
 public void initialize(DirectoryIsEmpty arg0) {
  String [] strarr = arg0.excludes();
  if (strarr == null) {
   strarr = new String[]{};
  }
  excludes = Arrays.asList(strarr);
 }

 @Override
 public boolean isValid(File file, ConstraintValidatorContext context)
 {
  if (excludes.contains(file.getName())) {
   return true;
  }
  File [] files = file.listFiles();
  if (files != null && files.length > 0) {
   return false;
  } else {
   return true;
  }
 }
}

Let's take a look at this validator in more detail.

#11 Here we see the class implements the ConstraintValidator interface with the specific generics being DirectoryIsEmpty, and FileDirectoryIsEmpty obviously refers to the @DirectoryIsEmpty validation constraint annotation. File defines the @DirectoryIsEmpty annotation must be put on a class that extends File or on a File property or parameter. This of course make sense because our business requirement states the directory picked by the user must be empty if it is to be valid. Directories in Java are represented by the File object.

#13 This property will be used to hold a list of directory names excluded from the validator.

#16 The initialize method is part of the ConstraintValidator interface and can be overridden to do custom initialization of the validator.

#17 Here the excludes() method is called to get the String[] holding the names of the directories to exclude from validation. The excludes element may or may not be defined so the validator must handle both cases.  If the excludes element is defined and the excludes() method call returns a String[] of directory names to be excluded from validation, then the validator must ensure it does not validate those directories. If the excludes element is not defined and the excludes() method call returns nothing, then the validator should not exclude any directory from validation.

#25 The isValid method is part of the ConstraintValidator interface. This is where the magic happens. Code this method to perform any validation you need. Return true if valid, false otherwise.

#27 The code checks the excludes property to see if the list contains the name of the directory being validated. If the list does contain the name, then return true (#28) because that directory should be excluded from validation which means it is automatically valid.

#30 Use the File#listFiles method to get a list of all files and sub-directories. If there are files or sub-directories (#31) return false because the directory the user selected is not empty and fails the business requirement. Otherwise, return true (#34) because the directory the user selected is empty and this satisfies the business requirement.

You've learned how to create your own validation constraint annotation and its associated validator. You've also learned how easily the bean validation framework is extended and hopefully you recognize the great opportunities this gives you.

References
Bernard, Emmanuel. (2013, April). Bean Validation specification. beanvalidation.org. Retrieved January 21, 2015, from http://beanvalidation.org/1.1/spec/

JSR 349: Bean Validation 1.1. (n.d.). Java Community Process. Retrieved January 21, 2015, from https://jcp.org/en/jsr/detail?id=349

Enjoy!

No comments:

Post a Comment