Tuesday, January 26, 2010

JSR 303 Bean Validation: @AssertMethodAsTrue - A reusable constraint annotation that spans multiple properties.

I have made my own hands-on on this Bean Validation based on the [hibernate-validator-4.0.0.Beta2] and the experience was great. One thing that I have observed though is that there is no built-in annotation to check for a constraint spanning multiple properties, or to check if a certain property is valid based on other properties. One obvious example to this is validating date ranges where you would usually have two properties like startDate and endDate where it doesn't make sense to have a startDate property that is later than the endDate. You could say - why not create a new class-level constraint and
apply it like below:
@ValidateDateRange(start="startDate", end="endDate")
public class MyEntity {
   private Date startDate;
   private Date endDate;
   ...
}
Well, this is good because it is likely that I can reuse this constraint in other entities needing validation on date ranges (I just realized in writing this that it also interesting to post a blog on an implementation of the @ValidateDateRange annotation above). The following are some of the annotations which are reusable to other entities.
  • @NotNull
  • @NotEmpty
  • @Size
  • @Min
  • @Max
  • @DecimalMin
  • @DecimalMax
  • @Email
  • @Pattern
But how about other validation spanning multiple properties that is not likely to be reused in other entities (just like the sample class-level constraint @ValidPassengerCount in the Bean Validator documentation)? I believe, that to create two additional classes to support such simple validation requirement of an specific entity is too much work especially that it is most likely that in addition to checking if the number of passengers is valid , you will also have two or more other validation requirements on the same entity. For example, if I have three business logic for an entity to be validated, it means creating more or less six additional classes to support the three constraint validation. The sad thing is, I cannot reuse such custom constraint validations to other entities.

The Bean Validation reference implementation has @AssertTrue annotation which can be applied to a property or getter method, but an exception is raised if you will try to access other properties value inside a getter method.

So how then could we easily validate a business logic constraint that spans multiple properties without too much work? Our answer is the reusable @AssertMethodAsTrue class-level constraint annotation that accepts a method name as a parameter. The method name that you set as parameter should return a boolean value. An example would be:
@AssertMethodAsTrue(value="isPassengerCountValid", message="Invalid passenger count!")
public class Car {
    @Min(2)
    private int seatCount;
    @NotNull
    List passengers;

    public boolean isPassengerCountValid(){
        if(this.seatCount >= passengers.size()){
        return true;
        }
        return false;
    }
}
The definition of the @AssertMethodAsTrue annotation are as follows:
package blogspot.soadev.validator;

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.ConstraintPayload;

@Target( { TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = {AssertMethodAsTrueValidator.class} )
@Documented

public @interface AssertMethodAsTrue {
    String message() default "{value} returned false";
    String value() default "isValid";
    Class[] groups() default {};
    Class[] payload() default {};       
}

And the corresponding implementing validator class that use reflection:
package blogspot.soadev.validator;

import java.lang.reflect.Method;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class AssertMethodAsTrueValidator implements ConstraintValidator{    
    private String methodName;
    
    public void initialize(AssertMethodAsTrue assertMethodAsTrue) {
        methodName =  assertMethodAsTrue.value();
    }
    public boolean isValid(Object object,
                           ConstraintValidatorContext constraintValidatorContext) {
        
        try {
            Class clazz = object.getClass();
            Method validate = clazz.getMethod(methodName, new Class[0]);
            return (Boolean) validate.invoke(object);
        } catch (Throwable e) {
            System.err.println(e);
        }
        return false;
    }
}
Since it is also most likely that you will have more than one method to validate, then an @AssertMethodAsTrueList would also be needed. The code are as follows:
package blogspot.soadev.validator;
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;

@Target(value={TYPE, ANNOTATION_TYPE})
@Retention(value=RUNTIME)
@Documented

public @interface AssertMethodAsTrueList {
    AssertMethodAsTrue[] value() default {};
}
With the @AssertMethodAsTrueList above you can have multiple @AssertMethodAsTrue on a single entity:
@AssertMethodAsTrueList({
    @AssertMethodAsTrue(value="isPassengerCountValid", message="Invalid passenger count!"),
    @AssertMethodAsTrue(value="isTirePressureIdeal")
})
public class Car {
    ...
    public void isPassengerCountValid(){...}
    public void isTirePressureIdeal(){...}
}

Related Posts

Cheers to the JSR 303 Bean Validation!

6 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Hi Pino,

    thanks for the informative post. I recently wrote on a similar approach in my blog, too:


    http://musingsofaprogrammingaddict.blogspot.com/2010/02/generic-class-level-constraint-for-bean.html


    If you like, check it out - I'd be interested in your feedback.

    Gunnar

    ReplyDelete
  3. Thanks for the feedback Gunnar. I will sure look in to your post. FYI- I was also able came by your blog for sometime before.

    regards,

    Pino

    ReplyDelete
  4. Couldn't you just use the AssertTrue annotation on a method like:

    @AssertTrue(message="Error Message")
    private boolean isValid(){
    ...validation logic...
    }

    Or are you trying to make it so error messages are more specific to each individual situation?

    ReplyDelete
  5. Hi Dan,
    You cannot use the @AssertTrue on a validation that span multiple properties, like for example if you are checking if property1 is greater than property2 inside your method, then you will receive exception.

    Pino

    ReplyDelete
  6. Actually you can, why would you receive an exception?

    ReplyDelete