Showing posts with label Java EE6. Show all posts
Showing posts with label Java EE6. Show all posts

Friday, January 29, 2010

JSR 303 Bean Validation: @ValidateDateRange - A reusable constraint annotation to validate date ranges

In a previous blog-post, I have described my experience on JSR 303 Bean Validation, and in the process, this @ValidateDateRange constraint annotation came upon into my mind. I thought that it would be useful so it inspired me to create its implementation and eventually made this blog-post.

The @ValidateDateRange is a class-level constraint annotation that will validate a date range represented by two java.util.Date properties of an object. This can be reused in any class.
@ValidateDateRange(start="startDate", end="endDate")
public class MyEntity {
   private Date startDate;
   private Date endDate;
   ...
}

The annotation definition 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 java.util.Date;

import javax.validation.Constraint;
import javax.validation.ConstraintPayload;

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

public @interface ValidateDateRange {
    String message() default "{end} should be later than {start}";
    String start();
    String end();
    Class[] groups() default {};
    Class[] payload() default {};       
}
And the implementing DateRangeValidator are as follows:
package blogspot.soadev.validator;

import java.lang.reflect.Field;
import java.util.Date;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class DateRangeValidator implements ConstraintValidator{
    private String start;
    private String end;
    
    public void initialize(ValidateDateRange validateDateRange) {
        start = validateDateRange.start();
        end =  validateDateRange.end();
    }

    public boolean isValid(Object object,
                           ConstraintValidatorContext constraintValidatorContext) {
        try {
            Class clazz = object.getClass();
            Date startDate = null;
            Method startGetter = clazz.getMethod(getAccessorMethodName(start), new Class[0]);
            Object startGetterResult = startGetter.invoke(object, null);
            if (startGetterResult != null && startGetterResult instanceof Date){
                startDate = (Date) startGetterResult;
            }else{
                return false;
            }
            Date endDate = null;
            Method endGetter = clazz.getMethod(getAccessorMethodName(end), new Class[0]);
            Object endGetterResult = endGetter.invoke(object, null);
            if (endGetterResult == null){
                return true;
            }
            if (endGetterResult instanceof Date){
                endDate = (Date) endGetterResult;
            }
            return (startDate.before(endDate));           
        } catch (Throwable e) {
            System.err.println(e);
        }

        return false;
    }
    private String getAccessorMethodName(String property){
        StringBuilder builder = new StringBuilder("get");
        builder.append(Character.toUpperCase(property.charAt(0))); 
        builder.append(property.substring(1));
        return builder.toString();
    }
}

Related Posts


Cheers!

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!