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