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-async-beta
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.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
 * @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);
            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();
        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) {
                    }

                    boolean onAccessor = accessor != null
                            && Modifier.isPublic(accessor.getModifiers())
                            && accessor.getDeclaringClass().equals(clazz)
                            && (accessor.isAnnotationPresent(Validate.class) || accessor
                                    .isAnnotationPresent(ValidateNestedProperties.class));
                    boolean onMutator = mutator != null
                            && Modifier.isPublic(mutator.getModifiers())
                            && mutator.getDeclaringClass().equals(clazz)
                            && (mutator.isAnnotationPresent(Validate.class) || mutator
                                    .isAnnotationPresent(ValidateNestedProperties.class));
                    boolean onField = field != null
                            && !Modifier.isStatic(field.getModifiers())
                            && field.getDeclaringClass().equals(clazz)
                            && (field.isAnnotationPresent(Validate.class) || field
                                    .isAnnotationPresent(ValidateNestedProperties.class));

                    // I don't think George Boole would like this ...
                    int count = 0;
                    if (onAccessor) ++count;
                    if (onMutator) ++count;
                    if (onField) ++count;

                    // must be 0 or 1
                    if (count > 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");
                        if (onAccessor) {
                            boolean hasSimple = accessor.isAnnotationPresent(Validate.class);
                            boolean hasNested = accessor
                                    .isAnnotationPresent(ValidateNestedProperties.class);
                            buf.append("--> Getter method ").append(accessor.getName()).append(
                                    " is annotated with ");
                            if (hasSimple)
                                buf.append("@Validate");
                            if (hasSimple && hasNested)
                                buf.append(" and ");
                            if (hasNested)
                                buf.append("@ValidateNestedProperties");
                            buf.append('\n');
                        }
                        if (onMutator) {
                            boolean hasSimple = mutator.isAnnotationPresent(Validate.class);
                            boolean hasNested = mutator
                                    .isAnnotationPresent(ValidateNestedProperties.class);
                            buf.append("--> Setter method ").append(mutator.getName()).append(
                                    " is annotated with ");
                            if (hasSimple)
                                buf.append("@Validate");
                            if (hasSimple && hasNested)
                                buf.append(" and ");
                            if (hasNested)
                                buf.append("@ValidateNestedProperties");
                            buf.append('\n');
                        }
                        if (onField) {
                            boolean hasSimple = field.isAnnotationPresent(Validate.class);
                            boolean hasNested = field
                                    .isAnnotationPresent(ValidateNestedProperties.class);
                            buf.append("--> Field ").append(field.getName()).append(
                                    " is annotated with ");
                            if (hasSimple)
                                buf.append("@Validate");
                            if (hasSimple && hasNested)
                                buf.append(" and ");
                            if (hasNested)
                                buf.append("@ValidateNestedProperties");
                            buf.append('\n');
                        }
                        throw new StripesRuntimeException(buf.toString());
                    }

                    // after the conflict check, stop processing fields we've already seen
                    if (seen.contains(propertyName))
                        continue;

                    // get the @Validate and/or @ValidateNestedProperties
                    Validate simple;
                    ValidateNestedProperties nested;
                    if (onAccessor) {
                        simple = accessor.getAnnotation(Validate.class);
                        nested = accessor.getAnnotation(ValidateNestedProperties.class);
                        seen.add(propertyName);
                    }
                    else if (onMutator) {
                        simple = mutator.getAnnotation(Validate.class);
                        nested = mutator.getAnnotation(ValidateNestedProperties.class);
                        seen.add(propertyName);
                    }
                    else if (onField) {
                        simple = field.getAnnotation(Validate.class);
                        nested = field.getAnnotation(ValidateNestedProperties.class);
                        seen.add(propertyName);
                    }
                    else {
                        simple = null;
                        nested = null;
                    }

                    // 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);
                                }
                            }
                        }
                    }
                }
            }
        }
        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;
        }

        // Print out a pretty debug message showing what validations got configured
        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 : "");

        return Collections.unmodifiableMap(meta);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy