JSR 303 - Bean Validation with OpenJPA - Part II

on July 25, 2011
  
In the previous article, we have seen how to use built-in constraints defined by Bean Validation Specification. In this article, we will see how to implement our own custom constraints.

If the built-in constraints do not meet your requirements, you can create your own custom validators and constraints. There are two ways of doing this, Implementing from scratch or Combine constraints from already existing constraints(Compound Constraints).

Implementing from scratch:
To implement a new constraint from scratch you first need to know how JSR-303 constraints work.

A constraint is basically a pair of an annotation and its associated validator class. When a bean is validated, it is being scanned for all the constraint annotations. Once such annotation is found its associated validator is created and initialized with the annotation (the annotation in a way serves as the configuration for its validator). How does the framework know which validator to instantiate? well... the annotation indicates it, and it's best explained by an example.

In this example, we will create a 'User' bean and validate the user name, whether it is reserved name or not. First, create a @ReservedUser annotation as shown below,
package com.bean.validators;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

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

    String message() default "This UserName is reserved word."; 
    //String message() default "{username.reserved.message}";
    Class[] groups() default {};
    public abstract Class[] payload() default {};
} 
An annotation type is defined using the @interface keyword. All the defined attributes in above annotation are specified in the specification and mandatory for all constraint annotations. The specification of the Bean Validation API demands that all constraint annotations should define the below attributes:
  • message: This attribute can be used to set a custom error message that will be displayed if the constraint defined by the annotation is not met. If we want to set a message bundle key instead of a literal message, we should surround it with braces. So we can set message to either "This user name is reserved word." or "{username.reserved.message}". See the below NOTE for more information on this.
  • groups: This attribute can be used to associate a constraint with one or more validation processing groups. Validation processing groups can be used to influence the order in which constraints get validated, or to validate a bean only partially.
  • payload: This attribute can be used to attach extra meta information to a constraint. The Bean Validation standard does not define any standard metadata that can be used, but specific libraries can define their own metadata.
In addition, we annotate the annotation type with a couple of meta annotations:
  • @Target({ METHOD, FIELD, ANNOTATION_TYPE }): Says that methods, fields and annotation declarations may be annotated with @ReservedUser (but not type declarations).
  • @Retention(RUNTIME): Specifies that annotations of this type will be available at runtime by the means of reflection.
  • @Constraint(validatedBy = UserValidator.class): Specifies the validator to be used to validate elements annotated with @ReservedUser.
  • @Documented: Says that the use of @UpperCase will be contained in the JavaDoc of elements annotated with it.
NOTE: The specification define quite a powerful message interpolation mechanism for the error messages. The message can contain placeholders (surrounded by curly brackets) which can be replaced by the attributes defined in the annotation itself. Furthermore, the placeholders can also refer to keys defined in a resource bundle. In the later case, the placeholders will be replaced by their associated values in the bundle(see the commented line above). For example, as we did in the @ReservedUser annotation, it is considered a best practice to assign a default message value. This value actually consists of one parameter placeholder which refers to a key in a resource bundle ("username.reserved.message"). When the error message is resolved, the value of this key is looked up in the resource bundle and when found replaces the placeholder. By default, the validator will look for a resource bundle named ValidationMessages.properties in the classpath.
    Next, we need to implement a Constraint Validator, which is able to validate elements with the @ReservedUser annotation. To do so, we have to implement the interface ConstraintValidator as shown below,
    package com.bean.validators;
    
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    
    public class UserValidator implements ConstraintValidator<ReservedUser, String> {
        public void initialize(ReservedUser constraintAnnotation) {
             //nothing to do
        }
        public boolean isValid(String username, ConstraintValidatorContext cvc) {
            if (username == null){
                return true;
            }else{
                if(username.equalsIgnoreCase("SATISH"))
                   return false;
                else
                   return true;
            }
        }
    }
    
    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 ReservedUser), 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 annotation), but as @ReservedUser doesn't define any such attributes, we have nothing to do here.

    What's interesting for us, is the isValid() method. It determines whether a given object is valid according to the @ReservedUser annotation or not.

    Next, create 'User.java' bean and define the custom constraint for username as shown below,
    package com.sample.bean;
    
    import com.bean.validators.ReservedUser;
    
    public class User {
     
     @ReservedUser(message = "Please use another username.")
     private String userName;
     private String userId;
     private int age;
     public String getUserName() {
      return userName;
     }
     public void setUserName(String userName) {
      this.userName = userName;
     }
     public String getUserId() {
      return userId;
     }
     public void setUserId(String userId) {
      this.userId = userId;
     }
     public int getAge() {
      return age;
     }
     public void setAge(int age) {
      this.age = age;
     }
    } 
    
    Now, create 'UserTest.java' program to test the validation as shown below,
    package com.bean.test;
    
    import static java.lang.System.out;
    import java.util.Calendar;
    import java.util.Iterator;
    import java.util.Set;
    import javax.validation.ConstraintViolation;
    import javax.validation.Validation;
    import javax.validation.Validator;
    import javax.validation.ValidatorFactory;
    
    import com.sample.bean.User;
    
    public class UserTest {
     
     private static Validator validator;
    
     public static void main(String args[]) {
            ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
            validator = factory.getValidator();
            Calendar cal = Calendar.getInstance();
            User u = new User();
            u.setUserName("satish");
            Set<ConstraintViolation<User>> constraintViolations = validator.validate(u);
            int count = constraintViolations.size();
            out.println("Total voilations: "+count);        
            Iterator it = constraintViolations.iterator();
            while(it.hasNext()){
                ConstraintViolation cv=(ConstraintViolation)it.next();
                out.println(cv.getMessage());
            }        
        }
    } 
    
    Thats it, you're done. You can test it by changing the name in setUserName().

    Compound Constraints:
    Sometimes there is no real need to create constraint entirely from scratch. It is often the case where a constraint is either a specific variation of another constraint, or a combination of other constraints. The specification acknowledges that and makes it even easier to define such constraints.

    So, let's compose a new constraint annotation @ValidUserName, that comprises the constraints @NotNull, @Size and @ReservedUser as shown below,
    package com.bean.validators;
    
    import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
    import static java.lang.annotation.ElementType.FIELD;
    import static java.lang.annotation.ElementType.METHOD;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    import java.lang.annotation.Documented;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    import javax.validation.Constraint;
    import javax.validation.Payload;
    import javax.validation.ReportAsSingleViolation;
    import javax.validation.constraints.NotNull;
    import javax.validation.constraints.Size;
    
    @NotNull
    @Size(min = 2, max = 14)
    @ReservedUser
    @Target( { METHOD, FIELD, ANNOTATION_TYPE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = {})
    @Documented
    @ReportAsSingleViolation
    public @interface ValidUserName {
    
        String message() default "This Username is not Valid.";
        Class<?>[] groups() default {};
        public abstract Class<? extends Payload>[] payload() default {};
    } 
    
    As you can see, no validator is associated with this annotation. Instead, it is annotated with the @ReservedUser annotation which holds the reserved username validation. By default, when the validation is performed, all the constraints are evaluated (in our case, @NotNull, @Size, @ReservedUser constraints) and register any encountered violations. Sometimes you'd like only one error message to be reported. This is where the @ReportAsSingleViolation annotation becomes useful. This annotation indicates that on any constraint violation of any of the constraints, only one violation will be reported and all other reported violations will then be ignored.

    Now, we have created a compound constraint @ValidUserName. We can define only this constraint for validating username instead of @NotNull, @Size and @ReservedUser as shown below,
    package com.sample.bean;
    
    import com.bean.validators.ValidUserName;
    
    public class User {
     
     @ValidUserName
     private String userName;
    
            //Other fields
    } 
    
    In the next article, we will see about Validation Groups.
         

    0 comments:

    Post a Comment