blob: 5629e8070ec267144e766fc1c7310132ef833921 [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.jsr303;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.UnexpectedTypeException;
import javax.validation.Valid;
import javax.validation.ValidationException;
import javax.validation.groups.Default;
import org.apache.bval.jsr303.util.ConstraintDefinitionValidator;
import org.apache.bval.jsr303.util.SecureActions;
import org.apache.bval.model.Features;
import org.apache.bval.model.MetaBean;
import org.apache.bval.model.MetaProperty;
import org.apache.bval.util.AccessStrategy;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.reflect.TypeUtils;
/**
* Description: implements uniform handling of JSR303 {@link Constraint}
* annotations, including composed constraints and the resolution of
* {@link ConstraintValidator} implementations.
*
* @version $Rev: 996992 $ $Date: 2010-09-14 12:05:40 -0500 (Tue, 14 Sep 2010) $
*/
public final class AnnotationProcessor {
/** {@link ApacheFactoryContext} used */
private final ApacheFactoryContext factoryContext;
/**
* Create a new {@link AnnotationProcessor} instance.
*
* @param factoryContext
*/
public AnnotationProcessor(ApacheFactoryContext factoryContext) {
this.factoryContext = factoryContext;
}
/**
* Process JSR303 annotations.
*
* @param prop
* potentially null
* @param owner
* bean type
* @param element
* whose annotations to read
* @param access
* strategy for <code>prop</code>
* @param appender
* handling accumulation
* @return whether any processing took place
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
public boolean processAnnotations(MetaProperty prop, Class<?> owner, AnnotatedElement element,
AccessStrategy access, AppendValidation appender) throws IllegalAccessException, InvocationTargetException {
boolean changed = false;
for (Annotation annotation : element.getDeclaredAnnotations()) {
changed |= processAnnotation(annotation, prop, owner, access, appender);
}
return changed;
}
/**
* Convenience method to process a single class-level annotation.
*
* @param <A>
* annotation type
* @param annotation
* to process
* @param owner
* bean type
* @param appender
* handling accumulation
* @return whether any processing took place
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
public final <A extends Annotation> boolean processAnnotation(A annotation, Class<?> owner, AppendValidation appender)
throws IllegalAccessException, InvocationTargetException {
return processAnnotation(annotation, null, owner, null, appender);
}
/**
* Process a single annotation.
*
* @param <A>
* annotation type
* @param annotation
* to process
* @param prop
* potentially null
* @param owner
* bean type
* @param access
* strategy for <code>prop</code>
* @param appender
* handling accumulation
* @return whether any processing took place
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
public <A extends Annotation> boolean processAnnotation(A annotation, MetaProperty prop, Class<?> owner,
AccessStrategy access, AppendValidation appender) throws IllegalAccessException, InvocationTargetException {
if (annotation instanceof Valid) {
return addAccessStrategy(prop, access);
}
/**
* An annotation is considered a constraint definition if its retention
* policy contains RUNTIME and if the annotation itself is annotated
* with javax.validation.Constraint.
*/
Constraint vcAnno = annotation.annotationType().getAnnotation(Constraint.class);
if (vcAnno != null) {
ConstraintDefinitionValidator.validateConstraintDefinition(annotation);
Class<? extends ConstraintValidator<A, ?>>[] validatorClasses;
validatorClasses = findConstraintValidatorClasses(annotation, vcAnno);
return applyConstraint(annotation, validatorClasses, prop, owner, access, appender);
}
/**
* Multi-valued constraints: To support this requirement, the bean
* validation provider treats regular annotations (annotations not
* annotated by @Constraint) whose value element has a return type of an
* array of constraint annotations in a special way.
*/
Object result =
SecureActions.getAnnotationValue(annotation, ConstraintAnnotationAttributes.VALUE.getAttributeName());
if (result instanceof Annotation[]) {
boolean changed = false;
for (Annotation each : (Annotation[]) result) {
changed |= processAnnotation(each, prop, owner, access, appender);
}
return changed;
}
return false;
}
/**
* Add the specified {@link AccessStrategy} to <code>prop</code>; noop if
* <code>prop == null</code>.
*
* @param prop
* @param access
* @return whether anything took place.
*/
public boolean addAccessStrategy(MetaProperty prop, AccessStrategy access) {
if (prop == null) {
return false;
}
AccessStrategy[] strategies = prop.getFeature(Features.Property.REF_CASCADE);
if (strategies == null) {
strategies = new AccessStrategy[] { access };
prop.putFeature(Features.Property.REF_CASCADE, strategies);
} else if (!ArrayUtils.contains(strategies, access)) {
prop.putFeature(Features.Property.REF_CASCADE, ArrayUtils.add(strategies, access));
}
return true;
}
/**
* Find available {@link ConstraintValidation} classes for a given
* constraint annotation.
*
* @param annotation
* @param vcAnno
* @return {@link ConstraintValidation} implementation class array
*/
@SuppressWarnings("unchecked")
private <A extends Annotation> Class<? extends ConstraintValidator<A, ?>>[] findConstraintValidatorClasses(
A annotation, Constraint vcAnno) {
if (vcAnno == null) {
vcAnno = annotation.annotationType().getAnnotation(Constraint.class);
}
Class<? extends ConstraintValidator<A, ?>>[] validatorClasses;
Class<A> annotationType = (Class<A>) annotation.annotationType();
validatorClasses = factoryContext.getFactory().getConstraintsCache().getConstraintValidators(annotationType);
if (validatorClasses == null) {
validatorClasses = (Class<? extends ConstraintValidator<A, ?>>[]) vcAnno.validatedBy();
if (validatorClasses.length == 0) {
validatorClasses =
factoryContext.getFactory().getDefaultConstraints().getValidatorClasses(annotationType);
}
}
return validatorClasses;
}
/**
* Apply a constraint to the specified <code>appender</code>.
*
* @param annotation
* constraint annotation
* @param constraintClasses
* known {@link ConstraintValidator} implementation classes for
* <code>annotation</code>
* @param prop
* meta-property
* @param owner
* type
* @param access
* strategy
* @param appender
* @return success flag
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
private <A extends Annotation> boolean applyConstraint(A annotation,
Class<? extends ConstraintValidator<A, ?>>[] constraintClasses, MetaProperty prop, Class<?> owner,
AccessStrategy access, AppendValidation appender) throws IllegalAccessException, InvocationTargetException {
final ConstraintValidator<A, ?> validator =
getConstraintValidator(annotation, constraintClasses, owner, access);
final AnnotationConstraintBuilder<A> builder =
new AnnotationConstraintBuilder<A>(constraintClasses, validator, annotation, owner, access);
// JSR-303 3.4.4: Add implicit groups
if (prop != null && prop.getParentMetaBean() != null) {
MetaBean parentMetaBean = prop.getParentMetaBean();
// If:
// - the owner is an interface
// - the class of the metabean being build is different than the
// owner
// - and only the Default group is defined
// Then: add the owner interface as implicit groups
if (builder.getConstraintValidation().getOwner().isInterface()
&& parentMetaBean.getBeanClass() != builder.getConstraintValidation().getOwner()
&& builder.getConstraintValidation().getGroups().size() == 1
&& builder.getConstraintValidation().getGroups().contains(Default.class)) {
Set<Class<?>> groups = builder.getConstraintValidation().getGroups();
groups.add(builder.getConstraintValidation().getOwner());
builder.getConstraintValidation().setGroups(groups);
}
}
// If already building a constraint composition tree, ensure that:
// - the parent groups are inherited
// - the parent payload is inherited
if (appender instanceof AppendValidationToBuilder) {
AppendValidationToBuilder avb = (AppendValidationToBuilder) appender;
builder.getConstraintValidation().setGroups(avb.getInheritedGroups());
builder.getConstraintValidation().setPayload(avb.getInheritedPayload());
}
// process composed constraints:
// here are not other superclasses possible, because annotations do not
// inherit!
processAnnotations(prop, owner, annotation.annotationType(), access, new AppendValidationToBuilder(builder));
// Even if the validator is null, it must be added to mimic the RI impl
appender.append(builder.getConstraintValidation());
return true;
}
private <A extends Annotation, T> ConstraintValidator<A, ? super T> getConstraintValidator(A annotation,
Class<? extends ConstraintValidator<A, ?>>[] constraintClasses, Class<?> owner, AccessStrategy access) {
if (constraintClasses != null && constraintClasses.length > 0) {
Type type = determineTargetedType(owner, access);
/**
* spec says in chapter 3.5.3.: The ConstraintValidator chosen to
* validate a declared type T is the one where the type supported by
* the ConstraintValidator is a supertype of T and where there is no
* other ConstraintValidator whose supported type is a supertype of
* T and not a supertype of the chosen ConstraintValidator supported
* type.
*/
Map<Type, Class<? extends ConstraintValidator<A, ?>>> validatorTypes =
getValidatorsTypes(constraintClasses);
final List<Type> assignableTypes = new ArrayList<Type>(constraintClasses.length);
fillAssignableTypes(type, validatorTypes.keySet(), assignableTypes);
reduceAssignableTypes(assignableTypes);
checkOneType(assignableTypes, type, owner, annotation, access);
@SuppressWarnings("unchecked")
final ConstraintValidator<A, ? super T> validator =
(ConstraintValidator<A, ? super T>) factoryContext.getFactory().getConstraintValidatorFactory()
.getInstance(validatorTypes.get(assignableTypes.get(0)));
if (validator == null) {
throw new ValidationException("Factory returned null validator for: "
+ validatorTypes.get(assignableTypes.get(0)));
}
return validator;
// NOTE: validator initialization deferred until append phase
}
return null;
}
private static void checkOneType(List<Type> types, Type targetType, Class<?> owner, Annotation anno,
AccessStrategy access) {
if (types.isEmpty()) {
StringBuilder buf =
new StringBuilder().append("No validator could be found for type ").append(stringForType(targetType))
.append(". See: @").append(anno.annotationType().getSimpleName()).append(" at ").append(
stringForLocation(owner, access));
throw new UnexpectedTypeException(buf.toString());
} else if (types.size() > 1) {
StringBuilder buf = new StringBuilder();
buf.append("Ambiguous validators for type ");
buf.append(stringForType(targetType));
buf.append(". See: @").append(anno.annotationType().getSimpleName()).append(" at ").append(
stringForLocation(owner, access));
buf.append(". Validators are: ");
boolean comma = false;
for (Type each : types) {
if (comma)
buf.append(", ");
comma = true;
buf.append(each);
}
throw new UnexpectedTypeException(buf.toString());
}
}
/** implements spec chapter 3.5.3. ConstraintValidator resolution algorithm. */
private static Type determineTargetedType(Class<?> owner, AccessStrategy access) {
// if the constraint declaration is hosted on a class or an interface,
// the targeted type is the class or the interface.
if (access == null)
return owner;
Type type = access.getJavaType();
if (type == null)
return Object.class;
if (type instanceof Class<?>)
type = ClassUtils.primitiveToWrapper((Class<?>) type);
return type;
}
private static String stringForType(Type clazz) {
if (clazz instanceof Class<?>) {
if (((Class<?>) clazz).isArray()) {
return ((Class<?>) clazz).getComponentType().getName() + "[]";
} else {
return ((Class<?>) clazz).getName();
}
} else {
return clazz.toString();
}
}
private static String stringForLocation(Class<?> owner, AccessStrategy access) {
if (access != null) {
return access.toString();
} else {
return owner.getName();
}
}
private static void fillAssignableTypes(Type type, Set<Type> validatorsTypes, List<Type> suitableTypes) {
for (Type validatorType : validatorsTypes) {
if (org.apache.commons.lang3.reflect.TypeUtils.isAssignable(type, validatorType)
&& !suitableTypes.contains(validatorType)) {
suitableTypes.add(validatorType);
}
}
}
/**
* Tries to reduce all assignable classes down to a single class.
*
* @param assignableTypes
* The set of all classes which are assignable to the class of
* the value to be validated and which are handled by at least
* one of the validators for the specified constraint.
*/
private static void reduceAssignableTypes(List<Type> assignableTypes) {
if (assignableTypes.size() <= 1) {
return; // no need to reduce
}
boolean removed;
do {
removed = false;
final Type type = assignableTypes.get(0);
for (int i = 1; i < assignableTypes.size(); i++) {
Type nextType = assignableTypes.get(i);
if (TypeUtils.isAssignable(nextType, type)) {
assignableTypes.remove(0);
i--;
removed = true;
} else if (TypeUtils.isAssignable(type, nextType)) {
assignableTypes.remove(i--);
removed = true;
}
}
} while (removed && assignableTypes.size() > 1);
}
/**
* Given a set of {@link ConstraintValidator} implementation classes, map
* those to their target types.
*
* @param constraintValidatorClasses
* @return {@link Map} of {@link Type} : {@link ConstraintValidator} subtype
*/
private static <A extends Annotation> Map<Type, Class<? extends ConstraintValidator<A, ?>>> getValidatorsTypes(
Class<? extends ConstraintValidator<A, ?>>[] constraintValidatorClasses) {
Map<Type, Class<? extends ConstraintValidator<A, ?>>> validatorsTypes =
new HashMap<Type, Class<? extends ConstraintValidator<A, ?>>>();
for (Class<? extends ConstraintValidator<A, ?>> validatorType : constraintValidatorClasses) {
Type validatedType =
TypeUtils.getTypeArguments(validatorType, ConstraintValidator.class).get(
ConstraintValidator.class.getTypeParameters()[1]);
if (validatedType == null) {
throw new ValidationException(String.format("Could not detect validated type for %s", validatorType));
}
if (validatedType instanceof GenericArrayType) {
Type componentType = TypeUtils.getArrayComponentType(validatedType);
if (componentType instanceof Class<?>) {
validatedType = Array.newInstance((Class<?>) componentType, 0).getClass();
}
}
validatorsTypes.put(validatedType, validatorType);
}
return validatorsTypes;
}
}