blob: 39ca237bcc362e6f22a305a0f682562424243bcc [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.util;
import java.lang.annotation.Annotation;
import java.lang.annotation.Repeatable;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.validation.Constraint;
import javax.validation.ConstraintDeclarationException;
import javax.validation.ConstraintDefinitionException;
import javax.validation.ConstraintTarget;
import javax.validation.OverridesAttribute;
import javax.validation.Payload;
import javax.validation.ValidationException;
import javax.validation.constraintvalidation.ValidationTarget;
import org.apache.bval.jsr.ApacheValidatorFactory;
import org.apache.bval.jsr.ConstraintAnnotationAttributes;
import org.apache.bval.jsr.ConstraintAnnotationAttributes.Worker;
import org.apache.bval.jsr.ConstraintCached.ConstraintValidatorInfo;
import org.apache.bval.jsr.metadata.Meta;
import org.apache.bval.util.Exceptions;
import org.apache.bval.util.Lazy;
import org.apache.bval.util.ObjectUtils;
import org.apache.bval.util.StringUtils;
import org.apache.bval.util.Validate;
import org.apache.bval.util.reflection.Reflection;
import org.apache.commons.weaver.privilizer.Privilizing;
import org.apache.commons.weaver.privilizer.Privilizing.CallTo;
/**
* Manages (constraint) annotations according to the BV spec.
*
* @since 2.0
*/
@Privilizing(@CallTo(Reflection.class))
public class AnnotationsManager {
private static final class OverriddenAnnotationSpecifier {
final Class<? extends Annotation> annotationType;
final boolean impliesSingleComposingConstraint;
final int constraintIndex;
OverriddenAnnotationSpecifier(OverridesAttribute annotation) {
this(annotation.constraint(), annotation.constraintIndex());
}
OverriddenAnnotationSpecifier(Class<? extends Annotation> annotationType, int constraintIndex) {
super();
this.annotationType = annotationType;
this.impliesSingleComposingConstraint = constraintIndex < 0;
this.constraintIndex = Math.max(constraintIndex, 0);
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || !obj.getClass().equals(getClass())) {
return false;
}
final OverriddenAnnotationSpecifier other = (OverriddenAnnotationSpecifier) obj;
return Objects.equals(annotationType, other.annotationType) && constraintIndex == other.constraintIndex;
}
@Override
public int hashCode() {
return Objects.hash(annotationType, constraintIndex);
}
}
private class Composition {
<A extends Annotation> Optional<ConstraintAnnotationAttributes.Worker<A>> validWorker(
ConstraintAnnotationAttributes attr, Class<A> type) {
return Optional.of(type).map(attr::analyze).filter(Worker::isValid);
}
final Lazy<Map<OverriddenAnnotationSpecifier, Map<String, String>>> overrides = new Lazy<>(HashMap::new);
final Annotation[] components;
Composition(Class<? extends Annotation> annotationType) {
// TODO detect recursion
components = getDeclaredConstraints(annotationType);
if (!isComposed()) {
return;
}
final Map<Class<? extends Annotation>, AtomicInteger> constraintCounts = new HashMap<>();
for (Annotation a : components) {
constraintCounts.computeIfAbsent(a.annotationType(), k -> new AtomicInteger()).incrementAndGet();
}
// create a map of overridden constraints to overridden attributes:
for (Method m : Reflection.getDeclaredMethods(annotationType)) {
final String from = m.getName();
for (OverridesAttribute overridesAttribute : m.getDeclaredAnnotationsByType(OverridesAttribute.class)) {
final String to =
Optional.of(overridesAttribute.name()).filter(StringUtils::isNotBlank).orElse(from);
final OverriddenAnnotationSpecifier spec = new OverriddenAnnotationSpecifier(overridesAttribute);
final int count = constraintCounts.get(spec.annotationType).get();
if (spec.impliesSingleComposingConstraint) {
Exceptions.raiseUnless(count == 1, ConstraintDefinitionException::new,
"Expected a single composing %s constraint", spec.annotationType);
} else if (count <= spec.constraintIndex) {
Exceptions.raise(ConstraintDefinitionException::new,
"Expected at least %s composing %s constraints", spec.constraintIndex + 1,
spec.annotationType);
}
final Map<String, String> attributeMapping =
overrides.get().computeIfAbsent(spec, k -> new HashMap<>());
if (attributeMapping.containsKey(to)) {
Exceptions.raise(ConstraintDefinitionException::new,
"Attempt to override %s#%s() index %d from multiple sources",
overridesAttribute.constraint(), to, overridesAttribute.constraintIndex());
}
attributeMapping.put(to, from);
}
}
}
boolean isComposed() {
return components.length > 0;
}
Annotation[] getComponents(Annotation source) {
final Class<?>[] groups =
ConstraintAnnotationAttributes.GROUPS.analyze(source.annotationType()).read(source);
final Class<? extends Payload>[] payload =
ConstraintAnnotationAttributes.PAYLOAD.analyze(source.annotationType()).read(source);
final Optional<ConstraintTarget> constraintTarget =
validWorker(ConstraintAnnotationAttributes.VALIDATION_APPLIES_TO, source.annotationType())
.map(w -> w.read(source));
final Map<Class<? extends Annotation>, AtomicInteger> constraintCounts = new HashMap<>();
return Stream.of(components).map(c -> {
final int index =
constraintCounts.computeIfAbsent(c.annotationType(), k -> new AtomicInteger()).getAndIncrement();
final AnnotationProxyBuilder<Annotation> proxyBuilder = buildProxyFor(c);
proxyBuilder.setGroups(groups);
proxyBuilder.setPayload(payload);
if (constraintTarget.isPresent()
&& validWorker(ConstraintAnnotationAttributes.VALIDATION_APPLIES_TO, c.annotationType())
.isPresent()) {
proxyBuilder.setValidationAppliesTo(constraintTarget.get());
}
overrides.optional().map(o -> o.get(new OverriddenAnnotationSpecifier(c.annotationType(), index)))
.ifPresent(m -> {
final Map<String, Object> sourceAttributes = readAttributes(source);
m.forEach((k, v) -> proxyBuilder.setValue(k, sourceAttributes.get(v)));
});
return proxyBuilder.isChanged() ? proxyBuilder.createAnnotation() : c;
}).toArray(Annotation[]::new);
}
}
private static final Set<ConstraintAnnotationAttributes> CONSTRAINT_ATTRIBUTES =
Collections.unmodifiableSet(EnumSet.complementOf(EnumSet.of(ConstraintAnnotationAttributes.VALUE)));
private static final Set<Class<? extends Annotation>> VALIDATED_CONSTRAINT_TYPES = new HashSet<>();
public static Map<String, Object> readAttributes(Annotation a) {
final Lazy<Map<String, Object>> result = new Lazy<>(LinkedHashMap::new);
Stream.of(Reflection.getDeclaredMethods(a.annotationType())).filter(m -> m.getParameterCount() == 0)
.forEach(m -> {
final boolean mustUnset = Reflection.setAccessible(m, true);
try {
result.get().put(m.getName(), m.invoke(a));
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
Exceptions.raise(ValidationException::new, e, "Caught exception reading attributes of %s", a);
} finally {
if (mustUnset) {
Reflection.setAccessible(m, false);
}
}
});
return result.optional().map(Collections::unmodifiableMap).orElseGet(Collections::emptyMap);
}
public static boolean isAnnotationDirectlyPresent(AnnotatedElement e, Class<? extends Annotation> t) {
return substitute(e).filter(s -> s.isAnnotationPresent(t)).isPresent();
}
public static <T extends Annotation> T getAnnotation(AnnotatedElement e, Class<T> annotationClass) {
return substitute(e).map(s -> s.getAnnotation(annotationClass)).orElse(null);
}
@SuppressWarnings("unchecked")
public static <T extends Annotation> T[] getDeclaredAnnotationsByType(AnnotatedElement e,
Class<T> annotationClass) {
return substitute(e).map(s -> s.getDeclaredAnnotationsByType(annotationClass))
.orElse((T[]) ObjectUtils.EMPTY_ANNOTATION_ARRAY);
}
/**
* Accounts for {@link Constraint} meta-annotation AND {@link Repeatable}
* constraint annotations.
*
* @param meta
* @return Annotation[]
*/
public static Annotation[] getDeclaredConstraints(Meta<?> meta) {
return getDeclaredConstraints(meta.getHost());
}
private static Annotation[] getDeclaredConstraints(AnnotatedElement e) {
final Annotation[] declaredAnnotations =
substitute(e).map(AnnotatedElement::getDeclaredAnnotations).orElse(ObjectUtils.EMPTY_ANNOTATION_ARRAY);
if (declaredAnnotations.length == 0) {
return declaredAnnotations;
}
// collect constraint explicitly nested into repeatable containers:
final Map<Class<? extends Annotation>, Annotation[]> repeated = new HashMap<>();
for (Annotation a : declaredAnnotations) {
final Class<? extends Annotation> annotationType = a.annotationType();
final Worker<? extends Annotation> w = ConstraintAnnotationAttributes.VALUE.analyze(annotationType);
if (w.isValid()
&& ((Class<?>) w.getSpecificType()).getComponentType().isAnnotationPresent(Constraint.class)) {
repeated.put(annotationType, w.read(a));
}
}
Stream<Annotation> constraints = Stream.of(declaredAnnotations)
.filter(a -> a.annotationType().isAnnotationPresent(Constraint.class));
if (!repeated.isEmpty()) {
constraints = constraints.peek(c -> Exceptions.raiseIf(
Optional.of(c.annotationType()).map(t -> t.getAnnotation(Repeatable.class)).map(Repeatable::value)
.filter(repeated::containsKey).isPresent(),
ConstraintDeclarationException::new,
"Simultaneous declaration of repeatable constraint and associated container on %s", e));
constraints = Stream.concat(constraints, repeated.values().stream().flatMap(Stream::of));
}
return constraints.toArray(Annotation[]::new);
}
private static Optional<AnnotatedElement> substitute(AnnotatedElement e) {
if (e instanceof Parameter) {
final Parameter p = (Parameter) e;
if (p.getDeclaringExecutable() instanceof Constructor<?>) {
final Constructor<?> ctor = (Constructor<?>) p.getDeclaringExecutable();
final Class<?> dc = ctor.getDeclaringClass();
if (!(dc.getDeclaringClass() == null || Modifier.isStatic(dc.getModifiers()))) {
// found ctor for non-static inner class
final Annotation[][] parameterAnnotations = ctor.getParameterAnnotations();
if (parameterAnnotations.length == ctor.getParameterCount() - 1) {
final Parameter[] parameters = ctor.getParameters();
final int idx = ObjectUtils.indexOf(parameters, p);
if (idx == 0) {
return Optional.empty();
}
return Optional.of(parameters[idx - 1]);
}
Validate.validState(parameterAnnotations.length == ctor.getParameterCount(),
"Cannot make sense of parameter annotations of %s", ctor);
}
}
}
return Optional.of(e);
}
private final ApacheValidatorFactory validatorFactory;
private final ConcurrentMap<Class<?>, Composition> compositions;
private final ConcurrentMap<Class<?>, Method[]> constraintAttributes;
public AnnotationsManager(ApacheValidatorFactory validatorFactory) {
super();
this.validatorFactory = Validate.notNull(validatorFactory);
compositions = new ConcurrentHashMap<>();
constraintAttributes = new ConcurrentHashMap<>();
}
public void validateConstraintDefinition(Class<? extends Annotation> type) {
if (VALIDATED_CONSTRAINT_TYPES.contains(type)) {
return;
}
Exceptions.raiseUnless(type.isAnnotationPresent(Constraint.class), ConstraintDefinitionException::new,
"%s is not a validation constraint", type);
final Set<ValidationTarget> supportedTargets = supportedTargets(type);
final Map<String, Method> attributes =
Stream.of(Reflection.getDeclaredMethods(type)).filter(m -> m.getParameterCount() == 0)
.collect(Collectors.toMap(Method::getName, Function.identity()));
if (supportedTargets.size() > 1
&& !attributes.containsKey(ConstraintAnnotationAttributes.VALIDATION_APPLIES_TO.getAttributeName())) {
Exceptions.raise(ConstraintDefinitionException::new,
"Constraint %s is both generic and cross-parameter but lacks %s attribute", type.getName(),
ConstraintAnnotationAttributes.VALIDATION_APPLIES_TO);
}
for (ConstraintAnnotationAttributes attr : CONSTRAINT_ATTRIBUTES) {
if (attributes.containsKey(attr.getAttributeName())) {
Exceptions.raiseUnless(attr.analyze(type).isValid(), ConstraintDefinitionException::new,
"%s declared invalid type for attribute %s", type, attr);
if (!attr.isValidDefaultValue(attributes.get(attr.getAttributeName()).getDefaultValue())) {
Exceptions.raise(ConstraintDefinitionException::new,
"%s declares invalid default value for attribute %s", type, attr);
}
if (attr == ConstraintAnnotationAttributes.VALIDATION_APPLIES_TO) {
if (supportedTargets.size() == 1) {
Exceptions.raise(ConstraintDefinitionException::new,
"Pure %s constraint %s should not declare attribute %s",
supportedTargets.iterator().next(), type, attr);
}
}
} else if (attr.isMandatory()) {
Exceptions.raise(ConstraintDefinitionException::new, "%s does not declare mandatory attribute %s",
type, attr);
}
attributes.remove(attr.getAttributeName());
}
attributes.keySet().forEach(k -> Exceptions.raiseIf(k.startsWith("valid"),
ConstraintDefinitionException::new, "Invalid constraint attribute %s", k));
VALIDATED_CONSTRAINT_TYPES.add(type);
}
/**
* Retrieve the composing constraints for the specified constraint
* {@link Annotation}.
*
* @param a
* @return {@link Annotation}[]
*/
public Annotation[] getComposingConstraints(Annotation a) {
return getComposition(a.annotationType()).getComponents(a);
}
/**
* Learn whether {@code a} is composed.
*
* @param a
* @return {@code boolean}
*/
public boolean isComposed(Annotation a) {
return getComposition(a.annotationType()).isComposed();
}
/**
* Get the supported targets for {@code constraintType}.
*
* @param constraintType
* @return {@link Set} of {@link ValidationTarget}
*/
public <A extends Annotation> Set<ValidationTarget> supportedTargets(Class<A> constraintType) {
final Set<ConstraintValidatorInfo<A>> constraintValidatorInfo =
validatorFactory.getConstraintsCache().getConstraintValidatorInfo(constraintType);
final Stream<Set<ValidationTarget>> s;
if (constraintValidatorInfo.isEmpty()) {
// must be for composition:
s = Stream.of(new Composition(constraintType).components).map(Annotation::annotationType)
.map(this::supportedTargets);
} else {
s = constraintValidatorInfo.stream().map(ConstraintValidatorInfo::getSupportedTargets);
}
return s.flatMap(Collection::stream)
.collect(Collectors.toCollection(() -> EnumSet.noneOf(ValidationTarget.class)));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public <A extends Annotation> AnnotationProxyBuilder<A> buildProxyFor(Class<A> type) {
return new AnnotationProxyBuilder<>(type, constraintAttributes);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public <A extends Annotation> AnnotationProxyBuilder<A> buildProxyFor(A instance) {
return new AnnotationProxyBuilder<>(instance, constraintAttributes);
}
private Composition getComposition(Class<? extends Annotation> annotationType) {
return compositions.computeIfAbsent(annotationType, ct -> {
final Set<ValidationTarget> composedTargets = supportedTargets(annotationType);
final Composition result = new Composition(annotationType);
Stream.of(result.components).map(Annotation::annotationType).forEach(at -> {
final Set<ValidationTarget> composingTargets = supportedTargets(at);
if (Collections.disjoint(composingTargets, composedTargets)) {
Exceptions.raise(ConstraintDefinitionException::new,
"Attempt to compose %s of %s but validator types are incompatible", annotationType.getName(),
at.getName());
}
});
return result;
});
}
}