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