Next I include a modified version of ClassValidator that uses Field level annotations and @Description to be added to InvalidValue bean:
ClassValidator
Code:
package org.hibernate.validator;
import org.hibernate.MappingException;
import org.hibernate.annotations.*;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import java.beans.Introspector;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.StringTokenizer;
/**
* Engine that take a bean and check every expressed annotation restrictions
* @author Gavin King
*/
public class ClassValidator<T> {
private final Class<T> beanClass;
private final List<Validator> beanValidators = new ArrayList<Validator>();
private final List<Validator> propertyValidators =
new ArrayList<Validator>();
private final List<Method> propertyGetters = new ArrayList<Method>();
private final List<Validator> propertyFieldsValidators =
new ArrayList<Validator>();
private final List<Field> propertyFields = new ArrayList<Field>();
private final Map<Validator, String> messages =
new HashMap<Validator, String>();
private final ResourceBundle messageBundle;
/**
* create the validator engine for this bean type
* @param beanClass bean Class to be validated
*/
public ClassValidator(Class<T> beanClass) {
this(beanClass, null);
}
/**
* create the validator engine for a particular bean class, using a resource bundle
* for message rendering on violation *
* @param beanClass bean Class to be validated
* @param resourceBundle The resource bundle to get messages from
*/
public ClassValidator(Class<T> beanClass, ResourceBundle resourceBundle) {
this.beanClass = beanClass;
this.messageBundle = resourceBundle;
Annotation[] classAnnotations = beanClass.getAnnotations();
for (int i = 0; i < classAnnotations.length; i++) {
Annotation classAnnotation = classAnnotations[i];
Validator beanValidator = createValidator(classAnnotation);
if (beanValidator != null)
beanValidators.add(beanValidator);
}
Method[] methods = beanClass.getMethods();
for (int i = 0; i < methods.length; i++) {
Method method = methods[i];
Annotation[] methodAnnotations = method.getAnnotations();
for (int j = 0; j < methodAnnotations.length; j++) {
Annotation methodAnnotation = methodAnnotations[j];
Validator propertyValidator = createValidator(methodAnnotation);
if (propertyValidator != null) {
propertyValidators.add(propertyValidator);
propertyGetters.add(method);
}
}
}
// Get annotations for all declared Fields
Field[] fields = beanClass.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
Annotation[] fieldAnnotations = field.getAnnotations();
for (int j = 0; j < fieldAnnotations.length; j++) {
Annotation fieldAnnotation = fieldAnnotations[j];
Validator propertyFieldsValidator = createValidator(
fieldAnnotation);
if (propertyFieldsValidator != null) {
propertyFieldsValidators.add(propertyFieldsValidator);
propertyFields.add(field);
}
}
}
}
// private static final Set<String> methodNames = new HashSet<String>();
// static {
// methodNames.add("toString");
// methodNames.add("hashCode");
// methodNames.add("equals");
// methodNames.add("annotationType");
// }
private Validator createValidator(Annotation annotation) {
try {
ValidatorClass validatorClass = annotation.annotationType()
.getAnnotation(ValidatorClass.class);
if (validatorClass == null)
return null;
Validator beanValidator = validatorClass.value().newInstance();
beanValidator.initialize(annotation);
String messageTemplate = (String) annotation.getClass()
.getMethod("message").invoke(annotation);
String message = replace(messageTemplate, annotation);
messages.put(beanValidator, message);
return beanValidator;
} catch (Exception e) {
throw new IllegalArgumentException(
"could not instantiate ClassValidator", e);
}
}
/**
* apply constraints on a bean instance and return all the failures.
* @param bean Bean object to validate
* @return array of InvalidValues
*/
public InvalidValue[] getInvalidValues(T bean) {
return getInvalidValues(bean, null);
}
/**
* apply constraints on a method from a bean instance and return all the failures.
* @param bean Bean object to validate
* @param method Method to validate
* @return array of InvalidValues
*/
public InvalidValue[] getInvalidValues(T bean, String method) {
if (!beanClass.isInstance(bean)) {
throw new IllegalArgumentException("not an instance of: " +
bean.getClass());
}
List<InvalidValue> results = new ArrayList<InvalidValue>();
if (method == null) {
for (int i = 0; i < beanValidators.size(); i++) {
Validator validator = beanValidators.get(i);
if (!validator.isValid(bean)) {
results.add(new InvalidValue(messages.get(validator),
beanClass, null, bean, bean, null));
}
}
}
for (int i = 0; i < propertyValidators.size(); i++) {
Method getter = propertyGetters.get(i);
String propertyName = getPropertyName(getter);
if ((method != null) && !method.equalsIgnoreCase(propertyName)) {
continue;
}
Object value;
try {
value = getter.invoke(bean);
} catch (Exception e) {
throw new IllegalStateException("could not get property value",
e);
}
Validator validator = propertyValidators.get(i);
if (!validator.isValid(value)) {
String propertyDescription = getPropertyDescription(getter);
results.add(new InvalidValue(messages.get(validator), beanClass,
propertyName, value, bean, propertyDescription));
}
}
for (int i = 0; i < propertyFieldsValidators.size(); i++) {
Field field = propertyFields.get(i);
String propertyName = field.getName();
if ((method != null) && !method.equalsIgnoreCase(propertyName)) {
continue;
}
Object value;
try {
value = field.get(bean);
} catch (IllegalAccessException e) {
throw new IllegalStateException("could not get property value",
e);
//} catch (java.lang.reflect.InvocationTargetException e) {
// throw new IllegalStateException("could not get property value",
// e);
}
Validator validator = propertyFieldsValidators.get(i);
if (!validator.isValid(value)) {
String propertyDescription = getFieldDescription(field);
results.add(new InvalidValue(messages.get(validator), beanClass,
propertyName, value, bean, propertyDescription));
}
}
return results.toArray(new InvalidValue[results.size()]);
}
private String getFieldDescription(Field field) {
Description description = field.getAnnotation(Description.class);
String retorno = null;
if (description != null) {
retorno = replace(description.value(), null);
}
return retorno;
}
private String getPropertyDescription(Method getter) {
Description description = getter.getAnnotation(Description.class);
String retorno = null;
if (description != null) {
retorno = replace(description.value(), null);
}
return retorno;
}
private String getPropertyName(Method getter) {
String name = getter.getName();
if (name.startsWith("is")) {
name = name.substring(2);
} else if (name.startsWith("get")) {
name = name.substring(3);
}
String propertyName = Introspector.decapitalize(name);
return propertyName;
}
private String replace(String message, Annotation parameters) {
StringTokenizer tokens = new StringTokenizer(message, "{}", true);
StringBuffer buf = new StringBuffer();
boolean escaped = false;
while (tokens.hasMoreTokens()) {
String token = tokens.nextToken();
if ("{".equals(token)) {
escaped = true;
} else if ("}".equals(token)) {
escaped = false;
} else if (!escaped) {
buf.append(token);
} else {
Method member = null;
if (parameters != null) {
try {
member = parameters.getClass().getMethod(token);
} catch (NoSuchMethodException nsfme) {
member = null;
}
}
if (member != null) {
try {
buf.append(member.invoke(parameters));
} catch (Exception e) {
throw new IllegalArgumentException(
"could not render message", e);
}
} else if (messageBundle != null) {
String string = messageBundle.getString(token);
if (string != null)
buf.append(string);
}
}
}
return buf.toString();
}
/**
* apply the registred constraints rules on the hibernate metadata (to be applied on DB schema...)
* @param persistentClass hibernate metadata
*/
public void apply(PersistentClass persistentClass) {
Iterator<Validator> validators = beanValidators.iterator();
while (validators.hasNext()) {
Validator validator = validators.next();
if (validator instanceof PersistentClassConstraint) {
((PersistentClassConstraint) validator).apply(persistentClass);
}
}
validators = propertyValidators.iterator();
Iterator<Method> getters = propertyGetters.iterator();
while (validators.hasNext()) {
Validator validator = validators.next();
String propertyName = getPropertyName(getters.next());
if (validator instanceof PropertyConstraint) {
try {
Property property = persistentClass.getProperty(
propertyName);
((PropertyConstraint) validator).apply(property);
} catch (MappingException pnfe) {
//do nothing
}
}
}
}
}
InvalidValueCode:
package org.hibernate.validator;
/**
* A single violation of a class level or method level constraint.
*
* @author Gavin King
*/
public class InvalidValue {
private final String message;
private final Object value;
private final String propertyName;
private final String propertyDescription;
private final Class beanClass;
private final Object bean;
public InvalidValue(String message, Class beanClass, String propertyName, Object value, Object bean, String propertyDescription) {
this.message = message;
this.value = value;
this.beanClass = beanClass;
this.propertyName = propertyName;
this.propertyDescription = propertyDescription;
this.bean = bean;
}
public Class getBeanClass() {
return beanClass;
}
public String getMessage() {
return message;
}
public String getPropertyName() {
return propertyName;
}
public String getPropertyDescription() {
return propertyDescription;
}
public Object getValue() {
return value;
}
public Object getBean() {
return bean;
}
public String toString() {
return propertyName + ' ' + message;
}
}
Code:
package org.hibernate.annotations;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;
[b]Description annotation[/b]
/**
* Makes a Description about the annotated element.
* @author Jesus Marin
*/
@Target({ TYPE, METHOD, FIELD })
@Retention(RUNTIME)
public @interface Description {
/**
* value of description
*/
String value();
}
CheckInList annotationCode:
package org.hibernate.validator;
import static java.lang.annotation.ElementType.METHOD;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;
/**
* Defines a list of items to be used to CHECK a varchar field.
* @author Jesus Marin
*/
@ValidatorClass(CheckInListValidator.class)
@Target(METHOD)
@Retention(RUNTIME)
public @interface CheckInList {
/**
* array list to be used
*/
String[] items();
/**
* default message
*/
String message() default "Values are not allowed";
}
CheckInList validatorCode:
package org.hibernate.validator;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Property;
/**
* Do a check against a list of values.
*
* @author Jesus Marin
*/
public class CheckInListValidator implements Validator<CheckInList>,
PropertyConstraint {
private String[] values;
public void initialize(CheckInList parameters) {
values = parameters.items();
}
public boolean isValid(Object value) {
boolean retorno = false;
if (value == null) {
retorno = true;
} else if (!(value instanceof String)) {
retorno = false;
} else {
String string = (String) value;
for (int i = 0; i < values.length; i++) {
if (string.equals(values[i])) {
retorno = true;
}
}
}
return retorno;
}
public void apply(Property property) {
Column col = (Column) property.getColumnIterator().next();
col.setCheckConstraint(col.getName() + " is not an allowed value");
}
} // end class CheckInListValidator
The @Description idea was taken from Steeve and sounded very good to me. This way one can constraint a property and at the same time give an error according to the field description.
Besides I overloaded the ClassValidator getInvalidValues method in order to be able to check only one field constraint and not all at once.