/* Copyright 2007 Ben Gunter
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sourceforge.stripes.validation;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import net.sourceforge.stripes.config.Configuration;
import net.sourceforge.stripes.controller.ParameterName;
import net.sourceforge.stripes.exception.StripesRuntimeException;
import net.sourceforge.stripes.util.Log;
import net.sourceforge.stripes.util.ReflectUtil;
/**
* An implementation of {@link ValidationMetadataProvider} that scans classes and their superclasses
* for properties annotated with {@link Validate} and/or {@link ValidateNestedProperties} and
* exposes the validation metadata specified by those annotations. When searching for annotations,
* this implementation looks first at the property's read method (getter), then its write method
* (setter), and finally at the field itself.
*
* @author Ben Gunter, Freddy Daoud
* @since Stripes 1.5
*/
public class DefaultValidationMetadataProvider implements ValidationMetadataProvider {
private static final Log log = Log.getInstance(DefaultValidationMetadataProvider.class);
private Configuration configuration;
/** Map class -> field -> validation meta data */
private final Map<Class<?>, Map<String, ValidationMetadata>> cache = new ConcurrentHashMap<Class<?>, Map<String, ValidationMetadata>>();
/** Currently does nothing except store a reference to {@code configuration}. */
public void init(Configuration configuration) throws Exception {
this.configuration = configuration;
}
/** Get the {@link Configuration} object that was passed into {@link #init(Configuration)}. */
public Configuration getConfiguration() {
return configuration;
}
public Map<String, ValidationMetadata> getValidationMetadata(Class<?> beanType) {
Map<String, ValidationMetadata> meta = cache.get(beanType);
if (meta == null) {
meta = loadForClass(beanType);
logDebugMessageForConfiguredValidations(beanType, meta);
cache.put(beanType, meta);
}
return meta;
}
public ValidationMetadata getValidationMetadata(Class<?> beanType, ParameterName field) {
return getValidationMetadata(beanType).get(field.getStrippedName());
}
/**
* Get validation information for all the properties and nested properties of the given class.
* The {@link Validate} and/or {@link ValidateNestedProperties} annotations may be applied to
* the property's read method, write method, or field declaration. If a property has a
* {@link ValidateNestedProperties} annotation, then the nested properties named in its
* {@link Validate} annotations will be included as well.
*
* @param beanType a class
* @return A map of (possibly nested) property names to {@link ValidationMetadata} for the
* property.
* @throws StripesRuntimeException if conflicts are found in the validation annotations
*/
protected Map<String, ValidationMetadata> loadForClass(Class<?> beanType) {
Map<String, ValidationMetadata> meta = new HashMap<String, ValidationMetadata>();
@SuppressWarnings("unchecked")
Map<String, AnnotationInfo> annotationInfoMap =
getAnnotationInfoMap(beanType, Validate.class, ValidateNestedProperties.class);
for (String propertyName : annotationInfoMap.keySet()) {
AnnotationInfo annotationInfo = annotationInfoMap.get(propertyName);
// get the @Validate and/or @ValidateNestedProperties
Validate simple = annotationInfo.getAnnotation(Validate.class);
ValidateNestedProperties nested = annotationInfo.getAnnotation(ValidateNestedProperties.class);
Class<?> clazz = annotationInfo.getTargetClass();
// add to allow list if @Validate present
if (simple != null) {
if (simple.field() == null || "".equals(simple.field())) {
meta.put(propertyName, new ValidationMetadata(propertyName, simple));
}
else {
log.warn("Field name present in @Validate but should be omitted: ",
clazz, ", property ", propertyName, ", given field name ",
simple.field());
}
}
// add all sub-properties referenced in @ValidateNestedProperties
if (nested != null) {
Validate[] validates = nested.value();
if (validates != null) {
for (Validate validate : validates) {
if (validate.field() != null && !"".equals(validate.field())) {
String fullName = propertyName + '.' + validate.field();
if (meta.containsKey(fullName)) {
log.warn("More than one nested @Validate with same field name: "
+ validate.field() + " on property " + propertyName);
}
meta.put(fullName, new ValidationMetadata(fullName, validate));
}
else {
log.warn("Field name missing from nested @Validate: ", clazz,
", property ", propertyName);
}
}
}
}
}
return Collections.unmodifiableMap(meta);
}
/**
* Looks at a class's properties, searching for the specified annotations on the properties
* (field, getter method, or setter method). An exception is thrown if annotations are found
* in more than one of those three places.
*
* @param beanType the class on which to look for annotations.
* @param annotationClasses the classes of the annotations for which to look for.
* @return a map of property names to AnnotationInfo objects, which contain the class on which
* the annotations were found (if any), and the annotation objects that correspond to the
* annotation classes.
*/
protected Map<String, AnnotationInfo> getAnnotationInfoMap(Class<?> beanType,
Class<? extends Annotation>... annotationClasses)
{
Map<String, AnnotationInfo> annotationInfoMap = new HashMap<String, AnnotationInfo>();
Set<String> seen = new HashSet<String>();
try {
for (Class<?> clazz = beanType; clazz != null; clazz = clazz.getSuperclass()) {
List<PropertyDescriptor> pds = new ArrayList<PropertyDescriptor>(
Arrays.asList(ReflectUtil.getPropertyDescriptors(clazz)));
// Also look at public fields
Field[] publicFields = clazz.getFields();
for (Field field : publicFields) {
pds.add(new PropertyDescriptor(field.getName(), null, null));
}
for (PropertyDescriptor pd : pds) {
String propertyName = pd.getName();
Method accessor = pd.getReadMethod();
Method mutator = pd.getWriteMethod();
Field field = null;
try {
field = clazz.getDeclaredField(propertyName);
}
catch (NoSuchFieldException e) {
}
// this method throws an exception if there are conflicts
AnnotationInfo annotationInfo = getAnnotationInfo(clazz, propertyName,
new PropertyWrapper[] {
new PropertyWrapper(accessor),
new PropertyWrapper(mutator),
new PropertyWrapper(field),
},
annotationClasses);
// after the conflict check, stop processing fields we've already seen
if (seen.contains(propertyName))
continue;
if (annotationInfo.atLeastOneAnnotationFound()) {
annotationInfoMap.put(propertyName, annotationInfo);
seen.add(propertyName);
}
}
}
}
catch (RuntimeException e) {
log.error(e, "Failure checking @Validate annotations ", getClass().getName());
throw e;
}
catch (Exception e) {
log.error(e, "Failure checking @Validate annotations ", getClass().getName());
StripesRuntimeException sre = new StripesRuntimeException(e.getMessage(), e);
sre.setStackTrace(e.getStackTrace());
throw sre;
}
return annotationInfoMap;
}
/**
* Looks at a class's properties, searching for the specified annotations on the given property
* objects. An exception is thrown if annotations are found in more than one of the specified
* property accessors (normally field, getter method, and setter method).
*
* @param clazz the class on which to look for annotations.
* @param propertyName the name of the property.
* @param propertyWrappers the property accessors.
* @param annotationClasses the classes of the annotations for which to look for.
* @return an AnnotationInfo object, which contains the class on which the annotations were found
* (if any), and the annotation objects that correspond to the annotation classes.
*/
protected AnnotationInfo getAnnotationInfo(Class<?> clazz, String propertyName,
PropertyWrapper[] propertyWrappers, Class<? extends Annotation>... annotationClasses)
{
AnnotationInfo annotationInfo = new AnnotationInfo(clazz);
Map<PropertyWrapper, Map<Class<? extends Annotation>, Annotation>> map =
new HashMap<PropertyWrapper, Map<Class<? extends Annotation>, Annotation>>();
for (PropertyWrapper property : propertyWrappers) {
Map<Class<? extends Annotation>, Annotation> annotationMap =
new HashMap<Class<? extends Annotation>, Annotation>();
for (Class<? extends Annotation> annotationClass : annotationClasses) {
Annotation annotation = findAnnotation(clazz, property, annotationClass);
if (annotation != null) {
annotationMap.put(annotationClass, annotation);
}
}
if (!annotationMap.isEmpty()) {
map.put(property, annotationMap);
}
}
// must be 0 or 1
if (map.size() > 1) {
StringBuilder buf = new StringBuilder(
"There are conflicting @Validate and/or @ValidateNestedProperties annotations in ")
.append(clazz)
.append(". The following elements are improperly annotated for the '")
.append(propertyName)
.append("' property:\n");
for (PropertyWrapper property : map.keySet()) {
Map<Class<? extends Annotation>, Annotation> annotationMap = map.get(property);
buf.append("--> ").append(property.getType()).append(' ')
.append(property.getName()).append(" is annotated with ");
for (Class<?> cls : annotationMap.keySet()) {
buf.append('@').append(cls.getSimpleName()).append(' ');
}
buf.append('\n');
}
throw new StripesRuntimeException(buf.toString());
}
if (!map.isEmpty()) {
annotationInfo.setAnnotationMap(map.entrySet().iterator().next().getValue());
}
return annotationInfo;
}
/**
* Returns an annotation (or <code>null</code> if none is found) for the given property
* accessor of a class. The property object must not be <code>null</code>, must be declared on
* the class, must be public if it is a method, and must not be static if it is a field, for it
* to be considered eligible to having the annotation.
*
* @param clazz the class on which to look for the annotation.
* @param property the property accessor.
* @param annotationClass the class of the annotation to look for.
* @return the annotation object, or <code>null</code> if no annotation was found.
*/
protected Annotation findAnnotation(Class<?> clazz, PropertyWrapper property,
Class<? extends Annotation> annotationClass)
{
AccessibleObject accessible = property.getAccessibleObject();
if (accessible != null
&& property.getDeclaringClass().equals(clazz)
&& ( (accessible.getClass().equals(Method.class) && Modifier.isPublic(property.getModifiers()))
|| (accessible.getClass().equals(Field.class) && !Modifier.isStatic(property.getModifiers()))
))
{
return accessible.getAnnotation(annotationClass);
}
return null;
}
/**
* Prints out a pretty debug message showing what validations got configured.
*/
protected void logDebugMessageForConfiguredValidations(Class<?> beanType, Map<String, ValidationMetadata> meta) {
StringBuilder builder = new StringBuilder(128);
for (Map.Entry<String, ValidationMetadata> entry : meta.entrySet()) {
if (builder.length() > 0) {
builder.append(", ");
}
builder.append(entry.getKey());
builder.append("->");
builder.append(entry.getValue());
}
log.debug("Loaded validations for ActionBean ", beanType.getSimpleName(), ": ",
builder.length() > 0 ? builder : "<none>");
}
/**
* Contains the class on which the annotations were found (if any), and the annotation objects
* that correspond to the annotation classes.
*/
protected class AnnotationInfo {
private Class<?> targetClass;
private Map<Class<? extends Annotation>, Annotation> annotationMap;
public AnnotationInfo(Class<?> targetClass) {
this.targetClass = targetClass;
}
public Class<?> getTargetClass() {
return targetClass;
}
public void setAnnotationMap(Map<Class<? extends Annotation>, Annotation> annotationMap) {
this.annotationMap = annotationMap;
}
@SuppressWarnings("unchecked")
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
return (T) annotationMap.get(annotationClass);
}
public boolean atLeastOneAnnotationFound() {
return !(annotationMap == null || annotationMap.isEmpty());
}
}
/**
* For some reason, methods common to both the Field and Method classes are not in their parent
* class, AccessibleObject, so this class works around that limitation.
*/
protected class PropertyWrapper {
private Field field;
private Method method;
private String type;
public PropertyWrapper(Field field) {
this.field = field;
this.type = "Field";
}
public PropertyWrapper(Method method) {
this.method = method;
this.type = "Method";
}
public AccessibleObject getAccessibleObject() {
return field != null ? field : method;
}
public String getName() {
return field != null ? field.getName() : method.getName();
}
public Class<?> getDeclaringClass() {
return field != null ? field.getDeclaringClass() : method.getDeclaringClass();
}
public int getModifiers() {
return field != null ? field.getModifiers() : method.getModifiers();
}
public String getType() {
return type;
}
}
}