blob: df7f911e7c964887da035ad22b035ea40b1b9d45 [file] [log] [blame]
/*
* 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);
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);
}
}
}
}