All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.sourceforge.stripes.validation.DefaultValidationMetadataProvider Maven / Gradle / Ivy

There is a newer version: 1.7.0-beta4
Show newest version
/* 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, Map> cache = new ConcurrentHashMap, Map>();

    /** 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 getValidationMetadata(Class beanType) {
        Map 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 loadForClass(Class beanType) {
        Map meta = new HashMap();

        @SuppressWarnings("unchecked")
        Map 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 getAnnotationInfoMap(Class beanType,
        Class... annotationClasses)
    {
        Map annotationInfoMap = new HashMap();

        Set seen = new HashSet();
        try {
            for (Class clazz = beanType; clazz != null; clazz = clazz.getSuperclass()) {
                List pds = new ArrayList(
                        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... annotationClasses)
    {
        AnnotationInfo annotationInfo = new AnnotationInfo(clazz);

        Map, Annotation>> map =
            new HashMap, Annotation>>();

        for (PropertyWrapper property : propertyWrappers) {
            Map, Annotation> annotationMap =
                new HashMap, Annotation>();

            for (Class 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, 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 null if none is found) for the given property
     * accessor of a class. The property object must not be null, 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 null if no annotation was found.
     */
    protected Annotation findAnnotation(Class clazz, PropertyWrapper property,
        Class 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 meta) {
        StringBuilder builder = new StringBuilder(128);
        for (Map.Entry 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 : "");
    }

    /**
     * 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, Annotation> annotationMap;

        public AnnotationInfo(Class targetClass) {
            this.targetClass = targetClass;
        }

        public Class getTargetClass() {
            return targetClass;
        }

        public void setAnnotationMap(Map, Annotation> annotationMap) {
            this.annotationMap = annotationMap;
        }

        @SuppressWarnings("unchecked")
        public  T getAnnotation(Class 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;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy