org.apache.bval.jsr.BeanDescriptorImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.bval.jsr.BeanDescriptorImpl.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 org.apache.bval.jsr;

import org.apache.bval.Validate;
import org.apache.bval.jsr.groups.Group;
import org.apache.bval.jsr.groups.GroupConversionDescriptorImpl;
import org.apache.bval.jsr.util.ClassHelper;
import org.apache.bval.jsr.xml.AnnotationIgnores;
import org.apache.bval.model.Features;
import org.apache.bval.model.MetaBean;
import org.apache.bval.model.MetaConstructor;
import org.apache.bval.model.MetaMethod;
import org.apache.bval.model.MetaParameter;
import org.apache.bval.model.MetaProperty;
import org.apache.bval.model.Validation;
import org.apache.bval.util.AccessStrategy;
import org.apache.bval.util.reflection.Reflection;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.weaver.privilizer.Privilizing;
import org.apache.commons.weaver.privilizer.Privilizing.CallTo;

import javax.validation.Constraint;
import javax.validation.ConstraintDeclarationException;
import javax.validation.ConstraintTarget;
import javax.validation.Valid;
import javax.validation.groups.ConvertGroup;
import javax.validation.metadata.BeanDescriptor;
import javax.validation.metadata.ConstraintDescriptor;
import javax.validation.metadata.ConstructorDescriptor;
import javax.validation.metadata.ExecutableDescriptor;
import javax.validation.metadata.GroupConversionDescriptor;
import javax.validation.metadata.MethodDescriptor;
import javax.validation.metadata.MethodType;
import javax.validation.metadata.ParameterDescriptor;
import javax.validation.metadata.PropertyDescriptor;
import javax.validation.metadata.ReturnValueDescriptor;
import java.beans.Introspector;
import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * Description: Implements {@link BeanDescriptor}.<br/>
 */
@Privilizing(@CallTo(Reflection.class))
public class BeanDescriptorImpl extends ElementDescriptorImpl implements BeanDescriptor {
    private static final CopyOnWriteArraySet<ConstraintValidation<?>> NO_CONSTRAINTS = new CopyOnWriteArraySet<ConstraintValidation<?>>();
    private static final Validation[] EMPTY_VALIDATION = new Validation[0];

    /**
     * The {@link ApacheFactoryContext} (not) used by this
     * {@link BeanDescriptorImpl}
     */
    private final Set<ConstructorDescriptor> constrainedConstructors;
    private final Set<MethodDescriptor> containedMethods;
    private final ExecutableMeta meta;
    private final Boolean isBeanConstrained;
    private final Set<PropertyDescriptor> validatedProperties;

    protected BeanDescriptorImpl(final ApacheFactoryContext factoryContext, final MetaBean metaBean) {
        super(metaBean, metaBean.getBeanClass(), metaBean.getValidations());

        Set<PropertyDescriptor> procedureDescriptors = metaBean.getFeature(JsrFeatures.Bean.PROPERTIES);
        if (procedureDescriptors == null) {
            procedureDescriptors = new HashSet<PropertyDescriptor>();
            for (final MetaProperty prop : metaBean.getProperties()) {
                if (prop.getValidations().length > 0
                        || (prop.getMetaBean() != null || prop.getFeature(Features.Property.REF_CASCADE) != null)) {
                    procedureDescriptors.add(getPropertyDescriptor(prop));
                }
            }
            procedureDescriptors = metaBean.initFeature(JsrFeatures.Bean.PROPERTIES, procedureDescriptors);
        }

        ExecutableMeta executables = metaBean.getFeature(JsrFeatures.Bean.EXECUTABLES);
        if (executables == null) { // caching the result of it is important to avoid to compute it for each Validator
            executables = new ExecutableMeta(factoryContext, metaBean, getConstraintDescriptors());
            executables = metaBean.initFeature(JsrFeatures.Bean.EXECUTABLES, executables);
        }

        validatedProperties = Collections.unmodifiableSet(procedureDescriptors);
        meta = executables;
        isBeanConstrained = meta.isBeanConstrained;
        containedMethods = toConstrained(meta.methodConstraints.values());
        constrainedConstructors = toConstrained(meta.contructorConstraints.values());
    }

    private static void addGroupConvertion(final MetaProperty prop, final PropertyDescriptorImpl edesc) {
        boolean fieldFound = false;
        boolean methodFound = false;
        Class<?> current = prop.getParentMetaBean().getBeanClass();
        while (current != null && current != Object.class && (!methodFound || !fieldFound)) {
            if (!fieldFound) {
                final Field field = Reflection.getDeclaredField(current, prop.getName());
                if (field != null) {
                    processConvertGroup(edesc, field);
                    fieldFound = true;
                }
            }
            if (!methodFound) {
                final String name = Character.toUpperCase(prop.getName().charAt(0)) + prop.getName().substring(1);
                Method m = Reflection.getDeclaredMethod(current, "get" + name);
                if (m == null) {
                    final Method isAccessor = Reflection.getDeclaredMethod(current, "is" + name);
                    if (isAccessor != null && boolean.class.equals(isAccessor.getReturnType())) {
                        m = isAccessor;
                    }
                }
                if (m != null) {
                    processConvertGroup(edesc, m);
                    methodFound = true;
                }
            }
            current = current.getSuperclass();
        }

        final Collection<Annotation> annotations = prop.getFeature(JsrFeatures.Property.ANNOTATIONS_TO_PROCESS);
        if (annotations != null) {
            for (final Annotation a : annotations) {
                if (ConvertGroup.List.class.isInstance(a)) {
                    for (final ConvertGroup convertGroup : ConvertGroup.List.class.cast(a).value()) {
                        edesc.addGroupConversion(new GroupConversionDescriptorImpl(new Group(convertGroup.from()),
                                new Group(convertGroup.to())));
                    }
                }
                if (ConvertGroup.class.isInstance(a)) {
                    final ConvertGroup convertGroup = ConvertGroup.class.cast(a);
                    edesc.addGroupConversion(new GroupConversionDescriptorImpl(new Group(convertGroup.from()),
                            new Group(convertGroup.to())));
                }
            }
            annotations.clear();
        }
        if (!edesc.getGroupConversions().isEmpty() && !edesc.isCascaded()) {
            throw new ConstraintDeclarationException("@Valid is needed for group conversion");
        }
    }

    private static void processConvertGroup(final ElementDescriptorImpl edesc, final AccessibleObject accessible) {
        final ConvertGroup.List convertGroupList = accessible.getAnnotation(ConvertGroup.List.class);
        if (convertGroupList != null) {
            for (final ConvertGroup convertGroup : convertGroupList.value()) {
                edesc.addGroupConversion(new GroupConversionDescriptorImpl(new Group(convertGroup.from()),
                        new Group(convertGroup.to())));
            }
        }
        final ConvertGroup convertGroup = accessible.getAnnotation(ConvertGroup.class);
        if (convertGroup != null) {
            edesc.addGroupConversion(new GroupConversionDescriptorImpl(new Group(convertGroup.from()),
                    new Group(convertGroup.to())));
        }
    }

    /**
     * Returns true if the bean involves validation:
     * <ul>
     * <li>a constraint is hosted on the bean itself</li>
     * <li>a constraint is hosted on one of the bean properties, OR</li>
     * <li>a bean property is marked for cascade (<code>@Valid</code>)</li>
     * </ul>
     *
     * @return true if the bean involves validation
     */
    public boolean isBeanConstrained() {
        return isBeanConstrained;
    }

    /**
     * Return the property level constraints for a given propertyName or {@code null} if
     * either the property does not exist or has no constraint. The returned
     * object (and associated objects including ConstraintDescriptors) are
     * immutable.
     *
     * @param propertyName property evaluated
     */
    public PropertyDescriptor getConstraintsForProperty(String propertyName) {
        if (propertyName == null || propertyName.trim().length() == 0) {
            throw new IllegalArgumentException("propertyName cannot be null or empty");
        }
        final MetaProperty prop = metaBean.getProperty(propertyName);
        if (prop == null) {
            return null;
        }
        // If no constraints and not cascaded, return null
        if (prop.getValidations().length == 0 && prop.getFeature(Features.Property.REF_CASCADE) == null) {
            return null;
        }
        return getPropertyDescriptor(prop);
    }

    private PropertyDescriptor getPropertyDescriptor(final MetaProperty prop) {
        PropertyDescriptorImpl edesc = prop.getFeature(JsrFeatures.Property.PropertyDescriptor);
        if (edesc == null) {
            edesc = new PropertyDescriptorImpl(prop);
            addGroupConvertion(prop, edesc);
            prop.putFeature(JsrFeatures.Property.PropertyDescriptor, edesc);
        }
        return edesc;
    }

    /**
     * {@inheritDoc}
     *
     * @return the property descriptors having at least a constraint defined
     */
    public Set<PropertyDescriptor> getConstrainedProperties() {
        return Collections.unmodifiableSet(validatedProperties);
    }

    public MethodDescriptor getInternalConstraintsForMethod(final String methodName,
            final Class<?>... parameterTypes) {
        if (methodName == null) {
            throw new IllegalArgumentException("Method name can't be null");
        }
        return meta.methodConstraints.get(methodName + Arrays.toString(parameterTypes));
    }

    public MethodDescriptor getConstraintsForMethod(final String methodName, final Class<?>... parameterTypes) {
        if (methodName == null) {
            throw new IllegalArgumentException("Method name can't be null");
        }
        final MethodDescriptor methodDescriptor = meta.methodConstraints
                .get(methodName + Arrays.toString(parameterTypes));
        if (methodDescriptor != null
                && (methodDescriptor.hasConstrainedParameters() || methodDescriptor.hasConstrainedReturnValue())) {
            return methodDescriptor;
        }
        return null;
    }

    public Set<MethodDescriptor> getConstrainedMethods(MethodType methodType, MethodType... methodTypes) {
        final Set<MethodDescriptor> desc = new HashSet<MethodDescriptor>();
        desc.addAll(filter(containedMethods, methodType));
        if (methodTypes != null) {
            for (final MethodType type : methodTypes) {
                desc.addAll(filter(containedMethods, type));
            }
        }
        return desc;
    }

    private static Collection<MethodDescriptor> filter(final Set<MethodDescriptor> containedMethods,
            final MethodType type) {
        final Collection<MethodDescriptor> list = new ArrayList<MethodDescriptor>();
        for (final MethodDescriptor d : containedMethods) {
            final boolean getter = d.getParameterDescriptors().isEmpty()
                    && (d.getName().startsWith("get") || (d.getName().startsWith("is")
                            && boolean.class.equals(d.getReturnValueDescriptor().getElementClass())));

            switch (type) {
            case GETTER:
                if (getter) {
                    list.add(d);
                }
                break;

            case NON_GETTER:
                if (!getter) {
                    list.add(d);
                }
            }
        }
        return list;
    }

    public ConstructorDescriptor getConstraintsForConstructor(final Class<?>... parameterTypes) {
        final ConstructorDescriptor descriptor = meta.contructorConstraints.get(Arrays.toString(parameterTypes));
        if (descriptor != null
                && (descriptor.hasConstrainedParameters() || descriptor.hasConstrainedReturnValue())) {
            return descriptor;
        }
        return null;
    }

    public Set<ConstructorDescriptor> getConstrainedConstructors() {
        return constrainedConstructors;
    }

    /**
     * {@inheritDoc}
     */
    public String toString() {
        return "BeanDescriptorImpl{" + "returnType=" + elementClass + '}';
    }

    private static <A extends ExecutableDescriptor> Set<A> toConstrained(final Collection<A> src) {
        final Set<A> dest = new HashSet<A>();
        for (final A d : src) {
            if (d.hasConstrainedParameters() || d.hasConstrainedReturnValue()) {
                dest.add(d);
            }
        }
        return Collections.unmodifiableSet(dest);
    }

    private static class ExecutableMeta {
        private final ApacheFactoryContext factoryContext;
        private final AnnotationProcessor annotationProcessor;
        private final MetaBean metaBean;
        private final Map<String, MethodDescriptor> methodConstraints = new HashMap<String, MethodDescriptor>();
        private final Map<String, ConstructorDescriptor> contructorConstraints = new HashMap<String, ConstructorDescriptor>();
        private Boolean isBeanConstrained = null;

        private ExecutableMeta(final ApacheFactoryContext factoryContext, final MetaBean metaBean1,
                final Collection<ConstraintDescriptor<?>> constraintDescriptors) {
            this.metaBean = metaBean1;
            this.factoryContext = factoryContext;
            this.annotationProcessor = new AnnotationProcessor(factoryContext.getFactory());

            buildExecutableDescriptors();

            boolean hasAnyContraints;
            if (constraintDescriptors.isEmpty()) {
                hasAnyContraints = false;
                for (final MetaProperty mprop : metaBean.getProperties()) {
                    if (!getConstraintDescriptors(mprop.getValidations()).isEmpty()) {
                        hasAnyContraints = true;
                        break;
                    }
                }
            } else {
                hasAnyContraints = true;
            }

            // cache isBeanConstrained
            if (hasAnyContraints) {
                isBeanConstrained = true;
            } else {
                isBeanConstrained = false;
                for (final MetaProperty mprop : metaBean.getProperties()) {
                    if (mprop.getMetaBean() != null || mprop.getFeature(Features.Property.REF_CASCADE) != null) {
                        isBeanConstrained = true;
                        break;
                    }
                }
            }
        }

        private void buildConstructorConstraints() throws InvocationTargetException, IllegalAccessException {
            for (final Constructor<?> cons : Reflection.getDeclaredConstructors(metaBean.getBeanClass())) {
                final ConstructorDescriptorImpl consDesc = new ConstructorDescriptorImpl(metaBean,
                        EMPTY_VALIDATION);
                contructorConstraints.put(Arrays.toString(cons.getParameterTypes()), consDesc);

                final List<String> names = factoryContext.getParameterNameProvider().getParameterNames(cons);
                final boolean isInnerClass = cons.getDeclaringClass().getEnclosingClass() != null
                        && !Modifier.isStatic(cons.getDeclaringClass().getModifiers());

                final AnnotationIgnores annotationIgnores = factoryContext.getFactory().getAnnotationIgnores();

                {
                    final Annotation[][] paramsAnnos = cons.getParameterAnnotations();

                    int idx = 0;
                    if (isInnerClass) { // paramsAnnos.length = parameterTypes.length - 1 in this case
                        final ParameterDescriptorImpl paramDesc = new ParameterDescriptorImpl(metaBean,
                                EMPTY_VALIDATION, names.get(idx));
                        consDesc.getParameterDescriptors().add(paramDesc);
                        idx++;
                    }

                    for (final Annotation[] paramAnnos : paramsAnnos) {
                        if (annotationIgnores.isIgnoreAnnotationOnParameter(cons, idx)) {
                            consDesc.getParameterDescriptors()
                                    .add(new ParameterDescriptorImpl(metaBean, EMPTY_VALIDATION, names.get(idx)));
                        } else if (cons.getParameterTypes().length > idx) {
                            ParameterAccess access = new ParameterAccess(cons.getParameterTypes()[idx], idx);
                            consDesc.addValidations(
                                    processAnnotations(consDesc, paramAnnos, access, idx, names.get(idx))
                                            .getValidations());
                        } // else anonymous class so that's fine
                        idx++;
                    }

                    if (!annotationIgnores.isIgnoreAnnotations(cons)) {
                        for (final Annotation anno : cons.getAnnotations()) {
                            if (Valid.class.isInstance(anno)) {
                                consDesc.setCascaded(true);
                            } else {
                                processAnnotations(null, consDesc, cons.getDeclaringClass(), anno);
                            }
                        }
                    }
                }

                if (annotationIgnores.isIgnoreAnnotationOnCrossParameter(cons)
                        && consDesc.getCrossParameterDescriptor() != null) {
                    consDesc.setCrossParameterDescriptor(null);
                }
                if (annotationIgnores.isIgnoreAnnotationOnReturn(cons)
                        && consDesc.getReturnValueDescriptor() != null) {
                    consDesc.setReturnValueDescriptor(null);
                }

                final MetaConstructor metaConstructor = metaBean.getConstructor(cons);
                if (metaConstructor != null) {
                    for (final Annotation anno : metaConstructor.getAnnotations()) {
                        if (Valid.class.isInstance(anno)) {
                            consDesc.setCascaded(true);
                        } else {
                            processAnnotations(null, consDesc, cons.getDeclaringClass(), anno);
                        }
                    }

                    // parameter validations
                    final Collection<MetaParameter> paramsAnnos = metaConstructor.getParameters();
                    for (final MetaParameter paramAnnos : paramsAnnos) {
                        final int idx = paramAnnos.getIndex();
                        final ParameterAccess access = new ParameterAccess(cons.getParameterTypes()[idx], idx);
                        processAnnotations(consDesc, paramAnnos.getAnnotations(), access, idx, names.get(idx));
                    }
                }

                if (consDesc.getGroupConversions().isEmpty() || consDesc.isCascaded()) {
                    ensureNotNullDescriptors(cons.getDeclaringClass(), consDesc);
                } else {
                    throw new ConstraintDeclarationException("@Valid is needed to define a group conversion");
                }
            }
        }

        private void ensureNotNullDescriptors(final Class<?> returnType,
                final InvocableElementDescriptor consDesc) {
            // can't be null
            if (consDesc.getCrossParameterDescriptor() == null) {
                consDesc.setCrossParameterDescriptor(new CrossParameterDescriptorImpl(metaBean, NO_CONSTRAINTS));
            }
            if (consDesc.getReturnValueDescriptor() == null) {
                consDesc.setReturnValueDescriptor(
                        new ReturnValueDescriptorImpl(metaBean, returnType, NO_CONSTRAINTS, consDesc.isCascaded()));
            }
            // enforce it since ReturnValueDescriptor can be created before cascaded is set to true
            final ReturnValueDescriptorImpl returnValueDescriptor = ReturnValueDescriptorImpl.class
                    .cast(consDesc.getReturnValueDescriptor());
            returnValueDescriptor.setCascaded(consDesc.isCascaded());
            if (returnValueDescriptor.getGroupConversions().isEmpty()) {
                // loop to not forget to map calling addGroupConversion()
                for (final GroupConversionDescriptor c : consDesc.getGroupConversions()) {
                    returnValueDescriptor.addGroupConversion(c);
                }
            }
        }

        private void processAnnotations(final Method mtd, final InvocableElementDescriptor consDesc,
                final Class<?> clazz, final Annotation anno)
                throws InvocationTargetException, IllegalAccessException {
            if (mtd == null
                    || !factoryContext.getFactory().getAnnotationIgnores().isIgnoreAnnotationOnReturn(mtd)) {
                final ReturnAccess returnAccess = new ReturnAccess(clazz);
                final AppendValidationToList validations = new AppendValidationToList();
                processAnnotation(anno, consDesc, returnAccess, validations);
                final List<ConstraintValidation<?>> list = removeFromListValidationAppliesTo(
                        validations.getValidations(), ConstraintTarget.PARAMETERS);
                consDesc.addValidations(list);

                ReturnValueDescriptorImpl returnValueDescriptor = ReturnValueDescriptorImpl.class
                        .cast(consDesc.getReturnValueDescriptor());
                if (consDesc.getReturnValueDescriptor() == null) {
                    returnValueDescriptor = new ReturnValueDescriptorImpl(metaBean, clazz, list,
                            consDesc.isCascaded());
                    consDesc.setReturnValueDescriptor(returnValueDescriptor);
                } else {
                    returnValueDescriptor.getMutableConstraintDescriptors().addAll(list);
                }
            }

            if (mtd == null || !factoryContext.getFactory().getAnnotationIgnores()
                    .isIgnoreAnnotationOnCrossParameter(mtd)) {
                final ParametersAccess parametersAccess = new ParametersAccess();
                final AppendValidationToList validations = new AppendValidationToList();
                processAnnotation(anno, consDesc, parametersAccess, validations);
                final List<ConstraintValidation<?>> list = removeFromListValidationAppliesTo(
                        validations.getValidations(), ConstraintTarget.RETURN_VALUE);
                consDesc.addValidations(list);
                if (consDesc.getCrossParameterDescriptor() == null) {
                    consDesc.setCrossParameterDescriptor(new CrossParameterDescriptorImpl(metaBean, list));
                } else {
                    CrossParameterDescriptorImpl.class.cast(consDesc.getCrossParameterDescriptor())
                            .getMutableConstraintDescriptors().addAll(list);
                }
            }
        }

        private static List<ConstraintValidation<?>> removeFromListValidationAppliesTo(
                final List<ConstraintValidation<?>> validations, final ConstraintTarget constraint) {
            final Iterator<ConstraintValidation<?>> i = validations.iterator();
            while (i.hasNext()) {
                if (constraint.equals(i.next().getValidationAppliesTo())) {
                    i.remove();
                }
            }
            return validations;
        }

        private void buildMethodConstraints() throws InvocationTargetException, IllegalAccessException {

            final Class<?> current = metaBean.getBeanClass();
            final List<Class<?>> classHierarchy = ClassHelper
                    .fillFullClassHierarchyAsList(new ArrayList<Class<?>>(), current);
            classHierarchy.remove(current);

            for (final Method method : Reflection.getDeclaredMethods(current)) {
                if (Modifier.isStatic(method.getModifiers()) || method.isSynthetic()) {
                    continue;
                }

                final boolean propertyAccessor = method.getParameterTypes().length == 0
                        && (method.getName().startsWith("get") && !Void.TYPE.equals(method.getReturnType())
                                || method.getName().startsWith("is")
                                        && Boolean.TYPE.equals(method.getReturnType()));

                final String key = method.getName() + Arrays.toString(method.getParameterTypes());
                MethodDescriptorImpl methodDesc = MethodDescriptorImpl.class.cast(methodConstraints.get(key));
                if (methodDesc == null) {
                    methodDesc = new MethodDescriptorImpl(metaBean, EMPTY_VALIDATION, method);
                    methodConstraints.put(key, methodDesc);
                } else {
                    continue;
                }

                final Collection<Method> parents = new ArrayList<Method>();
                for (final Class<?> clazz : classHierarchy) {
                    final Method overridden = Reflection.getDeclaredMethod(clazz, method.getName(),
                            method.getParameterTypes());
                    if (overridden != null) {
                        parents.add(overridden);
                        processMethod(overridden, methodDesc);
                    }
                }

                processMethod(method, methodDesc);

                ensureNotNullDescriptors(method.getReturnType(), methodDesc);

                if (parents != null) {
                    if (parents.size() > 1) {
                        for (final Method parent : parents) {
                            final MethodDescriptor parentDec = factoryContext.getValidator()
                                    .getConstraintsForClass(parent.getDeclaringClass())
                                    .getConstraintsForMethod(parent.getName(), parent.getParameterTypes());
                            if (parentDec != null) {
                                ensureNoParameterConstraint(InvocableElementDescriptor.class.cast(parentDec),
                                        "Parameter constraints can't be defined for parallel interfaces/parents");
                            } else {
                                ensureMethodDoesntDefineParameterConstraint(methodDesc);
                            }
                            ensureNoReturnValueAddedInChild(methodDesc.getReturnValueDescriptor(), parentDec,
                                    "Return value constraints should be the same for parent and children");
                        }
                    } else if (parents.size() == 1) {
                        final Method parent = parents.iterator().next();
                        final MethodDescriptor parentDesc = factoryContext.getValidator()
                                .getConstraintsForClass(parent.getDeclaringClass())
                                .getConstraintsForMethod(parent.getName(), parent.getParameterTypes());
                        ensureNoReturnValueAddedInChild(methodDesc.getReturnValueDescriptor(), parentDesc,
                                "Return value constraints should be at least the same for parent and children");

                        if (parentDesc == null) {
                            ensureMethodDoesntDefineParameterConstraint(methodDesc);
                        } else {
                            final Iterator<ParameterDescriptor> parentPd = parentDesc.getParameterDescriptors()
                                    .iterator();
                            for (final ParameterDescriptor pd : methodDesc.getParameterDescriptors()) {
                                final ParameterDescriptor next = parentPd.next();
                                if (pd.getConstraintDescriptors().size() != next.getConstraintDescriptors()
                                        .size()) {
                                    throw new ConstraintDeclarationException(
                                            "child shouldn't get more constraint than parent");
                                }
                                if (pd.isCascaded() != next.isCascaded()) { // @Valid
                                    throw new ConstraintDeclarationException(
                                            "child shouldn't get more constraint than parent");
                                }
                            }
                        }
                    }

                    final Class<?>[] interfaces = method.getDeclaringClass().getInterfaces();
                    final Collection<Method> itfWithThisMethod = new ArrayList<Method>();
                    for (final Class<?> i : interfaces) {
                        final Method m = Reflection.getDeclaredMethod(i, method.getName(),
                                method.getParameterTypes());
                        if (m != null) {
                            itfWithThisMethod.add(m);
                        }
                    }
                    if (itfWithThisMethod.size() > 1) {
                        for (final Method m : itfWithThisMethod) {
                            ensureNoConvertGroup(m, "ConvertGroup can't be used in parallel interfaces");
                        }
                    } else if (itfWithThisMethod.size() == 1) {
                        ensureNoConvertGroup(itfWithThisMethod.iterator().next(),
                                "ConvertGroup can't be used in interface AND parent class");
                    }

                    int returnValid = 0;
                    if (method.getAnnotation(Valid.class) != null) {
                        returnValid++;
                    }
                    for (final Class<?> clazz : classHierarchy) {
                        final Method overridden = Reflection.getDeclaredMethod(clazz, method.getName(),
                                method.getParameterTypes());
                        if (overridden != null) {
                            if (overridden.getAnnotation(Valid.class) != null) {
                                returnValid++;
                            }
                        }
                    }
                    if (returnValid > 1
                            && !(interfaces.length == returnValid && method.getAnnotation(Valid.class) == null)) {
                        throw new ConstraintDeclarationException(
                                "@Valid on returned value can't be set more than once");
                    }
                }

                if (propertyAccessor) {
                    final MetaProperty prop = metaBean
                            .getProperty(Introspector.decapitalize(method.getName().substring(3)));
                    if (prop != null && prop.getFeature(Features.Property.REF_CASCADE) != null) {
                        methodDesc.setCascaded(true);
                    }
                }

                if (!methodDesc.getGroupConversions().isEmpty() && !methodDesc.isCascaded()) {
                    throw new ConstraintDeclarationException("@Valid is needed to define a group conversion");
                }
            }

            for (final Class<?> parent : classHierarchy) {
                final BeanDescriptorImpl desc = BeanDescriptorImpl.class
                        .cast(factoryContext.getValidator().getConstraintsForClass(parent));
                for (final String s : desc.meta.methodConstraints.keySet()) {
                    if (!methodConstraints.containsKey(s)) { // method from the parent only
                        methodConstraints.put(s, desc.meta.methodConstraints.get(s));
                    }
                }
            }
        }

        private void ensureMethodDoesntDefineParameterConstraint(MethodDescriptorImpl methodDesc) {
            for (final ParameterDescriptor pd : methodDesc.getParameterDescriptors()) {
                if (!pd.getConstraintDescriptors().isEmpty()) {
                    throw new ConstraintDeclarationException("child shouldn't get more constraint than parent");
                }
                if (pd.isCascaded()) { // @Valid
                    throw new ConstraintDeclarationException("child shouldn't get more constraint than parent");
                }
            }
        }

        private void ensureNoReturnValueAddedInChild(final ReturnValueDescriptor returnValueDescriptor,
                final MethodDescriptor parentMtdDesc, final String msg) {
            if (parentMtdDesc == null) {
                return;
            }
            final ReturnValueDescriptor parentReturnDesc = parentMtdDesc.getReturnValueDescriptor();
            if (parentReturnDesc.isCascaded() && !returnValueDescriptor.isCascaded() || parentReturnDesc
                    .getConstraintDescriptors().size() > returnValueDescriptor.getConstraintDescriptors().size()) {
                throw new ConstraintDeclarationException(msg);
            }
        }

        private static void ensureNoParameterConstraint(final InvocableElementDescriptor constraintsForMethod,
                final String msg) {
            for (final ParameterDescriptor parameterDescriptor : constraintsForMethod.getParameterDescriptors()) {
                if (!parameterDescriptor.getConstraintDescriptors().isEmpty() || parameterDescriptor.isCascaded()) {
                    throw new ConstraintDeclarationException(msg);
                }
            }
        }

        private static void ensureNoConvertGroup(final Method method, final String msg) {
            for (final Annotation[] annotations : method.getParameterAnnotations()) {
                for (final Annotation a : annotations) {
                    if (ConvertGroup.class.isInstance(a)) {
                        throw new ConstraintDeclarationException(msg);
                    }
                }
            }
            if (method.getAnnotation(ConvertGroup.class) != null) {
                throw new ConstraintDeclarationException(msg);
            }
        }

        private void processMethod(final Method method, final MethodDescriptorImpl methodDesc)
                throws InvocationTargetException, IllegalAccessException {
            final AnnotationIgnores annotationIgnores = factoryContext.getFactory().getAnnotationIgnores();

            { // reflection
                if (!annotationIgnores.isIgnoreAnnotations(method)) {
                    // return value validations and/or cross-parameter validation
                    for (final Annotation anno : method.getAnnotations()) {
                        if (anno instanceof Valid || anno instanceof Validate) {
                            methodDesc.setCascaded(true);
                        } else {
                            processAnnotations(method, methodDesc, method.getReturnType(), anno);
                        }
                    }
                }

                // parameter validations
                final Annotation[][] paramsAnnos = method.getParameterAnnotations();
                int idx = 0;
                final List<String> names = factoryContext.getParameterNameProvider().getParameterNames(method);
                for (final Annotation[] paramAnnos : paramsAnnos) {
                    if (annotationIgnores.isIgnoreAnnotationOnParameter(method, idx)) {
                        final ParameterDescriptorImpl parameterDescriptor = new ParameterDescriptorImpl(metaBean,
                                EMPTY_VALIDATION, names.get(idx));
                        parameterDescriptor.setIndex(idx);
                        methodDesc.getParameterDescriptors().add(parameterDescriptor);
                    } else {
                        final ParameterAccess access = new ParameterAccess(method.getParameterTypes()[idx], idx);
                        processAnnotations(methodDesc, paramAnnos, access, idx, names.get(idx));
                    }
                    idx++;
                }
            }

            if (annotationIgnores.isIgnoreAnnotationOnCrossParameter(method)
                    && methodDesc.getCrossParameterDescriptor() != null) {
                methodDesc.setCrossParameterDescriptor(null);
            }
            if (annotationIgnores.isIgnoreAnnotationOnReturn(method)
                    && methodDesc.getReturnValueDescriptor() != null) {
                methodDesc.setReturnValueDescriptor(null);
            }

            final MetaMethod metaMethod = metaBean.getMethod(method);
            if (metaMethod != null) {
                for (final Annotation anno : metaMethod.getAnnotations()) {
                    if (anno instanceof Valid) {
                        methodDesc.setCascaded(true);
                    } else {
                        // set first param as null to force it to be read
                        processAnnotations(null, methodDesc, method.getReturnType(), anno);
                    }
                }

                // parameter validations
                final Collection<MetaParameter> paramsAnnos = metaMethod.getParameters();
                final List<String> names = factoryContext.getParameterNameProvider().getParameterNames(method);
                for (final MetaParameter paramAnnos : paramsAnnos) {
                    final int idx = paramAnnos.getIndex();
                    final ParameterAccess access = new ParameterAccess(method.getParameterTypes()[idx], idx);
                    processAnnotations(methodDesc, paramAnnos.getAnnotations(), access, idx, names.get(idx));
                }
            }
        }

        private AppendValidationToList processAnnotations(InvocableElementDescriptor methodDesc,
                Annotation[] paramAnnos, AccessStrategy access, int idx, String name)
                throws InvocationTargetException, IllegalAccessException {
            final AppendValidationToList validations = new AppendValidationToList();
            boolean cascaded = false;

            Group[] from = null;
            Group[] to = null;

            for (final Annotation anno : paramAnnos) {
                if (anno instanceof Valid || anno instanceof Validate) {
                    cascaded = true;
                } else if (ConvertGroup.class.isInstance(anno)) {
                    final ConvertGroup cg = ConvertGroup.class.cast(anno);
                    from = new Group[] { new Group(cg.from()) };
                    to = new Group[] { new Group(cg.to()) };
                } else if (ConvertGroup.List.class.isInstance(anno)) {
                    final ConvertGroup.List cgl = ConvertGroup.List.class.cast(anno);
                    final ConvertGroup[] groups = cgl.value();
                    from = new Group[groups.length];
                    to = new Group[groups.length];
                    for (int i = 0; i < to.length; i++) {
                        from[i] = new Group(groups[i].from());
                        to[i] = new Group(groups[i].to());
                    }
                } else {
                    processConstraint(anno, methodDesc, access, validations);
                }
            }

            ParameterDescriptorImpl paramDesc = null;
            for (final ParameterDescriptor pd : methodDesc.getParameterDescriptors()) {
                if (pd.getIndex() == idx) {
                    paramDesc = ParameterDescriptorImpl.class.cast(pd);
                }
            }

            if (paramDesc == null) {
                paramDesc = new ParameterDescriptorImpl(Class.class.cast(access.getJavaType()), // set from getParameterTypes() so that's a Class<?>
                        validations.getValidations().toArray(new Validation[validations.getValidations().size()]),
                        name);
                paramDesc.setIndex(idx);
                final List<ParameterDescriptor> parameterDescriptors = methodDesc.getParameterDescriptors();
                if (!parameterDescriptors.contains(paramDesc)) {
                    parameterDescriptors.add(paramDesc);
                }
                paramDesc.setCascaded(cascaded);
            } else {
                final List<ConstraintValidation<?>> newValidations = validations.getValidations();
                for (final ConstraintValidation<?> validation : newValidations) { // don't add it if exactly the same is already here
                    boolean alreadyHere = false;
                    for (final ConstraintDescriptor<?> existing : paramDesc.getMutableConstraintDescriptors()) {
                        if (existing.getAnnotation().annotationType()
                                .equals(validation.getAnnotation().annotationType())) { // TODO: make it a bit finer
                            alreadyHere = true;
                            break;
                        }
                    }
                    if (!alreadyHere) {
                        paramDesc.getMutableConstraintDescriptors().add(validation);
                    }
                }
                if (cascaded) {
                    paramDesc.setCascaded(true);
                } // else keep previous config
            }
            if (from != null) {
                if (paramDesc.isCascaded()) {
                    for (int i = 0; i < from.length; i++) {
                        paramDesc.addGroupConversion(new GroupConversionDescriptorImpl(from[i], to[i]));
                    }
                } else {
                    throw new ConstraintDeclarationException("Group conversion is only relevant for @Valid cases");
                }
            }
            return validations;
        }

        private <A extends Annotation> void processAnnotation(final A annotation,
                final InvocableElementDescriptor desc, final AccessStrategy access,
                final AppendValidation validations) throws InvocationTargetException, IllegalAccessException {
            if (annotation.annotationType().getName().startsWith("java.lang.annotation.")) {
                return;
            }

            if (annotation instanceof Valid || annotation instanceof Validate) {
                desc.setCascaded(true);
            } else if (ConvertGroup.class.isInstance(annotation) && ReturnAccess.class.isInstance(access)) { // access is just tested to ensure to not read it twice with cross parameter
                final ConvertGroup cg = ConvertGroup.class.cast(annotation);
                desc.addGroupConversion(
                        new GroupConversionDescriptorImpl(new Group(cg.from()), new Group(cg.to())));
            } else if (ConvertGroup.List.class.isInstance(annotation) && ReturnAccess.class.isInstance(access)) {
                final ConvertGroup.List cgl = ConvertGroup.List.class.cast(annotation);
                for (final ConvertGroup cg : cgl.value()) {
                    desc.addGroupConversion(
                            new GroupConversionDescriptorImpl(new Group(cg.from()), new Group(cg.to())));
                }
            } else {
                processConstraint(annotation, desc, access, validations);
            }
        }

        private <A extends Annotation> void processConstraint(final A annotation,
                final InvocableElementDescriptor desc, final AccessStrategy access,
                final AppendValidation validations) throws IllegalAccessException, InvocationTargetException {
            final Constraint vcAnno = annotation.annotationType().getAnnotation(Constraint.class);
            if (vcAnno == null) {
                /*
                 * Multi-valued constraints
                 */
                final ConstraintAnnotationAttributes.Worker<? extends Annotation> worker = ConstraintAnnotationAttributes.VALUE
                        .analyze(annotation.annotationType());
                if (worker.isValid()) {
                    final Object value = worker.read(annotation);
                    if (Annotation[].class.isInstance(value)) {
                        final Annotation[] children = Annotation[].class.cast(value);
                        if (children != null) {
                            for (Annotation child : children) {
                                processAnnotation(child, desc, access, validations); // recursion
                            }
                        }
                    }
                }
            } else {
                annotationProcessor.processAnnotation(annotation, null,
                        ClassUtils.primitiveToWrapper((Class<?>) access.getJavaType()), access, validations, true);
            }
        }

        private void buildExecutableDescriptors() {
            try {
                buildMethodConstraints();
                buildConstructorConstraints();
            } catch (final Exception ex) {
                if (RuntimeException.class.isInstance(ex)) {
                    throw RuntimeException.class.cast(ex);
                }
                throw new IllegalArgumentException(ex.getMessage(), ex);
            }
        }
    }
}