com.agimatec.validation.jsr303.Jsr303MetaBeanFactory.java Source code

Java tutorial

Introduction

Here is the source code for com.agimatec.validation.jsr303.Jsr303MetaBeanFactory.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 com.agimatec.validation.jsr303;

import com.agimatec.validation.MetaBeanFactory;
import com.agimatec.validation.jsr303.groups.Group;
import com.agimatec.validation.jsr303.util.SecureActions;
import com.agimatec.validation.jsr303.util.TypeUtils;
import com.agimatec.validation.jsr303.xml.MetaConstraint;
import com.agimatec.validation.model.Features;
import com.agimatec.validation.model.MetaBean;
import com.agimatec.validation.model.MetaProperty;
import com.agimatec.validation.util.AccessStrategy;
import com.agimatec.validation.util.FieldAccess;
import com.agimatec.validation.util.MethodAccess;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.ClassUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.validation.*;
import javax.validation.groups.Default;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Description: process the class annotations for JSR303 constraint validations
 * to build the MetaBean with information from annotations and JSR303 constraint
 * mappings (defined in xml)<br/>
 * User: roman.stumm <br/>
 * Date: 01.04.2008 <br/>
 * Time: 14:12:51 <br/>
 * Copyright: Agimatec GmbH 2008
 */
public class Jsr303MetaBeanFactory implements MetaBeanFactory {
    protected static final Log log = LogFactory.getLog(Jsr303MetaBeanFactory.class);
    protected static final String ANNOTATION_VALUE = "value";
    protected final AgimatecFactoryContext factoryContext;

    public Jsr303MetaBeanFactory(AgimatecFactoryContext factoryContext) {
        this.factoryContext = factoryContext;
    }

    private ConstraintValidatorFactory getConstraintValidatorFactory() {
        return factoryContext.getConstraintValidatorFactory();
    }

    private ConstraintDefaults getDefaultConstraints() {
        return factoryContext.getFactory().getDefaultConstraints();

    }

    /**
     * add the validation features to the metabean that come from jsr303
     * annotations in the beanClass
     */
    public void buildMetaBean(MetaBean metabean) {
        try {
            final Class<?> beanClass = metabean.getBeanClass();
            processGroupSequence(beanClass, metabean);
            for (Class interfaceClass : beanClass.getInterfaces()) {
                processClass(interfaceClass, metabean);
            }

            // process class, superclasses and interfaces
            List<Class> classSequence = new ArrayList<Class>();
            Class theClass = beanClass;
            while (theClass != null && theClass != Object.class) {
                classSequence.add(theClass);
                theClass = theClass.getSuperclass();
            }
            // start with superclasses and go down the hierarchy so that
            // the child classes are processed last to have the chance to overwrite some declarations
            // of their superclasses and that they see what they inherit at the time of processing
            for (int i = classSequence.size() - 1; i >= 0; i--) {
                Class eachClass = classSequence.get(i);
                processClass(eachClass, metabean);
            }
        } catch (IllegalAccessException e) {
            throw new IllegalArgumentException(e);
        } catch (InvocationTargetException e) {
            throw new IllegalArgumentException(e.getTargetException());
        }
    }

    /**
     * process class annotations, field and method annotations
     *
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     */
    private void processClass(Class<?> beanClass, MetaBean metabean)
            throws IllegalAccessException, InvocationTargetException {
        if (!factoryContext.getFactory().getAnnotationIgnores().isIgnoreAnnotations(beanClass)) { // ignore on class level

            processAnnotations(null, beanClass, beanClass, null, new AppendValidationToMeta(metabean));

            Field[] fields = beanClass.getDeclaredFields();
            for (Field field : fields) {
                MetaProperty metaProperty = metabean.getProperty(field.getName());
                // create a property for those fields for which there is not yet a MetaProperty
                if (!factoryContext.getFactory().getAnnotationIgnores().isIgnoreAnnotations(field)) {
                    if (metaProperty == null) {
                        metaProperty = createMetaProperty(field.getName(), field.getType());
                        /*if (*/
                        processAnnotations(metaProperty, beanClass, field, new FieldAccess(field),
                                new AppendValidationToMeta(metaProperty));//) {
                        metabean.putProperty(metaProperty.getName(), metaProperty);
                        //}
                    } else {
                        processAnnotations(metaProperty, beanClass, field, new FieldAccess(field),
                                new AppendValidationToMeta(metaProperty));
                    }
                }
            }
            Method[] methods = beanClass.getDeclaredMethods();
            for (Method method : methods) {

                String propName = null;
                if (method.getParameterTypes().length == 0) {
                    propName = MethodAccess.getPropertyName(method);
                }
                if (propName != null) {
                    if (!factoryContext.getFactory().getAnnotationIgnores().isIgnoreAnnotations(method)) {
                        MetaProperty metaProperty = metabean.getProperty(propName);
                        // create a property for those methods for which there is not yet a MetaProperty
                        if (metaProperty == null) {
                            metaProperty = createMetaProperty(propName, method.getReturnType());
                            /*if (*/
                            processAnnotations(metaProperty, beanClass, method, new MethodAccess(propName, method),
                                    new AppendValidationToMeta(metaProperty));//) {
                            metabean.putProperty(propName, metaProperty);
                            //}
                        } else {
                            processAnnotations(metaProperty, beanClass, method, new MethodAccess(propName, method),
                                    new AppendValidationToMeta(metaProperty));
                        }
                    }
                }
            }
        }
        addXmlConstraints(beanClass, metabean);
    }

    /** add cascade validation and constraints from xml mappings */
    private void addXmlConstraints(Class<?> beanClass, MetaBean metabean)
            throws IllegalAccessException, InvocationTargetException {
        for (MetaConstraint<?, ? extends Annotation> meta : factoryContext.getFactory()
                .getMetaConstraints(beanClass)) {
            MetaProperty metaProperty;
            if (meta.getAccessStrategy() == null) { // class level
                metaProperty = null;
            } else { // property level
                metaProperty = metabean.getProperty(meta.getAccessStrategy().getPropertyName());
                if (metaProperty == null) {
                    metaProperty = createMetaProperty(meta.getAccessStrategy().getPropertyName(),
                            meta.getAccessStrategy().getJavaType());
                    metabean.putProperty(metaProperty.getName(), metaProperty);
                }
            }
            Class<? extends ConstraintValidator<?, ?>>[] constraintClasses = findConstraintValidatorClasses(
                    meta.getAnnotation(), null);
            applyConstraint(meta.getAnnotation(), constraintClasses, metaProperty, beanClass,
                    meta.getAccessStrategy(),
                    new AppendValidationToMeta(metaProperty == null ? metabean : metaProperty));
        }
        for (AccessStrategy access : factoryContext.getFactory().getValidAccesses(beanClass)) {
            MetaProperty metaProperty = metabean.getProperty(access.getPropertyName());
            if (metaProperty == null) {
                metaProperty = createMetaProperty(access.getPropertyName(), access.getJavaType());
                metabean.putProperty(metaProperty.getName(), metaProperty);
            }
            processValid(metaProperty, access);
        }
    }

    private MetaProperty createMetaProperty(String propName, Type type) {
        MetaProperty metaProperty;
        metaProperty = new MetaProperty();
        metaProperty.setName(propName);
        metaProperty.setType(type);
        return metaProperty;
    }

    private boolean processAnnotations(MetaProperty prop, Class owner, AnnotatedElement element,
            AccessStrategy access, AppendValidation appender)
            throws IllegalAccessException, InvocationTargetException {

        boolean changed = false;
        for (Annotation annotation : element.getDeclaredAnnotations()) {
            changed |= processAnnotation(annotation, prop, owner, access, appender);
        }
        return changed;
    }

    private boolean processAnnotation(Annotation annotation, MetaProperty prop, Class owner, AccessStrategy access,
            AppendValidation appender) throws IllegalAccessException, InvocationTargetException {
        if (annotation instanceof Valid) {
            return processValid(prop, access);
        } else {
            /**
             * An annotation is considered a constraint definition if its retention
             * policy contains RUNTIME and if the annotation itself is annotated with
             * javax.validation.Constraint.
             */
            Constraint vcAnno = annotation.annotationType().getAnnotation(Constraint.class);
            if (vcAnno != null) {
                Class<? extends ConstraintValidator<?, ?>>[] validatorClasses;
                validatorClasses = findConstraintValidatorClasses(annotation, vcAnno);
                return applyConstraint(annotation, validatorClasses, prop, owner, access, appender);
            } else {
                /**
                 * Multi-valued constraints:
                 * To support this requirement, the bean validation provider treats
                 * regular annotations (annotations not annotated by @Constraint)
                 * whose value element has a return type of an array of
                 * constraint annotations in a special way.
                 */
                Object result = SecureActions.getAnnotationValue(annotation, ANNOTATION_VALUE);
                if (result != null && result instanceof Annotation[]) {
                    boolean changed = false;
                    for (Annotation each : (Annotation[]) result) {
                        changed |= processAnnotation(each, prop, owner, access, appender);
                    }
                    return changed;
                }
            }
        }
        return false;
    }

    protected Class<? extends ConstraintValidator<?, ?>>[] findConstraintValidatorClasses(Annotation annotation,
            Constraint vcAnno) {
        if (vcAnno == null) {
            vcAnno = annotation.annotationType().getAnnotation(Constraint.class);
        }
        Class<? extends ConstraintValidator<?, ?>>[] validatorClasses;
        validatorClasses = factoryContext.getFactory().getConstraintsCache()
                .getConstraintValidators(annotation.annotationType());
        if (validatorClasses == null) {
            validatorClasses = vcAnno.validatedBy();
            if (validatorClasses.length == 0) {
                validatorClasses = getDefaultConstraints().getValidatorClasses(annotation.annotationType());
            }
        }
        return validatorClasses;
    }

    private boolean processValid(MetaProperty prop, AccessStrategy access) {
        if (prop != null/* && prop.getMetaBean() == null*/) {
            AccessStrategy[] strategies = prop.getFeature(Features.Property.REF_CASCADE);
            if (strategies == null) {
                strategies = new AccessStrategy[] { access };
                prop.putFeature(Features.Property.REF_CASCADE, strategies);
            } else {
                if (!ArrayUtils.contains(strategies, access)) {
                    AccessStrategy[] strategies_new = new AccessStrategy[strategies.length + 1];
                    System.arraycopy(strategies, 0, strategies_new, 0, strategies.length);
                    strategies_new[strategies.length] = access;
                    prop.putFeature(Features.Property.REF_CASCADE, strategies_new);
                }
            }
            return true;
        }
        return false;
    }

    private void processGroupSequence(Class<?> beanClass, MetaBean metabean) {
        GroupSequence annotation = beanClass.getAnnotation(GroupSequence.class);
        List<Group> groupSeq = metabean.getFeature(Jsr303Features.Bean.GROUP_SEQUENCE);
        if (groupSeq == null) {
            groupSeq = new ArrayList(annotation == null ? 1 : annotation.value().length);
            metabean.putFeature(Jsr303Features.Bean.GROUP_SEQUENCE, groupSeq);
        }
        Class<?>[] groupClasses = factoryContext.getFactory().getDefaultSequence(beanClass);
        if (groupClasses == null || groupClasses.length == 0) {
            if (annotation == null) {
                groupSeq.add(Group.DEFAULT);
                return;
            } else {
                groupClasses = annotation.value();
            }
        }
        boolean containsDefault = false;
        for (Class<?> groupClass : groupClasses) {
            if (groupClass.getName().equals(beanClass.getName())) {
                groupSeq.add(Group.DEFAULT);
                containsDefault = true;
            } else if (groupClass.getName().equals(Default.class.getName())) {
                throw new GroupDefinitionException("'Default.class' must not appear in @GroupSequence! Use '"
                        + beanClass.getSimpleName() + ".class' instead.");
            } else {
                groupSeq.add(new Group(groupClass));
            }
        }
        if (!containsDefault) {
            throw new GroupDefinitionException(
                    "Redefined default group sequence must contain " + beanClass.getName());
        }
        if (log.isDebugEnabled()) {
            log.debug("Default group sequence for bean " + beanClass.getName() + " is: " + groupSeq);
        }
    }

    /**
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     */
    protected boolean applyConstraint(Annotation annotation,
            Class<? extends ConstraintValidator<?, ?>>[] constraintClasses, MetaProperty prop, Class owner,
            AccessStrategy access, AppendValidation appender)
            throws IllegalAccessException, InvocationTargetException {

        final ConstraintValidator validator;
        if (constraintClasses != null) {
            Type type = determineTargetedType(owner, access);
            /**
             * spec says in chapter 3.5.3.:
             * The ConstraintValidator chosen to validate a
             * declared type T is the one where the type supported by the
             * ConstraintValidator is a supertype of T and where
             * there is no other ConstraintValidator whose supported type is a
             * supertype of T and not a supertype of the chosen
             * ConstraintValidator supported type.
             */
            Map<Type, Class<? extends ConstraintValidator<?, ?>>> validatorTypes = TypeUtils
                    .getValidatorsTypes(constraintClasses);
            final List<Type> assignableTypes = new ArrayList(constraintClasses.length);
            fillAssignableTypes(type, validatorTypes.keySet(), assignableTypes);
            reduceAssignableTypes(assignableTypes);
            checkOneType(assignableTypes, type, owner, annotation, access);
            validator = getConstraintValidatorFactory().getInstance(validatorTypes.get(assignableTypes.get(0)));
            validator.initialize(annotation);
        } else {
            validator = null;
        }
        final AnnotationConstraintBuilder builder = new AnnotationConstraintBuilder(constraintClasses, validator,
                annotation, owner, access);
        // process composed constraints:
        // here are not other superclasses possible, because annotations do not inherit!
        if (processAnnotations(prop, owner, annotation.annotationType(), access,
                new AppendValidationToBuilder(builder)) || validator != null) { // recursion!
            appender.append(builder.getConstraintValidation());
            return true;
        } else {
            return false;
        }
    }

    private void checkOneType(List<Type> types, Type targetType, Class owner, Annotation anno,
            AccessStrategy access) {

        if (types.isEmpty()) {
            StringBuilder buf = new StringBuilder().append("No validator could be found for type ")
                    .append(stringForType(targetType)).append(". See: @")
                    .append(anno.annotationType().getSimpleName()).append(" at ")
                    .append(stringForLocation(owner, access));
            throw new UnexpectedTypeException(buf.toString());
        } else if (types.size() > 1) {
            StringBuilder buf = new StringBuilder();
            buf.append("Ambiguous validators for type ");
            buf.append(stringForType(targetType));
            buf.append(". See: @").append(anno.annotationType().getSimpleName()).append(" at ")
                    .append(stringForLocation(owner, access));
            buf.append(". Validators are: ");
            boolean comma = false;
            for (Type each : types) {
                if (comma)
                    buf.append(", ");
                comma = true;
                buf.append(each);
            }
            throw new UnexpectedTypeException(buf.toString());
        }
    }

    /** implements spec chapter 3.5.3. ConstraintValidator resolution algorithm. */
    private Type determineTargetedType(Class owner, AccessStrategy access) {
        // if the constraint declaration is hosted on a class or an interface,
        // the targeted type is the class or the interface.
        if (access == null)
            return owner;
        Type type = access.getJavaType();
        if (type == null)
            return Object.class;
        if (type instanceof Class)
            type = ClassUtils.primitiveToWrapper((Class) type);
        return type;
    }

    private String stringForType(Type clazz) {
        if (clazz instanceof Class) {
            if (((Class) clazz).isArray()) {
                return ((Class) clazz).getComponentType().getName() + "[]";
            } else {
                return ((Class) clazz).getName();
            }
        } else {
            return clazz.toString();
        }
    }

    private String stringForLocation(Class owner, AccessStrategy access) {
        if (access != null) {
            return access.toString();
        } else {
            return owner.getName();
        }
    }

    private void fillAssignableTypes(Type type, Set<Type> validatorsTypes, List<Type> suitableTypes) {
        for (Type validatorType : validatorsTypes) {
            if (TypeUtils.isAssignable(validatorType, type) && !suitableTypes.contains(validatorType)) {
                suitableTypes.add(validatorType);
            }
        }
    }

    /**
     * Tries to reduce all assignable classes down to a single class.
     *
     * @param assignableTypes The set of all classes which are assignable to the class of the value to be validated and
     *                        which are handled by at least one of the validators for the specified constraint.
     */
    private void reduceAssignableTypes(List<Type> assignableTypes) {
        if (assignableTypes.size() <= 1) {
            return; // no need to reduce
        }
        boolean removed;
        do {
            removed = false;
            final Type type = assignableTypes.get(0);
            for (int i = 1; i < assignableTypes.size(); i++) {
                Type nextType = assignableTypes.get(i);
                if (TypeUtils.isAssignable(type, nextType)) {
                    assignableTypes.remove(0);
                    i--;
                    removed = true;
                } else if (TypeUtils.isAssignable(nextType, type)) {
                    assignableTypes.remove(i--);
                    removed = true;
                }
            }
        } while (removed && assignableTypes.size() > 1);
    }
}