Kiss My App

Friday, December 10, 2004

Validation via Annotation

I've been beating around the bush with posting some of my ideas for validation via annoations. The implementation is extremely simple and offers a protocol independent way of validating input on setter calls. If the validation stuff doesn't interest you, the method of plugging annotations might be applicable to other things.


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Validate {
Class<? extends ValidationHandler> value();
}


Notice that this annotation is meant for annotating other annotations (!) and has a single 'class' value which will be used to handle validation.


public interface ValidationHandler<T> {
public void validate(T settings, Object value) throws ValidationException;
public Class<T> getSettingsType();
}


This is where the validation logic goes. The ValidationHandler is generic and is defined for a specific settings type-- which will be an annotation in our example. To give you some insight at this point, a 'settings' annotation would have minimum and maximum lengths for a String on some bean property's setter, such as 'setLogin(String login)'. Now for an implementation:


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Validate(ValidateExprHandler.class)
public @interface ValidateExpr {
String value();
}

public class ValidateExprHandler
implements ValidationHandler<ValidateExpr> {

public void validate(ValidateExpr validateExpr, Object value)
throws ValidationException {
String i = (value != null) ? value.toString() : "";
Pattern p = Pattern.compile(validateExpr.value());
Matcher m = p.matcher(i);

if (!m.matches()) {
throw new ValidationException(i
+ " does not match the pattern "
+ validateExpr.value());
}
}

public Class getSettingsType() {
return ValidateExpr.class;
}
}


The above example would be used to validate something like an email address or some other user input. The ValidateExpr annotation is annotated with the Validate annotation with the class value of its handler-- ValidateExprHandler.class. Again, the annotation defines what class type is going to handle it. Annotating your object becomes quite simple once you've written a validate annotation and it's handler (two type declarations and two methods).


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

@ValidateRequired
public String getPassword() {
return password;
}


The final piece to the puzzle is the code to introspect and evaluate the handlers. For brevity's sake, I'm just including the tricky part of it all.


public static void validateProperty(PropertyDescriptor p, Object value)
throws ValidationException {
Method m = p.getWriteMethod();
if (m != null) {
Annotation[] a = m.getAnnotations();
Validate v = null;
Annotation s = null;
ValidationHandler h = null;
for (int i = 0; i < a.length; i++) {
v = a[i].annotationType().getAnnotation(Validate.class);
if (v != null) {
try {
h = v.value().newInstance();
s = m.getAnnotation(h.getSettingsType());
h.validate(s, value);
} catch (InstantiationException ie) {
} catch (IllegalAccessException iae) {
}
}
}
}
}


You may be thinking that there's too much reflection going on, but annotations are statically typed, so you wouldn't have to think twice about caching all the validators for an object.

I was thinking this same kind of annotation could be used for converters in the view, where again, we define a single Converter annotation that has a ConversionHandler type with two methods: stringToObject(T settings, String str) and objectToString(T settings, Object obj).

The reason why I like this way of providing behavior is that by annotating, we have not changed the behavior of the object itself. In addition, you have type safety and IDE support and the implementation is protocol generic so it can be used within JSF, Struts, or before persisting.



1 Comments:

Post a Comment

<< Home