Create a new API for handling merged annotations
Add new `MergedAnnotations` and `MergedAnnotation` interfaces that attempt to provide a uniform way for dealing with merged annotations. Specifically, the new API provides alternatives for the static methods in `AnnotationUtils` and `AnnotatedElementUtils` and supports Spring's comprehensive annotation attribute `@AliasFor` features. The interfaces also open the possibility of the same API being exposed from the `AnnotationMetadata` interface. Additional utility classes for collecting, filtering and selecting annotations have also been added. Typical usage for the new API would be something like: MergedAnnotations.from(Example.class) .stream(MyAnnotation.class) .map(a -> a.getString("value")) .forEach(System.out::println); Closes gh-21697
This commit is contained in:
parent
fdacda8b01
commit
4972d85ae0
|
@ -0,0 +1,226 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Abstract base class for {@link MergedAnnotation} implementations.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
* @param <A> the annotation type
|
||||
*/
|
||||
abstract class AbstractMergedAnnotation<A extends Annotation>
|
||||
implements MergedAnnotation<A> {
|
||||
|
||||
@Nullable
|
||||
private volatile A synthesizedAnnotation;
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isDirectlyPresent() {
|
||||
return isPresent() && getDepth() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMetaPresent() {
|
||||
return isPresent() && getDepth() > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNonDefaultValue(String attributeName) {
|
||||
return !hasDefaultValue(attributeName);
|
||||
}
|
||||
|
||||
public byte getByte(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, Byte.class);
|
||||
}
|
||||
|
||||
public byte[] getByteArray(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, byte[].class);
|
||||
}
|
||||
|
||||
public boolean getBoolean(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, Boolean.class);
|
||||
}
|
||||
|
||||
public boolean[] getBooleanArray(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, boolean[].class);
|
||||
}
|
||||
|
||||
public char getChar(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, Character.class);
|
||||
}
|
||||
|
||||
public char[] getCharArray(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, char[].class);
|
||||
}
|
||||
|
||||
public short getShort(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, Short.class);
|
||||
}
|
||||
|
||||
public short[] getShortArray(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, short[].class);
|
||||
}
|
||||
|
||||
public int getInt(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, Integer.class);
|
||||
}
|
||||
|
||||
public int[] getIntArray(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, int[].class);
|
||||
}
|
||||
|
||||
public long getLong(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, Long.class);
|
||||
}
|
||||
|
||||
public long[] getLongArray(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, long[].class);
|
||||
}
|
||||
|
||||
public double getDouble(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, Double.class);
|
||||
}
|
||||
|
||||
public double[] getDoubleArray(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, double[].class);
|
||||
}
|
||||
|
||||
public float getFloat(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, Float.class);
|
||||
}
|
||||
|
||||
public float[] getFloatArray(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, float[].class);
|
||||
}
|
||||
|
||||
public String getString(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, String.class);
|
||||
}
|
||||
|
||||
public String[] getStringArray(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, String[].class);
|
||||
}
|
||||
|
||||
public Class<?> getClass(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, Class.class);
|
||||
}
|
||||
|
||||
public Class<?>[] getClassArray(String attributeName) {
|
||||
return getRequiredAttributeValue(attributeName, Class[].class);
|
||||
}
|
||||
|
||||
public <E extends Enum<E>> E getEnum(String attributeName, Class<E> type) {
|
||||
Assert.notNull(type, "Type must not be null");
|
||||
return getRequiredAttributeValue(attributeName, type);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <E extends Enum<E>> E[] getEnumArray(String attributeName, Class<E> type) {
|
||||
Assert.notNull(type, "Type must not be null");
|
||||
Class<?> arrayType = Array.newInstance(type, 0).getClass();
|
||||
return (E[]) getRequiredAttributeValue(attributeName, arrayType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Object> getValue(String attributeName) {
|
||||
return getValue(attributeName, Object.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> getValue(String attributeName, Class<T> type) {
|
||||
return Optional.ofNullable(getAttributeValue(attributeName, type));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Object> getDefaultValue(String attributeName) {
|
||||
return getDefaultValue(attributeName, Object.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MergedAnnotation<A> filterDefaultValues() {
|
||||
return filterAttributes(this::hasNonDefaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> asMap(MapValues... options) {
|
||||
return asMap(null, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<A> synthesize(
|
||||
@Nullable Predicate<? super MergedAnnotation<A>> condition)
|
||||
throws NoSuchElementException {
|
||||
|
||||
if (condition == null || condition.test(this)) {
|
||||
return Optional.of(synthesize());
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public A synthesize() {
|
||||
if (!isPresent()) {
|
||||
throw new NoSuchElementException("Unable to synthesize missing annotation");
|
||||
}
|
||||
A synthesized = this.synthesizedAnnotation;
|
||||
if (synthesized == null) {
|
||||
synthesized = createSynthesized();
|
||||
this.synthesizedAnnotation = synthesized;
|
||||
}
|
||||
return synthesized;
|
||||
}
|
||||
|
||||
private <T> T getRequiredAttributeValue(String attributeName, Class<T> type) {
|
||||
T value = getAttributeValue(attributeName, type);
|
||||
if (value == null) {
|
||||
throw new NoSuchElementException("No attribute named '" + attributeName +
|
||||
"' present in merged annotation " + getType());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying attribute value.
|
||||
* @param attributeName the attribute name
|
||||
* @param type the type to return (see {@link MergedAnnotation} class
|
||||
* documentation for details).
|
||||
* @return the attribute value or {@code null} if the value is not found and
|
||||
* is not required
|
||||
* @throws IllegalArgumentException if the source type is not compatible
|
||||
* @throws NoSuchElementException if the value is required but not found
|
||||
*/
|
||||
@Nullable
|
||||
protected abstract <T> T getAttributeValue(String attributeName, Class<T> type);
|
||||
|
||||
/**
|
||||
* Factory method used to create the synthesized annotation.
|
||||
*/
|
||||
protected abstract A createSynthesized();
|
||||
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Callback interface that can be used to filter specific annotation types.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface AnnotationFilter {
|
||||
|
||||
/**
|
||||
* {@link AnnotationFilter} that matches annotations is in the
|
||||
* {@code java.lang.*} or in the
|
||||
* {@code org.springframework.lang.*} package.
|
||||
*/
|
||||
static final AnnotationFilter PLAIN = packages("java.lang",
|
||||
"org.springframework.lang");
|
||||
|
||||
/**
|
||||
* {@link AnnotationFilter} that matches annotations in the
|
||||
* {@code java.lang.*} package.
|
||||
*/
|
||||
static final AnnotationFilter JAVA = packages("java.lang");
|
||||
|
||||
/**
|
||||
* {@link AnnotationFilter} that never matches and can be used when no
|
||||
* filtering is needed.
|
||||
*/
|
||||
static final AnnotationFilter NONE = new AnnotationFilter() {
|
||||
|
||||
@Override
|
||||
public boolean matches(@Nullable String typeName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "No annotation filtering";
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Test if the given annotation matches the filter.
|
||||
* @param annotation the annotation to test
|
||||
* @return {@code true} if the annotation matches
|
||||
*/
|
||||
default boolean matches(@Nullable Annotation annotation) {
|
||||
return matches(annotation != null ? annotation.annotationType() : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the given type matches the filter.
|
||||
* @param type the annotation type to test
|
||||
* @return {@code true} if the annotation matches
|
||||
*/
|
||||
default boolean matches(@Nullable Class<?> type) {
|
||||
return matches(type != null ? type.getName() : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the given type name matches the filter.
|
||||
* @param typeName the annotation type to test
|
||||
* @return {@code true} if the annotation matches
|
||||
*/
|
||||
boolean matches(@Nullable String typeName);
|
||||
|
||||
/**
|
||||
* Return a new {@link AnnotationFilter} that matches annotations in the
|
||||
* specified packages.
|
||||
* @param packages the annotation packages that should match
|
||||
* @return a new {@link AnnotationFilter} instance
|
||||
*/
|
||||
static AnnotationFilter packages(String... packages) {
|
||||
return new PackagesAnnotationFilter(packages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an {@link AnnotationFilter} that is the most appropriate for, and
|
||||
* will always match the given annotation type. Whenever possible,
|
||||
* {@link AnnotationFilter#PLAIN} will be returned.
|
||||
* @param annotationType the annotation type to check
|
||||
* @return the most appropriate annotation filter
|
||||
*/
|
||||
static AnnotationFilter mostAppropriateFor(@Nullable Class<?> annotationType) {
|
||||
return PLAIN.matches(annotationType) ? NONE : PLAIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an {@link AnnotationFilter} that is the most appropriate for, and
|
||||
* will always match all the given annotation types. Whenever possible,
|
||||
* {@link AnnotationFilter#PLAIN} will be returned.
|
||||
* @param annotationTypes the annotation types to check
|
||||
* @return the most appropriate annotation filter
|
||||
*/
|
||||
static AnnotationFilter mostAppropriateFor(Class<?>... annotationTypes) {
|
||||
Assert.notNull(annotationTypes, "AnnotationTypes must not be null");
|
||||
return mostAppropriateFor(Arrays.asList(annotationTypes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an {@link AnnotationFilter} that is the most appropriate for, and
|
||||
* will always match all the given annotation type. Whenever possible,
|
||||
* {@link AnnotationFilter#PLAIN} will be returned.
|
||||
* @param annotationType the annotation type to check
|
||||
* @return the most appropriate annotation filter
|
||||
*/
|
||||
static AnnotationFilter mostAppropriateFor(@Nullable String annotationType) {
|
||||
return PLAIN.matches(annotationType) ? NONE : PLAIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an {@link AnnotationFilter} that is the most appropriate for, and
|
||||
* will always match all the given annotation types. Whenever possible,
|
||||
* {@link AnnotationFilter#PLAIN} will be returned.
|
||||
* @param annotationTypes the annotation types to check
|
||||
* @return the most appropriate annotation filter
|
||||
*/
|
||||
static AnnotationFilter mostAppropriateFor(String... annotationTypes) {
|
||||
Assert.notNull(annotationTypes, "AnnotationTypes must not be null");
|
||||
return mostAppropriateFor(Arrays.asList(annotationTypes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an {@link AnnotationFilter} that is the most appropriate for, and
|
||||
* will always match all the given annotation types. Whenever possible,
|
||||
* {@link AnnotationFilter#PLAIN} will be returned.
|
||||
* @param annotationTypes the annotation types to check (may be class names
|
||||
* or class types)
|
||||
* @return the most appropriate annotation filter
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static AnnotationFilter mostAppropriateFor(Collection<?> annotationTypes) {
|
||||
Assert.notNull(annotationTypes, "AnnotationTypes must not be null");
|
||||
for (Object annotationType : annotationTypes) {
|
||||
if (annotationType == null) {
|
||||
continue;
|
||||
}
|
||||
Assert.isTrue(
|
||||
annotationType instanceof Class || annotationType instanceof String,
|
||||
"AnnotationType must be a Class or String");
|
||||
if (annotationType instanceof Class
|
||||
&& PLAIN.matches((Class<Annotation>) annotationType)) {
|
||||
return NONE;
|
||||
}
|
||||
if (annotationType instanceof String
|
||||
&& PLAIN.matches((String) annotationType)) {
|
||||
return NONE;
|
||||
}
|
||||
}
|
||||
return PLAIN;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,619 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
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.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Provides mapping information for a single annotation (or meta-annotation) in
|
||||
* the context of a root annotation type.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
* @see AnnotationTypeMappings
|
||||
*/
|
||||
class AnnotationTypeMapping {
|
||||
|
||||
@Nullable
|
||||
private final AnnotationTypeMapping parent;
|
||||
|
||||
private final AnnotationTypeMapping root;
|
||||
|
||||
private final int depth;
|
||||
|
||||
private final Class<? extends Annotation> annotationType;
|
||||
|
||||
@Nullable
|
||||
private final Annotation annotation;
|
||||
|
||||
private final AttributeMethods attributes;
|
||||
|
||||
private final MirrorSets mirrorSets;
|
||||
|
||||
private final int[] aliasMappings;
|
||||
|
||||
private final int[] conventionMappings;
|
||||
|
||||
private final Map<Method, List<Method>> aliasedBy;
|
||||
|
||||
private final Set<Method> claimedAliases = new HashSet<>();
|
||||
|
||||
|
||||
AnnotationTypeMapping(Class<? extends Annotation> annotationType) {
|
||||
this(null, annotationType, null);
|
||||
}
|
||||
|
||||
AnnotationTypeMapping(AnnotationTypeMapping parent, Annotation annotation) {
|
||||
this(parent, annotation.annotationType(), annotation);
|
||||
}
|
||||
|
||||
AnnotationTypeMapping(@Nullable AnnotationTypeMapping parent,
|
||||
Class<? extends Annotation> annotationType, @Nullable Annotation annotation) {
|
||||
|
||||
this.parent = parent;
|
||||
this.root = parent != null ? parent.getRoot() : this;
|
||||
this.depth = parent == null ? 0 : parent.getDepth() + 1;
|
||||
this.annotationType = annotationType;
|
||||
this.annotation = annotation;
|
||||
this.attributes = AttributeMethods.forAnnotationType(annotationType);
|
||||
this.mirrorSets = new MirrorSets();
|
||||
this.aliasMappings = filledIntArray(this.attributes.size(), -1);
|
||||
this.conventionMappings = filledIntArray(this.attributes.size(), -1);
|
||||
this.aliasedBy = resolveAliasedForTargets();
|
||||
processAliases();
|
||||
addConventionMappings();
|
||||
}
|
||||
|
||||
|
||||
private Map<Method, List<Method>> resolveAliasedForTargets() {
|
||||
Map<Method, List<Method>> aliasedBy = new HashMap<>();
|
||||
for (int i = 0; i < this.attributes.size(); i++) {
|
||||
Method attribute = this.attributes.get(i);
|
||||
AliasFor aliasFor = AnnotationsScanner.getDeclaredAnnotation(attribute,
|
||||
AliasFor.class);
|
||||
if (aliasFor != null) {
|
||||
Method target = resolveAliasTarget(attribute, aliasFor);
|
||||
aliasedBy.computeIfAbsent(target, key -> new ArrayList<>()).add(
|
||||
attribute);
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableMap(aliasedBy);
|
||||
}
|
||||
|
||||
private Method resolveAliasTarget(Method attribute, AliasFor aliasFor) {
|
||||
return resolveAliasTarget(attribute, aliasFor, true);
|
||||
}
|
||||
|
||||
private Method resolveAliasTarget(Method attribute, AliasFor aliasFor, boolean checkAliasPair) {
|
||||
if (StringUtils.hasText(aliasFor.value()) &&
|
||||
StringUtils.hasText(aliasFor.attribute())) {
|
||||
throw new AnnotationConfigurationException(String.format(
|
||||
"In @AliasFor declared on %s, attribute 'attribute' and its alias "
|
||||
+ "'value' are present with values of '%s' and '%s', but "
|
||||
+ "only one is permitted.",
|
||||
AttributeMethods.describe(attribute), aliasFor.attribute(),
|
||||
aliasFor.value()));
|
||||
}
|
||||
Class<? extends Annotation> targetAnnotation = aliasFor.annotation();
|
||||
if (targetAnnotation == Annotation.class) {
|
||||
targetAnnotation = this.annotationType;
|
||||
}
|
||||
String targetAttributeName = aliasFor.attribute();
|
||||
if (!StringUtils.hasLength(targetAttributeName)) {
|
||||
targetAttributeName = aliasFor.value();
|
||||
}
|
||||
if (!StringUtils.hasLength(targetAttributeName)) {
|
||||
targetAttributeName = attribute.getName();
|
||||
}
|
||||
Method target = AttributeMethods.forAnnotationType(targetAnnotation)
|
||||
.get(targetAttributeName);
|
||||
if (target == null) {
|
||||
if (targetAnnotation == this.annotationType) {
|
||||
throw new AnnotationConfigurationException(String.format(
|
||||
"@AliasFor declaration on %s declares an "
|
||||
+ "alias for '%s' which is not present.",
|
||||
AttributeMethods.describe(attribute), targetAttributeName));
|
||||
}
|
||||
throw new AnnotationConfigurationException(String.format(
|
||||
"%s is declared as an @AliasFor nonexistent %s.",
|
||||
StringUtils.capitalize(AttributeMethods.describe(attribute)),
|
||||
AttributeMethods.describe(targetAnnotation, targetAttributeName)));
|
||||
}
|
||||
if (target == attribute) {
|
||||
throw new AnnotationConfigurationException(String.format(
|
||||
"@AliasFor declaration on %s points to itself. "
|
||||
+ "Specify 'annotation' to point to a same-named "
|
||||
+ "attribute on a meta-annotation.",
|
||||
AttributeMethods.describe(attribute)));
|
||||
}
|
||||
if (!isCompatibleReturnType(attribute.getReturnType(), target.getReturnType())) {
|
||||
throw new AnnotationConfigurationException(String.format(
|
||||
"Misconfigured aliases: %s and %s must declare the same return type.",
|
||||
AttributeMethods.describe(attribute),
|
||||
AttributeMethods.describe(target)));
|
||||
}
|
||||
if (isAliasPair(target) && checkAliasPair) {
|
||||
AliasFor targetAliasFor = target.getAnnotation(AliasFor.class);
|
||||
if (targetAliasFor == null) {
|
||||
throw new AnnotationConfigurationException(
|
||||
String.format("%s must be declared as an @AliasFor '%s'.",
|
||||
StringUtils.capitalize(AttributeMethods.describe(target)),
|
||||
attribute.getName()));
|
||||
}
|
||||
Method mirror = resolveAliasTarget(target, targetAliasFor, false);
|
||||
if (mirror != attribute) {
|
||||
throw new AnnotationConfigurationException(String.format(
|
||||
"%s must be declared as an @AliasFor '%s', not '%s'.",
|
||||
StringUtils.capitalize(AttributeMethods.describe(target)),
|
||||
attribute.getName(), mirror.getName()));
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
private boolean isAliasPair(Method target) {
|
||||
return target.getDeclaringClass().equals(this.annotationType);
|
||||
}
|
||||
|
||||
private boolean isCompatibleReturnType(Class<?> attributeType, Class<?> targetType) {
|
||||
return Objects.equals(attributeType, targetType) ||
|
||||
Objects.equals(attributeType, targetType.getComponentType());
|
||||
}
|
||||
|
||||
private void processAliases() {
|
||||
List<Method> aliases = new ArrayList<>();
|
||||
for (int i = 0; i < this.attributes.size(); i++) {
|
||||
aliases.clear();
|
||||
aliases.add(this.attributes.get(i));
|
||||
collectAliases(aliases);
|
||||
if (aliases.size() > 1) {
|
||||
processAliases(aliases);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void collectAliases(List<Method> aliases) {
|
||||
AnnotationTypeMapping mapping = this;
|
||||
while (mapping != null) {
|
||||
int size = aliases.size();
|
||||
for (int j = 0; j < size; j++) {
|
||||
List<Method> additional = mapping.aliasedBy.get(aliases.get(j));
|
||||
if (additional != null) {
|
||||
aliases.addAll(additional);
|
||||
}
|
||||
}
|
||||
mapping = mapping.parent;
|
||||
}
|
||||
}
|
||||
|
||||
private void processAliases(List<Method> aliases) {
|
||||
int rootAttributeIndex = getFirstRootAttributeIndex(aliases);
|
||||
AnnotationTypeMapping mapping = this;
|
||||
while (mapping != null) {
|
||||
if (rootAttributeIndex != -1 && mapping != this.root) {
|
||||
for (int i = 0; i < mapping.attributes.size(); i++) {
|
||||
if (aliases.contains(mapping.attributes.get(i))) {
|
||||
mapping.aliasMappings[i] = rootAttributeIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
mapping.mirrorSets.updateFrom(aliases);
|
||||
mapping.claimedAliases.addAll(aliases);
|
||||
mapping = mapping.parent;
|
||||
}
|
||||
}
|
||||
|
||||
private int getFirstRootAttributeIndex(Collection<Method> aliases) {
|
||||
AttributeMethods rootAttributes = this.root.getAttributes();
|
||||
for (int i = 0; i < rootAttributes.size(); i++) {
|
||||
if (aliases.contains(rootAttributes.get(i))) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void addConventionMappings() {
|
||||
if (this.depth == 0) {
|
||||
return;
|
||||
}
|
||||
AttributeMethods rootAttributes = this.root.getAttributes();
|
||||
int[] mappings = this.conventionMappings;
|
||||
for (int i = 0; i < mappings.length; i++) {
|
||||
String name = this.attributes.get(i).getName();
|
||||
MirrorSet mirrors = getMirrorSets().getAssigned(i);
|
||||
int mapped = rootAttributes.indexOf(name);
|
||||
if (!MergedAnnotation.VALUE.equals(name) && mapped != -1) {
|
||||
mappings[i] = mapped;
|
||||
if (mirrors != null) {
|
||||
for (int j = 0; j < mirrors.size(); j++) {
|
||||
mappings[mirrors.getAttributeIndex(j)] = mapped;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called after all mappings have been set. At this point no further
|
||||
* lookups from child mappings will occur.
|
||||
*/
|
||||
void afterAllMappingsSet() {
|
||||
validateAllAliasesClaimed();
|
||||
for (int i = 0; i < this.mirrorSets.size(); i++) {
|
||||
validateMirrorSet(this.mirrorSets.get(i));
|
||||
}
|
||||
this.claimedAliases.clear();
|
||||
}
|
||||
|
||||
private void validateAllAliasesClaimed() {
|
||||
for (int i = 0; i < this.attributes.size(); i++) {
|
||||
Method attribute = this.attributes.get(i);
|
||||
AliasFor aliasFor = AnnotationsScanner.getDeclaredAnnotation(attribute, AliasFor.class);
|
||||
if (aliasFor != null && !this.claimedAliases.contains(attribute)) {
|
||||
Method target = resolveAliasTarget(attribute, aliasFor);
|
||||
throw new AnnotationConfigurationException(String.format(
|
||||
"@AliasFor declaration on %s declares an alias for %s which is not meta-present.",
|
||||
AttributeMethods.describe(attribute),
|
||||
AttributeMethods.describe(target)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMirrorSet(MirrorSet mirrorSet) {
|
||||
Method firstAttribute = mirrorSet.get(0);
|
||||
Object firstDefaultValue = firstAttribute.getDefaultValue();
|
||||
for (int i = 1; i <= mirrorSet.size() - 1; i++) {
|
||||
Method mirrorAttribute = mirrorSet.get(i);
|
||||
Object mirrorDefaultValue = mirrorAttribute.getDefaultValue();
|
||||
if (firstDefaultValue == null || mirrorDefaultValue == null) {
|
||||
throw new AnnotationConfigurationException(String.format(
|
||||
"Misconfigured aliases: %s and %s must declare default values.",
|
||||
AttributeMethods.describe(firstAttribute),
|
||||
AttributeMethods.describe(mirrorAttribute)));
|
||||
}
|
||||
if (!ObjectUtils.nullSafeEquals(firstDefaultValue, mirrorDefaultValue)) {
|
||||
throw new AnnotationConfigurationException(String.format(
|
||||
"Misconfigured aliases: %s and %s must declare the same default value.",
|
||||
AttributeMethods.describe(firstAttribute),
|
||||
AttributeMethods.describe(mirrorAttribute)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the root mapping.
|
||||
* @return the root mapping
|
||||
*/
|
||||
AnnotationTypeMapping getRoot() {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the parent mapping or {@code null}.
|
||||
* @return the parent mapping
|
||||
*/
|
||||
@Nullable
|
||||
AnnotationTypeMapping getParent() {
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the depth of this mapping.
|
||||
* @return the depth of the mapping
|
||||
*/
|
||||
int getDepth() {
|
||||
return this.depth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the type of the mapped annotation.
|
||||
* @return the annotation type
|
||||
*/
|
||||
Class<? extends Annotation> getAnnotationType() {
|
||||
return this.annotationType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the source annotation for this mapping. This will be the
|
||||
* meta-annotation, or {@code null} if this is the root mapping.
|
||||
* @return the source annotation of the mapping
|
||||
*/
|
||||
@Nullable
|
||||
Annotation getAnnotation() {
|
||||
return this.annotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the annotation attributes for the mapping annotation type.
|
||||
* @return the attribute methods
|
||||
*/
|
||||
AttributeMethods getAttributes() {
|
||||
return this.attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the related index of an alias mapped attribute, or {@code -1} if
|
||||
* there is no mapping. The resulting value is the index of the attribute on
|
||||
* the root annotation that can be invoked in order to obtain the actual
|
||||
* value.
|
||||
* @param attributeIndex the attribute index of the source attribute
|
||||
* @return the mapped attribute index or {@code -1}
|
||||
*/
|
||||
int getAliasMapping(int attributeIndex) {
|
||||
return this.aliasMappings[attributeIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the related index of a convention mapped attribute, or {@code -1}
|
||||
* if there is no mapping. The resulting value is the index of the attribute
|
||||
* on the root annotation that can be invoked in order to obtain the actual
|
||||
* value.
|
||||
* @param attributeIndex the attribute index of the source attribute
|
||||
* @return the mapped attribute index or {@code -1}
|
||||
*/
|
||||
int getConventionMapping(int attributeIndex) {
|
||||
return this.conventionMappings[attributeIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if the specified value is equivalent to the default value of the
|
||||
* attribute at the given index.
|
||||
* @param attributeIndex the attribute index of the source attribute
|
||||
* @param value the value to check
|
||||
* @param valueExtractor the value extractor used to extract value from any
|
||||
* nested annotations
|
||||
* @return {@code true} if the value is equivalent to the default value
|
||||
*/
|
||||
boolean isEquivalentToDefaultValue(int attributeIndex, Object value,
|
||||
BiFunction<Method, Object, Object> valueExtractor) {
|
||||
Method attribute = this.attributes.get(attributeIndex);
|
||||
return isEquivalentToDefaultValue(attribute, value, valueExtractor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the mirror sets for this type mapping.
|
||||
* @return the mirrorSets the attribute mirror sets.
|
||||
*/
|
||||
MirrorSets getMirrorSets() {
|
||||
return this.mirrorSets;
|
||||
}
|
||||
|
||||
private static int[] filledIntArray(int size, int value) {
|
||||
int[] array = new int[size];
|
||||
Arrays.fill(array, value);
|
||||
return array;
|
||||
}
|
||||
|
||||
private static boolean isEquivalentToDefaultValue(Method attribute, Object value,
|
||||
BiFunction<Method, Object, Object> valueExtractor) {
|
||||
return areEquivalent(attribute.getDefaultValue(), value, valueExtractor);
|
||||
}
|
||||
|
||||
private static boolean areEquivalent(@Nullable Object value,
|
||||
@Nullable Object extractedValue,
|
||||
BiFunction<Method, Object, Object> valueExtractor) {
|
||||
if (ObjectUtils.nullSafeEquals(value, extractedValue)) {
|
||||
return true;
|
||||
}
|
||||
if (value instanceof Class && extractedValue instanceof String) {
|
||||
return areEquivalent((Class<?>) value, (String) extractedValue);
|
||||
}
|
||||
if (value instanceof Class[] && extractedValue instanceof String[]) {
|
||||
return areEquivalent((Class[]) value, (String[]) extractedValue);
|
||||
}
|
||||
if (value instanceof Annotation) {
|
||||
return areEquivalent((Annotation) value, extractedValue, valueExtractor);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean areEquivalent(Class<?>[] value, String[] extractedValue) {
|
||||
if (value.length != extractedValue.length) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < value.length; i++) {
|
||||
if (!areEquivalent(value[i], extractedValue[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean areEquivalent(Class<?> value, String extractedValue) {
|
||||
return value.getName().equals(extractedValue);
|
||||
}
|
||||
|
||||
private static boolean areEquivalent(Annotation value, @Nullable Object extractedValue,
|
||||
BiFunction<Method, Object, Object> valueExtractor) {
|
||||
AttributeMethods attributes = AttributeMethods.forAnnotationType(
|
||||
value.annotationType());
|
||||
for (int i = 0; i < attributes.size(); i++) {
|
||||
Method attribute = attributes.get(i);
|
||||
if (!areEquivalent(ReflectionUtils.invokeMethod(attribute, value),
|
||||
valueExtractor.apply(attribute, extractedValue), valueExtractor)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A collection of {@link MirrorSet} instances that provides details of all
|
||||
* defined mirrors.
|
||||
*/
|
||||
class MirrorSets {
|
||||
|
||||
private MirrorSet[] mirrorSets;
|
||||
|
||||
private final MirrorSet[] assigned;
|
||||
|
||||
|
||||
MirrorSets() {
|
||||
this.assigned = new MirrorSet[attributes.size()];
|
||||
this.mirrorSets = new MirrorSet[0];
|
||||
}
|
||||
|
||||
|
||||
void updateFrom(Collection<Method> aliases) {
|
||||
MirrorSet mirrorSet = null;
|
||||
int size = 0;
|
||||
int last = -1;
|
||||
for (int i = 0; i < attributes.size(); i++) {
|
||||
Method attribute = attributes.get(i);
|
||||
if (aliases.contains(attribute)) {
|
||||
size++;
|
||||
if (size > 1) {
|
||||
if (mirrorSet == null) {
|
||||
mirrorSet = new MirrorSet();
|
||||
this.assigned[last] = mirrorSet;
|
||||
}
|
||||
this.assigned[i] = mirrorSet;
|
||||
}
|
||||
last = i;
|
||||
}
|
||||
}
|
||||
if (mirrorSet != null) {
|
||||
mirrorSet.update();
|
||||
LinkedHashSet<MirrorSet> unique = new LinkedHashSet<>(
|
||||
Arrays.asList(this.assigned));
|
||||
unique.remove(null);
|
||||
this.mirrorSets = unique.toArray(new MirrorSet[0]);
|
||||
}
|
||||
}
|
||||
|
||||
int size() {
|
||||
return this.mirrorSets.length;
|
||||
}
|
||||
|
||||
MirrorSet get(int index) {
|
||||
return this.mirrorSets[index];
|
||||
}
|
||||
|
||||
@Nullable
|
||||
MirrorSet getAssigned(int attributeIndex) {
|
||||
return this.assigned[attributeIndex];
|
||||
}
|
||||
|
||||
int[] resolve(@Nullable Object source, @Nullable Object annotation,
|
||||
BiFunction<Method, Object, Object> valueExtractor) {
|
||||
|
||||
int[] result = new int[attributes.size()];
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = i;
|
||||
}
|
||||
for (int i = 0; i < size(); i++) {
|
||||
MirrorSet mirrorSet = get(i);
|
||||
int resolved = mirrorSet.resolve(source, annotation, valueExtractor);
|
||||
for (int j = 0; j < mirrorSet.size; j++) {
|
||||
result[mirrorSet.indexes[j]] = resolved;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A single set of mirror attributes.
|
||||
*/
|
||||
class MirrorSet {
|
||||
|
||||
private int size;
|
||||
|
||||
private final int[] indexes = new int[attributes.size()];
|
||||
|
||||
|
||||
void update() {
|
||||
this.size = 0;
|
||||
Arrays.fill(this.indexes, -1);
|
||||
for (int i = 0; i < MirrorSets.this.assigned.length; i++) {
|
||||
if (MirrorSets.this.assigned[i] == this) {
|
||||
this.indexes[this.size] = i;
|
||||
this.size++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<A> int resolve(@Nullable Object source, @Nullable A annotation,
|
||||
BiFunction<Method, Object, Object> valueExtractor) {
|
||||
|
||||
int result = -1;
|
||||
Object lastValue = null;
|
||||
for (int i = 0; i < this.size; i++) {
|
||||
Method attribute = attributes.get(this.indexes[i]);
|
||||
Object value = valueExtractor.apply(attribute, annotation);
|
||||
boolean isDefaultValue = value == null || isEquivalentToDefaultValue(
|
||||
attribute, value, valueExtractor);
|
||||
if (isDefaultValue || ObjectUtils.nullSafeEquals(lastValue, value)) {
|
||||
continue;
|
||||
}
|
||||
if (lastValue != null &&
|
||||
!ObjectUtils.nullSafeEquals(lastValue, value)) {
|
||||
String on = (source != null) ? " declared on " + source : "";
|
||||
throw new AnnotationConfigurationException(String.format(
|
||||
"Different @AliasFor mirror values for annotation [%s]%s, "
|
||||
+ "attribute '%s' and its alias '%s' are declared with values of [%s] and [%s].",
|
||||
getAnnotationType().getName(), on,
|
||||
attributes.get(result).getName(),
|
||||
attribute.getName(),
|
||||
ObjectUtils.nullSafeToString(lastValue),
|
||||
ObjectUtils.nullSafeToString(value)));
|
||||
}
|
||||
result = this.indexes[i];
|
||||
lastValue = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int size() {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
Method get(int index) {
|
||||
int attributeIndex = this.indexes[index];
|
||||
return attributes.get(attributeIndex);
|
||||
}
|
||||
|
||||
int getAttributeIndex(int index) {
|
||||
return this.indexes[index];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
|
||||
/**
|
||||
* Provides {@link AnnotationTypeMapping} information for a single source
|
||||
* annotation type. Performs a recursive breadth first crawl of all
|
||||
* meta-annotations to ultimately provide a quick way to map the attributes of
|
||||
* root {@link Annotation}.
|
||||
*
|
||||
* <p>Supports convention based merging of meta-annotations as well as implicit
|
||||
* and explicit {@link AliasFor @AliasFor} aliases. Also provide information
|
||||
* about mirrored attributes.
|
||||
*
|
||||
* <p>This class is designed to be cached so that meta-annotations only need to
|
||||
* be searched once, regardless of how many times they are actually used.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
* @see AnnotationTypeMapping
|
||||
*/
|
||||
final class AnnotationTypeMappings {
|
||||
|
||||
private static final IntrospectionFailureLogger failureLogger = IntrospectionFailureLogger.DEBUG;
|
||||
|
||||
private static final Map<AnnotationFilter, Cache> cache = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
|
||||
private final AnnotationFilter filter;
|
||||
|
||||
private final List<AnnotationTypeMapping> mappings;
|
||||
|
||||
|
||||
private AnnotationTypeMappings(AnnotationFilter filter,
|
||||
Class<? extends Annotation> annotationType) {
|
||||
this.filter = filter;
|
||||
this.mappings = new ArrayList<>();
|
||||
addAllMappings(annotationType);
|
||||
this.mappings.forEach(AnnotationTypeMapping::afterAllMappingsSet);
|
||||
}
|
||||
|
||||
|
||||
private void addAllMappings(Class<? extends Annotation> annotationType) {
|
||||
Deque<AnnotationTypeMapping> queue = new ArrayDeque<>();
|
||||
addIfPossible(queue, null, annotationType, null);
|
||||
while (!queue.isEmpty()) {
|
||||
AnnotationTypeMapping mapping = queue.removeFirst();
|
||||
this.mappings.add(mapping);
|
||||
addMetaAnnotationsToQueue(queue, mapping);
|
||||
}
|
||||
}
|
||||
|
||||
private void addMetaAnnotationsToQueue(Deque<AnnotationTypeMapping> queue,
|
||||
AnnotationTypeMapping parent) {
|
||||
|
||||
Annotation[] metaAnnotations = AnnotationsScanner.getDeclaredAnnotations(
|
||||
parent.getAnnotationType(), false);
|
||||
for (Annotation metaAnnotation : metaAnnotations) {
|
||||
if (!isMappable(parent, metaAnnotation)) {
|
||||
continue;
|
||||
}
|
||||
Annotation[] repeatedAnnotations = RepeatableContainers.standardRepeatables()
|
||||
.findRepeatedAnnotations(metaAnnotation);
|
||||
if (repeatedAnnotations != null) {
|
||||
for (Annotation repeatedAnnotation : repeatedAnnotations) {
|
||||
if (!isMappable(parent, metaAnnotation)) {
|
||||
continue;
|
||||
}
|
||||
addIfPossible(queue, parent, repeatedAnnotation);
|
||||
}
|
||||
}
|
||||
else {
|
||||
addIfPossible(queue, parent, metaAnnotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addIfPossible(Deque<AnnotationTypeMapping> queue,
|
||||
AnnotationTypeMapping parent, Annotation annotation) {
|
||||
addIfPossible(queue, parent, annotation.annotationType(), annotation);
|
||||
}
|
||||
|
||||
private void addIfPossible(Deque<AnnotationTypeMapping> queue,
|
||||
@Nullable AnnotationTypeMapping parent,
|
||||
Class<? extends Annotation> annotationType, @Nullable Annotation annotation) {
|
||||
|
||||
try {
|
||||
queue.addLast(new AnnotationTypeMapping(parent, annotationType, annotation));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
if (ex instanceof AnnotationConfigurationException) {
|
||||
throw (AnnotationConfigurationException) ex;
|
||||
}
|
||||
if (failureLogger.isEnabled()) {
|
||||
failureLogger.log(
|
||||
"Failed to introspect meta-annotation "
|
||||
+ annotationType.getName(),
|
||||
(parent != null) ? parent.getAnnotationType() : null, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isMappable(AnnotationTypeMapping parent, Annotation metaAnnotation) {
|
||||
return !this.filter.matches(metaAnnotation) &&
|
||||
!AnnotationFilter.PLAIN.matches(parent.getAnnotationType()) &&
|
||||
!isAlreadyMapped(parent, metaAnnotation);
|
||||
}
|
||||
|
||||
private boolean isAlreadyMapped(AnnotationTypeMapping parent,
|
||||
Annotation metaAnnotation) {
|
||||
|
||||
Class<? extends Annotation> annotationType = metaAnnotation.annotationType();
|
||||
AnnotationTypeMapping mapping = parent;
|
||||
while (mapping != null) {
|
||||
if (mapping.getAnnotationType().equals(annotationType)) {
|
||||
return true;
|
||||
}
|
||||
mapping = mapping.getParent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the total number of contained mappings.
|
||||
* @return the total number of mappings
|
||||
*/
|
||||
int size() {
|
||||
return this.mappings.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an individual mapping from this instance. Index {@code 0} will
|
||||
* always be return the root mapping, higer indexes will return
|
||||
* meta-annotation mappings.
|
||||
* @param index the index to return
|
||||
* @return the {@link AnnotationTypeMapping}
|
||||
* @throws IndexOutOfBoundsException if the index is out of range
|
||||
* (<tt>index < 0 || index >= size()</tt>)
|
||||
*/
|
||||
AnnotationTypeMapping get(int index) {
|
||||
return this.mappings.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@link AnnotationTypeMappings} for the specified annotation type.
|
||||
* @param annotationType the source annotation type
|
||||
* @return type mappings for the annotation type
|
||||
*/
|
||||
static AnnotationTypeMappings forAnnotationType(
|
||||
Class<? extends Annotation> annotationType) {
|
||||
|
||||
return forAnnotationType(annotationType,
|
||||
AnnotationFilter.mostAppropriateFor(annotationType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@link AnnotationTypeMappings} for the specified annotation type.
|
||||
* @param annotationType the source annotation type
|
||||
* @param annotationFilter the annotation filter used to limit which
|
||||
* annotations are considered
|
||||
* @return type mappings for the annotation type
|
||||
*/
|
||||
static AnnotationTypeMappings forAnnotationType(
|
||||
Class<? extends Annotation> annotationType,
|
||||
AnnotationFilter annotationFilter) {
|
||||
|
||||
Assert.notNull(annotationType, "AnnotationType must not be null");
|
||||
Assert.notNull(annotationFilter, "AnnotationFilter must not be null");
|
||||
return cache.computeIfAbsent(annotationFilter, Cache::new).get(annotationType);
|
||||
}
|
||||
|
||||
static void clearCache() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cache created per {@link AnnotationFilter}.
|
||||
*/
|
||||
private static class Cache {
|
||||
|
||||
private final AnnotationFilter filter;
|
||||
|
||||
private final Map<Class<? extends Annotation>, AnnotationTypeMappings> mappings;
|
||||
|
||||
|
||||
/**
|
||||
* Create a cache instance with the specified filter.
|
||||
* @param filter the annotation filter
|
||||
*/
|
||||
Cache(AnnotationFilter filter) {
|
||||
this.filter = filter;
|
||||
this.mappings = new ConcurrentReferenceHashMap<>();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return or create {@link AnnotationTypeMappings} for the specified
|
||||
* annotation type.
|
||||
* @param annotationType the annotation type
|
||||
* @return a new or existing {@link AnnotationTypeMapping} instance
|
||||
*/
|
||||
AnnotationTypeMappings get(Class<? extends Annotation> annotationType) {
|
||||
return this.mappings.computeIfAbsent(annotationType, this::createMappings);
|
||||
}
|
||||
|
||||
AnnotationTypeMappings createMappings(
|
||||
Class<? extends Annotation> annotationType) {
|
||||
return new AnnotationTypeMappings(this.filter, annotationType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Callback interface used to process annotations.
|
||||
*
|
||||
* @param <C> the context type
|
||||
* @param <R> the result type
|
||||
* @author Phillip Webb
|
||||
* @see AnnotationsScanner
|
||||
* @see TypeMappedAnnotations
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface AnnotationsProcessor<C, R> {
|
||||
|
||||
/**
|
||||
* Called when an aggregate is about to be processed. This method may return
|
||||
* a {@code non-null} result to short-circuit any further processing.
|
||||
* @param context context information relevant to the processor
|
||||
* @param aggregateIndex the aggregate index about to be processed
|
||||
* @return a {@code non-null} result if no further processing is required
|
||||
*/
|
||||
@Nullable
|
||||
default R doWithAggregate(C context, int aggregateIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an array of annotations can be processed. This method may
|
||||
* return a {@code non-null} result to short-circuit any further processing.
|
||||
* @param context context information relevant to the processor
|
||||
* @param aggregateIndex the aggregate index of the provided annotations
|
||||
* @param source the original source of the annotations, if known
|
||||
* @param annotations the annotations to process (this array may contain
|
||||
* {@code null} elements)
|
||||
* @return a {@code non-null} result if no further processing is required
|
||||
*/
|
||||
@Nullable
|
||||
R doWithAnnotations(C context, int aggregateIndex, @Nullable Object source,
|
||||
Annotation[] annotations);
|
||||
|
||||
/**
|
||||
* Return the final result to be returned. By default this method returns
|
||||
* the last process result.
|
||||
* @param result the last early exit result, or {@code null}.
|
||||
* @return the final result to be returned to the caller
|
||||
*/
|
||||
@Nullable
|
||||
default R finish(@Nullable R result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,552 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Member;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiPredicate;
|
||||
|
||||
import org.springframework.core.BridgeMethodResolver;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Scanner to search for relevant annotations on the hierarchy of an
|
||||
* {@link AnnotatedElement}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
* @see AnnotationsProcessor
|
||||
*/
|
||||
abstract class AnnotationsScanner {
|
||||
|
||||
private static final Annotation[] NO_ANNOTATIONS = {};
|
||||
|
||||
private static final Method[] NO_METHODS = {};
|
||||
|
||||
|
||||
private static final Map<AnnotatedElement, Annotation[]> declaredAnnotationCache =
|
||||
new ConcurrentReferenceHashMap<>(256);
|
||||
|
||||
private static final Map<Class<?>, Method[]> baseTypeMethodsCache =
|
||||
new ConcurrentReferenceHashMap<>(256);
|
||||
|
||||
|
||||
private AnnotationsScanner() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Scan the hierarchy of the specified element for relevant annotations and
|
||||
* call the processor as required.
|
||||
* @param context an optional context object that will be passed back to the
|
||||
* processor
|
||||
* @param source the source element to scan
|
||||
* @param searchStrategy the search strategy to use
|
||||
* @param processor the processor that receives the annotations
|
||||
* @return the result of {@link AnnotationsProcessor#finish(Object)}
|
||||
*/
|
||||
@Nullable
|
||||
static <C, R> R scan(C context, AnnotatedElement source,
|
||||
SearchStrategy searchStrategy, AnnotationsProcessor<C, R> processor) {
|
||||
return scan(context, source, searchStrategy, processor, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan the hierarchy of the specified element for relevant annotations and
|
||||
* call the processor as required.
|
||||
* @param context an optional context object that will be passed back to the
|
||||
* processor
|
||||
* @param source the source element to scan
|
||||
* @param searchStrategy the search strategy to use
|
||||
* @param processor the processor that receives the annotations
|
||||
* @param classFilter an optional filter that can be used to entirely filter
|
||||
* out a specific class from the hierarchy
|
||||
* @return the result of {@link AnnotationsProcessor#finish(Object)}
|
||||
*/
|
||||
@Nullable
|
||||
static <C, R> R scan(C context, AnnotatedElement source,
|
||||
SearchStrategy searchStrategy, AnnotationsProcessor<C, R> processor,
|
||||
@Nullable BiPredicate<C, Class<?>> classFilter) {
|
||||
|
||||
R result = process(context, source, searchStrategy, processor, classFilter);
|
||||
return processor.finish(result);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static <C, R> R process(C context, AnnotatedElement source,
|
||||
SearchStrategy searchStrategy, AnnotationsProcessor<C, R> processor,
|
||||
@Nullable BiPredicate<C, Class<?>> classFilter) {
|
||||
|
||||
if (source instanceof Class) {
|
||||
return processClass(context, (Class<?>) source, searchStrategy, processor, classFilter);
|
||||
}
|
||||
if (source instanceof Method) {
|
||||
return processMethod(context, (Method) source, searchStrategy, processor, classFilter);
|
||||
}
|
||||
return processElement(context, source, processor, classFilter);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static <C, R> R processClass(C context, Class<?> source,
|
||||
SearchStrategy searchStrategy, AnnotationsProcessor<C, R> processor,
|
||||
@Nullable BiPredicate<C, Class<?>> classFilter) {
|
||||
|
||||
switch (searchStrategy) {
|
||||
case DIRECT:
|
||||
return processElement(context, source,
|
||||
processor, classFilter);
|
||||
case INHERITED_ANNOTATIONS:
|
||||
return processClassInheritedAnnotations(context, source,
|
||||
processor, classFilter);
|
||||
case SUPER_CLASS:
|
||||
return processClassHierarchy(context, new int[] { 0 }, source,
|
||||
processor, classFilter, false);
|
||||
case EXHAUSTIVE:
|
||||
return processClassHierarchy(context, new int[] { 0 }, source,
|
||||
processor, classFilter, true);
|
||||
}
|
||||
throw new IllegalStateException("Unsupported search strategy " + searchStrategy);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static <C, R> R processClassInheritedAnnotations(C context, Class<?> source,
|
||||
AnnotationsProcessor<C, R> processor,
|
||||
@Nullable BiPredicate<C, Class<?>> classFilter) {
|
||||
|
||||
if (isWithoutHierarchy(source)) {
|
||||
return processElement(context, source, processor, classFilter);
|
||||
}
|
||||
Annotation[] relevant = null;
|
||||
int remaining = Integer.MAX_VALUE;
|
||||
int aggregateIndex = 0;
|
||||
Class<?> root = source;
|
||||
while (source != null && source != Object.class
|
||||
&& !hasPlainJavaAnnotationsOnly(source) && remaining > 0) {
|
||||
R result = processor.doWithAggregate(context, aggregateIndex);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
if (isFiltered(source, context, classFilter)) {
|
||||
continue;
|
||||
}
|
||||
Annotation[] declaredAnnotations =
|
||||
getDeclaredAnnotations(context, source, classFilter, true);
|
||||
if (relevant == null && declaredAnnotations.length > 0) {
|
||||
relevant = root.getAnnotations();
|
||||
remaining = relevant.length;
|
||||
}
|
||||
for (int i = 0; i < declaredAnnotations.length; i++) {
|
||||
if (declaredAnnotations[i] != null) {
|
||||
boolean isRelevant = false;
|
||||
for (int relevantIndex = 0; relevantIndex < relevant.length; relevantIndex++) {
|
||||
if (relevant[relevantIndex] != null &&
|
||||
declaredAnnotations[i].annotationType() == relevant[relevantIndex].annotationType()) {
|
||||
isRelevant = true;
|
||||
relevant[relevantIndex] = null;
|
||||
remaining--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isRelevant) {
|
||||
declaredAnnotations[i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
result = processor.doWithAnnotations(context, aggregateIndex, source, declaredAnnotations);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
source = source.getSuperclass();
|
||||
aggregateIndex++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static <C, R> R processClassHierarchy(C context, int[] aggregateIndex,
|
||||
Class<?> source, AnnotationsProcessor<C, R> processor,
|
||||
@Nullable BiPredicate<C, Class<?>> classFilter, boolean includeInterfaces) {
|
||||
|
||||
R result = processor.doWithAggregate(context, aggregateIndex[0]);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
if (hasPlainJavaAnnotationsOnly(source)) {
|
||||
return null;
|
||||
}
|
||||
Annotation[] annotations = getDeclaredAnnotations(context, source, classFilter, false);
|
||||
result = processor.doWithAnnotations(context, aggregateIndex[0], source, annotations);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
aggregateIndex[0]++;
|
||||
if (includeInterfaces) {
|
||||
for (Class<?> interfaceType : source.getInterfaces()) {
|
||||
R interfacesResult = processClassHierarchy(context, aggregateIndex,
|
||||
interfaceType, processor, classFilter, includeInterfaces);
|
||||
if (interfacesResult != null) {
|
||||
return interfacesResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
Class<?> superclass = source.getSuperclass();
|
||||
if (superclass != Object.class && superclass != null) {
|
||||
R superclassResult = processClassHierarchy(context, aggregateIndex,
|
||||
superclass, processor, classFilter, includeInterfaces);
|
||||
if (superclassResult != null) {
|
||||
return superclassResult;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static <C, R> R processMethod(C context, Method source,
|
||||
SearchStrategy searchStrategy, AnnotationsProcessor<C, R> processor,
|
||||
@Nullable BiPredicate<C, Class<?>> classFilter) {
|
||||
|
||||
switch (searchStrategy) {
|
||||
case DIRECT:
|
||||
case INHERITED_ANNOTATIONS:
|
||||
return processMethodInheritedAnnotations(context, source,
|
||||
processor, classFilter);
|
||||
case SUPER_CLASS:
|
||||
return processMethodHierarchy(context, new int[] { 0 }, source.getDeclaringClass(),
|
||||
processor, classFilter, source, false);
|
||||
case EXHAUSTIVE:
|
||||
return processMethodHierarchy(context, new int[] { 0 }, source.getDeclaringClass(),
|
||||
processor, classFilter, source, true);
|
||||
}
|
||||
throw new IllegalStateException("Unsupported search strategy " + searchStrategy);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static <C, R> R processMethodInheritedAnnotations(C context, Method source,
|
||||
AnnotationsProcessor<C, R> processor,
|
||||
@Nullable BiPredicate<C, Class<?>> classFilter) {
|
||||
|
||||
R result = processor.doWithAggregate(context, 0);
|
||||
return result != null ? result :
|
||||
processMethodAnnotations(context, 0, source, processor, classFilter);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static <C, R> R processMethodHierarchy(C context, int[] aggregateIndex,
|
||||
Class<?> sourceClass, AnnotationsProcessor<C, R> processor,
|
||||
@Nullable BiPredicate<C, Class<?>> classFilter, Method rootMethod,
|
||||
boolean includeInterfaces) {
|
||||
|
||||
R result = processor.doWithAggregate(context, aggregateIndex[0]);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
if (hasPlainJavaAnnotationsOnly(sourceClass)) {
|
||||
return null;
|
||||
}
|
||||
boolean calledProcessor = false;
|
||||
if (sourceClass == rootMethod.getDeclaringClass()) {
|
||||
result = processMethodAnnotations(context, aggregateIndex[0],
|
||||
rootMethod, processor, classFilter);
|
||||
calledProcessor = true;
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (Method candidateMethod : getBaseTypeMethods(context, sourceClass, classFilter)) {
|
||||
if (candidateMethod != null && isOverride(rootMethod, candidateMethod)) {
|
||||
result = processMethodAnnotations(context, aggregateIndex[0],
|
||||
candidateMethod, processor, classFilter);
|
||||
calledProcessor = true;
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Modifier.isPrivate(rootMethod.getModifiers())) {
|
||||
return null;
|
||||
}
|
||||
if (calledProcessor) {
|
||||
aggregateIndex[0]++;
|
||||
}
|
||||
if (includeInterfaces) {
|
||||
for (Class<?> interfaceType : sourceClass.getInterfaces()) {
|
||||
R interfacesResult = processMethodHierarchy(context, aggregateIndex,
|
||||
interfaceType, processor, classFilter, rootMethod, includeInterfaces);
|
||||
if (interfacesResult != null) {
|
||||
return interfacesResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
Class<?> superclass = sourceClass.getSuperclass();
|
||||
if (superclass != Object.class && superclass != null) {
|
||||
R superclassResult = processMethodHierarchy(context, aggregateIndex,
|
||||
superclass, processor, classFilter, rootMethod, includeInterfaces);
|
||||
if (superclassResult != null) {
|
||||
return superclassResult;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static <C> Method[] getBaseTypeMethods(C context, Class<?> baseType,
|
||||
@Nullable BiPredicate<C, Class<?>> classFilter) {
|
||||
|
||||
if (baseType == Object.class || hasPlainJavaAnnotationsOnly(baseType) ||
|
||||
isFiltered(baseType, context, classFilter)) {
|
||||
return NO_METHODS;
|
||||
}
|
||||
|
||||
Method[] methods = baseTypeMethodsCache.get(baseType);
|
||||
if (methods == null) {
|
||||
boolean isInterface = baseType.isInterface();
|
||||
methods = isInterface ? baseType.getMethods()
|
||||
: ReflectionUtils.getDeclaredMethods(baseType);
|
||||
int cleared = 0;
|
||||
for (int i = 0; i < methods.length; i++) {
|
||||
if ((!isInterface && Modifier.isPrivate(methods[i].getModifiers())) ||
|
||||
hasPlainJavaAnnotationsOnly(methods[i]) ||
|
||||
getDeclaredAnnotations(methods[i], false).length == 0) {
|
||||
methods[i] = null;
|
||||
cleared++;
|
||||
}
|
||||
}
|
||||
if (cleared == methods.length) {
|
||||
methods = NO_METHODS;
|
||||
}
|
||||
baseTypeMethodsCache.put(baseType, methods);
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
|
||||
private static boolean isOverride(Method rootMethod, Method candidateMethod) {
|
||||
return !Modifier.isPrivate(candidateMethod.getModifiers()) &&
|
||||
candidateMethod.getName().equals(rootMethod.getName()) &&
|
||||
hasSameParameterTypes(rootMethod, candidateMethod);
|
||||
}
|
||||
|
||||
private static boolean hasSameParameterTypes(Method rootMethod,
|
||||
Method candidateMethod) {
|
||||
|
||||
if (candidateMethod.getParameterCount() != rootMethod.getParameterCount()) {
|
||||
return false;
|
||||
}
|
||||
Class<?>[] rootParameterTypes = rootMethod.getParameterTypes();
|
||||
Class<?>[] candidateParameterTypes = candidateMethod.getParameterTypes();
|
||||
if (Arrays.equals(candidateParameterTypes, rootParameterTypes)) {
|
||||
return true;
|
||||
}
|
||||
return hasSameGenericTypeParameters(rootMethod, candidateMethod,
|
||||
rootParameterTypes);
|
||||
}
|
||||
|
||||
private static boolean hasSameGenericTypeParameters(Method rootMethod,
|
||||
Method candidateMethod, Class<?>[] rootParameterTypes) {
|
||||
|
||||
Class<?> sourceDeclaringClass = rootMethod.getDeclaringClass();
|
||||
Class<?> candidateDeclaringClass = candidateMethod.getDeclaringClass();
|
||||
if (!candidateDeclaringClass.isAssignableFrom(sourceDeclaringClass)) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < rootParameterTypes.length; i++) {
|
||||
Class<?> resolvedParameterType = ResolvableType.forMethodParameter(
|
||||
candidateMethod, i, sourceDeclaringClass).resolve();
|
||||
if (rootParameterTypes[i] != resolvedParameterType) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static <C, R> R processMethodAnnotations(C context, int aggregateIndex,
|
||||
Method source, AnnotationsProcessor<C, R> processor,
|
||||
@Nullable BiPredicate<C, Class<?>> classFilter) {
|
||||
|
||||
Annotation[] annotations = getDeclaredAnnotations(context, source, classFilter,
|
||||
false);
|
||||
R result = processor.doWithAnnotations(context, aggregateIndex, source,
|
||||
annotations);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(source);
|
||||
if (bridgedMethod != source) {
|
||||
Annotation[] bridgedAnnotations = getDeclaredAnnotations(context,
|
||||
bridgedMethod, classFilter, true);
|
||||
for (int i = 0; i < bridgedAnnotations.length; i++) {
|
||||
if (ObjectUtils.containsElement(annotations, bridgedAnnotations[i])) {
|
||||
bridgedAnnotations[i] = null;
|
||||
}
|
||||
}
|
||||
return processor.doWithAnnotations(context, aggregateIndex, source,
|
||||
bridgedAnnotations);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static <C, R> R processElement(C context, AnnotatedElement source,
|
||||
AnnotationsProcessor<C, R> processor,
|
||||
@Nullable BiPredicate<C, Class<?>> classFilter) {
|
||||
|
||||
R result = processor.doWithAggregate(context, 0);
|
||||
return result != null ? result
|
||||
: processor.doWithAnnotations(context, 0, source,
|
||||
getDeclaredAnnotations(context, source, classFilter, false));
|
||||
}
|
||||
|
||||
private static <C, R> Annotation[] getDeclaredAnnotations(C context,
|
||||
AnnotatedElement source, @Nullable BiPredicate<C, Class<?>> classFilter,
|
||||
boolean copy) {
|
||||
|
||||
if (source instanceof Class &&
|
||||
isFiltered((Class<?>) source, context, classFilter)) {
|
||||
return NO_ANNOTATIONS;
|
||||
}
|
||||
if (source instanceof Method &&
|
||||
isFiltered(((Method) source).getDeclaringClass(), context, classFilter)) {
|
||||
return NO_ANNOTATIONS;
|
||||
}
|
||||
return getDeclaredAnnotations(source, copy);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
static <A extends Annotation> A getDeclaredAnnotation(AnnotatedElement source,
|
||||
Class<A> annotationType) {
|
||||
|
||||
Annotation[] annotations = getDeclaredAnnotations(source, false);
|
||||
for (int i = 0; i < annotations.length; i++) {
|
||||
if (annotations[i] != null &&
|
||||
annotationType.equals(annotations[i].annotationType())) {
|
||||
return (A) annotations[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Annotation[] getDeclaredAnnotations(AnnotatedElement source,
|
||||
boolean defensive) {
|
||||
|
||||
boolean cached = true;
|
||||
Annotation[] annotations = declaredAnnotationCache.get(source);
|
||||
if (annotations == null) {
|
||||
annotations = source.getDeclaredAnnotations();
|
||||
if (annotations.length != 0) {
|
||||
boolean allIgnored = true;
|
||||
for (int i = 0; i < annotations.length; i++) {
|
||||
Annotation annotation = annotations[i];
|
||||
if (isIgnorable(annotation.annotationType()) ||
|
||||
!AttributeMethods.forAnnotationType(
|
||||
annotation.annotationType()).isValid(annotation)) {
|
||||
annotations[i] = null;
|
||||
}
|
||||
else {
|
||||
allIgnored = false;
|
||||
}
|
||||
}
|
||||
annotations = allIgnored ? NO_ANNOTATIONS : annotations;
|
||||
if (source instanceof Class || source instanceof Member) {
|
||||
declaredAnnotationCache.put(source, annotations);
|
||||
cached = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!defensive || annotations.length == 0 || !cached) {
|
||||
return annotations;
|
||||
}
|
||||
return annotations.clone();
|
||||
}
|
||||
|
||||
private static boolean isIgnorable(Class<?> annotationType) {
|
||||
return (annotationType == Nullable.class ||
|
||||
annotationType == FunctionalInterface.class);
|
||||
}
|
||||
|
||||
private static <C> boolean isFiltered(Class<?> sourceClass, C context,
|
||||
@Nullable BiPredicate<C, Class<?>> classFilter) {
|
||||
return classFilter != null && classFilter.test(context, sourceClass);
|
||||
}
|
||||
|
||||
static boolean isKnownEmpty(AnnotatedElement source,
|
||||
SearchStrategy searchStrategy, AnnotationFilter annotationFilter) {
|
||||
if (annotationFilter == AnnotationFilter.PLAIN &&
|
||||
hasPlainJavaAnnotationsOnly(source)) {
|
||||
return true;
|
||||
}
|
||||
if (searchStrategy == SearchStrategy.DIRECT || isWithoutHierarchy(source)) {
|
||||
if (source instanceof Method && ((Method) source).isBridge()) {
|
||||
return false;
|
||||
}
|
||||
return getDeclaredAnnotations(source, false).length == 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean hasPlainJavaAnnotationsOnly(@Nullable Object annotatedElement) {
|
||||
Class<?> type = null;
|
||||
if (annotatedElement instanceof Class) {
|
||||
type = (Class<?>) annotatedElement;
|
||||
}
|
||||
else if (annotatedElement instanceof Member) {
|
||||
type = ((Member) annotatedElement).getDeclaringClass();
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
String name = type.getName();
|
||||
return type.equals(Ordered.class) ||
|
||||
name.startsWith("java") ||
|
||||
name.startsWith("org.springframework.lang.");
|
||||
}
|
||||
|
||||
private static boolean isWithoutHierarchy(AnnotatedElement source) {
|
||||
if (source == Object.class) {
|
||||
return true;
|
||||
}
|
||||
if (source instanceof Class) {
|
||||
Class<?> sourceClass = (Class<?>) source;
|
||||
return sourceClass.getSuperclass() == Object.class &&
|
||||
sourceClass.getInterfaces().length == 0;
|
||||
}
|
||||
if (source instanceof Method) {
|
||||
Method sourceMethod = (Method) source;
|
||||
return Modifier.isPrivate(sourceMethod.getModifiers()) ||
|
||||
isWithoutHierarchy(sourceMethod.getDeclaringClass());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void clearCache() {
|
||||
declaredAnnotationCache.clear();
|
||||
baseTypeMethodsCache.clear();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,316 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
|
||||
/**
|
||||
* Provides a quick way to access the attribute methods of an {@link Annotation}
|
||||
* with consistent ordering as well as a few useful utility methods.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
*/
|
||||
final class AttributeMethods {
|
||||
|
||||
static final AttributeMethods NONE = new AttributeMethods(null, new Method[0]);
|
||||
|
||||
|
||||
private static final Map<Class<? extends Annotation>, AttributeMethods> cache =
|
||||
new ConcurrentReferenceHashMap<>();
|
||||
|
||||
private static final Comparator<Method> methodComparator = (m1, m2) -> {
|
||||
if (m1 != null && m2 != null) {
|
||||
return m1.getName().compareTo(m2.getName());
|
||||
}
|
||||
return m1 != null ? -1 : 1;
|
||||
};
|
||||
|
||||
@Nullable
|
||||
private final Class<? extends Annotation> annotationType;
|
||||
|
||||
private final Method[] attributeMethods;
|
||||
|
||||
private final boolean[] canThrowTypeNotPresentException;
|
||||
|
||||
private final boolean hasDefaultValueMethod;
|
||||
|
||||
private final boolean hasNestedAnnotation;
|
||||
|
||||
|
||||
private AttributeMethods(@Nullable Class<? extends Annotation> annotationType,
|
||||
Method[] attributeMethods) {
|
||||
this.annotationType = annotationType;
|
||||
this.attributeMethods = attributeMethods;
|
||||
this.canThrowTypeNotPresentException = new boolean[attributeMethods.length];
|
||||
boolean foundDefaultValueMethod = false;
|
||||
boolean foundNestedAnnotation = false;
|
||||
for (int i = 0; i < attributeMethods.length; i++) {
|
||||
Method method = this.attributeMethods[i];
|
||||
Class<?> type = method.getReturnType();
|
||||
if (method.getDefaultValue() != null) {
|
||||
foundDefaultValueMethod = true;
|
||||
}
|
||||
if (type.isAnnotation() ||
|
||||
(type.isArray() && type.getComponentType().isAnnotation())) {
|
||||
foundNestedAnnotation = true;
|
||||
}
|
||||
method.setAccessible(true);
|
||||
this.canThrowTypeNotPresentException[i] =
|
||||
type == Class.class ||
|
||||
type == Class[].class ||
|
||||
type.isEnum();
|
||||
}
|
||||
this.hasDefaultValueMethod = foundDefaultValueMethod;
|
||||
this.hasNestedAnnotation = foundNestedAnnotation;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return if this instance only contains only a single attribute named
|
||||
* {@code value}.
|
||||
* @return {@code true} if this is only a value attribute
|
||||
*/
|
||||
boolean isOnlyValueAttribute() {
|
||||
return this.attributeMethods.length == 1 &&
|
||||
MergedAnnotation.VALUE.equals(this.attributeMethods[0].getName());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns {@code true} if values from the given annotation can be safely
|
||||
* accessed without causing any {@link TypeNotPresentException
|
||||
* TypeNotPresentExceptions}.
|
||||
* @param annotation the annotation to check
|
||||
* @return {@true} if all values are present
|
||||
* @see #validate(Annotation)
|
||||
*/
|
||||
boolean isValid(Annotation annotation) {
|
||||
assertAnnotation(annotation);
|
||||
for (int i = 0; i < size(); i++) {
|
||||
if (canThrowTypeNotPresentException(i)) {
|
||||
try {
|
||||
get(i).invoke(annotation);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if values from the given annotation can be safely accessed without
|
||||
* causing any {@link TypeNotPresentException TypeNotPresentExceptions}. In
|
||||
* particular, this method is designed to cover Google App Engine's late
|
||||
* arrival of such exceptions for {@code Class} values (instead of the more
|
||||
* typical early {@code Class.getAnnotations() failure}.
|
||||
* @param annotation the annotation to validate
|
||||
* @throws IllegalStateException if a declared {@code Class} attribute could
|
||||
* not be read
|
||||
* @see #isValid(Annotation)
|
||||
*/
|
||||
void validate(Annotation annotation) {
|
||||
assertAnnotation(annotation);
|
||||
for (int i = 0; i < size(); i++) {
|
||||
if (canThrowTypeNotPresentException(i)) {
|
||||
try {
|
||||
get(i).invoke(annotation);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
throw new IllegalStateException(
|
||||
"Could not obtain annotation attribute value for "
|
||||
+ get(i).getName() + " declared on "
|
||||
+ annotation.annotationType(),
|
||||
ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void assertAnnotation(Annotation annotation) {
|
||||
Assert.notNull(annotation, "Annotation must not be null");
|
||||
if (this.annotationType != null) {
|
||||
Assert.isInstanceOf(this.annotationType, annotation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the attribute with the specified name or {@code null} if no
|
||||
* matching attribute exists.
|
||||
* @param name the attribute name to find
|
||||
* @return the attribute method or {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
Method get(String name) {
|
||||
int index = indexOf(name);
|
||||
return index != -1 ? this.attributeMethods[index] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the attribute at the specified index.
|
||||
* @param index the index of the attribute to return
|
||||
* @return the attribute method
|
||||
* @throws IndexOutOfBoundsException if the index is out of range
|
||||
* (<tt>index < 0 || index >= size()</tt>)
|
||||
*/
|
||||
Method get(int index) {
|
||||
return this.attributeMethods[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if the attribute at the specified index could throw a
|
||||
* {@link TypeNotPresentException} when accessed.
|
||||
* @param index the index of the attribute to check
|
||||
* @return {@code true} if the attribute can throw a
|
||||
* {@link TypeNotPresentException}
|
||||
*/
|
||||
boolean canThrowTypeNotPresentException(int index) {
|
||||
return this.canThrowTypeNotPresentException[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of the attribute with the specified name, or {@code -1}
|
||||
* if there is no attribute with the name.
|
||||
* @param name the name to find
|
||||
* @return the index of the attribute, or {@code -1}
|
||||
*/
|
||||
int indexOf(String name) {
|
||||
for (int i = 0; i < this.attributeMethods.length; i++) {
|
||||
if (this.attributeMethods[i].getName().equals(name)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of the specified attribute , or {@code -1} if the
|
||||
* attribute is not not in this collection.
|
||||
* @param attribute the attribute to find
|
||||
* @return the index of the attribute, or {@code -1}
|
||||
*/
|
||||
int indexOf(Method attribute) {
|
||||
for (int i = 0; i < this.attributeMethods.length; i++) {
|
||||
if (this.attributeMethods[i].equals(attribute)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of attributes in this collection.
|
||||
* @return the number of attributes
|
||||
*/
|
||||
int size() {
|
||||
return this.attributeMethods.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if at least one of the attribute methods has a default value.
|
||||
* @return if there is at least one attribute method with a default value
|
||||
*/
|
||||
boolean hasDefaultValueMethod() {
|
||||
return this.hasDefaultValueMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if at least on of the attribute methods is a nested annotation.
|
||||
* @return if there is at least one attribute method with a annotation type
|
||||
*/
|
||||
boolean hasNestedAnnotation() {
|
||||
return this.hasNestedAnnotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the attribute methods for the given annotation type.
|
||||
* @param annotationType the annotation type
|
||||
* @return the attribute methods for the annotation
|
||||
*/
|
||||
static AttributeMethods forAnnotationType(
|
||||
@Nullable Class<? extends Annotation> annotationType) {
|
||||
|
||||
if (annotationType == null) {
|
||||
return NONE;
|
||||
}
|
||||
return cache.computeIfAbsent(annotationType, AttributeMethods::compute);
|
||||
}
|
||||
|
||||
private static AttributeMethods compute(Class<? extends Annotation> annotationType) {
|
||||
Method[] methods = annotationType.getDeclaredMethods();
|
||||
int size = methods.length;
|
||||
for (int i = 0; i < methods.length; i++) {
|
||||
if (!isAttributeMethod(methods[i])) {
|
||||
methods[i] = null;
|
||||
size--;
|
||||
}
|
||||
}
|
||||
if (size == 0) {
|
||||
return NONE;
|
||||
}
|
||||
Arrays.sort(methods, methodComparator);
|
||||
Method[] attributeMethods = new Method[size];
|
||||
System.arraycopy(methods, 0, attributeMethods, 0, size);
|
||||
return new AttributeMethods(annotationType, attributeMethods);
|
||||
}
|
||||
|
||||
private static boolean isAttributeMethod(Method method) {
|
||||
return method.getParameterCount() == 0 && method.getReturnType() != void.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a description for the given attribute method suitable to use in
|
||||
* exception messages and logs.
|
||||
* @param attribute the attribute to describe
|
||||
* @return a description of the attribute
|
||||
*/
|
||||
static String describe(@Nullable Method attribute) {
|
||||
if (attribute == null) {
|
||||
return "(none)";
|
||||
}
|
||||
return describe(attribute.getDeclaringClass(), attribute.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a description for the given attribute method suitable to use in
|
||||
* exception messages and logs.
|
||||
* @param annotationType the annotation type
|
||||
* @param attributeName the attribute name
|
||||
* @return a description of the attribute
|
||||
*/
|
||||
static String describe(@Nullable Class<?> annotationType,
|
||||
@Nullable String attributeName) {
|
||||
if (attributeName == null) {
|
||||
return "(none)";
|
||||
}
|
||||
String in = annotationType != null ?
|
||||
" in annotation [" + annotationType.getName() + "]" :
|
||||
"";
|
||||
return "attribute '" + attributeName + "'" + in;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Log facade used to handle annotation introspection failures (in particular
|
||||
* {@code TypeNotPresentExceptions}). Allows annotation processing to continue,
|
||||
* assuming that when Class attribute values are not resolvable the annotation
|
||||
* should effectively disappear.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
*/
|
||||
enum IntrospectionFailureLogger {
|
||||
|
||||
DEBUG {
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return getLogger().isDebugEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String message) {
|
||||
getLogger().debug(message);
|
||||
}
|
||||
},
|
||||
|
||||
INFO {
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return getLogger().isInfoEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String message) {
|
||||
getLogger().info(message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@Nullable
|
||||
private static Log logger;
|
||||
|
||||
|
||||
abstract boolean isEnabled();
|
||||
|
||||
void log(String message, @Nullable Object source, Exception ex) {
|
||||
String on = source != null ? " on " + source : "";
|
||||
log(message + on + ": " + ex);
|
||||
}
|
||||
|
||||
abstract void log(String message);
|
||||
|
||||
private static Log getLogger() {
|
||||
Log logger = IntrospectionFailureLogger.logger;
|
||||
if (logger == null) {
|
||||
logger = LogFactory.getLog(MergedAnnotation.class);
|
||||
IntrospectionFailureLogger.logger = logger;
|
||||
}
|
||||
return logger;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,597 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* A single merged annotation returned from a {@link MergedAnnotations}
|
||||
* collection. Presents a view onto an annotation where attribute values may
|
||||
* have been "merged" from different source values.
|
||||
*
|
||||
* <p>Attribute values may be accessed using the various {@code get} methods.
|
||||
* For example, to access an {@code int} attribute the {@link #getInt(String)}
|
||||
* method would be used.
|
||||
*
|
||||
* <p>Note that attribute values are <b>not</b> converted when accessed. For
|
||||
* example, it is not possible to call {@link #getString(String)} if the
|
||||
* underlying attribute is an {@code int}. The only exception to this rule is
|
||||
* {@code Class} and {@code Class[]} values which may be accessed as
|
||||
* {@code String} and {@code String[]} respectively to prevent potential early
|
||||
* class initialization.
|
||||
*
|
||||
* <p>If necessary, a {@link MergedAnnotation} can be {@link #synthesize()
|
||||
* synthesized} back into an actual {@link java.lang.annotation.Annotation}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
* @param <A> the annotation type
|
||||
* @see MergedAnnotations
|
||||
* @see MergedAnnotationPredicates
|
||||
*/
|
||||
public interface MergedAnnotation<A extends Annotation> {
|
||||
|
||||
/**
|
||||
* The attribute name for annotations with a single element.
|
||||
*/
|
||||
String VALUE = "value";
|
||||
|
||||
/**
|
||||
* Return the class name of the actual annotation type.
|
||||
* @return the annotation type
|
||||
*/
|
||||
String getType();
|
||||
|
||||
/**
|
||||
* Return if the annotation is present on the source. Considers
|
||||
* {@link #isDirectlyPresent() direct annotations}, and
|
||||
* {@link #isMetaPresent() meta-annotation} annotations within the context
|
||||
* of the {@link SearchStrategy} used.
|
||||
* @return {@code true} if the annotation is present
|
||||
*/
|
||||
boolean isPresent();
|
||||
|
||||
/**
|
||||
* Return if the annotation is directly present on the source. A directly
|
||||
* present annotation is one that the user has explicitly defined and not
|
||||
* one that is {@link #isMetaPresent() meta-present} or
|
||||
* {@link Inherited @Inherited}.
|
||||
* @return {@code true} if the annotation is directly present
|
||||
*/
|
||||
boolean isDirectlyPresent();
|
||||
|
||||
/**
|
||||
* Return if the annotation is meta-present on the source. A meta-present
|
||||
* annotation is an annotation that the user hasn't explicitly defined, but
|
||||
* has been used as a meta-annotation somewhere in the annotation hierarchy.
|
||||
* @return {@code true} if the annotation is meta-present
|
||||
*/
|
||||
boolean isMetaPresent();
|
||||
|
||||
/**
|
||||
* Return the depth of this annotation related to its use as a
|
||||
* meta-annotation. A directly declared annotation has a depth of {@code 0},
|
||||
* a meta-annotation has a depth of {@code 1}, a meta-annotation on a
|
||||
* meta-annotation has a depth of {@code 2}, etc. A {@link #missing()
|
||||
* missing} annotation will always return a depth of {@code -1}.
|
||||
* @return the annotation depth or {@code -1} if the annotation is missing
|
||||
*/
|
||||
int getDepth();
|
||||
|
||||
/**
|
||||
* Return the index of the aggregate collection containing this annotation.
|
||||
* Can be used to reorder a stream of annotations, for example, to give a
|
||||
* higher priority to annotations declared on a superclass or interface. A
|
||||
* {@link #missing() missing} annotation will always return an aggregate
|
||||
* index of {@code -1}.
|
||||
* @return the aggregate index (starting at {@code 0}) or {@code -1} if the
|
||||
* annotation is missing
|
||||
*/
|
||||
int getAggregateIndex();
|
||||
|
||||
/**
|
||||
* Return the source that ultimately declared the annotation, or
|
||||
* {@code null} if the source is not known. If this merged annotation was
|
||||
* created {@link MergedAnnotations#from(java.lang.reflect.AnnotatedElement)
|
||||
* from} an {@link AnnotatedElement} then this source will be an element of
|
||||
* the same type. If the annotation was loaded without using reflection, the
|
||||
* source can be of any type, but should have a sensible {@code toString()}.
|
||||
* Meta-annotations will return the same source as the {@link #getParent()}.
|
||||
* @return the source, or {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
Object getSource();
|
||||
|
||||
/**
|
||||
* Return the parent of the meta-annotation, or {@code null} if the
|
||||
* annotation is not {@link #isMetaPresent() meta-present}.
|
||||
* @return the parent annotation or {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
MergedAnnotation<?> getParent();
|
||||
|
||||
/**
|
||||
* Return if the specified attribute name as a non-default value when
|
||||
* compared to the annotation declaration.
|
||||
* @param attributeName the attribute name
|
||||
* @return {@code true} if the attribute value is different from the default
|
||||
* value
|
||||
*/
|
||||
boolean hasNonDefaultValue(String attributeName);
|
||||
|
||||
/**
|
||||
* Return if the specified attribute name as a default value when compared
|
||||
* to the annotation declaration.
|
||||
* @param attributeName the attribute name
|
||||
* @return {@code true} if the attribute value is the same as the default
|
||||
* value
|
||||
*/
|
||||
boolean hasDefaultValue(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required byte attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as a byte
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
byte getByte(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required byte array attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as a byte array
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
byte[] getByteArray(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required boolean attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as a boolean
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
boolean getBoolean(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required boolean array attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as a boolean array
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
boolean[] getBooleanArray(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required char attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as a char
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
char getChar(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required char array attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as a char array
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
char[] getCharArray(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required short attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as a short
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
short getShort(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required short array attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as a short array
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
short[] getShortArray(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required int attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as an int
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
int getInt(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required int array attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as an int array
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
int[] getIntArray(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required long attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as a long
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
long getLong(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required long array attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as a long array
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
long[] getLongArray(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required double attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as a double
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
double getDouble(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required double array attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as a double array
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
double[] getDoubleArray(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required float attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as a float
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
float getFloat(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required float array attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as a float array
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
float[] getFloatArray(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required string attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as a string
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
String getString(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required string array attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as a string array
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
String[] getStringArray(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required class attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as a class
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
Class<?> getClass(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required class array attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return the value as a class array
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
Class<?>[] getClassArray(String attributeName) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required enum attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @param type the enum type
|
||||
* @return the value as a enum
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
<E extends Enum<E>> E getEnum(String attributeName, Class<E> type)
|
||||
throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required enum array attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @param type the enum type
|
||||
* @return the value as a enum array
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
<E extends Enum<E>> E[] getEnumArray(String attributeName, Class<E> type)
|
||||
throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required annotation attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @param type the annotation type
|
||||
* @return the value as a {@link MergedAnnotation}
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
<T extends Annotation> MergedAnnotation<T> getAnnotation(String attributeName,
|
||||
Class<T> type) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return a required annotation array attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @param type the annotation type
|
||||
* @return the value as a {@link MergedAnnotation} array
|
||||
* @throws NoSuchElementException if there is no matching attribute
|
||||
*/
|
||||
<T extends Annotation> MergedAnnotation<T>[] getAnnotationArray(String attributeName,
|
||||
Class<T> type) throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return an optional attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @return an optional value or {@link Optional#empty()} if there is no
|
||||
* matching attribute
|
||||
*/
|
||||
Optional<Object> getValue(String attributeName);
|
||||
|
||||
/**
|
||||
* Return an optional attribute value from the annotation.
|
||||
* @param attributeName the attribute name
|
||||
* @param type the attribute type. Must be compatible with the underlying
|
||||
* attribute type or {@code Object.class}.
|
||||
* @return an optional value or {@link Optional#empty()} if there is no
|
||||
* matching attribute
|
||||
*/
|
||||
<T> Optional<T> getValue(String attributeName, Class<T> type);
|
||||
|
||||
/**
|
||||
* Return the default attribute value from the annotation as specified in
|
||||
* the annotation declaration.
|
||||
* @param attributeName the attribute name
|
||||
* @return an optional of the default value or {@link Optional#empty()} if
|
||||
* there is no matching attribute or no defined default
|
||||
*/
|
||||
Optional<Object> getDefaultValue(String attributeName);
|
||||
|
||||
/**
|
||||
* Return the default attribute value from the annotation as specified in
|
||||
* the annotation declaration.
|
||||
* @param attributeName the attribute name
|
||||
* @param type the attribute type. Must be compatible with the underlying
|
||||
* attribute type or {@code Object.class}.
|
||||
* @return an optional of the default value or {@link Optional#empty()} if
|
||||
* there is no matching attribute or no defined default
|
||||
*/
|
||||
<T> Optional<T> getDefaultValue(String attributeName, Class<T> type);
|
||||
|
||||
/**
|
||||
* Return a new view of the annotation with all attributes that have default
|
||||
* values removed.
|
||||
* @return a filtered view of the annotation without any attributes that
|
||||
* have a default value
|
||||
* @see #filterAttributes(Predicate)
|
||||
*/
|
||||
MergedAnnotation<A> filterDefaultValues();
|
||||
|
||||
/**
|
||||
* Return a new view of the annotation with only attributes that match the
|
||||
* given predicate.
|
||||
* @param predicate a predicate used to filter attribute names
|
||||
* @return a filtered view of the annotation
|
||||
* @see #filterDefaultValues()
|
||||
* @see MergedAnnotationPredicates
|
||||
*/
|
||||
MergedAnnotation<A> filterAttributes(Predicate<String> predicate);
|
||||
|
||||
/**
|
||||
* Return a new view of the annotation that exposes non-merged attribute
|
||||
* values. Methods from this view will return attribute values with only
|
||||
* alias mirroring rules applied. Aliases to parent attributes will not be
|
||||
* applied.
|
||||
* @return a non-merged view of the annotation
|
||||
*/
|
||||
MergedAnnotation<A> withNonMergedAttributes();
|
||||
|
||||
/**
|
||||
* Return an immutable {@link Map} that contains all the annotation
|
||||
* attributes. The {@link MapValues} options may be used to change the way
|
||||
* that values are added.
|
||||
* @param options map value options
|
||||
* @return a map containing the attributes and values
|
||||
*/
|
||||
Map<String, Object> asMap(MapValues... options);
|
||||
|
||||
/**
|
||||
* Return a {@link Map} of the supplied type that contains all the
|
||||
* annotation attributes. The {@link MapValues} options may be used to
|
||||
* change the way that values are added.
|
||||
* @param factory a map factory or {@code null} to return an immutable map.
|
||||
* @param options map value options
|
||||
* @return a map containing the attributes and values
|
||||
*/
|
||||
<T extends Map<String, Object>> T asMap(
|
||||
@Nullable Function<MergedAnnotation<?>, T> factory, MapValues... options);
|
||||
|
||||
/**
|
||||
* Return a type-safe synthesized version of this annotation that can be
|
||||
* used directly in code. The result is synthesized using a JDK
|
||||
* {@link Proxy} and as a result may incur a computational cost when first
|
||||
* invoked.
|
||||
* @return a sythesized version of the annotation.
|
||||
* @throws NoSuchElementException on a missing annotation
|
||||
*/
|
||||
A synthesize() throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Optionally return type-safe synthesized version of this annotation based
|
||||
* on a condition predicate. The result is synthesized using a JDK
|
||||
* {@link Proxy} and as a result may incur a computational cost when first
|
||||
* invoked.
|
||||
* @param condition the test to determine if the annotation can be
|
||||
* sythesized
|
||||
* @return a optional containing the sythesized version of the annotation or
|
||||
* an empty optional if the condition doesn't match
|
||||
* @throws NoSuchElementException on a missing annotation
|
||||
* @see MergedAnnotationPredicates
|
||||
*/
|
||||
Optional<A> synthesize(@Nullable Predicate<? super MergedAnnotation<A>> condition)
|
||||
throws NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Return an {@link MergedAnnotation} that represents a missing annotation
|
||||
* (i.e. one that is not present).
|
||||
* @return an instance representing a missing annotation
|
||||
*/
|
||||
static <A extends Annotation> MergedAnnotation<A> missing() {
|
||||
return MissingMergedAnnotation.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link MergedAnnotation} instance from the specified
|
||||
* annotation.
|
||||
* @param annotation the annotation to include
|
||||
* @return a {@link MergedAnnotation} instance containing the annotation
|
||||
*/
|
||||
static <A extends Annotation> MergedAnnotation<A> from(A annotation) {
|
||||
return from(null, annotation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link MergedAnnotations} instance from the specified
|
||||
* annotation.
|
||||
* @param source the source for the annotation. This source is used only for
|
||||
* information and logging. It does not need to <em>actually</em> contain
|
||||
* the specified annotations and it will not be searched.
|
||||
* @param annotation the annotation to include
|
||||
* @return a {@link MergedAnnotation} instance for the annotation
|
||||
*/
|
||||
static <A extends Annotation> MergedAnnotation<A> from(@Nullable Object source,
|
||||
A annotation) {
|
||||
return TypeMappedAnnotation.from(source, annotation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link MergedAnnotations} instance from the specified
|
||||
* annotation type. The resulting annotation will not have any attribute
|
||||
* values, but may still be used to query default values.
|
||||
* @param annotationType the annotation type
|
||||
* @return a {@link MergedAnnotation} instance for the annotation
|
||||
*/
|
||||
static <A extends Annotation> MergedAnnotation<A> from(Class<A> annotationType) {
|
||||
return from(null, annotationType, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link MergedAnnotations} instance from the specified
|
||||
* annotation type and attributes maps.
|
||||
* @param annotationType the annotation type
|
||||
* @param attributes the annotation attributes or {@code null} if just
|
||||
* default values should be used
|
||||
* @return a {@link MergedAnnotation} instance for the annotation and
|
||||
* attributes
|
||||
* @see #from(AnnotatedElement, Class, Map)
|
||||
*/
|
||||
static <A extends Annotation> MergedAnnotation<A> from(Class<A> annotationType,
|
||||
@Nullable Map<String, ?> attributes) {
|
||||
return from(null, annotationType, attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link MergedAnnotations} instance from the specified
|
||||
* annotation type and attributes maps.
|
||||
* @param source the source for the annotation. This source is used only for
|
||||
* information and logging. It does not need to <em>actually</em> contain
|
||||
* the specified annotations and it will not be searched.
|
||||
* @param annotationType the annotation type
|
||||
* @param attributes the annotation attributes or {@code null} if just
|
||||
* default values should be used
|
||||
* @return a {@link MergedAnnotation} instance for the annotation and
|
||||
* attributes
|
||||
*/
|
||||
static <A extends Annotation> MergedAnnotation<A> from(
|
||||
@Nullable AnnotatedElement source, Class<A> annotationType,
|
||||
@Nullable Map<String, ?> attributes) {
|
||||
return TypeMappedAnnotation.from(source, annotationType, attributes);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Options that effect the way map values are
|
||||
* {@link MergedAnnotation#asMap(MapValues...) converted}.
|
||||
*/
|
||||
enum MapValues {
|
||||
|
||||
/**
|
||||
* Add class or class array attributes as strings.
|
||||
*/
|
||||
CLASS_TO_STRING,
|
||||
|
||||
/**
|
||||
* Convert any nested annotation or annotation arrays to maps rather
|
||||
* than synthesizing the values.
|
||||
*/
|
||||
ANNOTATION_TO_MAP;
|
||||
|
||||
|
||||
protected final boolean isIn(MapValues... options) {
|
||||
for (MapValues candidate : options) {
|
||||
if (candidate == this) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a {@link MapValues} array from a set of
|
||||
* boolean flags.
|
||||
* @param classToString if {@link MapValues#CLASS_TO_STRING} is included
|
||||
* @param annotationsToMap if {@link MapValues#ANNOTATION_TO_MAP} is
|
||||
* included
|
||||
* @return a new {@link MapValues} array
|
||||
*/
|
||||
public static MapValues[] of(boolean classToString, boolean annotationsToMap) {
|
||||
EnumSet<MapValues> result = EnumSet.noneOf(MapValues.class);
|
||||
addIfTrue(result, MapValues.CLASS_TO_STRING, classToString);
|
||||
addIfTrue(result, MapValues.ANNOTATION_TO_MAP, annotationsToMap);
|
||||
return result.toArray(new MapValues[0]);
|
||||
}
|
||||
|
||||
private static <T> void addIfTrue(Set<T> result, T value, boolean test) {
|
||||
if (test) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.IntFunction;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collector.Characteristics;
|
||||
|
||||
import org.springframework.core.annotation.MergedAnnotation.MapValues;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* Collector implementations that provide various reduction operations for
|
||||
* {@link MergedAnnotation MergedAnnotations}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
*/
|
||||
public abstract class MergedAnnotationCollectors {
|
||||
|
||||
private static final Characteristics[] NO_CHARACTERISTICS = {};
|
||||
|
||||
private static final Characteristics[] IDENTITY_FINISH_CHARACTERISTICS = {
|
||||
Characteristics.IDENTITY_FINISH };
|
||||
|
||||
|
||||
private MergedAnnotationCollectors() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a new {@link Collector} that accumulates merged annotations to a
|
||||
* {@link LinkedHashSet} containing {@link MergedAnnotation#synthesize()
|
||||
* synthesized} versions.
|
||||
* @param <A> the annotation type
|
||||
* @return a {@link Collector} which collects and synthesizes the
|
||||
* annotations into a {@link Set}
|
||||
*/
|
||||
public static <A extends Annotation> Collector<MergedAnnotation<A>, ?, Set<A>> toAnnotationSet() {
|
||||
return Collector.of(ArrayList<A>::new, (list, annotation) -> list.add(annotation.synthesize()),
|
||||
MergedAnnotationCollectors::addAll, LinkedHashSet::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Collector} that accumulates merged annotations to an
|
||||
* {@link Annotation} array containing {@link MergedAnnotation#synthesize()
|
||||
* synthesized} versions.
|
||||
* @param <A> the annotation type
|
||||
* @return a {@link Collector} which collects and synthesizes the
|
||||
* annotations into an {@code Annotation[]}
|
||||
* @see #toAnnotationArray(IntFunction)
|
||||
*/
|
||||
public static <A extends Annotation> Collector<MergedAnnotation<A>, ?, Annotation[]> toAnnotationArray() {
|
||||
return toAnnotationArray(Annotation[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Collector} that accumulates merged annotations to an
|
||||
* {@link Annotation} array containing {@link MergedAnnotation#synthesize()
|
||||
* synthesized} versions.
|
||||
* @param <A> the annotation type
|
||||
* @param <R> the resulting array type
|
||||
* @param generator a function which produces a new array of the desired
|
||||
* type and the provided length
|
||||
* @return a {@link Collector} which collects and synthesizes the
|
||||
* annotations into an annotation array
|
||||
* @see #toAnnotationArray
|
||||
*/
|
||||
public static <R extends Annotation, A extends R> Collector<MergedAnnotation<A>, ?, R[]> toAnnotationArray(
|
||||
IntFunction<R[]> generator) {
|
||||
|
||||
return Collector.of(ArrayList::new, (list, annotation) -> list.add(annotation.synthesize()),
|
||||
MergedAnnotationCollectors::addAll, list -> list.toArray(generator.apply(list.size())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Collector} that accumulates merged annotations to an
|
||||
* {@link MultiValueMap} with items {@link MultiValueMap#add(Object, Object)
|
||||
* added} from each merged annotation
|
||||
* {@link MergedAnnotation#asMap(MapValues...) as a map}.
|
||||
* @param <A> the annotation type
|
||||
* @param options the map conversion options
|
||||
* @return a {@link Collector} which collects and synthesizes the
|
||||
* annotations into a {@link LinkedMultiValueMap}
|
||||
* @see #toMultiValueMap(Function, MapValues...)
|
||||
*/
|
||||
public static <A extends Annotation> Collector<MergedAnnotation<A>, ?, MultiValueMap<String, Object>> toMultiValueMap(
|
||||
MapValues... options) {
|
||||
|
||||
return toMultiValueMap(Function.identity(), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Collector} that accumulates merged annotations to an
|
||||
* {@link MultiValueMap} with items {@link MultiValueMap#add(Object, Object)
|
||||
* added} from each merged annotation
|
||||
* {@link MergedAnnotation#asMap(MapValues...) as a map}.
|
||||
* @param <A> the annotation type
|
||||
* @param options the map conversion options
|
||||
* @param finisher the finisher function for the new {@link MultiValueMap}
|
||||
* @return a {@link Collector} which collects and synthesizes the
|
||||
* annotations into a {@link LinkedMultiValueMap}
|
||||
* @see #toMultiValueMap(MapValues...)
|
||||
*/
|
||||
public static <A extends Annotation> Collector<MergedAnnotation<A>, ?, MultiValueMap<String, Object>> toMultiValueMap(
|
||||
Function<MultiValueMap<String, Object>, MultiValueMap<String, Object>> finisher,
|
||||
MapValues... options) {
|
||||
|
||||
Assert.notNull(finisher, "Finisher must not be null");
|
||||
Characteristics[] characteristics = isSameInstance(finisher, Function.identity()) ?
|
||||
IDENTITY_FINISH_CHARACTERISTICS :
|
||||
NO_CHARACTERISTICS;
|
||||
return Collector.of(LinkedMultiValueMap::new,
|
||||
(map, annotation) -> annotation.asMap(options).forEach(map::add),
|
||||
MergedAnnotationCollectors::merge, finisher, characteristics);
|
||||
}
|
||||
|
||||
private static boolean isSameInstance(Object instance, Object candidate) {
|
||||
return instance == candidate;
|
||||
}
|
||||
|
||||
private static <E, L extends List<E>> L addAll(L list, L additions) {
|
||||
list.addAll(additions);
|
||||
return list;
|
||||
}
|
||||
|
||||
private static <K, V> MultiValueMap<K, V> merge(MultiValueMap<K, V> map,
|
||||
MultiValueMap<K, V> additions) {
|
||||
map.addAll(additions);
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Predicate implementations that provide various test operations for
|
||||
* {@link MergedAnnotation MergedAnnotations}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
*/
|
||||
public abstract class MergedAnnotationPredicates {
|
||||
|
||||
private MergedAnnotationPredicates() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a new {@link Predicate} that evaluates {@code true} if the
|
||||
* {@link MergedAnnotation#getType() merged annotation type} is contained in
|
||||
* the specified array.
|
||||
* @param <A> the annotation type
|
||||
* @param typeNames the names that should be matched
|
||||
* @return a {@link Predicate} to test the annotation type
|
||||
*/
|
||||
public static <A extends Annotation> Predicate<MergedAnnotation<? extends A>> typeIn(
|
||||
String... typeNames) {
|
||||
|
||||
Assert.notNull(typeNames, "TypeNames must not be null");
|
||||
return annotation -> ObjectUtils.containsElement(typeNames, annotation.getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Predicate} that evaluates {@code true} if the
|
||||
* {@link MergedAnnotation#getType() merged annotation type} is contained in
|
||||
* the specified array.
|
||||
* @param <A> the annotation type
|
||||
* @param types the types that should be matched
|
||||
* @return a {@link Predicate} to test the annotation type
|
||||
*/
|
||||
public static <A extends Annotation> Predicate<MergedAnnotation<? extends A>> typeIn(
|
||||
Class<?>... types) {
|
||||
|
||||
Assert.notNull(types, "Types must not be null");
|
||||
return annotation -> Arrays.stream(types)
|
||||
.anyMatch(type -> type.getName().equals(annotation.getType()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Predicate} that evaluates {@code true} if the
|
||||
* {@link MergedAnnotation#getType() merged annotation type} is contained in
|
||||
* the collection.
|
||||
* @param <A> the annotation type
|
||||
* @param types the type names or classes that should be matched
|
||||
* @return a {@link Predicate} to test the annotation type
|
||||
*/
|
||||
public static <A extends Annotation> Predicate<MergedAnnotation<? extends A>> typeIn(
|
||||
Collection<?> types) {
|
||||
|
||||
Assert.notNull(types, "Types must not be null");
|
||||
return annotation -> types.stream()
|
||||
.map(type -> type instanceof Class ? ((Class<?>) type).getName() : type.toString())
|
||||
.anyMatch(typeName -> typeName.equals(annotation.getType()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new stateful, single use {@link Predicate} that matches only
|
||||
* the first run of an extracted value. For example,
|
||||
* {@code MergedAnnotationPredicates.firstRunOf(MergedAnnotation::depth)}
|
||||
* will return the first annotation and a subsequent run of the same depth.
|
||||
* NOTE: this predicate only matches the first first run, once the extracted
|
||||
* value changes the predicate always returns {@code false}.
|
||||
* @param valueExtractor function used to extract the value to check
|
||||
* @return a {@link Predicate} that matches the first run of the extracted
|
||||
* values
|
||||
*/
|
||||
public static <A extends Annotation> Predicate<MergedAnnotation<A>> firstRunOf(
|
||||
Function<? super MergedAnnotation<A>, ?> valueExtractor) {
|
||||
|
||||
Assert.notNull(valueExtractor, "ValueExtractor must not be null");
|
||||
return new FirstRunOfPredicate<>(valueExtractor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new stateful, single use {@link Predicate} that matches
|
||||
* annotations that are unique based on extracted key. For example
|
||||
* {@code MergedAnnotationPredicates.unique(MergedAnnotation::type)} will
|
||||
* match the first time a unique type is seen.
|
||||
* @param keyExtractor function used to extract the key used to test for
|
||||
* uniqueness
|
||||
* @return a {@link Predicate} that matches unique annotation based on the
|
||||
* extracted key
|
||||
*/
|
||||
public static <A extends Annotation, K> Predicate<MergedAnnotation<A>> unique(
|
||||
Function<? super MergedAnnotation<A>, K> keyExtractor) {
|
||||
|
||||
Assert.notNull(keyExtractor, "KeyExtractor must not be null");
|
||||
return new UniquePredicate<>(keyExtractor);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link Predicate} implementation used for
|
||||
* {@link MergedAnnotationPredicates#firstRunOf(Function)}.
|
||||
*/
|
||||
private static class FirstRunOfPredicate<A extends Annotation>
|
||||
implements Predicate<MergedAnnotation<A>> {
|
||||
|
||||
private final Function<? super MergedAnnotation<A>, ?> valueExtractor;
|
||||
|
||||
private boolean hasLastValue;
|
||||
|
||||
@Nullable
|
||||
private Object lastValue;
|
||||
|
||||
|
||||
FirstRunOfPredicate(
|
||||
Function<? super MergedAnnotation<A>, ?> valueExtractor) {
|
||||
this.valueExtractor = valueExtractor;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean test(@Nullable MergedAnnotation<A> annotation) {
|
||||
if (!this.hasLastValue) {
|
||||
this.hasLastValue = true;
|
||||
this.lastValue = this.valueExtractor.apply(annotation);
|
||||
}
|
||||
Object value = this.valueExtractor.apply(annotation);
|
||||
return ObjectUtils.nullSafeEquals(value, this.lastValue);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link Predicate} implementation used for
|
||||
* {@link MergedAnnotationPredicates#unique(Function)}.
|
||||
*/
|
||||
private static class UniquePredicate<A extends Annotation, K>
|
||||
implements Predicate<MergedAnnotation<A>> {
|
||||
|
||||
private final Function<? super MergedAnnotation<A>, K> keyExtractor;
|
||||
|
||||
private final Set<K> seen = new HashSet<>();
|
||||
|
||||
|
||||
UniquePredicate(Function<? super MergedAnnotation<A>, K> keyExtractor) {
|
||||
this.keyExtractor = keyExtractor;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean test(@Nullable MergedAnnotation<A> annotation) {
|
||||
K key = this.keyExtractor.apply(annotation);
|
||||
return this.seen.add(key);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
|
||||
/**
|
||||
* Strategy interface used to select between two {@link MergedAnnotation}
|
||||
* instances.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
* @param <A> the annotation type
|
||||
* @see MergedAnnotationSelectors
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface MergedAnnotationSelector<A extends Annotation> {
|
||||
|
||||
/**
|
||||
* Return {@code true} if the existing annotation is known to be the best
|
||||
* candidate and any subsequent selections may be skipped.
|
||||
* @param annotation the annotation to check
|
||||
* @return {@code true} if the annotation is known to be the best candidate
|
||||
*/
|
||||
default boolean isBestCandidate(MergedAnnotation<A> annotation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the annotation that should be used.
|
||||
* @param existing an existing annotation returned from an earlier result
|
||||
* @param candidate a candidate annotation that may be better suited
|
||||
* @return the most appropriate annotation from the {@code existing} or
|
||||
* {@code candidate}
|
||||
*/
|
||||
MergedAnnotation<A> select(MergedAnnotation<A> existing,
|
||||
MergedAnnotation<A> candidate);
|
||||
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* {@link MergedAnnotationSelector} implementations that provide various options
|
||||
* for {@link MergedAnnotation MergedAnnotations}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
* @see MergedAnnotations#get(Class, Predicate, MergedAnnotationSelector)
|
||||
* @see MergedAnnotations#get(String, Predicate, MergedAnnotationSelector)
|
||||
*/
|
||||
public abstract class MergedAnnotationSelectors {
|
||||
|
||||
private static final MergedAnnotationSelector<?> NEAREST = new Nearest();
|
||||
|
||||
private static final MergedAnnotationSelector<?> FIRST_DIRECTLY_DECLARED = new FirstDirectlyDeclared();
|
||||
|
||||
|
||||
private MergedAnnotationSelectors() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Select the nearest annotation, i.e. the one with the lowest depth.
|
||||
* @return a selector that picks the annotation with the lowest depth
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <A extends Annotation> MergedAnnotationSelector<A> nearest() {
|
||||
return (MergedAnnotationSelector<A>) NEAREST;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the first directly declared annotation when possible. If not direct
|
||||
* annotations are declared then the earliest annotation is selected.
|
||||
* @return a selector that picks the first directly declared annotation whenever possible
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <A extends Annotation> MergedAnnotationSelector<A> firstDirectlyDeclared() {
|
||||
return (MergedAnnotationSelector<A>) FIRST_DIRECTLY_DECLARED;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link MergedAnnotationSelector} to select the nearest annotation.
|
||||
*/
|
||||
private static class Nearest implements MergedAnnotationSelector<Annotation> {
|
||||
|
||||
@Override
|
||||
public boolean isBestCandidate(MergedAnnotation<Annotation> annotation) {
|
||||
return annotation.getDepth() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MergedAnnotation<Annotation> select(MergedAnnotation<Annotation> existing,
|
||||
MergedAnnotation<Annotation> candidate) {
|
||||
if (candidate.getDepth() < existing.getDepth()) {
|
||||
return candidate;
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link MergedAnnotationSelector} to select the first directly declared
|
||||
* annotation.
|
||||
*/
|
||||
private static class FirstDirectlyDeclared implements MergedAnnotationSelector<Annotation> {
|
||||
|
||||
@Override
|
||||
public boolean isBestCandidate(MergedAnnotation<Annotation> annotation) {
|
||||
return annotation.getDepth() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MergedAnnotation<Annotation> select(MergedAnnotation<Annotation> existing,
|
||||
MergedAnnotation<Annotation> candidate) {
|
||||
if (existing.getDepth() > 0 && candidate.getDepth() == 0) {
|
||||
return candidate;
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,412 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Provides access to a collection of merged annotations, usually obtained from
|
||||
* a from a source such as a {@link Class} or {@link Method}. Each merged
|
||||
* annotation represent a view where the attribute values may be "merged" from
|
||||
* different source values, typically:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Explicit and Implicit {@link AliasFor @AliasFor} declarations on one or
|
||||
* attributes within the annotation.</li>
|
||||
* <li>Explicit {@link AliasFor @AliasFor} declarations for a
|
||||
* meta-annotation.</li>
|
||||
* <li>Convention based attribute aliases for a meta-annotation</li>
|
||||
* <li>From a meta-annotation declaration.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>For example, a {@code @PostMapping} annotation might be defined as follows:
|
||||
*
|
||||
* <pre class="code">
|
||||
* @Retention(RetentionPolicy.RUNTIME)
|
||||
* @RequestMapping(method = RequestMethod.POST)
|
||||
* public @interface PostMapping {
|
||||
*
|
||||
* @AliasFor(attribute = "path")
|
||||
* String[] value() default {};
|
||||
*
|
||||
* @AliasFor(attribute = "value")
|
||||
* String[] path() default {};
|
||||
*
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* If a method is annotated with {@code @PostMapping("/home")} it will contain
|
||||
* merged annotations for both {@code @PostMapping} and the meta-annotation
|
||||
* {@code @RequestMapping}. The merged view of the {@code @RequestMapping}
|
||||
* annotation will contain the following attributes:
|
||||
*
|
||||
* <p><table>
|
||||
* <tr>
|
||||
* <th>Name</th>
|
||||
* <th>Value</th>
|
||||
* <th>Source</th>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>value</td>
|
||||
* <td>"/home"</td>
|
||||
* <td>Declared {@code @PostMapping}</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>path</td>
|
||||
* <td>"/home"</td>
|
||||
* <td>Explicit {@code @AliasFor}</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>method</td>
|
||||
* <td>RequestMethod.POST</td>
|
||||
* <td>Declared meta-annotation</td>
|
||||
* </tr>
|
||||
* </table>
|
||||
*
|
||||
* <p>{@link MergedAnnotations} can be obtained {@link #from(AnnotatedElement)
|
||||
* from} any Java {@link AnnotatedElement}. They may also used for sources that
|
||||
* don't use reflection (such as those that directly parse bytecode).
|
||||
*
|
||||
* <p>Different {@link SearchStrategy search strategies} can be used to locate
|
||||
* related source elements that contain the annotations to be aggregated
|
||||
* together. For example, {@link SearchStrategy#EXHAUSTIVE} will search both
|
||||
* superclasses and implemented interfaces.
|
||||
*
|
||||
* <p>From a {@link MergedAnnotations} instance you can either {@link #get(String)}
|
||||
* a single annotation, or {@link #stream() stream all annotations} or just
|
||||
* those that match {@link #stream(String) a specific type}. You can also
|
||||
* quickly tell if an annotation {@link #isPresent(String) is present}.
|
||||
*
|
||||
* <p>Here are some typical examples:
|
||||
*
|
||||
* <pre class="code">
|
||||
* // is an annotation present or meta-present
|
||||
* mergedAnnotations.isPresent(ExampleAnnotation.class);
|
||||
*
|
||||
* // get the merged "value" attribute of ExampleAnnotation (either direct or
|
||||
* // meta-present)
|
||||
* mergedAnnotations.get(ExampleAnnotation.class).getString("value");
|
||||
*
|
||||
* // get all meta-annotations but no direct annotations
|
||||
* mergedAnnotations.stream().anyMatch(MergedAnnotation::isMetaPresent);
|
||||
*
|
||||
* // get all ExampleAnnotation declarations (include any meta-annotations) and
|
||||
* // print the merged "value" attributes
|
||||
* mergedAnnotations.stream(ExampleAnnotation.class).map(
|
||||
* a -> a.getString("value")).forEach(System.out::println);
|
||||
* </pre>
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
* @see MergedAnnotation
|
||||
* @see MergedAnnotationCollectors
|
||||
* @see MergedAnnotationPredicates
|
||||
* @see MergedAnnotationSelectors
|
||||
*/
|
||||
public interface MergedAnnotations extends Iterable<MergedAnnotation<Annotation>> {
|
||||
|
||||
/**
|
||||
* Return if the specified annotation is either directly present, or
|
||||
* meta-present. Equivalent to calling
|
||||
* {@code get(annotationType).isPresent()}.
|
||||
* @param annotationType the annotation type to check
|
||||
* @return {@code true} if the annotation is present
|
||||
*/
|
||||
<A extends Annotation> boolean isPresent(@Nullable Class<A> annotationType);
|
||||
|
||||
/**
|
||||
* Return if the specified annotation is directly present. Equivalent to
|
||||
* calling {@code get(annotationType).isDirectlyPresent()}.
|
||||
* @param annotationType the annotation type to check
|
||||
* @return {@code true} if the annotation is present
|
||||
*/
|
||||
boolean isPresent(@Nullable String annotationType);
|
||||
|
||||
/**
|
||||
* Return if the specified annotation is directly present. Equivalent to
|
||||
* calling {@code get(annotationType).isDirectlyPresent()}.
|
||||
* @param annotationType the annotation type to check
|
||||
* @return {@code true} if the annotation is present
|
||||
*/
|
||||
<A extends Annotation> boolean isDirectlyPresent(@Nullable Class<A> annotationType);
|
||||
|
||||
/**
|
||||
* Return if the specified annotation is either directly present, or
|
||||
* meta-present. Equivalent to calling
|
||||
* {@code get(annotationType).isPresent()}.
|
||||
* @param annotationType the annotation type to check
|
||||
* @return {@code true} if the annotation is present
|
||||
*/
|
||||
boolean isDirectlyPresent(@Nullable String annotationType);
|
||||
|
||||
/**
|
||||
* Return the {@link MergedAnnotationSelectors#nearest() nearest} matching
|
||||
* annotation or meta-annotation of the specified type, or
|
||||
* {@link MergedAnnotation#missing()} if none is present.
|
||||
* @param annotationType the annotation type to get
|
||||
* @return a {@link MergedAnnotation} instance
|
||||
*/
|
||||
<A extends Annotation> MergedAnnotation<A> get(@Nullable Class<A> annotationType);
|
||||
|
||||
/**
|
||||
* Return the {@link MergedAnnotationSelectors#nearest() nearest} matching
|
||||
* annotation or meta-annotation of the specified type, or
|
||||
* {@link MergedAnnotation#missing()} if none is present.
|
||||
* @param annotationType the annotation type to get
|
||||
* @param predicate a predicate that must match, or {@code null} if only
|
||||
* type matching is required
|
||||
* @return a {@link MergedAnnotation} instance
|
||||
* @see MergedAnnotationPredicates
|
||||
*/
|
||||
<A extends Annotation> MergedAnnotation<A> get(@Nullable Class<A> annotationType,
|
||||
@Nullable Predicate<? super MergedAnnotation<A>> predicate);
|
||||
|
||||
/**
|
||||
* Return a matching annotation or meta-annotation of the specified type, or
|
||||
* {@link MergedAnnotation#missing()} if none is present.
|
||||
* @param annotationType the annotation type to get
|
||||
* @param predicate a predicate that must match, or {@code null} if only
|
||||
* type matching is required
|
||||
* @param selector a selector used to choose the most appropriate annotation
|
||||
* within an aggregate, or {@code null} to select the
|
||||
* {@link MergedAnnotationSelectors#nearest() nearest}.
|
||||
* @return a {@link MergedAnnotation} instance
|
||||
* @see MergedAnnotationPredicates
|
||||
* @see MergedAnnotationSelectors
|
||||
*/
|
||||
<A extends Annotation> MergedAnnotation<A> get(@Nullable Class<A> annotationType,
|
||||
@Nullable Predicate<? super MergedAnnotation<A>> predicate,
|
||||
@Nullable MergedAnnotationSelector<A> selector);
|
||||
|
||||
/**
|
||||
* Return the {@link MergedAnnotationSelectors#nearest() nearest} matching
|
||||
* annotation or meta-annotation of the specified type, or
|
||||
* {@link MergedAnnotation#missing()} if none is present.
|
||||
* @param annotationType the annotation type to get
|
||||
* @return a {@link MergedAnnotation} instance
|
||||
*/
|
||||
<A extends Annotation> MergedAnnotation<A> get(@Nullable String annotationType);
|
||||
|
||||
/**
|
||||
* Return the {@link MergedAnnotationSelectors#nearest() nearest} matching
|
||||
* annotation or meta-annotation of the specified type, or
|
||||
* {@link MergedAnnotation#missing()} if none is present.
|
||||
* @param annotationType the annotation type to get
|
||||
* @param predicate a predicate that must match, or {@code null} if only
|
||||
* type matching is required
|
||||
* @return a {@link MergedAnnotation} instance
|
||||
* @see MergedAnnotationPredicates
|
||||
*/
|
||||
<A extends Annotation> MergedAnnotation<A> get(@Nullable String annotationType,
|
||||
@Nullable Predicate<? super MergedAnnotation<A>> predicate);
|
||||
|
||||
/**
|
||||
* Return a matching annotation or meta-annotation of the specified type, or
|
||||
* {@link MergedAnnotation#missing()} if none is present.
|
||||
* @param annotationType the annotation type to get
|
||||
* @param predicate a predicate that must match, or {@code null} if only
|
||||
* type matching is required
|
||||
* @param selector a selector used to choose the most appropriate annotation
|
||||
* within an aggregate, or {@code null} to select the
|
||||
* {@link MergedAnnotationSelectors#nearest() nearest}.
|
||||
* @return a {@link MergedAnnotation} instance
|
||||
* @see MergedAnnotationPredicates
|
||||
* @see MergedAnnotationSelectors
|
||||
*/
|
||||
<A extends Annotation> MergedAnnotation<A> get(@Nullable String annotationType,
|
||||
@Nullable Predicate<? super MergedAnnotation<A>> predicate,
|
||||
@Nullable MergedAnnotationSelector<A> selector);
|
||||
|
||||
/**
|
||||
* Stream all annotations and meta-annotations that match the specified
|
||||
* type. The resulting stream follows the same ordering rules are
|
||||
* {@link #stream()}.
|
||||
* @param annotationType the annotation type to match
|
||||
* @return a stream of matching annotations
|
||||
*/
|
||||
<A extends Annotation> Stream<MergedAnnotation<A>> stream(
|
||||
@Nullable Class<A> annotationType);
|
||||
|
||||
/**
|
||||
* Stream all annotations and meta-annotations that match the specified
|
||||
* type.The resulting stream follows the same ordering rules are
|
||||
* {@link #stream()}.
|
||||
* @param annotationType the annotation type to match
|
||||
* @return a stream of matching annotations
|
||||
*/
|
||||
<A extends Annotation> Stream<MergedAnnotation<A>> stream(
|
||||
@Nullable String annotationType);
|
||||
|
||||
/**
|
||||
* Stream all contained annotations and meta-annotations contained in this
|
||||
* collection. The resulting stream is ordered first by the
|
||||
* {@link MergedAnnotation#getAggregateIndex() aggregate index}, and then by
|
||||
* the annotation depth (with the closest annotations first). This ordering
|
||||
* means that, for most use-cases, the most suitable annotations appear
|
||||
* earliest in the stream.
|
||||
* @return a stream of annotations
|
||||
*/
|
||||
Stream<MergedAnnotation<Annotation>> stream();
|
||||
|
||||
/**
|
||||
* Create a new {@link MergedAnnotations} instance containing all
|
||||
* annotations and meta-annotations from the specified element. The
|
||||
* resulting instance will not include any inherited annotations, if you
|
||||
* want to include those as well you should use
|
||||
* {@link #from(AnnotatedElement, SearchStrategy)} with an appropriate
|
||||
* {@link SearchStrategy}.
|
||||
* @param element the source element
|
||||
* @return a {@link MergedAnnotations} instance containing the element
|
||||
* annotations
|
||||
*/
|
||||
static MergedAnnotations from(@Nullable AnnotatedElement element) {
|
||||
return from(element, SearchStrategy.DIRECT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link MergedAnnotations} instance containing all
|
||||
* annotations and meta-annotations from the specified element and,
|
||||
* depending on the {@link SearchStrategy}, related inherited elements.
|
||||
* @param element the source element
|
||||
* @param searchStrategy the search strategy to use
|
||||
* @return a {@link MergedAnnotations} instance containing the merged
|
||||
* element annotations
|
||||
*/
|
||||
static MergedAnnotations from(@Nullable AnnotatedElement element,
|
||||
SearchStrategy searchStrategy) {
|
||||
return from(element, searchStrategy, RepeatableContainers.standardRepeatables(),
|
||||
AnnotationFilter.PLAIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link MergedAnnotations} instance containing all
|
||||
* annotations and meta-annotations from the specified element and,
|
||||
* depending on the {@link SearchStrategy}, related inherited elements.
|
||||
* @param element the source element
|
||||
* @param searchStrategy the search strategy to use
|
||||
* @param repeatableContainers the repeatable containers that may be used by
|
||||
* the element annotations or the meta-annotations
|
||||
* @param annotationFilter an annotation filter used to restrict the
|
||||
* annotations considered
|
||||
* @return a {@link MergedAnnotations} instance containing the merged
|
||||
* element annotations
|
||||
*/
|
||||
static MergedAnnotations from(@Nullable AnnotatedElement element,
|
||||
SearchStrategy searchStrategy, RepeatableContainers repeatableContainers,
|
||||
AnnotationFilter annotationFilter) {
|
||||
return TypeMappedAnnotations.from(element, searchStrategy, repeatableContainers,
|
||||
annotationFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link MergedAnnotations} instance from the specified
|
||||
* annotations.
|
||||
* @param annotations the annotations to include
|
||||
* @return a {@link MergedAnnotations} instance containing the annotations
|
||||
* @see #from(Object, Annotation...)
|
||||
*/
|
||||
static MergedAnnotations from(Annotation... annotations) {
|
||||
return from(null, annotations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link MergedAnnotations} instance from the specified
|
||||
* annotations.
|
||||
* @param source the source for the annotations. This source is used only
|
||||
* for information and logging. It does not need to <em>actually</em>
|
||||
* contain the specified annotations and it will not be searched.
|
||||
* @param annotations the annotations to include
|
||||
* @return a {@link MergedAnnotations} instance containing the annotations
|
||||
* @see #from(Annotation...)
|
||||
* @see #from(AnnotatedElement)
|
||||
*/
|
||||
static MergedAnnotations from(@Nullable Object source, Annotation... annotations) {
|
||||
return from(source, annotations, RepeatableContainers.standardRepeatables(),
|
||||
AnnotationFilter.PLAIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link MergedAnnotations} instance from the specified
|
||||
* annotations.
|
||||
* @param source the source for the annotations. This source is used only
|
||||
* for information and logging. It does not need to <em>actually</em>
|
||||
* contain the specified annotations and it will not be searched.
|
||||
* @param annotations the annotations to include
|
||||
* @param repeatableContainers the repeatable containers that may be used by
|
||||
* meta-annotations
|
||||
* @param annotationFilter an annotation filter used to restrict the
|
||||
* annotations considered
|
||||
* @return a {@link MergedAnnotations} instance containing the annotations
|
||||
*/
|
||||
static MergedAnnotations from(@Nullable Object source, Annotation[] annotations,
|
||||
RepeatableContainers repeatableContainers,
|
||||
AnnotationFilter annotationFilter) {
|
||||
return TypeMappedAnnotations.from(source, annotations, repeatableContainers,
|
||||
annotationFilter);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Search strategies supported by
|
||||
* {@link MergedAnnotations#from(AnnotatedElement, SearchStrategy)}. Each
|
||||
* strategy creates a different set of aggregates that will be combined to
|
||||
* create the final {@link MergedAnnotations}.
|
||||
*/
|
||||
enum SearchStrategy {
|
||||
|
||||
/**
|
||||
* Find only directly declared annotations, without considering
|
||||
* {@link Inherited @Inherited} annotations and without searching
|
||||
* super-classes or implemented interfaces.
|
||||
*/
|
||||
DIRECT,
|
||||
|
||||
/**
|
||||
* Find all directly declared annotations as well any
|
||||
* {@link Inherited @Inherited} super-class annotations. This strategy
|
||||
* is only really useful when used with {@link Class} types since the
|
||||
* {@link Inherited @Inherited} annotation is ignored for all other
|
||||
* {@link AnnotatedElement annotated elements}. This strategy does not
|
||||
* search implemented interfaces.
|
||||
*/
|
||||
INHERITED_ANNOTATIONS,
|
||||
|
||||
/**
|
||||
* Find all directly declared and super-class annotations. This strategy
|
||||
* is similar to {@link #INHERITED_ANNOTATIONS} except the annotations
|
||||
* do not need to be meta-annotated with {@link Inherited @Inherited}.
|
||||
* This strategy does not search implemented interfaces.
|
||||
*/
|
||||
SUPER_CLASS,
|
||||
|
||||
/**
|
||||
* Perform a full search of all related elements, include those on any
|
||||
* super-classes or implemented interfaces. Superclass annotations do
|
||||
* not need to be meta-annotated with {@link Inherited @Inherited}.
|
||||
*/
|
||||
EXHAUSTIVE
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* A {@link AbstractMergedAnnotation} used as the implementation of
|
||||
* {@link MergedAnnotation#missing()}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
* @param <A> the annotation type
|
||||
*/
|
||||
final class MissingMergedAnnotation<A extends Annotation>
|
||||
extends AbstractMergedAnnotation<A> {
|
||||
|
||||
private static final MissingMergedAnnotation<?> INSTANCE = new MissingMergedAnnotation<>();
|
||||
|
||||
|
||||
private MissingMergedAnnotation() {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
throw new NoSuchElementException("Unable to get type for missing annotation");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPresent() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object getSource() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public MergedAnnotation<?> getParent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDepth() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAggregateIndex() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
public boolean hasNonDefaultValue(String attributeName) {
|
||||
throw new NoSuchElementException(
|
||||
"Unable to check non-default value for missing annotation");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasDefaultValue(String attributeName) {
|
||||
throw new NoSuchElementException(
|
||||
"Unable to check default value for missing annotation");
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> getValue(String attributeName, Class<T> type) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> getDefaultValue(@Nullable String attributeName, Class<T> type) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MergedAnnotation<A> filterAttributes(Predicate<String> predicate) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MergedAnnotation<A> withNonMergedAttributes() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> asMap(MapValues... options) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public <T extends Map<String, Object>> T asMap(
|
||||
@Nullable Function<MergedAnnotation<?>, T> factory, MapValues... options) {
|
||||
if (factory != null) {
|
||||
return factory.apply(this);
|
||||
}
|
||||
return (T) ((Map) Collections.emptyMap());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "(missing)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Annotation> MergedAnnotation<T> getAnnotation(String attributeName,
|
||||
Class<T> type) throws NoSuchElementException {
|
||||
|
||||
throw new NoSuchElementException(
|
||||
"Unable to get attribute value for missing annotation");
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Annotation> MergedAnnotation<T>[] getAnnotationArray(
|
||||
String attributeName, Class<T> type) throws NoSuchElementException {
|
||||
|
||||
throw new NoSuchElementException(
|
||||
"Unable to get attribute value for missing annotation");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected <T> T getAttributeValue(String attributeName, Class<T> type) {
|
||||
throw new NoSuchElementException(
|
||||
"Unable to get attribute value for missing annotation");
|
||||
}
|
||||
|
||||
protected A createSynthesized() {
|
||||
throw new NoSuchElementException("Unable to synthesize missing annotation");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <A extends Annotation> MergedAnnotation<A> getInstance() {
|
||||
return (MergedAnnotation<A>) INSTANCE;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link AnnotationFilter} implementation used for
|
||||
* {@link AnnotationFilter#packages(String...)}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
*/
|
||||
class PackagesAnnotationFilter implements AnnotationFilter {
|
||||
|
||||
private final String[] prefixes;
|
||||
|
||||
private final int hashCode;
|
||||
|
||||
|
||||
PackagesAnnotationFilter(String... packages) {
|
||||
Assert.notNull(packages, "Packages must not be null");
|
||||
this.prefixes = new String[packages.length];
|
||||
for (int i = 0; i < packages.length; i++) {
|
||||
Assert.hasText(packages[i], "Package must not have empty elements");
|
||||
this.prefixes[i] = packages[i] + ".";
|
||||
}
|
||||
Arrays.sort(this.prefixes);
|
||||
this.hashCode = Arrays.hashCode(this.prefixes);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean matches(@Nullable String annotationType) {
|
||||
if (annotationType != null) {
|
||||
for (String prefix : this.prefixes) {
|
||||
if (annotationType.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
PackagesAnnotationFilter other = (PackagesAnnotationFilter) obj;
|
||||
return Arrays.equals(this.prefixes, other.prefixes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Packages annotation filter: " +
|
||||
StringUtils.arrayToCommaDelimitedString(this.prefixes);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,290 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.Repeatable;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Strategy used to determine annotations that act as containers for other
|
||||
* annotations. The {@link #standardRepeatables()} method provides a default
|
||||
* strategy that respects Java's {@link Repeatable @Repeatable} support and
|
||||
* should be suitable for most situations.
|
||||
* <p> The {@link #of} method can be used to register relationships for
|
||||
* annotations that do not wish to use {@link Repeatable @Repeatable}.
|
||||
*
|
||||
* <p>To completely disable repeatable support use {@link #none()}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
*/
|
||||
public abstract class RepeatableContainers {
|
||||
|
||||
@Nullable
|
||||
private final RepeatableContainers parent;
|
||||
|
||||
|
||||
private RepeatableContainers(@Nullable RepeatableContainers parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add an additional explicit relationship between a contained and
|
||||
* repeatable annotation.
|
||||
* @param container the container type
|
||||
* @param repeatable the contained repeatable type
|
||||
* @return a new {@link RepeatableContainers} instance
|
||||
*/
|
||||
public RepeatableContainers and(Class<? extends Annotation> container,
|
||||
Class<? extends Annotation> repeatable) {
|
||||
|
||||
return new ExplicitRepeatableContainer(this, repeatable, container);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Annotation[] findRepeatedAnnotations(Annotation annotation) {
|
||||
if (this.parent == null) {
|
||||
return null;
|
||||
}
|
||||
return this.parent.findRepeatedAnnotations(annotation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (obj == this) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
RepeatableContainers other = (RepeatableContainers) obj;
|
||||
return Objects.equals(this.parent, other.parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return ObjectUtils.nullSafeHashCode(this.parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link RepeatableContainers} instance that searches using Java's
|
||||
* {@link Repeatable @Repeatable} annotation.
|
||||
* @return a {@link RepeatableContainers} instance
|
||||
*/
|
||||
public static RepeatableContainers standardRepeatables() {
|
||||
return StandardRepeatableContainers.INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link RepeatableContainers} instance that uses a defined
|
||||
* container and repeatable type.
|
||||
* @param repeatable the contained repeatable annotation
|
||||
* @param container the container annotation or {@code null}. If specified,
|
||||
* this annotation must declare a {@code value} attribute returning an array
|
||||
* of repeatable annotations. If not specified, the container will be
|
||||
* deduced by inspecting the {@code @Repeatable} annotation on
|
||||
* {@code repeatable}.
|
||||
* @return a {@link RepeatableContainers} instance
|
||||
*/
|
||||
public static RepeatableContainers of(Class<? extends Annotation> repeatable,
|
||||
@Nullable Class<? extends Annotation> container) {
|
||||
|
||||
return new ExplicitRepeatableContainer(null, repeatable, container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link RepeatableContainers} instance that does not expand any
|
||||
* repeatable annotations.
|
||||
* @return a {@link RepeatableContainers} instance
|
||||
*/
|
||||
public static RepeatableContainers none() {
|
||||
return NoRepeatableContainers.INSTANCE;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Standard {@link RepeatableContainers} implementation that searches using
|
||||
* Java's {@link Repeatable @Repeatable} annotation.
|
||||
*/
|
||||
private static class StandardRepeatableContainers extends RepeatableContainers {
|
||||
|
||||
private static final Map<Class<? extends Annotation>, Object> cache =
|
||||
new ConcurrentReferenceHashMap<>();
|
||||
|
||||
private static final Object NONE = new Object();
|
||||
|
||||
private static StandardRepeatableContainers INSTANCE =
|
||||
new StandardRepeatableContainers();
|
||||
|
||||
StandardRepeatableContainers() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
Annotation[] findRepeatedAnnotations(Annotation annotation) {
|
||||
Method method = getRepeatedAnnotationsMethod(annotation.annotationType());
|
||||
if (method != null) {
|
||||
return (Annotation[]) ReflectionUtils.invokeMethod(method, annotation);
|
||||
}
|
||||
return super.findRepeatedAnnotations(annotation);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Method getRepeatedAnnotationsMethod(
|
||||
Class<? extends Annotation> annotationType) {
|
||||
|
||||
Object result = cache.computeIfAbsent(annotationType,
|
||||
StandardRepeatableContainers::computeRepeatedAnnotationsMethod);
|
||||
return result != NONE ? (Method) result : null;
|
||||
}
|
||||
|
||||
private static Object computeRepeatedAnnotationsMethod(
|
||||
Class<? extends Annotation> annotationType) {
|
||||
|
||||
AttributeMethods methods = AttributeMethods.forAnnotationType(annotationType);
|
||||
if (methods.isOnlyValueAttribute()) {
|
||||
Method method = methods.get("value");
|
||||
if (method == null) {
|
||||
return NONE;
|
||||
}
|
||||
Class<?> returnType = method.getReturnType();
|
||||
if (returnType.isArray()) {
|
||||
Class<?> componentType = returnType.getComponentType();
|
||||
if (Annotation.class.isAssignableFrom(componentType)
|
||||
&& componentType.isAnnotationPresent(Repeatable.class)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NONE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A single explicit mapping.
|
||||
*/
|
||||
private static class ExplicitRepeatableContainer extends RepeatableContainers {
|
||||
|
||||
private final Class<? extends Annotation> repeatable;
|
||||
|
||||
private final Class<? extends Annotation> container;
|
||||
|
||||
private final Method valueMethod;
|
||||
|
||||
|
||||
ExplicitRepeatableContainer(@Nullable RepeatableContainers parent,
|
||||
Class<? extends Annotation> repeatable,
|
||||
@Nullable Class<? extends Annotation> container) {
|
||||
|
||||
super(parent);
|
||||
Assert.notNull(repeatable, "Repeatable must not be null");
|
||||
if (container == null) {
|
||||
container = deduceContainer(repeatable);
|
||||
}
|
||||
Method valueMethod = AttributeMethods.forAnnotationType(container).get("value");
|
||||
try {
|
||||
if (valueMethod == null) {
|
||||
throw new NoSuchMethodException("No value method found");
|
||||
}
|
||||
Class<?> returnType = valueMethod.getReturnType();
|
||||
if (!returnType.isArray() || returnType.getComponentType() != repeatable) {
|
||||
throw new AnnotationConfigurationException("Container type [" +
|
||||
container.getName() +
|
||||
"] must declare a 'value' attribute for an array of type [" +
|
||||
repeatable.getName() + "]");
|
||||
}
|
||||
}
|
||||
catch (AnnotationConfigurationException ex) {
|
||||
throw ex;
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
throw new AnnotationConfigurationException(
|
||||
"Invalid declaration of container type [" + container.getName() +
|
||||
"] for repeatable annotation [" + repeatable.getName() + "]",
|
||||
ex);
|
||||
}
|
||||
this.repeatable = repeatable;
|
||||
this.container = container;
|
||||
this.valueMethod = valueMethod;
|
||||
}
|
||||
|
||||
private Class<? extends Annotation> deduceContainer(
|
||||
Class<? extends Annotation> repeatable) {
|
||||
|
||||
Repeatable annotation = repeatable.getAnnotation(Repeatable.class);
|
||||
Assert.notNull(annotation, "Annotation type must be a repeatable annotation: " +
|
||||
"failed to resolve container type for " + repeatable.getName());
|
||||
return annotation.value();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
Annotation[] findRepeatedAnnotations(Annotation annotation) {
|
||||
if (this.container.isAssignableFrom(annotation.annotationType())) {
|
||||
return (Annotation[]) ReflectionUtils.invokeMethod(this.valueMethod, annotation);
|
||||
}
|
||||
return super.findRepeatedAnnotations(annotation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (super.equals(obj)) {
|
||||
ExplicitRepeatableContainer other = (ExplicitRepeatableContainer) obj;
|
||||
return this.container.equals(other.container) &&
|
||||
this.repeatable.equals(other.repeatable);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hashCode = super.hashCode();
|
||||
hashCode = 31 * hashCode + this.container.hashCode();
|
||||
hashCode = 31 * hashCode + this.repeatable.hashCode();
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* No repeatable containers.
|
||||
*/
|
||||
private static class NoRepeatableContainers extends RepeatableContainers {
|
||||
|
||||
private static NoRepeatableContainers INSTANCE = new NoRepeatableContainers();
|
||||
|
||||
NoRepeatableContainers() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Arrays;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* {@link InvocationHandler} for an {@link Annotation} that Spring has
|
||||
* <em>synthesized</em> (i.e., wrapped in a dynamic proxy) with additional
|
||||
* functionality.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
* @param <A> the annotation type
|
||||
* @see Annotation
|
||||
* @see AnnotationAttributeExtractor
|
||||
* @see AnnotationUtils#synthesizeAnnotation(Annotation, AnnotatedElement)
|
||||
*/
|
||||
class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation>
|
||||
implements InvocationHandler {
|
||||
|
||||
private final MergedAnnotation<?> annotation;
|
||||
|
||||
private final Class<A> type;
|
||||
|
||||
private final AttributeMethods attributes;
|
||||
|
||||
@Nullable
|
||||
private volatile Integer hashCode;
|
||||
|
||||
|
||||
private SynthesizedMergedAnnotationInvocationHandler(MergedAnnotation<A> annotation,
|
||||
Class<A> type) {
|
||||
|
||||
Assert.notNull(annotation, "Annotation must not be null");
|
||||
Assert.notNull(type, "Type must not be null");
|
||||
Assert.isTrue(type.isAnnotation(), "Type must be an annotation");
|
||||
this.annotation = annotation;
|
||||
this.type = type;
|
||||
this.attributes = AttributeMethods.forAnnotationType(type);
|
||||
for (int i = 0; i < this.attributes.size(); i++) {
|
||||
getAttributeValue(this.attributes.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) {
|
||||
if (ReflectionUtils.isEqualsMethod(method)) {
|
||||
return annotationEquals(args[0]);
|
||||
}
|
||||
if (ReflectionUtils.isHashCodeMethod(method)) {
|
||||
return annotationHashCode();
|
||||
}
|
||||
if (ReflectionUtils.isToStringMethod(method)) {
|
||||
return this.annotation.toString();
|
||||
}
|
||||
if (isAnnotationTypeMethod(method)) {
|
||||
return this.type;
|
||||
}
|
||||
if (this.attributes.indexOf(method.getName()) != -1) {
|
||||
return getAttributeValue(method);
|
||||
}
|
||||
throw new AnnotationConfigurationException(String.format(
|
||||
"Method [%s] is unsupported for synthesized annotation type [%s]", method,
|
||||
this.type));
|
||||
}
|
||||
|
||||
private boolean isAnnotationTypeMethod(Method method) {
|
||||
return Objects.equals(method.getName(), "annotationType")
|
||||
&& method.getParameterCount() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link Annotation#equals(Object)} for a definition of the required
|
||||
* algorithm.
|
||||
* @param other the other object to compare against
|
||||
*/
|
||||
private boolean annotationEquals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!this.type.isInstance(other)) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < this.attributes.size(); i++) {
|
||||
Method attribute = this.attributes.get(i);
|
||||
Object thisValue = getAttributeValue(attribute);
|
||||
Object otherValue = ReflectionUtils.invokeMethod(attribute, other);
|
||||
if (!ObjectUtils.nullSafeEquals(thisValue, otherValue)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link Annotation#hashCode()} for a definition of the required
|
||||
* algorithm.
|
||||
*/
|
||||
private int annotationHashCode() {
|
||||
Integer hashCode = this.hashCode;
|
||||
if (hashCode == null) {
|
||||
hashCode = computeHashCode();
|
||||
this.hashCode = hashCode;
|
||||
}
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
private Integer computeHashCode() {
|
||||
int hashCode = 0;
|
||||
for (int i = 0; i < this.attributes.size(); i++) {
|
||||
Method attribute = this.attributes.get(i);
|
||||
Object value = getAttributeValue(attribute);
|
||||
hashCode += (127 * attribute.getName().hashCode()) ^ getValueHashCode(value);
|
||||
}
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
private int getValueHashCode(Object value) {
|
||||
// Use Arrays.hashCode since ObjectUtils doesn't comply to to
|
||||
// Annotation#hashCode()
|
||||
if (value instanceof boolean[]) {
|
||||
return Arrays.hashCode((boolean[]) value);
|
||||
}
|
||||
if (value instanceof byte[]) {
|
||||
return Arrays.hashCode((byte[]) value);
|
||||
}
|
||||
if (value instanceof char[]) {
|
||||
return Arrays.hashCode((char[]) value);
|
||||
}
|
||||
if (value instanceof double[]) {
|
||||
return Arrays.hashCode((double[]) value);
|
||||
}
|
||||
if (value instanceof float[]) {
|
||||
return Arrays.hashCode((float[]) value);
|
||||
}
|
||||
if (value instanceof int[]) {
|
||||
return Arrays.hashCode((int[]) value);
|
||||
}
|
||||
if (value instanceof long[]) {
|
||||
return Arrays.hashCode((long[]) value);
|
||||
}
|
||||
if (value instanceof short[]) {
|
||||
return Arrays.hashCode((short[]) value);
|
||||
}
|
||||
if (value instanceof Object[]) {
|
||||
return Arrays.hashCode((Object[]) value);
|
||||
}
|
||||
return value.hashCode();
|
||||
}
|
||||
|
||||
private Object getAttributeValue(Method method) {
|
||||
String name = method.getName();
|
||||
Class<?> type = ClassUtils.resolvePrimitiveIfNecessary(method.getReturnType());
|
||||
return this.annotation.getValue(name, type).orElseThrow(
|
||||
() -> new NoSuchElementException("No value found for attribute named '"
|
||||
+ name + "' in merged annotation " + this.annotation.getType()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <A extends Annotation> A createProxy(MergedAnnotation<A> annotation,
|
||||
Class<A> type) {
|
||||
ClassLoader classLoader = type.getClassLoader();
|
||||
InvocationHandler handler = new SynthesizedMergedAnnotationInvocationHandler<>(
|
||||
annotation, type);
|
||||
Class<?>[] interfaces = isVisible(classLoader, SynthesizedAnnotation.class)
|
||||
? new Class<?>[] { type, SynthesizedAnnotation.class }
|
||||
: new Class<?>[] { type };
|
||||
return (A) Proxy.newProxyInstance(classLoader, interfaces, handler);
|
||||
}
|
||||
|
||||
private static boolean isVisible(ClassLoader classLoader, Class<?> interfaceClass) {
|
||||
try {
|
||||
return Class.forName(interfaceClass.getName(), false,
|
||||
classLoader) == interfaceClass;
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,614 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* {@link MergedAnnotation} that adapts attributes from a root annotation by
|
||||
* applying the mapping and mirroring rules of an {@link AnnotationTypeMapping}.
|
||||
*
|
||||
* <p>Root attribute values are extracted from a source object using a supplied
|
||||
* {@code BiFunction}. This allows various different annotation models to be
|
||||
* supported by the same class. For example, the attributes source might be an
|
||||
* actual {@link Annotation} instance where methods on the annotation instance
|
||||
* are {@link ReflectionUtils#invokeMethod(Method, Object) invoked} to extract
|
||||
* values. Equally, the source could be a simple {@link Map} with values
|
||||
* extracted using {@link Map#get(Object)}.
|
||||
*
|
||||
* <p>Extracted root attribute values must be compatible with the attribute
|
||||
* return type, namely:
|
||||
*
|
||||
* <p><table border="1">
|
||||
* <tr><th >Return Type</th><th >Extracted Type</th></tr>
|
||||
* <tr><td>Class</td><td>Class or String</td></tr>
|
||||
* <tr><td>Class[]</td><td>Class[] or String[]</td></tr>
|
||||
* <tr><td>Annotation</td><td>Annotation, Map or Object compatible with the value
|
||||
* extractor</td></tr>
|
||||
* <tr><td>Annotation[]</td><td>Annotation[], Map[] or Object[] where elements are
|
||||
* compatible with the value extractor</td></tr>
|
||||
* <tr><td>Other types</td><td>An exact match or the appropriate primitive wrapper</td></tr>
|
||||
* </table>
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
* @param <A> the annotation type
|
||||
* @see TypeMappedAnnotations
|
||||
*/
|
||||
final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnnotation<A> {
|
||||
|
||||
private final AnnotationTypeMapping mapping;
|
||||
|
||||
@Nullable
|
||||
private final Object source;
|
||||
|
||||
@Nullable
|
||||
private final Object rootAttributes;
|
||||
|
||||
private final BiFunction<Method, Object, Object> valueExtractor;
|
||||
|
||||
private final int aggregateIndex;
|
||||
|
||||
private final boolean useMergedValues;
|
||||
|
||||
@Nullable
|
||||
private final Predicate<String> attributeFilter;
|
||||
|
||||
private final int[] resolvedRootMirrors;
|
||||
|
||||
private final int[] resolvedMirrors;
|
||||
|
||||
@Nullable
|
||||
private String string;
|
||||
|
||||
|
||||
private TypeMappedAnnotation(AnnotationTypeMapping mapping,
|
||||
@Nullable Object source, @Nullable Object rootAttributes,
|
||||
BiFunction<Method, Object, Object> valueExtractor, int aggregateIndex) {
|
||||
|
||||
this(mapping, source, rootAttributes, valueExtractor, aggregateIndex, null);
|
||||
}
|
||||
|
||||
private TypeMappedAnnotation(AnnotationTypeMapping mapping,
|
||||
@Nullable Object source, @Nullable Object rootAttributes,
|
||||
BiFunction<Method, Object, Object> valueExtractor, int aggregateIndex,
|
||||
@Nullable int[] resolvedRootMirrors) {
|
||||
|
||||
this.source = source;
|
||||
this.rootAttributes = rootAttributes;
|
||||
this.valueExtractor = valueExtractor;
|
||||
this.mapping = mapping;
|
||||
this.aggregateIndex = aggregateIndex;
|
||||
this.useMergedValues = true;
|
||||
this.attributeFilter = null;
|
||||
this.resolvedRootMirrors = resolvedRootMirrors != null ? resolvedRootMirrors
|
||||
: mapping.getRoot().getMirrorSets().resolve(source, rootAttributes,
|
||||
this.valueExtractor);
|
||||
this.resolvedMirrors = getDepth() == 0 ? this.resolvedRootMirrors
|
||||
: mapping.getMirrorSets().resolve(source, this,
|
||||
this::getValueForMirrorResolution);
|
||||
}
|
||||
|
||||
private TypeMappedAnnotation(AnnotationTypeMapping mapping,
|
||||
@Nullable Object source, @Nullable Object rootAnnotation,
|
||||
BiFunction<Method, Object, Object> valueExtractor, int aggregateIndex,
|
||||
boolean useMergedValues, @Nullable Predicate<String> attributeFilter,
|
||||
int[] resolvedRootMirrors, int[] resolvedMirrors) {
|
||||
|
||||
this.source = source;
|
||||
this.rootAttributes = rootAnnotation;
|
||||
this.valueExtractor = valueExtractor;
|
||||
this.mapping = mapping;
|
||||
this.aggregateIndex = aggregateIndex;
|
||||
this.useMergedValues = useMergedValues;
|
||||
this.attributeFilter = attributeFilter;
|
||||
this.resolvedRootMirrors = resolvedRootMirrors;
|
||||
this.resolvedMirrors = resolvedMirrors;
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
private Object getValueForMirrorResolution(Method attribute, Object annotation) {
|
||||
int attributeIndex = this.mapping.getAttributes().indexOf(attribute);
|
||||
boolean valueAttribute = VALUE.equals(attribute.getName());
|
||||
return getValue(attributeIndex, !valueAttribute, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return getAnnotationType().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPresent() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDepth() {
|
||||
return this.mapping.getDepth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAggregateIndex() {
|
||||
return this.aggregateIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object getSource() {
|
||||
return this.source;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public MergedAnnotation<?> getParent() {
|
||||
AnnotationTypeMapping parentMapping = this.mapping.getParent();
|
||||
if (parentMapping == null) {
|
||||
return null;
|
||||
}
|
||||
return new TypeMappedAnnotation<>(parentMapping, this.source, this.rootAttributes,
|
||||
this.valueExtractor, this.aggregateIndex, this.resolvedRootMirrors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasDefaultValue(String attributeName) {
|
||||
int attributeIndex = getAttributeIndex(attributeName, true);
|
||||
Object value = getValue(attributeIndex, true, true);
|
||||
return value == null || this.mapping.isEquivalentToDefaultValue(attributeIndex, value,
|
||||
this.valueExtractor);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Annotation> MergedAnnotation<T> getAnnotation(String attributeName,
|
||||
Class<T> type) throws NoSuchElementException {
|
||||
|
||||
Assert.notNull(attributeName, "AttributeName must not be null");
|
||||
Assert.notNull(type, "Type must not be null");
|
||||
int attributeIndex = getAttributeIndex(attributeName, true);
|
||||
Method attribute = this.mapping.getAttributes().get(attributeIndex);
|
||||
Assert.isAssignable(type, attribute.getReturnType(),
|
||||
"Attribute " + attributeName + " type mismatch:");
|
||||
return (MergedAnnotation<T>) getRequiredValue(attributeIndex, Object.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Annotation> MergedAnnotation<T>[] getAnnotationArray(
|
||||
String attributeName, Class<T> type) throws NoSuchElementException {
|
||||
|
||||
Assert.notNull(attributeName, "AttributeName must not be null");
|
||||
Assert.notNull(type, "Type must not be null");
|
||||
int attributeIndex = getAttributeIndex(attributeName, true);
|
||||
Method attribute = this.mapping.getAttributes().get(attributeIndex);
|
||||
Class<?> componentType = attribute.getReturnType().getComponentType();
|
||||
Assert.notNull(componentType, () -> "Attribute " + attributeName + " is not an array");
|
||||
Assert.isAssignable(type, componentType, "Attribute " + attributeName + " component type mismatch:");
|
||||
return (MergedAnnotation<T>[]) getRequiredValue(attributeIndex, Object.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> getDefaultValue(String attributeName, Class<T> type) {
|
||||
int attributeIndex = getAttributeIndex(attributeName, false);
|
||||
if (attributeIndex == -1) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Method attribute = this.mapping.getAttributes().get(attributeIndex);
|
||||
return Optional.ofNullable(adapt(attribute, attribute.getDefaultValue(), type));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MergedAnnotation<A> filterAttributes(Predicate<String> predicate) {
|
||||
Assert.notNull(predicate, "Predicate must not be null");
|
||||
if (this.attributeFilter != null) {
|
||||
predicate = this.attributeFilter.and(predicate);
|
||||
}
|
||||
return new TypeMappedAnnotation<>(this.mapping, this.source, this.rootAttributes,
|
||||
this.valueExtractor, this.aggregateIndex, this.useMergedValues, predicate,
|
||||
this.resolvedRootMirrors, this.resolvedMirrors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MergedAnnotation<A> withNonMergedAttributes() {
|
||||
return new TypeMappedAnnotation<>(this.mapping, this.source, this.rootAttributes,
|
||||
this.valueExtractor, this.aggregateIndex, false, this.attributeFilter,
|
||||
this.resolvedRootMirrors, this.resolvedMirrors);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Map<String, Object>> T asMap(
|
||||
@Nullable Function<MergedAnnotation<?>, T> factory, MapValues... options) {
|
||||
|
||||
T map = factory != null ? factory.apply(this) : (T) new LinkedHashMap<String, Object>();
|
||||
Assert.state(map != null,
|
||||
"Factory used to create MergedAnnotation Map must not return null;");
|
||||
AttributeMethods attributes = this.mapping.getAttributes();
|
||||
for (int i = 0; i < attributes.size(); i++) {
|
||||
Method attribute = attributes.get(i);
|
||||
Object value = isFiltered(attribute.getName()) ?
|
||||
null :
|
||||
getValue(i, getTypeForMapOptions(attribute, options));
|
||||
if (value != null) {
|
||||
map.put(attribute.getName(),
|
||||
adaptValueForMapOptions(attribute, value, factory, options));
|
||||
}
|
||||
}
|
||||
return (factory != null) ? map : (T) Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
private Class<?> getTypeForMapOptions(Method attribute, MapValues[] options) {
|
||||
Class<?> attributeType = attribute.getReturnType();
|
||||
Class<?> componentType = attributeType.isArray() ?
|
||||
attributeType.getComponentType() :
|
||||
attributeType;
|
||||
if (MapValues.CLASS_TO_STRING.isIn(options) && componentType == Class.class) {
|
||||
return attributeType.isArray() ? String[].class : String.class;
|
||||
}
|
||||
return Object.class;
|
||||
}
|
||||
|
||||
private <T extends Map<String, Object>> Object adaptValueForMapOptions(
|
||||
Method attribute, Object value,
|
||||
@Nullable Function<MergedAnnotation<?>, T> factory, MapValues[] options) {
|
||||
|
||||
if (value instanceof MergedAnnotation) {
|
||||
MergedAnnotation<?> annotation = (MergedAnnotation<?>) value;
|
||||
return MapValues.ANNOTATION_TO_MAP.isIn(options) ?
|
||||
annotation.asMap(factory, options) :
|
||||
annotation.synthesize();
|
||||
}
|
||||
if (value instanceof MergedAnnotation[]) {
|
||||
MergedAnnotation<?>[] annotations = (MergedAnnotation<?>[]) value;
|
||||
if (MapValues.ANNOTATION_TO_MAP.isIn(options)) {
|
||||
Class<?> componentType = Map.class;
|
||||
if (factory != null) {
|
||||
componentType = factory.apply(this).getClass();
|
||||
}
|
||||
Object result = Array.newInstance(componentType, annotations.length);
|
||||
for (int i = 0; i < annotations.length; i++) {
|
||||
Array.set(result, i, annotations[i].asMap(factory, options));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Object result = Array.newInstance(
|
||||
attribute.getReturnType().getComponentType(), annotations.length);
|
||||
for (int i = 0; i < annotations.length; i++) {
|
||||
Array.set(result, i, annotations[i].synthesize());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected A createSynthesized() {
|
||||
return SynthesizedMergedAnnotationInvocationHandler.createProxy(this, getAnnotationType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String string = this.string;
|
||||
if (string == null) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("@");
|
||||
builder.append(getType());
|
||||
builder.append("(");
|
||||
for (int i = 0; i < this.mapping.getAttributes().size(); i++) {
|
||||
Method attribute = this.mapping.getAttributes().get(i);
|
||||
builder.append(i == 0 ? "" : ", ");
|
||||
builder.append(attribute.getName());
|
||||
builder.append("=");
|
||||
builder.append(toString(getValue(i, Object.class)));
|
||||
}
|
||||
builder.append(")");
|
||||
string = builder.toString();
|
||||
this.string = string;
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
private Object toString(@Nullable Object value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
if (value instanceof Class) {
|
||||
return ((Class<?>) value).getName();
|
||||
}
|
||||
if (value.getClass().isArray()) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("[");
|
||||
for (int i = 0; i < Array.getLength(value); i++) {
|
||||
builder.append(i == 0 ? "" : ", ");
|
||||
builder.append(toString(Array.get(value, i)));
|
||||
}
|
||||
builder.append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
return String.valueOf(value);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected <T> T getAttributeValue(String attributeName, Class<T> type) {
|
||||
int attributeIndex = getAttributeIndex(attributeName, false);
|
||||
return attributeIndex != -1 ? getValue(attributeIndex, type) : null;
|
||||
}
|
||||
|
||||
protected final <T> T getRequiredValue(int attributeIndex, Class<T> type) {
|
||||
T value = getValue(attributeIndex, type);
|
||||
if (value == null) {
|
||||
throw new NoSuchElementException(
|
||||
"No element at attribute index " + attributeIndex);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private <T> T getValue(int attributeIndex, Class<T> type) {
|
||||
Method attribute = this.mapping.getAttributes().get(attributeIndex);
|
||||
Object value = getValue(attributeIndex, true, true);
|
||||
if (value == null) {
|
||||
value = attribute.getDefaultValue();
|
||||
}
|
||||
return adapt(attribute, value, type);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object getValue(int attributeIndex, boolean useConventionMapping,
|
||||
boolean resolveMirrors) {
|
||||
AnnotationTypeMapping mapping = this.mapping;
|
||||
if (this.useMergedValues) {
|
||||
int mappedIndex = this.mapping.getAliasMapping(attributeIndex);
|
||||
if (mappedIndex == -1 && useConventionMapping) {
|
||||
mappedIndex = this.mapping.getConventionMapping(attributeIndex);
|
||||
}
|
||||
if (mappedIndex != -1) {
|
||||
mapping = mapping.getRoot();
|
||||
attributeIndex = mappedIndex;
|
||||
}
|
||||
}
|
||||
if (resolveMirrors) {
|
||||
attributeIndex = (mapping.getDepth() != 0 ?
|
||||
this.resolvedMirrors :
|
||||
this.resolvedRootMirrors)[attributeIndex];
|
||||
}
|
||||
if (attributeIndex == -1) {
|
||||
return null;
|
||||
}
|
||||
Method attribute = mapping.getAttributes().get(attributeIndex);
|
||||
if (mapping.getDepth() == 0) {
|
||||
return this.valueExtractor.apply(attribute, this.rootAttributes);
|
||||
}
|
||||
return getValueFromMetaAnnotation(attribute);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object getValueFromMetaAnnotation(Method attribute) {
|
||||
AnnotationTypeMapping mapping = this.mapping;
|
||||
if (this.useMergedValues && !VALUE.equals(attribute.getName())) {
|
||||
AnnotationTypeMapping candidate = mapping;
|
||||
while (candidate != null && candidate.getDepth() > 0) {
|
||||
int attributeIndex = candidate.getAttributes().indexOf(attribute.getName());
|
||||
if (attributeIndex != -1) {
|
||||
Method candidateAttribute = candidate.getAttributes().get(attributeIndex);
|
||||
if (candidateAttribute.getReturnType().equals(attribute.getReturnType())) {
|
||||
mapping = candidate;
|
||||
attribute = candidateAttribute;
|
||||
}
|
||||
}
|
||||
candidate = candidate.getParent();
|
||||
}
|
||||
}
|
||||
return ReflectionUtils.invokeMethod(attribute, mapping.getAnnotation());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
private <T> T adapt(Method attribute, @Nullable Object value, Class<T> type) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
value = adaptForAttribute(attribute, value);
|
||||
if (type == Object.class) {
|
||||
type = (Class<T>) getDefaultAdaptType(attribute);
|
||||
}
|
||||
else if (value instanceof Class && type == String.class) {
|
||||
value = ((Class<?>) value).getName();
|
||||
}
|
||||
else if (value instanceof Class[] && type == String[].class) {
|
||||
Class<?>[] classes = (Class[]) value;
|
||||
String[] names = new String[classes.length];
|
||||
for (int i = 0; i < classes.length; i++) {
|
||||
names[i] = classes[i].getName();
|
||||
}
|
||||
value = names;
|
||||
}
|
||||
else if (value instanceof MergedAnnotation && type.isAnnotation()) {
|
||||
MergedAnnotation<?> annotation = (MergedAnnotation<?>) value;
|
||||
value = annotation.synthesize();
|
||||
}
|
||||
else if (value instanceof MergedAnnotation[] && type.isArray()
|
||||
&& type.getComponentType().isAnnotation()) {
|
||||
MergedAnnotation<?>[] annotations = (MergedAnnotation<?>[]) value;
|
||||
Object array = Array.newInstance(type.getComponentType(), annotations.length);
|
||||
for (int i = 0; i < annotations.length; i++) {
|
||||
Array.set(array, i, annotations[i].synthesize());
|
||||
}
|
||||
value = array;
|
||||
}
|
||||
if (!type.isInstance(value)) {
|
||||
throw new IllegalArgumentException("Unable to adapt value of type " +
|
||||
value.getClass().getName() + " to " + type.getName());
|
||||
}
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Object adaptForAttribute(Method attribute, Object value) {
|
||||
Class<?> attributeType = ClassUtils.resolvePrimitiveIfNecessary(attribute.getReturnType());
|
||||
if (attributeType.isArray() && !value.getClass().isArray()) {
|
||||
Object array = Array.newInstance(value.getClass(), 1);
|
||||
Array.set(array, 0, value);
|
||||
return adaptForAttribute(attribute, array);
|
||||
}
|
||||
if (attributeType.isAnnotation()) {
|
||||
return adaptToMergedAnnotation(value,(Class<? extends Annotation>) attributeType);
|
||||
}
|
||||
if (attributeType.isArray() &&
|
||||
attributeType.getComponentType().isAnnotation() &&
|
||||
value.getClass().isArray()) {
|
||||
MergedAnnotation<?>[] result = new MergedAnnotation<?>[Array.getLength(value)];
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = adaptToMergedAnnotation(Array.get(value, i),
|
||||
(Class<? extends Annotation>) attributeType.getComponentType());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if ((attributeType == Class.class && value instanceof String) ||
|
||||
(attributeType == Class[].class && value instanceof String[])) {
|
||||
return value;
|
||||
}
|
||||
if (!attributeType.isInstance(value)) {
|
||||
throw new IllegalStateException("Attribute '" + attribute.getName() +
|
||||
"' in annotation " + getType() + " should be compatible with " +
|
||||
attributeType.getName() + " but a " + value.getClass().getName() +
|
||||
" value was returned");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private MergedAnnotation<?> adaptToMergedAnnotation(Object value,
|
||||
Class<? extends Annotation> annotationType) {
|
||||
|
||||
AnnotationFilter filter = AnnotationFilter.mostAppropriateFor(annotationType);
|
||||
AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(
|
||||
annotationType, filter).get(0);
|
||||
return new TypeMappedAnnotation<>(mapping, this.source, value,
|
||||
this.getValueExtractor(value), this.aggregateIndex);
|
||||
}
|
||||
|
||||
private BiFunction<Method, Object, Object> getValueExtractor(Object value) {
|
||||
if (value instanceof Annotation) {
|
||||
return ReflectionUtils::invokeMethod;
|
||||
}
|
||||
if (value instanceof Map) {
|
||||
return TypeMappedAnnotation::extractFromMap;
|
||||
}
|
||||
return this.valueExtractor;
|
||||
}
|
||||
|
||||
private Class<?> getDefaultAdaptType(Method attribute) {
|
||||
Class<?> attributeType = attribute.getReturnType();
|
||||
if (attributeType.isAnnotation()) {
|
||||
return MergedAnnotation.class;
|
||||
}
|
||||
if (attributeType.isArray() && attributeType.getComponentType().isAnnotation()) {
|
||||
return MergedAnnotation[].class;
|
||||
}
|
||||
return ClassUtils.resolvePrimitiveIfNecessary(attributeType);
|
||||
}
|
||||
|
||||
private int getAttributeIndex(String attributeName, boolean required) {
|
||||
Assert.hasText(attributeName, "AttributeName must not be null");
|
||||
int attributeIndex = isFiltered(attributeName) ?
|
||||
-1 :
|
||||
this.mapping.getAttributes().indexOf(attributeName);
|
||||
if (attributeIndex == -1 && required) {
|
||||
throw new NoSuchElementException("No attribute named '" + attributeName +
|
||||
"' present in merged annotation " + getType());
|
||||
}
|
||||
return attributeIndex;
|
||||
}
|
||||
|
||||
private boolean isFiltered(String attributeName) {
|
||||
if (this.attributeFilter != null) {
|
||||
return !this.attributeFilter.test(attributeName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Class<A> getAnnotationType() {
|
||||
return (Class<A>) this.mapping.getAnnotationType();
|
||||
}
|
||||
|
||||
static <A extends Annotation> MergedAnnotation<A> from(@Nullable Object source,
|
||||
A annotation) {
|
||||
|
||||
Assert.notNull(annotation, "Annotation must not be null");
|
||||
AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(
|
||||
annotation.annotationType());
|
||||
return new TypeMappedAnnotation<>(mappings.get(0), source, annotation,
|
||||
ReflectionUtils::invokeMethod, 0);
|
||||
}
|
||||
|
||||
static <A extends Annotation> MergedAnnotation<A> from(@Nullable Object source,
|
||||
Class<A> annotationType, @Nullable Map<String, ?> attributes) {
|
||||
|
||||
Assert.notNull(annotationType, "AnnotationType must not be null");
|
||||
AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(annotationType);
|
||||
return new TypeMappedAnnotation<>(mappings.get(0), source, attributes,
|
||||
TypeMappedAnnotation::extractFromMap, 0);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static <A extends Annotation> TypeMappedAnnotation<A> createIfPossible(
|
||||
AnnotationTypeMapping mapping, @Nullable Object source, Annotation annotation,
|
||||
int aggregateIndex, IntrospectionFailureLogger logger) {
|
||||
|
||||
try {
|
||||
return new TypeMappedAnnotation<>(mapping, source, annotation,
|
||||
ReflectionUtils::invokeMethod, aggregateIndex);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
if (ex instanceof AnnotationConfigurationException) {
|
||||
throw (AnnotationConfigurationException) ex;
|
||||
}
|
||||
if (logger.isEnabled()) {
|
||||
String type = mapping.getAnnotationType().getName();
|
||||
String item = mapping.getDepth() == 0 ?
|
||||
"annotation " + type :
|
||||
"meta-annotation " + type + " from " + mapping.getRoot().getAnnotationType().getName();
|
||||
logger.log("Failed to introspect " + item, source, ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
private static Object extractFromMap(Method attribute, @Nullable Object map) {
|
||||
return map != null ? ((Map<String, ?>) map).get(attribute.getName()) : null;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,697 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Spliterator;
|
||||
import java.util.Spliterators;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* {@link MergedAnnotations} implementation that searches for and adapts
|
||||
* annotations and meta-annotations using {@link AnnotationTypeMappings}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.1
|
||||
*/
|
||||
final class TypeMappedAnnotations implements MergedAnnotations {
|
||||
|
||||
private static final AnnotationFilter FILTER_ALL = annotationType -> true;
|
||||
|
||||
private static final MergedAnnotations NONE = new TypeMappedAnnotations(null,
|
||||
new Annotation[0], RepeatableContainers.none(), FILTER_ALL);
|
||||
|
||||
|
||||
@Nullable
|
||||
private final Object source;
|
||||
|
||||
@Nullable
|
||||
private final AnnotatedElement element;
|
||||
|
||||
@Nullable
|
||||
private final SearchStrategy searchStrategy;
|
||||
|
||||
@Nullable
|
||||
private final Annotation[] annotations;
|
||||
|
||||
private final RepeatableContainers repeatableContainers;
|
||||
|
||||
private final AnnotationFilter annotationFilter;
|
||||
|
||||
@Nullable
|
||||
private volatile List<Aggregate> aggregates;
|
||||
|
||||
|
||||
private TypeMappedAnnotations(AnnotatedElement element, SearchStrategy searchStrategy,
|
||||
RepeatableContainers repeatableContainers,
|
||||
AnnotationFilter annotationFilter) {
|
||||
|
||||
this.source = element;
|
||||
this.element = element;
|
||||
this.searchStrategy = searchStrategy;
|
||||
this.annotations = null;
|
||||
this.repeatableContainers = repeatableContainers;
|
||||
this.annotationFilter = annotationFilter;
|
||||
}
|
||||
|
||||
private TypeMappedAnnotations(@Nullable Object source, Annotation[] annotations,
|
||||
RepeatableContainers repeatableContainers,
|
||||
AnnotationFilter annotationFilter) {
|
||||
|
||||
this.source = source;
|
||||
this.element = null;
|
||||
this.searchStrategy = null;
|
||||
this.annotations = annotations;
|
||||
this.repeatableContainers = repeatableContainers;
|
||||
this.annotationFilter = annotationFilter;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public <A extends Annotation> boolean isPresent(@Nullable Class<A> annotationType) {
|
||||
if (annotationType == null || this.annotationFilter.matches(annotationType)) {
|
||||
return false;
|
||||
}
|
||||
return Boolean.TRUE.equals(scan(annotationType,
|
||||
IsPresent.get(this.repeatableContainers, this.annotationFilter, false)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPresent(@Nullable String annotationType) {
|
||||
if (annotationType == null || this.annotationFilter.matches(annotationType)) {
|
||||
return false;
|
||||
}
|
||||
return Boolean.TRUE.equals(scan(annotationType,
|
||||
IsPresent.get(this.repeatableContainers, this.annotationFilter, false)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends Annotation> boolean isDirectlyPresent(@Nullable Class<A> annotationType) {
|
||||
if (annotationType == null || this.annotationFilter.matches(annotationType)) {
|
||||
return false;
|
||||
}
|
||||
return Boolean.TRUE.equals(scan(annotationType,
|
||||
IsPresent.get(this.repeatableContainers, this.annotationFilter, true)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectlyPresent(@Nullable String annotationType) {
|
||||
if (annotationType == null || this.annotationFilter.matches(annotationType)) {
|
||||
return false;
|
||||
}
|
||||
return Boolean.TRUE.equals(scan(annotationType,
|
||||
IsPresent.get(this.repeatableContainers, this.annotationFilter, true)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends Annotation> MergedAnnotation<A> get(
|
||||
@Nullable Class<A> annotationType) {
|
||||
|
||||
return get(annotationType, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends Annotation> MergedAnnotation<A> get(
|
||||
@Nullable Class<A> annotationType,
|
||||
@Nullable Predicate<? super MergedAnnotation<A>> predicate) {
|
||||
|
||||
return get(annotationType, predicate, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends Annotation> MergedAnnotation<A> get(
|
||||
@Nullable Class<A> annotationType,
|
||||
@Nullable Predicate<? super MergedAnnotation<A>> predicate,
|
||||
@Nullable MergedAnnotationSelector<A> selector) {
|
||||
|
||||
if (annotationType == null || this.annotationFilter.matches(annotationType)) {
|
||||
return MergedAnnotation.missing();
|
||||
}
|
||||
MergedAnnotation<A> result = scan(annotationType,
|
||||
new MergedAnnotationFinder<>(annotationType, predicate, selector));
|
||||
return result != null ? result : MergedAnnotation.missing();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends Annotation> MergedAnnotation<A> get(
|
||||
@Nullable String annotationType) {
|
||||
return get(annotationType, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends Annotation> MergedAnnotation<A> get(@Nullable String annotationType,
|
||||
@Nullable Predicate<? super MergedAnnotation<A>> predicate) {
|
||||
return get(annotationType, predicate, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends Annotation> MergedAnnotation<A> get(@Nullable String annotationType,
|
||||
@Nullable Predicate<? super MergedAnnotation<A>> predicate,
|
||||
@Nullable MergedAnnotationSelector<A> selector) {
|
||||
|
||||
if (annotationType == null || this.annotationFilter.matches(annotationType)) {
|
||||
return MergedAnnotation.missing();
|
||||
}
|
||||
MergedAnnotation<A> result = scan(annotationType,
|
||||
new MergedAnnotationFinder<>(annotationType, predicate, selector));
|
||||
return result != null ? result : MergedAnnotation.missing();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends Annotation> Stream<MergedAnnotation<A>> stream(
|
||||
@Nullable Class<A> annotationType) {
|
||||
|
||||
if (this.annotationFilter == FILTER_ALL) {
|
||||
return Stream.empty();
|
||||
}
|
||||
return StreamSupport.stream(spliterator(annotationType), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends Annotation> Stream<MergedAnnotation<A>> stream(
|
||||
@Nullable String annotationType) {
|
||||
|
||||
if (this.annotationFilter == FILTER_ALL) {
|
||||
return Stream.empty();
|
||||
}
|
||||
return StreamSupport.stream(spliterator(annotationType), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<MergedAnnotation<Annotation>> stream() {
|
||||
if (this.annotationFilter == FILTER_ALL) {
|
||||
return Stream.empty();
|
||||
}
|
||||
return StreamSupport.stream(spliterator(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<MergedAnnotation<Annotation>> iterator() {
|
||||
if (this.annotationFilter == FILTER_ALL) {
|
||||
return Collections.emptyIterator();
|
||||
}
|
||||
return Spliterators.iterator(spliterator());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Spliterator<MergedAnnotation<Annotation>> spliterator() {
|
||||
if (this.annotationFilter == FILTER_ALL) {
|
||||
return Collections.<MergedAnnotation<Annotation>> emptyList().spliterator();
|
||||
}
|
||||
return spliterator(null);
|
||||
}
|
||||
|
||||
private <A extends Annotation> Spliterator<MergedAnnotation<A>> spliterator(
|
||||
@Nullable Object annotationType) {
|
||||
return new AggregatesSpliterator<>(annotationType, getAggregates());
|
||||
}
|
||||
|
||||
private List<Aggregate> getAggregates() {
|
||||
List<Aggregate> aggregates = this.aggregates;
|
||||
if (aggregates == null) {
|
||||
aggregates = scan(this, new AggregatesCollector());
|
||||
if (aggregates == null || aggregates.isEmpty()) {
|
||||
aggregates = Collections.emptyList();
|
||||
}
|
||||
this.aggregates = aggregates;
|
||||
}
|
||||
return aggregates;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private <C, R> R scan(C criteria, AnnotationsProcessor<C, R> processor) {
|
||||
if (this.annotations != null) {
|
||||
R result = processor.doWithAnnotations(criteria, 0, this.source, this.annotations);
|
||||
return processor.finish(result);
|
||||
}
|
||||
if (this.element != null && this.searchStrategy != null) {
|
||||
return AnnotationsScanner.scan(criteria, this.element, this.searchStrategy, processor);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static MergedAnnotations from(@Nullable AnnotatedElement element,
|
||||
SearchStrategy searchStrategy, RepeatableContainers repeatableContainers,
|
||||
AnnotationFilter annotationFilter) {
|
||||
|
||||
if (element == null || AnnotationsScanner.isKnownEmpty(element, searchStrategy, annotationFilter)) {
|
||||
return NONE;
|
||||
}
|
||||
return new TypeMappedAnnotations(element, searchStrategy, repeatableContainers,
|
||||
annotationFilter);
|
||||
}
|
||||
|
||||
static MergedAnnotations from(@Nullable Object source,
|
||||
Annotation[] annotations, RepeatableContainers repeatableContainers,
|
||||
AnnotationFilter annotationFilter) {
|
||||
|
||||
if (annotations.length == 0) {
|
||||
return NONE;
|
||||
}
|
||||
return new TypeMappedAnnotations(source, annotations, repeatableContainers, annotationFilter);
|
||||
}
|
||||
|
||||
private static boolean isMappingForType(@Nullable AnnotationTypeMapping mapping,
|
||||
AnnotationFilter annotationFilter, @Nullable Object requiredType) {
|
||||
|
||||
if (mapping == null) {
|
||||
return false;
|
||||
}
|
||||
Class<? extends Annotation> actualType = mapping.getAnnotationType();
|
||||
return !annotationFilter.matches(actualType) &&
|
||||
(requiredType == null || actualType == requiredType || actualType.getName().equals(requiredType));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link AnnotationsProcessor} used to detect if an annotation is directly
|
||||
* or meta-present.
|
||||
*/
|
||||
private static final class IsPresent
|
||||
implements AnnotationsProcessor<Object, Boolean> {
|
||||
|
||||
/**
|
||||
* Shared instances that save us needing to create a new processor for
|
||||
* the common combinations.
|
||||
*/
|
||||
private static final IsPresent[] SHARED;
|
||||
static {
|
||||
SHARED = new IsPresent[4];
|
||||
SHARED[0] = new IsPresent(RepeatableContainers.none(), AnnotationFilter.PLAIN, true);
|
||||
SHARED[1] = new IsPresent(RepeatableContainers.none(), AnnotationFilter.PLAIN, false);
|
||||
SHARED[2] = new IsPresent(RepeatableContainers.standardRepeatables(), AnnotationFilter.PLAIN, true);
|
||||
SHARED[3] = new IsPresent(RepeatableContainers.standardRepeatables(), AnnotationFilter.PLAIN, false);
|
||||
}
|
||||
|
||||
|
||||
private final RepeatableContainers repeatableContainers;
|
||||
|
||||
private final AnnotationFilter annotationFilter;
|
||||
|
||||
private final boolean directOnly;
|
||||
|
||||
|
||||
private IsPresent(RepeatableContainers repeatableContainers,
|
||||
AnnotationFilter annotationFilter, boolean directOnly) {
|
||||
|
||||
this.repeatableContainers = repeatableContainers;
|
||||
this.annotationFilter = annotationFilter;
|
||||
this.directOnly = directOnly;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Boolean doWithAnnotations(Object requiredType, int aggregateIndex,
|
||||
@Nullable Object source, Annotation[] annotations) {
|
||||
|
||||
for (Annotation annotation : annotations) {
|
||||
if (annotation != null) {
|
||||
Class<? extends Annotation> type = annotation.annotationType();
|
||||
if (type != null && !this.annotationFilter.matches(type)) {
|
||||
if (type == requiredType || type.getName().equals(requiredType)) {
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
Annotation[] repeatedAnnotations = this.repeatableContainers
|
||||
.findRepeatedAnnotations(annotation);
|
||||
if (repeatedAnnotations != null) {
|
||||
Boolean result = doWithAnnotations(requiredType, aggregateIndex,
|
||||
source, repeatedAnnotations);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (!this.directOnly) {
|
||||
AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(type);
|
||||
for (int i = 0; i < mappings.size(); i++) {
|
||||
AnnotationTypeMapping mapping = mappings.get(i);
|
||||
if (isMappingForType(mapping, this.annotationFilter, requiredType)) {
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static IsPresent get(RepeatableContainers repeatableContainers,
|
||||
AnnotationFilter annotationFilter, boolean directOnly) {
|
||||
|
||||
// Use a single shared instance for common combinations
|
||||
if (annotationFilter == AnnotationFilter.PLAIN) {
|
||||
if (repeatableContainers == RepeatableContainers.none()) {
|
||||
return SHARED[directOnly ? 0 : 1];
|
||||
}
|
||||
if (repeatableContainers == RepeatableContainers.standardRepeatables()) {
|
||||
return SHARED[directOnly ? 2 : 3];
|
||||
}
|
||||
}
|
||||
return new IsPresent(repeatableContainers, annotationFilter, directOnly);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link AnnotationsProcessor} that finds a single
|
||||
* {@link MergedAnnotation}.
|
||||
*/
|
||||
private class MergedAnnotationFinder<A extends Annotation>
|
||||
implements AnnotationsProcessor<Object, MergedAnnotation<A>> {
|
||||
|
||||
private final Object requiredType;
|
||||
|
||||
@Nullable
|
||||
private final Predicate<? super MergedAnnotation<A>> predicate;
|
||||
|
||||
private final MergedAnnotationSelector<A> selector;
|
||||
|
||||
@Nullable
|
||||
private MergedAnnotation<A> result;
|
||||
|
||||
|
||||
MergedAnnotationFinder(Object requiredType,
|
||||
@Nullable Predicate<? super MergedAnnotation<A>> predicate,
|
||||
@Nullable MergedAnnotationSelector<A> selector) {
|
||||
|
||||
this.requiredType = requiredType;
|
||||
this.predicate = predicate;
|
||||
this.selector = selector != null ? selector
|
||||
: MergedAnnotationSelectors.nearest();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public MergedAnnotation<A> doWithAggregate(Object context, int aggregateIndex) {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public MergedAnnotation<A> doWithAnnotations(Object type, int aggregateIndex,
|
||||
@Nullable Object source, Annotation[] annotations) {
|
||||
|
||||
for (Annotation annotation : annotations) {
|
||||
if (annotation != null &&
|
||||
!annotationFilter.matches(annotation)) {
|
||||
MergedAnnotation<A> result = process(type, aggregateIndex, source, annotation);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private MergedAnnotation<A> process(Object type, int aggregateIndex,
|
||||
@Nullable Object source, Annotation annotation) {
|
||||
Annotation[] repeatedAnnotations = repeatableContainers.findRepeatedAnnotations(
|
||||
annotation);
|
||||
if (repeatedAnnotations != null) {
|
||||
return doWithAnnotations(type, aggregateIndex, source,
|
||||
repeatedAnnotations);
|
||||
}
|
||||
AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(
|
||||
annotation.annotationType(),
|
||||
annotationFilter);
|
||||
for (int i = 0; i < mappings.size(); i++) {
|
||||
AnnotationTypeMapping mapping = mappings.get(i);
|
||||
if (isMappingForType(mapping, annotationFilter,
|
||||
this.requiredType)) {
|
||||
MergedAnnotation<A> candidate = TypeMappedAnnotation.createIfPossible(
|
||||
mapping, source, annotation, aggregateIndex,
|
||||
IntrospectionFailureLogger.INFO);
|
||||
if (candidate != null && (this.predicate == null
|
||||
|| this.predicate.test(candidate))) {
|
||||
if (this.selector.isBestCandidate(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
updateLastResult(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void updateLastResult(MergedAnnotation<A> candidate) {
|
||||
MergedAnnotation<A> lastResult = this.result;
|
||||
this.result = lastResult != null
|
||||
? this.selector.select(lastResult, candidate)
|
||||
: candidate;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public MergedAnnotation<A> finish(@Nullable MergedAnnotation<A> result) {
|
||||
return result != null ? result : this.result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link AnnotationsProcessor} that collects {@link Aggregate} instances.
|
||||
*/
|
||||
private class AggregatesCollector
|
||||
implements AnnotationsProcessor<Object, List<Aggregate>> {
|
||||
|
||||
private final List<Aggregate> aggregates = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public List<Aggregate> doWithAnnotations(Object criteria, int aggregateIndex,
|
||||
@Nullable Object source, Annotation[] annotations) {
|
||||
this.aggregates.add(createAggregate(aggregateIndex, source, annotations));
|
||||
return null;
|
||||
}
|
||||
|
||||
private Aggregate createAggregate(int aggregateIndex, @Nullable Object source,
|
||||
Annotation[] annotations) {
|
||||
List<Annotation> aggregateAnnotations = getAggregateAnnotations(annotations);
|
||||
return new Aggregate(aggregateIndex, source, aggregateAnnotations);
|
||||
}
|
||||
|
||||
private List<Annotation> getAggregateAnnotations(Annotation[] annotations) {
|
||||
List<Annotation> result = new ArrayList<>(annotations.length);
|
||||
addAggregateAnnotations(result, annotations);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void addAggregateAnnotations(List<Annotation> aggregateAnnotations,
|
||||
Annotation[] annotations) {
|
||||
for (Annotation annotation : annotations) {
|
||||
if (annotation != null
|
||||
&& !annotationFilter.matches(
|
||||
annotation)) {
|
||||
Annotation[] repeatedAnnotations = repeatableContainers.findRepeatedAnnotations(
|
||||
annotation);
|
||||
if (repeatedAnnotations != null) {
|
||||
addAggregateAnnotations(aggregateAnnotations,
|
||||
repeatedAnnotations);
|
||||
}
|
||||
else {
|
||||
aggregateAnnotations.add(annotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Aggregate> finish(@Nullable List<Aggregate> processResult) {
|
||||
return this.aggregates;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class Aggregate {
|
||||
|
||||
private final int aggregateIndex;
|
||||
|
||||
@Nullable
|
||||
private final Object source;
|
||||
|
||||
private final List<Annotation> annotations;
|
||||
|
||||
private final AnnotationTypeMappings[] mappings;
|
||||
|
||||
Aggregate(int aggregateIndex, @Nullable Object source,
|
||||
List<Annotation> annotations) {
|
||||
this.aggregateIndex = aggregateIndex;
|
||||
this.source = source;
|
||||
this.annotations = annotations;
|
||||
this.mappings = new AnnotationTypeMappings[annotations.size()];
|
||||
for (int i = 0; i < annotations.size(); i++) {
|
||||
this.mappings[i] = AnnotationTypeMappings.forAnnotationType(
|
||||
annotations.get(i).annotationType());
|
||||
}
|
||||
}
|
||||
|
||||
int size() {
|
||||
return this.annotations.size();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
AnnotationTypeMapping getMapping(int annotationIndex, int mappingIndex) {
|
||||
AnnotationTypeMappings mappings = getMappings(annotationIndex);
|
||||
return mappingIndex < mappings.size() ? mappings.get(mappingIndex) : null;
|
||||
}
|
||||
|
||||
AnnotationTypeMappings getMappings(int annotationIndex) {
|
||||
return this.mappings[annotationIndex];
|
||||
}
|
||||
|
||||
@Nullable
|
||||
<A extends Annotation> MergedAnnotation<A> createMergedAnnotationIfPossible(
|
||||
int annotationIndex, int mappingIndex,
|
||||
IntrospectionFailureLogger logger) {
|
||||
return TypeMappedAnnotation.createIfPossible(
|
||||
this.mappings[annotationIndex].get(mappingIndex), this.source,
|
||||
this.annotations.get(annotationIndex), this.aggregateIndex, logger);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Spliterator} used to consume merged annotations from the
|
||||
* aggregates in depth fist order.
|
||||
*/
|
||||
private class AggregatesSpliterator<A extends Annotation>
|
||||
implements Spliterator<MergedAnnotation<A>> {
|
||||
|
||||
@Nullable
|
||||
private final Object requiredType;
|
||||
|
||||
private final List<Aggregate> aggregates;
|
||||
|
||||
private int aggregateCursor;
|
||||
|
||||
@Nullable
|
||||
private int[] mappingCursors;
|
||||
|
||||
AggregatesSpliterator(@Nullable Object requiredType,
|
||||
List<Aggregate> aggregates) {
|
||||
this.requiredType = requiredType;
|
||||
this.aggregates = aggregates;
|
||||
this.aggregateCursor = 0;
|
||||
}
|
||||
|
||||
public boolean tryAdvance(Consumer<? super MergedAnnotation<A>> action) {
|
||||
while (this.aggregateCursor < this.aggregates.size()) {
|
||||
Aggregate aggregate = this.aggregates.get(this.aggregateCursor);
|
||||
if (tryAdvance(aggregate, action)) {
|
||||
return true;
|
||||
}
|
||||
this.aggregateCursor++;
|
||||
this.mappingCursors = null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean tryAdvance(Aggregate aggregate,
|
||||
Consumer<? super MergedAnnotation<A>> action) {
|
||||
if (this.mappingCursors == null) {
|
||||
this.mappingCursors = new int[aggregate.size()];
|
||||
}
|
||||
int lowestDepth = Integer.MAX_VALUE;
|
||||
int annotationResult = -1;
|
||||
for (int annotationIndex = 0; annotationIndex < aggregate.size(); annotationIndex++) {
|
||||
AnnotationTypeMapping mapping = getNextSuitableMapping(aggregate, annotationIndex);
|
||||
if (mapping != null && mapping.getDepth() < lowestDepth) {
|
||||
annotationResult = annotationIndex;
|
||||
lowestDepth = mapping.getDepth();
|
||||
}
|
||||
if (lowestDepth == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (annotationResult != -1) {
|
||||
MergedAnnotation<A> mergedAnnotation = aggregate.createMergedAnnotationIfPossible(
|
||||
annotationResult, this.mappingCursors[annotationResult],
|
||||
this.requiredType != null ? IntrospectionFailureLogger.INFO : IntrospectionFailureLogger.DEBUG);
|
||||
this.mappingCursors[annotationResult]++;
|
||||
if (mergedAnnotation == null) {
|
||||
return tryAdvance(aggregate, action);
|
||||
}
|
||||
action.accept(mergedAnnotation);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private AnnotationTypeMapping getNextSuitableMapping(Aggregate aggregate,
|
||||
int annotationIndex) {
|
||||
int[] cursors = this.mappingCursors;
|
||||
if (cursors != null) {
|
||||
AnnotationTypeMapping mapping;
|
||||
do {
|
||||
mapping = aggregate.getMapping(annotationIndex, cursors[annotationIndex]);
|
||||
if (isMappingForType(mapping, annotationFilter, this.requiredType)) {
|
||||
return mapping;
|
||||
}
|
||||
cursors[annotationIndex]++;
|
||||
}
|
||||
while (mapping != null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Spliterator<MergedAnnotation<A>> trySplit() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long estimateSize() {
|
||||
int size = 0;
|
||||
for (int aggregateIndex = this.aggregateCursor;
|
||||
aggregateIndex < this.aggregates.size(); aggregateIndex++) {
|
||||
Aggregate aggregate = this.aggregates.get(aggregateIndex);
|
||||
for (int annotationIndex = 0; annotationIndex < aggregate.size(); annotationIndex++) {
|
||||
AnnotationTypeMappings mappings = aggregate.getMappings(annotationIndex);
|
||||
int numberOfMappings = mappings.size();
|
||||
if (aggregateIndex == this.aggregateCursor && this.mappingCursors != null) {
|
||||
numberOfMappings -= Math.min(this.mappingCursors[annotationIndex], mappings.size());
|
||||
}
|
||||
size += numberOfMappings;
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int characteristics() {
|
||||
return NONNULL | IMMUTABLE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests to ensure back-compatibility with Spring Framework 5.1.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
*/
|
||||
public class AnnotationBackCompatibiltyTests {
|
||||
|
||||
@Test
|
||||
public void multiplRoutesToMetaAnnotation() {
|
||||
Class<WithMetaMetaTestAnnotation1AndMetaTestAnnotation2> source = WithMetaMetaTestAnnotation1AndMetaTestAnnotation2.class;
|
||||
// Merged annotation chooses lowest depth
|
||||
MergedAnnotation<TestAnnotation> mergedAnnotation = MergedAnnotations.from(source).get(TestAnnotation.class);
|
||||
assertThat(mergedAnnotation.getString("value")).isEqualTo("testAndMetaTest");
|
||||
// AnnotatedElementUtils finds first
|
||||
TestAnnotation previousVersion = AnnotatedElementUtils.getMergedAnnotation(source, TestAnnotation.class);
|
||||
assertThat(previousVersion.value()).isEqualTo("metaTest");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultValue() {
|
||||
DefaultValueAnnotation synthesized = MergedAnnotations.from(WithDefaultValue.class).get(DefaultValueAnnotation.class).synthesize();
|
||||
assertThat(synthesized).isInstanceOf(SynthesizedAnnotation.class);
|
||||
Object defaultValue = AnnotationUtils.getDefaultValue(synthesized, "enumValue");
|
||||
assertThat(defaultValue).isEqualTo(TestEnum.ONE);
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface TestAnnotation {
|
||||
|
||||
String value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@TestAnnotation("metaTest")
|
||||
@interface MetaTestAnnotation {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@TestAnnotation("testAndMetaTest")
|
||||
@MetaTestAnnotation
|
||||
@interface TestAndMetaTestAnnotation {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@MetaTestAnnotation
|
||||
@interface MetaMetaTestAnnotation {
|
||||
}
|
||||
|
||||
@MetaMetaTestAnnotation
|
||||
@TestAndMetaTestAnnotation
|
||||
static class WithMetaMetaTestAnnotation1AndMetaTestAnnotation2 {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface DefaultValueAnnotation {
|
||||
|
||||
@AliasFor("enumAlais")
|
||||
TestEnum enumValue() default TestEnum.ONE;
|
||||
|
||||
@AliasFor("enumValue")
|
||||
TestEnum enumAlais() default TestEnum.ONE;
|
||||
|
||||
}
|
||||
|
||||
@DefaultValueAnnotation
|
||||
static class WithDefaultValue {
|
||||
|
||||
}
|
||||
|
||||
static enum TestEnum {
|
||||
|
||||
ONE,
|
||||
|
||||
TWO
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link AnnotationFilter}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class AnnotationFilterTests {
|
||||
|
||||
private static final AnnotationFilter FILTER = annotationType -> ObjectUtils.nullSafeEquals(
|
||||
annotationType, TestAnnotation.class.getName());
|
||||
|
||||
@Test
|
||||
public void matchesAnnotationWhenAnnotationIsNullReturnsFalse() {
|
||||
TestAnnotation annotation = null;
|
||||
assertThat(FILTER.matches(annotation)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesAnnotationWhenMatchReturnsTrue() {
|
||||
TestAnnotation annotation = WithTestAnnotation.class.getDeclaredAnnotation(
|
||||
TestAnnotation.class);
|
||||
assertThat(FILTER.matches(annotation)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesAnnotationWhenNoMatchReturnsFalse() {
|
||||
OtherAnnotation annotation = WithOtherAnnotation.class.getDeclaredAnnotation(
|
||||
OtherAnnotation.class);
|
||||
assertThat(FILTER.matches(annotation)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesAnnotationClassWhenAnnotationClassIsNullReturnsFalse() {
|
||||
Class<Annotation> annotationType = null;
|
||||
assertThat(FILTER.matches(annotationType)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesAnnotationClassWhenMatchReturnsTrue() {
|
||||
Class<TestAnnotation> annotationType = TestAnnotation.class;
|
||||
assertThat(FILTER.matches(annotationType)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesAnnotationClassWhenNoMatchReturnsFalse() {
|
||||
Class<OtherAnnotation> annotationType = OtherAnnotation.class;
|
||||
assertThat(FILTER.matches(annotationType)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plainWhenJavaLangAnnotationReturnsTrue() {
|
||||
assertThat(AnnotationFilter.PLAIN.matches(Retention.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plainWhenSpringLangAnnotationReturnsTrue() {
|
||||
assertThat(AnnotationFilter.PLAIN.matches(Nullable.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plainWhenOtherAnnotationReturnsFalse() {
|
||||
assertThat(AnnotationFilter.PLAIN.matches(TestAnnotation.class)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void javaWhenJavaLangAnnotationReturnsTrue() {
|
||||
assertThat(AnnotationFilter.JAVA.matches(Retention.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void javaWhenSpringLangAnnotationReturnsFalse() {
|
||||
assertThat(AnnotationFilter.JAVA.matches(Nullable.class)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void javaWhenOtherAnnotationReturnsFalse() {
|
||||
assertThat(AnnotationFilter.JAVA.matches(TestAnnotation.class)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noneWhenNonNullReturnsFalse() {
|
||||
assertThat(AnnotationFilter.NONE.matches(Retention.class)).isFalse();
|
||||
assertThat(AnnotationFilter.NONE.matches(Nullable.class)).isFalse();
|
||||
assertThat(AnnotationFilter.NONE.matches(TestAnnotation.class)).isFalse();
|
||||
assertThat(AnnotationFilter.NONE.matches((Annotation) null)).isFalse();
|
||||
assertThat(AnnotationFilter.NONE.matches((Class<Annotation>) null)).isFalse();
|
||||
assertThat(AnnotationFilter.NONE.matches((String) null)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pacakgesReturnsPackagesAnnotationFilter() {
|
||||
assertThat(AnnotationFilter.packages("com.example")).isInstanceOf(
|
||||
PackagesAnnotationFilter.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mostAppropriateForCollectionWhenAnnotationTypesIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> AnnotationFilter.mostAppropriateFor(
|
||||
(Collection<Class<? extends Annotation>>) null)).withMessage(
|
||||
"AnnotationTypes must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mostAppropriateForCollectionReturnsPlainWhenPossible() {
|
||||
AnnotationFilter filter = AnnotationFilter.mostAppropriateFor(
|
||||
Arrays.asList(TestAnnotation.class, OtherAnnotation.class));
|
||||
assertThat(filter).isSameAs(AnnotationFilter.PLAIN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mostAppropriateForCollectionWhenCantUsePlainReturnsNone() {
|
||||
AnnotationFilter filter = AnnotationFilter.mostAppropriateFor(Arrays.asList(
|
||||
TestAnnotation.class, OtherAnnotation.class, Nullable.class));
|
||||
assertThat(filter).isSameAs(AnnotationFilter.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mostAppropriateForArrayWhenAnnotationTypesIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> AnnotationFilter.mostAppropriateFor(
|
||||
(Class<? extends Annotation>[]) null)).withMessage(
|
||||
"AnnotationTypes must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mostAppropriateForArrayReturnsPlainWhenPossible() {
|
||||
AnnotationFilter filter = AnnotationFilter.mostAppropriateFor(
|
||||
TestAnnotation.class, OtherAnnotation.class);
|
||||
assertThat(filter).isSameAs(AnnotationFilter.PLAIN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mostAppropriateForArrayWhenCantUsePlainReturnsNone() {
|
||||
AnnotationFilter filter = AnnotationFilter.mostAppropriateFor(
|
||||
TestAnnotation.class, OtherAnnotation.class, Nullable.class);
|
||||
assertThat(filter).isSameAs(AnnotationFilter.NONE);
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface TestAnnotation {
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation
|
||||
static class WithTestAnnotation {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface OtherAnnotation {
|
||||
|
||||
}
|
||||
|
||||
@OtherAnnotation
|
||||
static class WithOtherAnnotation {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -78,6 +78,24 @@ public class AnnotationIntrospectionFailureTests {
|
|||
exampleMetaAnnotationClass)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void filteredTypeInMetaAnnotationWhenUsingMergedAnnotationsHandlesException() throws Exception {
|
||||
FilteringClassLoader classLoader = new FilteringClassLoader(
|
||||
getClass().getClassLoader());
|
||||
Class<?> withExampleMetaAnnotation = ClassUtils.forName(
|
||||
WithExampleMetaAnnotation.class.getName(), classLoader);
|
||||
Class<Annotation> exampleAnnotationClass = (Class<Annotation>) ClassUtils.forName(
|
||||
ExampleAnnotation.class.getName(), classLoader);
|
||||
Class<Annotation> exampleMetaAnnotationClass = (Class<Annotation>) ClassUtils.forName(
|
||||
ExampleMetaAnnotation.class.getName(), classLoader);
|
||||
MergedAnnotations annotations = MergedAnnotations.from(withExampleMetaAnnotation);
|
||||
assertThat(annotations.get(exampleAnnotationClass).isPresent()).isFalse();
|
||||
assertThat(annotations.get(exampleMetaAnnotationClass).isPresent()).isFalse();
|
||||
assertThat(annotations.isPresent(exampleMetaAnnotationClass)).isFalse();
|
||||
assertThat(annotations.isPresent(exampleAnnotationClass)).isFalse();
|
||||
}
|
||||
|
||||
|
||||
static class FilteringClassLoader extends OverridingClassLoader {
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,825 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Tests for {@link AnnotationsScanner}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class AnnotationsScannerTests {
|
||||
|
||||
@Test
|
||||
public void directStrategyOnClassWhenNotAnnoatedScansNone() {
|
||||
Class<?> source = WithNoAnnotations.class;
|
||||
assertThat(scan(source, SearchStrategy.DIRECT)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directStrategyOnClassScansAnnotations() {
|
||||
Class<?> source = WithSingleAnnotation.class;
|
||||
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directStrategyOnClassWhenMultipleAnnotationsScansAnnotations() {
|
||||
Class<?> source = WithMultipleAnnotations.class;
|
||||
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly(
|
||||
"0:TestAnnotation1", "0:TestAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directStrategyOnClassWhenHasSuperclassScansOnlyDirect() {
|
||||
Class<?> source = WithSingleSuperclass.class;
|
||||
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directStrategyOnClassWhenHasInterfaceScansOnlyDirect() {
|
||||
Class<?> source = WithSingleInterface.class;
|
||||
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directStrategyOnClassHierarchyScansInCorrectOrder() {
|
||||
Class<?> source = WithHierarchy.class;
|
||||
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsStrategyOnClassWhenNotAnnoatedScansNone() {
|
||||
Class<?> source = WithNoAnnotations.class;
|
||||
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsStrategyOnClassScansAnnotations() {
|
||||
Class<?> source = WithSingleAnnotation.class;
|
||||
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsStrategyOnClassWhenMultipleAnnotationsScansAnnotations() {
|
||||
Class<?> source = WithMultipleAnnotations.class;
|
||||
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly(
|
||||
"0:TestAnnotation1", "0:TestAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsStrategyOnClassWhenHasSuperclassScansOnlyInherited() {
|
||||
Class<?> source = WithSingleSuperclass.class;
|
||||
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly(
|
||||
"0:TestAnnotation1", "1:TestInheritedAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsStrategyOnClassWhenHasInterfaceDoesNotIncludeInterfaces() {
|
||||
Class<?> source = WithSingleInterface.class;
|
||||
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsStrategyOnClassHierarchyScansInCorrectOrder() {
|
||||
Class<?> source = WithHierarchy.class;
|
||||
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly(
|
||||
"0:TestAnnotation1", "1:TestInheritedAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsStrategyOnClassWhenHasAnnotationOnBothClassesIncudesOnlyOne() {
|
||||
Class<?> source = WithSingleSuperclassAndDoubleInherited.class;
|
||||
assertThat(Arrays.stream(source.getAnnotations()).map(
|
||||
Annotation::annotationType).map(Class::getName)).containsExactly(
|
||||
TestInheritedAnnotation2.class.getName());
|
||||
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsOnly(
|
||||
"0:TestInheritedAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void superclassStrategyOnClassWhenNotAnnoatedScansNone() {
|
||||
Class<?> source = WithNoAnnotations.class;
|
||||
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void superclassStrategyOnClassScansAnnotations() {
|
||||
Class<?> source = WithSingleAnnotation.class;
|
||||
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void superclassStrategyOnClassWhenMultipleAnnotationsScansAnnotations() {
|
||||
Class<?> source = WithMultipleAnnotations.class;
|
||||
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly(
|
||||
"0:TestAnnotation1", "0:TestAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void superclassStrategyOnClassWhenHasSuperclassScansSuperclass() {
|
||||
Class<?> source = WithSingleSuperclass.class;
|
||||
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly(
|
||||
"0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void superclassStrategyOnClassWhenHasInterfaceDoesNotIncludeInterfaces() {
|
||||
Class<?> source = WithSingleInterface.class;
|
||||
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void superclassStrategyOnClassHierarchyScansInCorrectOrder() {
|
||||
Class<?> source = WithHierarchy.class;
|
||||
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly(
|
||||
"0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2",
|
||||
"2:TestAnnotation3");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyOnClassWhenNotAnnoatedScansNone() {
|
||||
Class<?> source = WithNoAnnotations.class;
|
||||
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyOnClassScansAnnotations() {
|
||||
Class<?> source = WithSingleAnnotation.class;
|
||||
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyOnClassWhenMultipleAnnotationsScansAnnotations() {
|
||||
Class<?> source = WithMultipleAnnotations.class;
|
||||
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly(
|
||||
"0:TestAnnotation1", "0:TestAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyOnClassWhenHasSuperclassScansSuperclass() {
|
||||
Class<?> source = WithSingleSuperclass.class;
|
||||
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly(
|
||||
"0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyOnClassWhenHasInterfaceDoesNotIncludeInterfaces() {
|
||||
Class<?> source = WithSingleInterface.class;
|
||||
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly(
|
||||
"0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyOnClassHierarchyScansInCorrectOrder() {
|
||||
Class<?> source = WithHierarchy.class;
|
||||
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly(
|
||||
"0:TestAnnotation1", "1:TestAnnotation5", "1:TestInheritedAnnotation5",
|
||||
"2:TestAnnotation6", "3:TestAnnotation2", "3:TestInheritedAnnotation2",
|
||||
"4:TestAnnotation3", "5:TestAnnotation4");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directStrategyOnMethodWhenNotAnnoatedScansNone() {
|
||||
Method source = methodFrom(WithNoAnnotations.class);
|
||||
assertThat(scan(source, SearchStrategy.DIRECT)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directStrategyOnMethodScansAnnotations() {
|
||||
Method source = methodFrom(WithSingleAnnotation.class);
|
||||
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directStrategyOnMethodWhenMultipleAnnotationsScansAnnotations() {
|
||||
Method source = methodFrom(WithMultipleAnnotations.class);
|
||||
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly(
|
||||
"0:TestAnnotation1", "0:TestAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directStrategyOnMethodWhenHasSuperclassScansOnlyDirect() {
|
||||
Method source = methodFrom(WithSingleSuperclass.class);
|
||||
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directStrategyOnMethodWhenHasInterfaceScansOnlyDirect() {
|
||||
Method source = methodFrom(WithSingleInterface.class);
|
||||
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directStrategyOnMethodHierarchyScansInCorrectOrder() {
|
||||
Method source = methodFrom(WithHierarchy.class);
|
||||
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsStrategyOnMethodWhenNotAnnoatedScansNone() {
|
||||
Method source = methodFrom(WithNoAnnotations.class);
|
||||
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsStrategyOnMethodScansAnnotations() {
|
||||
Method source = methodFrom(WithSingleAnnotation.class);
|
||||
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsStrategyOnMethodWhenMultipleAnnotationsScansAnnotations() {
|
||||
Method source = methodFrom(WithMultipleAnnotations.class);
|
||||
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly(
|
||||
"0:TestAnnotation1", "0:TestAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsMethodOnMethodWhenHasSuperclassIgnoresInherited() {
|
||||
Method source = methodFrom(WithSingleSuperclass.class);
|
||||
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsStrategyOnMethodWhenHasInterfaceDoesNotIncludeInterfaces() {
|
||||
Method source = methodFrom(WithSingleInterface.class);
|
||||
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsStrategyOnMethodHierarchyScansInCorrectOrder() {
|
||||
Method source = methodFrom(WithHierarchy.class);
|
||||
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void superclassStrategyOnMethodWhenNotAnnoatedScansNone() {
|
||||
Method source = methodFrom(WithNoAnnotations.class);
|
||||
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void superclassStrategyOnMethodScansAnnotations() {
|
||||
Method source = methodFrom(WithSingleAnnotation.class);
|
||||
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void superclassStrategyOnMethodWhenMultipleAnnotationsScansAnnotations() {
|
||||
Method source = methodFrom(WithMultipleAnnotations.class);
|
||||
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly(
|
||||
"0:TestAnnotation1", "0:TestAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void superclassStrategyOnMethodWhenHasSuperclassScansSuperclass() {
|
||||
Method source = methodFrom(WithSingleSuperclass.class);
|
||||
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly(
|
||||
"0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void superclassStrategyOnMethodWhenHasInterfaceDoesNotIncludeInterfaces() {
|
||||
Method source = methodFrom(WithSingleInterface.class);
|
||||
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void superclassStrategyOnMethodHierarchyScansInCorrectOrder() {
|
||||
Method source = methodFrom(WithHierarchy.class);
|
||||
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly(
|
||||
"0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2",
|
||||
"2:TestAnnotation3");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyOnMethodWhenNotAnnoatedScansNone() {
|
||||
Method source = methodFrom(WithNoAnnotations.class);
|
||||
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyOnMethodScansAnnotations() {
|
||||
Method source = methodFrom(WithSingleAnnotation.class);
|
||||
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyOnMethodWhenMultipleAnnotationsScansAnnotations() {
|
||||
Method source = methodFrom(WithMultipleAnnotations.class);
|
||||
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly(
|
||||
"0:TestAnnotation1", "0:TestAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyOnMethodWhenHasSuperclassScansSuperclass() {
|
||||
Method source = methodFrom(WithSingleSuperclass.class);
|
||||
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly(
|
||||
"0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyOnMethodWhenHasInterfaceDoesNotIncludeInterfaces() {
|
||||
Method source = methodFrom(WithSingleInterface.class);
|
||||
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly(
|
||||
"0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyOnMethodHierarchyScansInCorrectOrder() {
|
||||
Method source = methodFrom(WithHierarchy.class);
|
||||
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly(
|
||||
"0:TestAnnotation1", "1:TestAnnotation5", "1:TestInheritedAnnotation5",
|
||||
"2:TestAnnotation6", "3:TestAnnotation2", "3:TestInheritedAnnotation2",
|
||||
"4:TestAnnotation3", "5:TestAnnotation4");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyOnBridgeMethodScansAnnotations() throws Exception {
|
||||
Method source = BridgedMethod.class.getDeclaredMethod("method", Object.class);
|
||||
assertThat(source.isBridge()).isTrue();
|
||||
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly(
|
||||
"0:TestAnnotation1", "1:TestAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyOnBridgedMethodScansAnnotations() throws Exception {
|
||||
Method source = BridgedMethod.class.getDeclaredMethod("method", String.class);
|
||||
assertThat(source.isBridge()).isFalse();
|
||||
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly(
|
||||
"0:TestAnnotation1", "1:TestAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directStrategyOnBridgeMethodScansAnnotations() throws Exception {
|
||||
Method source = BridgedMethod.class.getDeclaredMethod("method", Object.class);
|
||||
assertThat(source.isBridge()).isTrue();
|
||||
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dirextStrategyOnBridgedMethodScansAnnotations() throws Exception {
|
||||
Method source = BridgedMethod.class.getDeclaredMethod("method", String.class);
|
||||
assertThat(source.isBridge()).isFalse();
|
||||
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyOnMethodWithIgnorablesScansAnnotations()
|
||||
throws Exception {
|
||||
Method source = methodFrom(Ignoreable.class);
|
||||
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyOnMethodWithMultipleCandidatesScansAnnotations()
|
||||
throws Exception {
|
||||
Method source = methodFrom(MultipleMethods.class);
|
||||
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyOnMethodWithGenericParameterOverrideScansAnnotations()
|
||||
throws Exception {
|
||||
Method source = ReflectionUtils.findMethod(GenericOverride.class, "method",
|
||||
String.class);
|
||||
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly(
|
||||
"0:TestAnnotation1", "1:TestAnnotation2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyOnMethodWithGenericParameterNonOverrideScansAnnotations()
|
||||
throws Exception {
|
||||
Method source = ReflectionUtils.findMethod(GenericNonOverride.class, "method",
|
||||
StringBuilder.class);
|
||||
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly(
|
||||
"0:TestAnnotation1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scanWhenProcessorReturnsFromDoWithAggregateExitsEarly() {
|
||||
String result = AnnotationsScanner.scan(this, WithSingleSuperclass.class,
|
||||
SearchStrategy.EXHAUSTIVE, new AnnotationsProcessor<Object, String>() {
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String doWithAggregate(Object context, int aggregateIndex) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String doWithAnnotations(Object context, int aggregateIndex,
|
||||
Object source, Annotation[] annotations) {
|
||||
throw new IllegalStateException("Should not call");
|
||||
}
|
||||
|
||||
});
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scanWhenProcessorReturnsFromDoWithAnnotationsExitsEarly() {
|
||||
List<Integer> indexes = new ArrayList<>();
|
||||
String result = AnnotationsScanner.scan(this, WithSingleSuperclass.class,
|
||||
SearchStrategy.EXHAUSTIVE,
|
||||
(context, aggregateIndex, source, annotations) -> {
|
||||
indexes.add(aggregateIndex);
|
||||
return "";
|
||||
});
|
||||
assertThat(result).isEmpty();
|
||||
assertThat(indexes).containsOnly(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scanWhenProcessorHasFinishMethodUsesFinishResult() {
|
||||
String result = AnnotationsScanner.scan(this, WithSingleSuperclass.class,
|
||||
SearchStrategy.EXHAUSTIVE, new AnnotationsProcessor<Object, String>() {
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String doWithAnnotations(Object context, int aggregateIndex,
|
||||
Object source, Annotation[] annotations) {
|
||||
return "K";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String finish(String result) {
|
||||
return "O" + result;
|
||||
}
|
||||
|
||||
});
|
||||
assertThat(result).isEqualTo("OK");
|
||||
}
|
||||
|
||||
private Method methodFrom(Class<?> type) {
|
||||
return ReflectionUtils.findMethod(type, "method");
|
||||
}
|
||||
|
||||
private Stream<String> scan(AnnotatedElement element, SearchStrategy searchStrategy) {
|
||||
List<String> result = new ArrayList<>();
|
||||
AnnotationsScanner.scan(this, element, searchStrategy,
|
||||
(criteria, aggregateIndex, source, annotations) -> {
|
||||
for (Annotation annotation : annotations) {
|
||||
if (annotation != null) {
|
||||
String name = ClassUtils.getShortName(
|
||||
annotation.annotationType());
|
||||
name = name.substring(name.lastIndexOf(".") + 1);
|
||||
result.add(aggregateIndex + ":" + name);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return result.stream();
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface TestAnnotation1 {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface TestAnnotation2 {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface TestAnnotation3 {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface TestAnnotation4 {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface TestAnnotation5 {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface TestAnnotation6 {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
static @interface TestInheritedAnnotation1 {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
static @interface TestInheritedAnnotation2 {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
static @interface TestInheritedAnnotation3 {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
static @interface TestInheritedAnnotation4 {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
static @interface TestInheritedAnnotation5 {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface OnSuperClass {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface OnInterface {
|
||||
|
||||
}
|
||||
|
||||
static class WithNoAnnotations {
|
||||
|
||||
public void method() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation1
|
||||
static class WithSingleAnnotation {
|
||||
|
||||
@TestAnnotation1
|
||||
public void method() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation1
|
||||
@TestAnnotation2
|
||||
static class WithMultipleAnnotations {
|
||||
|
||||
@TestAnnotation1
|
||||
@TestAnnotation2
|
||||
public void method() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation2
|
||||
@TestInheritedAnnotation2
|
||||
static class SingleSuperclass {
|
||||
|
||||
@TestAnnotation2
|
||||
@TestInheritedAnnotation2
|
||||
public void method() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation1
|
||||
static class WithSingleSuperclass extends SingleSuperclass {
|
||||
|
||||
@TestAnnotation1
|
||||
public void method() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TestInheritedAnnotation2
|
||||
static class WithSingleSuperclassAndDoubleInherited extends SingleSuperclass {
|
||||
|
||||
@TestAnnotation1
|
||||
public void method() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation1
|
||||
static class WithSingleInterface implements SingleInterface {
|
||||
|
||||
@TestAnnotation1
|
||||
public void method() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation2
|
||||
@TestInheritedAnnotation2
|
||||
static interface SingleInterface {
|
||||
|
||||
@TestAnnotation2
|
||||
@TestInheritedAnnotation2
|
||||
public void method();
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation1
|
||||
static class WithHierarchy extends HierarchySuperclass implements HierarchyInterface {
|
||||
|
||||
@TestAnnotation1
|
||||
public void method() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation2
|
||||
@TestInheritedAnnotation2
|
||||
static class HierarchySuperclass extends HierarchySuperSuperclass {
|
||||
|
||||
@TestAnnotation2
|
||||
@TestInheritedAnnotation2
|
||||
public void method() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation3
|
||||
static class HierarchySuperSuperclass implements HierarchySuperSuperclassInterface {
|
||||
|
||||
@TestAnnotation3
|
||||
public void method() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation4
|
||||
static interface HierarchySuperSuperclassInterface {
|
||||
|
||||
@TestAnnotation4
|
||||
public void method();
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation5
|
||||
@TestInheritedAnnotation5
|
||||
static interface HierarchyInterface extends HierarchyInterfaceInterface {
|
||||
|
||||
@TestAnnotation5
|
||||
@TestInheritedAnnotation5
|
||||
public void method();
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation6
|
||||
static interface HierarchyInterfaceInterface {
|
||||
|
||||
@TestAnnotation6
|
||||
public void method();
|
||||
|
||||
}
|
||||
|
||||
static class BridgedMethod implements BridgeMethod<String> {
|
||||
|
||||
@Override
|
||||
@TestAnnotation1
|
||||
public void method(String arg) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static interface BridgeMethod<T> {
|
||||
|
||||
@TestAnnotation2
|
||||
void method(T arg);
|
||||
|
||||
}
|
||||
|
||||
static class Ignoreable implements IgnoreableOverrideInterface1,
|
||||
IgnoreableOverrideInterface2, Serializable {
|
||||
|
||||
@TestAnnotation1
|
||||
public void method() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static interface IgnoreableOverrideInterface1 {
|
||||
|
||||
@Nullable
|
||||
public void method();
|
||||
|
||||
}
|
||||
|
||||
static interface IgnoreableOverrideInterface2 {
|
||||
|
||||
@Nullable
|
||||
public void method();
|
||||
|
||||
}
|
||||
|
||||
static abstract class MultipleMethods implements MultipleMethodsInterface {
|
||||
|
||||
@TestAnnotation1
|
||||
public void method() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface MultipleMethodsInterface {
|
||||
|
||||
@TestAnnotation2
|
||||
void method(String arg);
|
||||
|
||||
@TestAnnotation2
|
||||
void method1();
|
||||
|
||||
}
|
||||
|
||||
static class GenericOverride implements GenericOverrideInterface<String> {
|
||||
|
||||
@TestAnnotation1
|
||||
public void method(String argument) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static interface GenericOverrideInterface<T extends CharSequence> {
|
||||
|
||||
@TestAnnotation2
|
||||
void method(T argument);
|
||||
|
||||
}
|
||||
|
||||
static abstract class GenericNonOverride
|
||||
implements GenericNonOverrideInterface<String> {
|
||||
|
||||
@TestAnnotation1
|
||||
public void method(StringBuilder argument) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static interface GenericNonOverrideInterface<T extends CharSequence> {
|
||||
|
||||
@TestAnnotation2
|
||||
void method(T argument);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.BDDMockito.*;
|
||||
|
||||
/**
|
||||
* Tests for {@link AttributeMethods}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class AttributeMethodsTests {
|
||||
|
||||
@Test
|
||||
public void forAnnotationTypeWhenNullReturnsNone() {
|
||||
AttributeMethods methods = AttributeMethods.forAnnotationType(null);
|
||||
assertThat(methods).isSameAs(AttributeMethods.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forAnnotationTypeWhenHasNoAttributesReturnsNone() {
|
||||
AttributeMethods methods = AttributeMethods.forAnnotationType(NoAttributes.class);
|
||||
assertThat(methods).isSameAs(AttributeMethods.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forAnnotationTypeWhenHasMultipleAttributesReturnsAttributes() {
|
||||
AttributeMethods methods = AttributeMethods.forAnnotationType(
|
||||
MultipleAttributes.class);
|
||||
assertThat(methods.get("value").getName()).isEqualTo("value");
|
||||
assertThat(methods.get("intValue").getName()).isEqualTo("intValue");
|
||||
assertThat(getAll(methods)).flatExtracting(Method::getName).containsExactly(
|
||||
"intValue", "value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isOnlyValueAttributeWhenHasOnlyValueAttributeReturnsTrue() {
|
||||
AttributeMethods methods = AttributeMethods.forAnnotationType(ValueOnly.class);
|
||||
assertThat(methods.isOnlyValueAttribute()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isOnlyValueAttributeWhenHasOnlySingleNonValueAttributeReturnsFalse() {
|
||||
AttributeMethods methods = AttributeMethods.forAnnotationType(NonValueOnly.class);
|
||||
assertThat(methods.isOnlyValueAttribute()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isOnlyValueAttributeWhenHasOnlyMultipleAttributesIncludingValueReturnsFalse() {
|
||||
AttributeMethods methods = AttributeMethods.forAnnotationType(
|
||||
MultipleAttributes.class);
|
||||
assertThat(methods.isOnlyValueAttribute()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void indexOfNameReturnsIndex() {
|
||||
AttributeMethods methods = AttributeMethods.forAnnotationType(
|
||||
MultipleAttributes.class);
|
||||
assertThat(methods.indexOf("value")).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void indexOfMethodReturnsIndex() throws Exception {
|
||||
AttributeMethods methods = AttributeMethods.forAnnotationType(
|
||||
MultipleAttributes.class);
|
||||
Method method = MultipleAttributes.class.getDeclaredMethod("value");
|
||||
assertThat(methods.indexOf(method)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sizeReturnsSize() {
|
||||
AttributeMethods methods = AttributeMethods.forAnnotationType(
|
||||
MultipleAttributes.class);
|
||||
assertThat(methods.size()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canThrowTypeNotPresentExceptionWhenHasClassAttributeReturnsTrue() {
|
||||
AttributeMethods methods = AttributeMethods.forAnnotationType(ClassValue.class);
|
||||
assertThat(methods.canThrowTypeNotPresentException(0)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canThrowTypeNotPresentExceptionWhenHasClassArrayAttributeReturnsTrue() {
|
||||
AttributeMethods methods = AttributeMethods.forAnnotationType(
|
||||
ClassArrayValue.class);
|
||||
assertThat(methods.canThrowTypeNotPresentException(0)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canThrowTypeNotPresentExceptionWhenNotClassOrClassArrayAttributeReturnsFalse() {
|
||||
AttributeMethods methods = AttributeMethods.forAnnotationType(ValueOnly.class);
|
||||
assertThat(methods.canThrowTypeNotPresentException(0)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hasDefaultValueMethodWhenHasDefaultValueMethodReturnsTrue() {
|
||||
AttributeMethods methods = AttributeMethods.forAnnotationType(
|
||||
DefaultValueAttribute.class);
|
||||
assertThat(methods.hasDefaultValueMethod()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hasDefaultValueMethodWhenHasNoDefaultValueMethodsReturnsFalse() {
|
||||
AttributeMethods methods = AttributeMethods.forAnnotationType(
|
||||
MultipleAttributes.class);
|
||||
assertThat(methods.hasDefaultValueMethod()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isValidWhenHasTypeNotPresentExceptionReturnsFalse() {
|
||||
ClassValue annotation = mockAnnotation(ClassValue.class);
|
||||
given(annotation.value()).willThrow(TypeNotPresentException.class);
|
||||
AttributeMethods attributes = AttributeMethods.forAnnotationType(
|
||||
annotation.annotationType());
|
||||
assertThat(attributes.isValid(annotation)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public void isValidWhenDoesNotHaveTypeNotPresentExceptionReturnsTrue() {
|
||||
ClassValue annotation = mock(ClassValue.class);
|
||||
given(annotation.value()).willReturn((Class) InputStream.class);
|
||||
AttributeMethods attributes = AttributeMethods.forAnnotationType(
|
||||
annotation.annotationType());
|
||||
assertThat(attributes.isValid(annotation)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateWhenHasTypeNotPresentExceptionThrowsException() {
|
||||
ClassValue annotation = mockAnnotation(ClassValue.class);
|
||||
given(annotation.value()).willThrow(TypeNotPresentException.class);
|
||||
AttributeMethods attributes = AttributeMethods.forAnnotationType(
|
||||
annotation.annotationType());
|
||||
assertThatIllegalStateException().isThrownBy(
|
||||
() -> attributes.validate(annotation));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public void validateWhenDoesNotHaveTypeNotPresentExceptionThrowsNothing() {
|
||||
ClassValue annotation = mockAnnotation(ClassValue.class);
|
||||
given(annotation.value()).willReturn((Class) InputStream.class);
|
||||
AttributeMethods attributes = AttributeMethods.forAnnotationType(
|
||||
annotation.annotationType());
|
||||
attributes.validate(annotation);
|
||||
}
|
||||
|
||||
private List<Method> getAll(AttributeMethods attributes) {
|
||||
List<Method> result = new ArrayList<>(attributes.size());
|
||||
for (int i = 0; i < attributes.size(); i++) {
|
||||
result.add(attributes.get(i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
private <A extends Annotation> A mockAnnotation(Class<A> annotationType) {
|
||||
A annotation = mock(annotationType);
|
||||
given(annotation.annotationType()).willReturn((Class) annotationType);
|
||||
return annotation;
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface NoAttributes {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface MultipleAttributes {
|
||||
|
||||
int intValue();
|
||||
|
||||
String value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface ValueOnly {
|
||||
|
||||
String value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface NonValueOnly {
|
||||
|
||||
String test();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface ClassValue {
|
||||
|
||||
Class<?> value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface ClassArrayValue {
|
||||
|
||||
Class<?>[] value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface DefaultValueAttribute {
|
||||
|
||||
String one();
|
||||
|
||||
String two();
|
||||
|
||||
String three() default "3";
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.OverridingClassLoader;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link MergedAnnotation} to ensure the correct class loader is
|
||||
* used.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 5.2
|
||||
*/
|
||||
public class MergedAnnotationClassLoaderTests {
|
||||
|
||||
private static final String TEST_ANNOTATION = TestAnnotation.class.getName();
|
||||
|
||||
private static final String TEST_META_ANNOTATION = TestMetaAnnotation.class.getName();
|
||||
|
||||
private static final String WITH_TEST_ANNOTATION = WithTestAnnotation.class.getName();
|
||||
|
||||
private static final String TEST_REFERENCE = TestReference.class.getName();
|
||||
|
||||
@Test
|
||||
public void synthesizedUsesCorrectClassLoader() throws Exception {
|
||||
ClassLoader parent = getClass().getClassLoader();
|
||||
TestClassLoader child = new TestClassLoader(parent);
|
||||
Class<?> source = child.loadClass(WITH_TEST_ANNOTATION);
|
||||
Annotation annotation = getDeclaredAnnotation(source, TEST_ANNOTATION);
|
||||
Annotation metaAnnotation = getDeclaredAnnotation(annotation.annotationType(),
|
||||
TEST_META_ANNOTATION);
|
||||
// We should have loaded the source and initial annotation from child
|
||||
assertThat(source.getClassLoader()).isEqualTo(child);
|
||||
assertThat(annotation.getClass().getClassLoader()).isEqualTo(child);
|
||||
assertThat(annotation.annotationType().getClassLoader()).isEqualTo(child);
|
||||
// The meta-annotation should have been loaded by the parent
|
||||
assertThat(metaAnnotation.getClass().getClassLoader()).isEqualTo(parent);
|
||||
assertThat(metaAnnotation.getClass().getClassLoader()).isEqualTo(parent);
|
||||
assertThat(
|
||||
getEnumAttribute(metaAnnotation).getClass().getClassLoader()).isEqualTo(
|
||||
parent);
|
||||
assertThat(getClassAttribute(metaAnnotation).getClassLoader()).isEqualTo(child);
|
||||
// MergedAnnotation should follow the same class loader logic
|
||||
MergedAnnotations mergedAnnotations = MergedAnnotations.from(source);
|
||||
Annotation synthesized = mergedAnnotations.get(TEST_ANNOTATION).synthesize();
|
||||
Annotation synthesizedMeta = mergedAnnotations.get(
|
||||
TEST_META_ANNOTATION).synthesize();
|
||||
assertThat(synthesized.getClass().getClassLoader()).isEqualTo(child);
|
||||
assertThat(synthesized.annotationType().getClassLoader()).isEqualTo(child);
|
||||
assertThat(synthesizedMeta.getClass().getClassLoader()).isEqualTo(parent);
|
||||
assertThat(synthesizedMeta.getClass().getClassLoader()).isEqualTo(parent);
|
||||
assertThat(getClassAttribute(synthesizedMeta).getClassLoader()).isEqualTo(child);
|
||||
assertThat(
|
||||
getEnumAttribute(synthesizedMeta).getClass().getClassLoader()).isEqualTo(
|
||||
parent);
|
||||
assertThat(synthesized).isEqualTo(annotation);
|
||||
assertThat(synthesizedMeta).isEqualTo(metaAnnotation);
|
||||
// Also check utils version
|
||||
Annotation utilsMeta = AnnotatedElementUtils.getMergedAnnotation(source,
|
||||
TestMetaAnnotation.class);
|
||||
assertThat(utilsMeta.getClass().getClassLoader()).isEqualTo(parent);
|
||||
assertThat(utilsMeta.getClass().getClassLoader()).isEqualTo(parent);
|
||||
assertThat(getClassAttribute(utilsMeta).getClassLoader()).isEqualTo(child);
|
||||
assertThat(getEnumAttribute(utilsMeta).getClass().getClassLoader()).isEqualTo(
|
||||
parent);
|
||||
assertThat(utilsMeta).isEqualTo(metaAnnotation);
|
||||
}
|
||||
|
||||
private Class<?> getClassAttribute(Annotation annotation) throws Exception {
|
||||
return (Class<?>) getAttributeValue(annotation, "classValue");
|
||||
}
|
||||
|
||||
private Enum<?> getEnumAttribute(Annotation annotation) throws Exception {
|
||||
return (Enum<?>) getAttributeValue(annotation, "enumValue");
|
||||
}
|
||||
|
||||
private Object getAttributeValue(Annotation annotation, String name)
|
||||
throws Exception {
|
||||
Method classValueMethod = annotation.annotationType().getDeclaredMethod(name);
|
||||
classValueMethod.setAccessible(true);
|
||||
return classValueMethod.invoke(annotation);
|
||||
}
|
||||
|
||||
private Annotation getDeclaredAnnotation(Class<?> element, String annotationType) {
|
||||
for (Annotation annotation : element.getDeclaredAnnotations()) {
|
||||
if (annotation.annotationType().getName().equals(annotationType)) {
|
||||
return annotation;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class TestClassLoader extends OverridingClassLoader {
|
||||
|
||||
public TestClassLoader(ClassLoader parent) {
|
||||
super(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isEligibleForOverriding(String className) {
|
||||
return WITH_TEST_ANNOTATION.equals(className)
|
||||
|| TEST_ANNOTATION.equals(className)
|
||||
|| TEST_REFERENCE.equals(className);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface TestMetaAnnotation {
|
||||
|
||||
@AliasFor("d")
|
||||
String c() default "";
|
||||
|
||||
@AliasFor("c")
|
||||
String d() default "";
|
||||
|
||||
Class<?> classValue();
|
||||
|
||||
TestEnum enumValue();
|
||||
|
||||
}
|
||||
|
||||
@TestMetaAnnotation(classValue = TestReference.class, enumValue = TestEnum.TWO)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface TestAnnotation {
|
||||
|
||||
@AliasFor("b")
|
||||
String a() default "";
|
||||
|
||||
@AliasFor("a")
|
||||
String b() default "";
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation
|
||||
static class WithTestAnnotation {
|
||||
|
||||
}
|
||||
|
||||
static class TestReference {
|
||||
|
||||
}
|
||||
|
||||
static enum TestEnum {
|
||||
|
||||
ONE, TWO, THREE
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.Repeatable;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.annotation.MergedAnnotation.MapValues;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link MergedAnnotationCollectors}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class MergedAnnotationCollectorsTests {
|
||||
|
||||
@Test
|
||||
public void toAnnotationSetCollectsLinkedHashSetWithSynthesizedAnnotations() {
|
||||
Set<TestAnnotation> set = stream().collect(
|
||||
MergedAnnotationCollectors.toAnnotationSet());
|
||||
assertThat(set).isInstanceOf(LinkedHashSet.class).flatExtracting(
|
||||
TestAnnotation::value).containsExactly("a", "b", "c");
|
||||
assertThat(set).allMatch(SynthesizedAnnotation.class::isInstance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toAnnotationArrayCollectsAnnotationArrayWithSynthesizedAnnotations() {
|
||||
Annotation[] array = stream().collect(
|
||||
MergedAnnotationCollectors.toAnnotationArray());
|
||||
assertThat(Arrays.stream(array).map(
|
||||
annotation -> ((TestAnnotation) annotation).value())).containsExactly("a",
|
||||
"b", "c");
|
||||
assertThat(array).allMatch(SynthesizedAnnotation.class::isInstance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toSuppliedAnnotationArrayCollectsAnnotationArrayWithSynthesizedAnnotations() {
|
||||
TestAnnotation[] array = stream().collect(
|
||||
MergedAnnotationCollectors.toAnnotationArray(TestAnnotation[]::new));
|
||||
assertThat(Arrays.stream(array).map(TestAnnotation::value)).containsExactly("a",
|
||||
"b", "c");
|
||||
assertThat(array).allMatch(SynthesizedAnnotation.class::isInstance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toMultiValueMapCollectsMultiValueMap() {
|
||||
MultiValueMap<String, Object> map = stream().map(
|
||||
MergedAnnotation::filterDefaultValues).collect(
|
||||
MergedAnnotationCollectors.toMultiValueMap(
|
||||
MapValues.CLASS_TO_STRING));
|
||||
assertThat(map.get("value")).containsExactly("a", "b", "c");
|
||||
assertThat(map.get("extra")).containsExactly("java.lang.String",
|
||||
"java.lang.Integer");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toFinishedMultiValueMapCollectsMultiValueMap() {
|
||||
MultiValueMap<String, Object> map = stream().collect(
|
||||
MergedAnnotationCollectors.toMultiValueMap(result -> {
|
||||
result.add("finished", true);
|
||||
return result;
|
||||
}));
|
||||
assertThat(map.get("value")).containsExactly("a", "b", "c");
|
||||
assertThat(map.get("extra")).containsExactly(void.class, String.class,
|
||||
Integer.class);
|
||||
assertThat(map.get("finished")).containsExactly(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public void toFinishedMultiValueMapWhenFinisherIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> stream().collect(
|
||||
MergedAnnotationCollectors.toMultiValueMap((Function) null))).withMessage(
|
||||
"Finisher must not be null");
|
||||
}
|
||||
|
||||
private Stream<MergedAnnotation<TestAnnotation>> stream() {
|
||||
return MergedAnnotations.from(WithTestAnnotations.class).stream(
|
||||
TestAnnotation.class);
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Repeatable(TestAnnotations.class)
|
||||
@interface TestAnnotation {
|
||||
|
||||
@AliasFor("name")
|
||||
String value() default "";
|
||||
|
||||
@AliasFor("value")
|
||||
String name() default "";
|
||||
|
||||
Class<?> extra() default void.class;
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface TestAnnotations {
|
||||
|
||||
TestAnnotation[] value();
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation("a")
|
||||
@TestAnnotation(name = "b", extra = String.class)
|
||||
@TestAnnotation(name = "c", extra = Integer.class)
|
||||
static class WithTestAnnotations {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.Repeatable;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link MergedAnnotationPredicates}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class MergedAnnotationPredicatesTests {
|
||||
|
||||
@Test
|
||||
public void typeInStringArrayWhenNameMatchesAccepts() {
|
||||
MergedAnnotation<TestAnnotation> annotation = MergedAnnotations.from(
|
||||
WithTestAnnotation.class).get(TestAnnotation.class);
|
||||
assertThat(MergedAnnotationPredicates.typeIn(
|
||||
TestAnnotation.class.getName())).accepts(annotation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void typeInStringArrayWhenNameDoesNotMatchRejects() {
|
||||
MergedAnnotation<TestAnnotation> annotation = MergedAnnotations.from(
|
||||
WithTestAnnotation.class).get(TestAnnotation.class);
|
||||
assertThat(MergedAnnotationPredicates.typeIn(
|
||||
MissingAnnotation.class.getName())).rejects(annotation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void typeInStringArrayWhenStringArraysIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> MergedAnnotationPredicates.typeIn((String[]) null)).withMessage(
|
||||
"TypeNames must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void typeInClassArrayWhenNameMatchesAccepts() {
|
||||
MergedAnnotation<TestAnnotation> annotation = MergedAnnotations.from(
|
||||
WithTestAnnotation.class).get(TestAnnotation.class);
|
||||
assertThat(MergedAnnotationPredicates.typeIn(TestAnnotation.class)).accepts(
|
||||
annotation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void typeInClassArrayWhenNameDoesNotMatchRejects() {
|
||||
MergedAnnotation<TestAnnotation> annotation = MergedAnnotations.from(
|
||||
WithTestAnnotation.class).get(TestAnnotation.class);
|
||||
assertThat(MergedAnnotationPredicates.typeIn(MissingAnnotation.class)).rejects(
|
||||
annotation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void typeInClassArrayWhenClassArraysIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> MergedAnnotationPredicates.typeIn(
|
||||
(Class<Annotation>[]) null)).withMessage(
|
||||
"Types must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void typeInCollectionWhenMatchesStringInCollectionAccepts() {
|
||||
MergedAnnotation<TestAnnotation> annotation = MergedAnnotations.from(
|
||||
WithTestAnnotation.class).get(TestAnnotation.class);
|
||||
assertThat(MergedAnnotationPredicates.typeIn(
|
||||
Collections.singleton(TestAnnotation.class.getName()))).accepts(
|
||||
annotation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void typeInCollectionWhenMatchesClassInCollectionAccepts() {
|
||||
MergedAnnotation<TestAnnotation> annotation = MergedAnnotations.from(
|
||||
WithTestAnnotation.class).get(TestAnnotation.class);
|
||||
assertThat(MergedAnnotationPredicates.typeIn(
|
||||
Collections.singleton(TestAnnotation.class))).accepts(annotation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void typeInCollectionWhenDoesNotMatchAnyRejects() {
|
||||
MergedAnnotation<TestAnnotation> annotation = MergedAnnotations.from(
|
||||
WithTestAnnotation.class).get(TestAnnotation.class);
|
||||
assertThat(MergedAnnotationPredicates.typeIn(Arrays.asList(
|
||||
MissingAnnotation.class.getName(), MissingAnnotation.class))).rejects(
|
||||
annotation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void typeInCollectionWhenCollectionIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> MergedAnnotationPredicates.typeIn(
|
||||
(Collection<?>) null)).withMessage("Types must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void firstRunOfAcceptsOnlyFirstRun() {
|
||||
List<MergedAnnotation<TestAnnotation>> filtered = MergedAnnotations.from(
|
||||
WithMultipleTestAnnotation.class).stream(TestAnnotation.class).filter(
|
||||
MergedAnnotationPredicates.firstRunOf(
|
||||
this::firstCharOfValue)).collect(Collectors.toList());
|
||||
assertThat(filtered.stream().map(
|
||||
annotation -> annotation.getString("value"))).containsExactly("a1", "a2",
|
||||
"a3");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void firstRunOfWhenValueExtractorIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> MergedAnnotationPredicates.firstRunOf(null)).withMessage(
|
||||
"ValueExtractor must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uniqueAcceptsUniquely() {
|
||||
List<MergedAnnotation<TestAnnotation>> filtered = MergedAnnotations.from(
|
||||
WithMultipleTestAnnotation.class).stream(TestAnnotation.class).filter(
|
||||
MergedAnnotationPredicates.unique(
|
||||
this::firstCharOfValue)).collect(Collectors.toList());
|
||||
assertThat(filtered.stream().map(
|
||||
annotation -> annotation.getString("value"))).containsExactly("a1", "b1",
|
||||
"c1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uniqueWhenKeyExtractorIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> MergedAnnotationPredicates.unique(null)).withMessage(
|
||||
"KeyExtractor must not be null");
|
||||
}
|
||||
|
||||
private char firstCharOfValue(MergedAnnotation<TestAnnotation> annotation) {
|
||||
return annotation.getString("value").charAt(0);
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Repeatable(TestAnnotations.class)
|
||||
static @interface TestAnnotation {
|
||||
|
||||
String value() default "";
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface TestAnnotations {
|
||||
|
||||
TestAnnotation[] value();
|
||||
|
||||
}
|
||||
|
||||
static @interface MissingAnnotation {
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation("test")
|
||||
static class WithTestAnnotation {
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation("a1")
|
||||
@TestAnnotation("a2")
|
||||
@TestAnnotation("a3")
|
||||
@TestAnnotation("b1")
|
||||
@TestAnnotation("b2")
|
||||
@TestAnnotation("b3")
|
||||
@TestAnnotation("c1")
|
||||
@TestAnnotation("c2")
|
||||
@TestAnnotation("c3")
|
||||
static class WithMultipleTestAnnotation {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,306 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests that verify support for finding multiple composed annotations on a single
|
||||
* annotated element.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
public class MergedAnnotationsComposedOnSingleAnnotatedElementTests {
|
||||
|
||||
// See SPR-13486
|
||||
|
||||
@Test
|
||||
public void inheritedStrategyMultipleComposedAnnotationsOnClass() {
|
||||
assertInheritedStrategyBehavior(MultipleComposedCachesClass.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedStrategyMultipleInheritedComposedAnnotationsOnSuperclass() {
|
||||
assertInheritedStrategyBehavior(SubMultipleComposedCachesClass.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedStrategyMultipleNoninheritedComposedAnnotationsOnClass() {
|
||||
MergedAnnotations annotations = MergedAnnotations.from(
|
||||
MultipleNoninheritedComposedCachesClass.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS);
|
||||
assertThat(stream(annotations, "value")).containsExactly("noninheritedCache1",
|
||||
"noninheritedCache2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedStrategyMultipleNoninheritedComposedAnnotationsOnSuperclass() {
|
||||
MergedAnnotations annotations = MergedAnnotations.from(
|
||||
SubMultipleNoninheritedComposedCachesClass.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS);
|
||||
assertThat(annotations.stream(Cacheable.class)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedStrategyComposedPlusLocalAnnotationsOnClass() {
|
||||
assertInheritedStrategyBehavior(ComposedPlusLocalCachesClass.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedStrategyMultipleComposedAnnotationsOnInterface() {
|
||||
MergedAnnotations annotations = MergedAnnotations.from(
|
||||
MultipleComposedCachesOnInterfaceClass.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS);
|
||||
assertThat(annotations.stream(Cacheable.class)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedStrategyMultipleComposedAnnotationsOnMethod() throws Exception {
|
||||
assertInheritedStrategyBehavior(
|
||||
getClass().getDeclaredMethod("multipleComposedCachesMethod"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedStrategyComposedPlusLocalAnnotationsOnMethod() throws Exception {
|
||||
assertInheritedStrategyBehavior(
|
||||
getClass().getDeclaredMethod("composedPlusLocalCachesMethod"));
|
||||
}
|
||||
|
||||
private void assertInheritedStrategyBehavior(AnnotatedElement element) {
|
||||
MergedAnnotations annotations = MergedAnnotations.from(element,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS);
|
||||
assertThat(stream(annotations, "key")).containsExactly("fooKey", "barKey");
|
||||
assertThat(stream(annotations, "value")).containsExactly("fooCache", "barCache");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyMultipleComposedAnnotationsOnClass() {
|
||||
assertExhaustiveStrategyBehavior(MultipleComposedCachesClass.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyMultipleInheritedComposedAnnotationsOnSuperclass() {
|
||||
assertExhaustiveStrategyBehavior(SubMultipleComposedCachesClass.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyMultipleNoninheritedComposedAnnotationsOnClass() {
|
||||
MergedAnnotations annotations = MergedAnnotations.from(
|
||||
MultipleNoninheritedComposedCachesClass.class, SearchStrategy.EXHAUSTIVE);
|
||||
assertThat(stream(annotations, "value")).containsExactly("noninheritedCache1",
|
||||
"noninheritedCache2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyMultipleNoninheritedComposedAnnotationsOnSuperclass() {
|
||||
MergedAnnotations annotations = MergedAnnotations.from(
|
||||
SubMultipleNoninheritedComposedCachesClass.class,
|
||||
SearchStrategy.EXHAUSTIVE);
|
||||
assertThat(stream(annotations, "value")).containsExactly("noninheritedCache1",
|
||||
"noninheritedCache2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyComposedPlusLocalAnnotationsOnClass() {
|
||||
assertExhaustiveStrategyBehavior(ComposedPlusLocalCachesClass.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyMultipleComposedAnnotationsOnInterface() {
|
||||
assertExhaustiveStrategyBehavior(MultipleComposedCachesOnInterfaceClass.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyComposedCacheOnInterfaceAndLocalCacheOnClass() {
|
||||
assertExhaustiveStrategyBehavior(
|
||||
ComposedCacheOnInterfaceAndLocalCacheClass.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyMultipleComposedAnnotationsOnMethod() throws Exception {
|
||||
assertExhaustiveStrategyBehavior(
|
||||
getClass().getDeclaredMethod("multipleComposedCachesMethod"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyComposedPlusLocalAnnotationsOnMethod()
|
||||
throws Exception {
|
||||
assertExhaustiveStrategyBehavior(
|
||||
getClass().getDeclaredMethod("composedPlusLocalCachesMethod"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveStrategyMultipleComposedAnnotationsOnBridgeMethod()
|
||||
throws Exception {
|
||||
assertExhaustiveStrategyBehavior(getBridgeMethod());
|
||||
}
|
||||
|
||||
private void assertExhaustiveStrategyBehavior(AnnotatedElement element) {
|
||||
MergedAnnotations annotations = MergedAnnotations.from(element,
|
||||
SearchStrategy.EXHAUSTIVE);
|
||||
assertThat(stream(annotations, "key")).containsExactly("fooKey", "barKey");
|
||||
assertThat(stream(annotations, "value")).containsExactly("fooCache", "barCache");
|
||||
}
|
||||
|
||||
public Method getBridgeMethod() throws NoSuchMethodException {
|
||||
List<Method> methods = new ArrayList<>();
|
||||
ReflectionUtils.doWithLocalMethods(StringGenericParameter.class, method -> {
|
||||
if ("getFor".equals(method.getName())) {
|
||||
methods.add(method);
|
||||
}
|
||||
});
|
||||
Method bridgeMethod = methods.get(0).getReturnType().equals(Object.class)
|
||||
? methods.get(0)
|
||||
: methods.get(1);
|
||||
assertThat(bridgeMethod.isBridge()).isTrue();
|
||||
return bridgeMethod;
|
||||
}
|
||||
|
||||
private Stream<String> stream(MergedAnnotations annotations, String attributeName) {
|
||||
return annotations.stream(Cacheable.class).map(
|
||||
annotation -> annotation.getString(attributeName));
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@interface Cacheable {
|
||||
@AliasFor("cacheName")
|
||||
String value() default "";
|
||||
@AliasFor("value")
|
||||
String cacheName() default "";
|
||||
String key() default "";
|
||||
}
|
||||
|
||||
@Cacheable("fooCache")
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@interface FooCache {
|
||||
@AliasFor(annotation = Cacheable.class)
|
||||
String key() default "";
|
||||
}
|
||||
|
||||
@Cacheable("barCache")
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@interface BarCache {
|
||||
@AliasFor(annotation = Cacheable.class)
|
||||
String key();
|
||||
}
|
||||
|
||||
@Cacheable("noninheritedCache1")
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface NoninheritedCache1 {
|
||||
@AliasFor(annotation = Cacheable.class)
|
||||
String key() default "";
|
||||
}
|
||||
|
||||
@Cacheable("noninheritedCache2")
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface NoninheritedCache2 {
|
||||
@AliasFor(annotation = Cacheable.class)
|
||||
String key() default "";
|
||||
}
|
||||
|
||||
@FooCache(key = "fooKey")
|
||||
@BarCache(key = "barKey")
|
||||
private static class MultipleComposedCachesClass {
|
||||
}
|
||||
|
||||
private static class SubMultipleComposedCachesClass
|
||||
extends MultipleComposedCachesClass {
|
||||
}
|
||||
|
||||
@NoninheritedCache1
|
||||
@NoninheritedCache2
|
||||
private static class MultipleNoninheritedComposedCachesClass {
|
||||
}
|
||||
|
||||
private static class SubMultipleNoninheritedComposedCachesClass
|
||||
extends MultipleNoninheritedComposedCachesClass {
|
||||
}
|
||||
|
||||
@Cacheable(cacheName = "fooCache", key = "fooKey")
|
||||
@BarCache(key = "barKey")
|
||||
private static class ComposedPlusLocalCachesClass {
|
||||
}
|
||||
|
||||
@FooCache(key = "fooKey")
|
||||
@BarCache(key = "barKey")
|
||||
private interface MultipleComposedCachesInterface {
|
||||
}
|
||||
|
||||
private static class MultipleComposedCachesOnInterfaceClass implements MultipleComposedCachesInterface {
|
||||
}
|
||||
|
||||
@BarCache(key = "barKey")
|
||||
private interface ComposedCacheInterface {
|
||||
}
|
||||
|
||||
@Cacheable(cacheName = "fooCache", key = "fooKey")
|
||||
private static class ComposedCacheOnInterfaceAndLocalCacheClass implements ComposedCacheInterface {
|
||||
}
|
||||
|
||||
@FooCache(key = "fooKey")
|
||||
@BarCache(key = "barKey")
|
||||
private void multipleComposedCachesMethod() {
|
||||
}
|
||||
|
||||
@Cacheable(cacheName = "fooCache", key = "fooKey")
|
||||
@BarCache(key = "barKey")
|
||||
private void composedPlusLocalCachesMethod() {
|
||||
}
|
||||
|
||||
public interface GenericParameter<T> {
|
||||
T getFor(Class<T> cls);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class StringGenericParameter implements GenericParameter<String> {
|
||||
@FooCache(key = "fooKey")
|
||||
@BarCache(key = "barKey")
|
||||
@Override
|
||||
public String getFor(Class<String> cls) { return "foo"; }
|
||||
public String getFor(Integer integer) { return "foo"; }
|
||||
}
|
||||
|
||||
// @formatter:on
|
||||
|
||||
}
|
|
@ -0,0 +1,424 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Repeatable;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.util.Set;
|
||||
|
||||
import org.assertj.core.api.ThrowableTypeAssert;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link MergedAnnotations} and {@link RepeatableContainers} that
|
||||
* verify support for repeatable annotations.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
public class MergedAnnotationsRepeatableAnnotationTests {
|
||||
|
||||
// See SPR-13973
|
||||
|
||||
@Rule
|
||||
public ExpectedException exception = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsWhenNonRepeatableThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> getAnnotations(null, NonRepeatable.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS, getClass())).satisfies(
|
||||
this::nonRepeatableRequirements);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsWhenContainerMissingValueAttributeThrowsException() {
|
||||
assertThatAnnotationConfigurationException().isThrownBy(
|
||||
() -> getAnnotations(ContainerMissingValueAttribute.class,
|
||||
InvalidRepeatable.class, SearchStrategy.INHERITED_ANNOTATIONS,
|
||||
getClass())).satisfies(this::missingValueAttributeRequirements);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsWhenWhenNonArrayValueAttributeThrowsException() {
|
||||
assertThatAnnotationConfigurationException().isThrownBy(
|
||||
() -> getAnnotations(ContainerWithNonArrayValueAttribute.class,
|
||||
InvalidRepeatable.class, SearchStrategy.INHERITED_ANNOTATIONS,
|
||||
getClass())).satisfies(this::nonArrayValueAttributeRequirements);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsWhenWrongComponentTypeThrowsException() {
|
||||
assertThatAnnotationConfigurationException().isThrownBy(() -> getAnnotations(
|
||||
ContainerWithArrayValueAttributeButWrongComponentType.class,
|
||||
InvalidRepeatable.class, SearchStrategy.INHERITED_ANNOTATIONS,
|
||||
getClass())).satisfies(this::wrongComponentTypeRequirements);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsWhenOnClassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS, RepeatableClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsWhenWhenOnSuperclassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS, SubRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsWhenComposedOnClassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS, ComposedRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsWhenComposedMixedWithContainerOnClassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS,
|
||||
ComposedRepeatableMixedWithContainerClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsWhenComposedContainerForRepeatableOnClassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS, ComposedContainerClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsWhenNoninheritedComposedRepeatableOnClassReturnsAnnotations() {
|
||||
Set<Noninherited> annotations = getAnnotations(null, Noninherited.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS, NoninheritedRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A",
|
||||
"B", "C");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedAnnotationsWhenNoninheritedComposedRepeatableOnSuperclassReturnsAnnotations() {
|
||||
Set<Noninherited> annotations = getAnnotations(null, Noninherited.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS,
|
||||
SubNoninheritedRepeatableClass.class);
|
||||
assertThat(annotations).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveWhenNonRepeatableThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> getAnnotations(null,
|
||||
NonRepeatable.class, SearchStrategy.EXHAUSTIVE, getClass())).satisfies(
|
||||
this::nonRepeatableRequirements);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveWhenContainerMissingValueAttributeThrowsException() {
|
||||
assertThatAnnotationConfigurationException().isThrownBy(
|
||||
() -> getAnnotations(ContainerMissingValueAttribute.class,
|
||||
InvalidRepeatable.class, SearchStrategy.EXHAUSTIVE,
|
||||
getClass())).satisfies(this::missingValueAttributeRequirements);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveWhenWhenNonArrayValueAttributeThrowsException() {
|
||||
assertThatAnnotationConfigurationException().isThrownBy(
|
||||
() -> getAnnotations(ContainerWithNonArrayValueAttribute.class,
|
||||
InvalidRepeatable.class, SearchStrategy.EXHAUSTIVE,
|
||||
getClass())).satisfies(this::nonArrayValueAttributeRequirements);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveWhenWrongComponentTypeThrowsException() {
|
||||
assertThatAnnotationConfigurationException().isThrownBy(() -> getAnnotations(
|
||||
ContainerWithArrayValueAttributeButWrongComponentType.class,
|
||||
InvalidRepeatable.class, SearchStrategy.EXHAUSTIVE,
|
||||
getClass())).satisfies(this::wrongComponentTypeRequirements);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveWhenOnClassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.EXHAUSTIVE, RepeatableClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveWhenWhenOnSuperclassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.EXHAUSTIVE, SubRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveWhenComposedOnClassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.EXHAUSTIVE, ComposedRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveWhenComposedMixedWithContainerOnClassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.EXHAUSTIVE,
|
||||
ComposedRepeatableMixedWithContainerClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveWhenComposedContainerForRepeatableOnClassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.EXHAUSTIVE, ComposedContainerClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveAnnotationsWhenNoninheritedComposedRepeatableOnClassReturnsAnnotations() {
|
||||
Set<Noninherited> annotations = getAnnotations(null, Noninherited.class,
|
||||
SearchStrategy.EXHAUSTIVE, NoninheritedRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A",
|
||||
"B", "C");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exhaustiveAnnotationsWhenNoninheritedComposedRepeatableOnSuperclassReturnsAnnotations() {
|
||||
Set<Noninherited> annotations = getAnnotations(null, Noninherited.class,
|
||||
SearchStrategy.EXHAUSTIVE, SubNoninheritedRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A",
|
||||
"B", "C");
|
||||
}
|
||||
|
||||
private <A extends Annotation> Set<A> getAnnotations(
|
||||
Class<? extends Annotation> container, Class<A> repeatable,
|
||||
SearchStrategy searchStrategy, AnnotatedElement element) {
|
||||
RepeatableContainers containers = RepeatableContainers.of(repeatable, container);
|
||||
MergedAnnotations annotations = MergedAnnotations.from(element,
|
||||
searchStrategy, containers, AnnotationFilter.PLAIN);
|
||||
return annotations.stream(repeatable).collect(
|
||||
MergedAnnotationCollectors.toAnnotationSet());
|
||||
}
|
||||
|
||||
private void nonRepeatableRequirements(Exception ex) {
|
||||
assertThat(ex.getMessage()).startsWith(
|
||||
"Annotation type must be a repeatable annotation").contains(
|
||||
"failed to resolve container type for",
|
||||
NonRepeatable.class.getName());
|
||||
}
|
||||
|
||||
private void missingValueAttributeRequirements(Exception ex) {
|
||||
ex.printStackTrace();
|
||||
assertThat(ex.getMessage()).startsWith(
|
||||
"Invalid declaration of container type").contains(
|
||||
ContainerMissingValueAttribute.class.getName(),
|
||||
"for repeatable annotation", InvalidRepeatable.class.getName());
|
||||
assertThat(ex).hasCauseInstanceOf(NoSuchMethodException.class);
|
||||
}
|
||||
|
||||
private void nonArrayValueAttributeRequirements(Exception ex) {
|
||||
assertThat(ex.getMessage()).startsWith("Container type").contains(
|
||||
ContainerWithNonArrayValueAttribute.class.getName(),
|
||||
"must declare a 'value' attribute for an array of type",
|
||||
InvalidRepeatable.class.getName());
|
||||
}
|
||||
|
||||
private void wrongComponentTypeRequirements(Exception ex) {
|
||||
assertThat(ex.getMessage()).startsWith("Container type").contains(
|
||||
ContainerWithArrayValueAttributeButWrongComponentType.class.getName(),
|
||||
"must declare a 'value' attribute for an array of type",
|
||||
InvalidRepeatable.class.getName());
|
||||
}
|
||||
|
||||
private static ThrowableTypeAssert<AnnotationConfigurationException> assertThatAnnotationConfigurationException() {
|
||||
return assertThatExceptionOfType(AnnotationConfigurationException.class);
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface NonRepeatable {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface ContainerMissingValueAttribute {
|
||||
|
||||
// InvalidRepeatable[] value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface ContainerWithNonArrayValueAttribute {
|
||||
|
||||
InvalidRepeatable value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface ContainerWithArrayValueAttributeButWrongComponentType {
|
||||
|
||||
String[] value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface InvalidRepeatable {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@interface PeteRepeats {
|
||||
|
||||
PeteRepeat[] value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@Repeatable(PeteRepeats.class)
|
||||
@interface PeteRepeat {
|
||||
|
||||
String value();
|
||||
|
||||
}
|
||||
|
||||
@PeteRepeat("shadowed")
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@interface ForPetesSake {
|
||||
|
||||
@AliasFor(annotation = PeteRepeat.class)
|
||||
String value();
|
||||
|
||||
}
|
||||
|
||||
@PeteRepeat("shadowed")
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@interface ForTheLoveOfFoo {
|
||||
|
||||
@AliasFor(annotation = PeteRepeat.class)
|
||||
String value();
|
||||
|
||||
}
|
||||
|
||||
@PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") })
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@interface ComposedContainer {
|
||||
|
||||
}
|
||||
|
||||
@PeteRepeat("A")
|
||||
@PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") })
|
||||
static class RepeatableClass {
|
||||
|
||||
}
|
||||
|
||||
static class SubRepeatableClass extends RepeatableClass {
|
||||
|
||||
}
|
||||
|
||||
@ForPetesSake("B")
|
||||
@ForTheLoveOfFoo("C")
|
||||
@PeteRepeat("A")
|
||||
static class ComposedRepeatableClass {
|
||||
|
||||
}
|
||||
|
||||
@ForPetesSake("C")
|
||||
@PeteRepeats(@PeteRepeat("A"))
|
||||
@PeteRepeat("B")
|
||||
static class ComposedRepeatableMixedWithContainerClass {
|
||||
|
||||
}
|
||||
|
||||
@PeteRepeat("A")
|
||||
@ComposedContainer
|
||||
static class ComposedContainerClass {
|
||||
|
||||
}
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface Noninheriteds {
|
||||
|
||||
Noninherited[] value();
|
||||
|
||||
}
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Repeatable(Noninheriteds.class)
|
||||
@interface Noninherited {
|
||||
|
||||
@AliasFor("name")
|
||||
String value() default "";
|
||||
|
||||
@AliasFor("value")
|
||||
String name() default "";
|
||||
|
||||
}
|
||||
|
||||
@Noninherited(name = "shadowed")
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface ComposedNoninherited {
|
||||
|
||||
@AliasFor(annotation = Noninherited.class)
|
||||
String name() default "";
|
||||
|
||||
}
|
||||
|
||||
@ComposedNoninherited(name = "C")
|
||||
@Noninheriteds({ @Noninherited(value = "A"), @Noninherited(name = "B") })
|
||||
static class NoninheritedRepeatableClass {
|
||||
|
||||
}
|
||||
|
||||
static class SubNoninheritedRepeatableClass extends NoninheritedRepeatableClass {
|
||||
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link PackagesAnnotationFilter}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class PackagesAnnotationFilterTests {
|
||||
|
||||
@Test
|
||||
public void createWhenPackagesIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> new PackagesAnnotationFilter((String[]) null)).withMessage(
|
||||
"Packages must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWhenPackagesContainsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> new PackagesAnnotationFilter((String) null)).withMessage(
|
||||
"Package must not have empty elements");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWhenPackagesContainsEmptyTextThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> new PackagesAnnotationFilter("")).withMessage(
|
||||
"Package must not have empty elements");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesWhenInPackageReturnsTrue() {
|
||||
PackagesAnnotationFilter filter = new PackagesAnnotationFilter("com.example");
|
||||
assertThat(filter.matches("com.example.Component")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesWhenNotInPackageReturnsFalse() {
|
||||
PackagesAnnotationFilter filter = new PackagesAnnotationFilter("com.example");
|
||||
assertThat(filter.matches("org.springframework.sterotype.Component")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesWhenInSimilarPackageReturnsFalse() {
|
||||
PackagesAnnotationFilter filter = new PackagesAnnotationFilter("com.example");
|
||||
assertThat(filter.matches("com.examples.Component")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equalsAndHashCode() {
|
||||
PackagesAnnotationFilter filter1 = new PackagesAnnotationFilter("com.example",
|
||||
"org.springframework");
|
||||
PackagesAnnotationFilter filter2 = new PackagesAnnotationFilter(
|
||||
"org.springframework", "com.example");
|
||||
PackagesAnnotationFilter filter3 = new PackagesAnnotationFilter("com.examples");
|
||||
assertThat(filter1.hashCode()).isEqualTo(filter2.hashCode());
|
||||
assertThat(filter1).isEqualTo(filter1).isEqualTo(filter2).isNotEqualTo(filter3);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,283 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.Repeatable;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Tests for {@link RepeatableContainers}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class RepeatableContainersTests {
|
||||
|
||||
@Test
|
||||
public void standardRepeatablesWhenNonRepeatableReturnsNull() {
|
||||
Object[] values = findRepeatedAnnotationValues(
|
||||
RepeatableContainers.standardRepeatables(), WithNonRepeatable.class,
|
||||
NonRepeatable.class);
|
||||
assertThat(values).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void standardRepeatablesWhenSingleReturnsNull() {
|
||||
Object[] values = findRepeatedAnnotationValues(
|
||||
RepeatableContainers.standardRepeatables(),
|
||||
WithSingleStandardRepeatable.class, StandardRepeatable.class);
|
||||
assertThat(values).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void standardRepeatablesWhenContainerReturnsRepeats() {
|
||||
Object[] values = findRepeatedAnnotationValues(
|
||||
RepeatableContainers.standardRepeatables(), WithStandardRepeatables.class,
|
||||
StandardContainer.class);
|
||||
assertThat(values).containsExactly("a", "b");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void standardRepeatablesWhenContainerButNotRepeatableReturnsNull() {
|
||||
Object[] values = findRepeatedAnnotationValues(
|
||||
RepeatableContainers.standardRepeatables(), WithExplicitRepeatables.class,
|
||||
ExplicitContainer.class);
|
||||
assertThat(values).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ofExplicitWhenNonRepeatableReturnsNull() {
|
||||
Object[] values = findRepeatedAnnotationValues(
|
||||
RepeatableContainers.of(ExplicitRepeatable.class,
|
||||
ExplicitContainer.class),
|
||||
WithNonRepeatable.class, NonRepeatable.class);
|
||||
assertThat(values).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ofExplicitWhenStandardRepeatableContainerReturnsNull() {
|
||||
Object[] values = findRepeatedAnnotationValues(
|
||||
RepeatableContainers.of(ExplicitRepeatable.class,
|
||||
ExplicitContainer.class),
|
||||
WithStandardRepeatables.class, StandardContainer.class);
|
||||
assertThat(values).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ofExplicitWhenContainerReturnsRepeats() {
|
||||
Object[] values = findRepeatedAnnotationValues(
|
||||
RepeatableContainers.of(ExplicitRepeatable.class,
|
||||
ExplicitContainer.class),
|
||||
WithExplicitRepeatables.class, ExplicitContainer.class);
|
||||
assertThat(values).containsExactly("a", "b");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ofExplicitWhenHasNoValueThrowsException() {
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(
|
||||
() -> RepeatableContainers.of(ExplicitRepeatable.class,
|
||||
InvalidNoValue.class)).withMessageContaining(
|
||||
"Invalid declaration of container type ["
|
||||
+ InvalidNoValue.class.getName()
|
||||
+ "] for repeatable annotation ["
|
||||
+ ExplicitRepeatable.class.getName() + "]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ofExplicitWhenValueIsNotArrayThrowsException() {
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(
|
||||
() -> RepeatableContainers.of(ExplicitRepeatable.class,
|
||||
InvalidNotArray.class)).withMessage("Container type ["
|
||||
+ InvalidNotArray.class.getName()
|
||||
+ "] must declare a 'value' attribute for an array of type ["
|
||||
+ ExplicitRepeatable.class.getName() + "]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ofExplicitWhenValueIsArrayOfWrongTypeThrowsException() {
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(
|
||||
() -> RepeatableContainers.of(ExplicitRepeatable.class,
|
||||
InvalidWrongArrayType.class)).withMessage("Container type ["
|
||||
+ InvalidWrongArrayType.class.getName()
|
||||
+ "] must declare a 'value' attribute for an array of type ["
|
||||
+ ExplicitRepeatable.class.getName() + "]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ofExplicitWhenAnnotationIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> RepeatableContainers.of(null, null)).withMessage(
|
||||
"Repeatable must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ofExplicitWhenContainerIsNullDeducesContainer() {
|
||||
Object[] values = findRepeatedAnnotationValues(
|
||||
RepeatableContainers.of(StandardRepeatable.class, null),
|
||||
WithStandardRepeatables.class, StandardContainer.class);
|
||||
assertThat(values).containsExactly("a", "b");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ofExplicitWhenContainerIsNullAndNotRepeatableThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> RepeatableContainers.of(
|
||||
ExplicitRepeatable.class, null)).withMessage(
|
||||
"Annotation type must be a repeatable annotation: "
|
||||
+ "failed to resolve container type for "
|
||||
+ ExplicitRepeatable.class.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void standardAndExplicitReturnsRepeats() {
|
||||
RepeatableContainers repeatableContainers = RepeatableContainers.standardRepeatables().and(
|
||||
ExplicitContainer.class, ExplicitRepeatable.class);
|
||||
assertThat(findRepeatedAnnotationValues(repeatableContainers,
|
||||
WithStandardRepeatables.class, StandardContainer.class)).containsExactly(
|
||||
"a", "b");
|
||||
assertThat(findRepeatedAnnotationValues(repeatableContainers,
|
||||
WithExplicitRepeatables.class, ExplicitContainer.class)).containsExactly(
|
||||
"a", "b");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noneAlwaysReturnsNull() {
|
||||
Object[] values = findRepeatedAnnotationValues(
|
||||
RepeatableContainers.none(), WithStandardRepeatables.class,
|
||||
StandardContainer.class);
|
||||
assertThat(values).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equalsAndHashcode() {
|
||||
RepeatableContainers c1 = RepeatableContainers.of(ExplicitRepeatable.class,
|
||||
ExplicitContainer.class);
|
||||
RepeatableContainers c2 = RepeatableContainers.of(ExplicitRepeatable.class,
|
||||
ExplicitContainer.class);
|
||||
RepeatableContainers c3 = RepeatableContainers.standardRepeatables();
|
||||
RepeatableContainers c4 = RepeatableContainers.standardRepeatables().and(
|
||||
ExplicitContainer.class, ExplicitRepeatable.class);
|
||||
assertThat(c1.hashCode()).isEqualTo(c2.hashCode());
|
||||
assertThat(c1).isEqualTo(c1).isEqualTo(c2);
|
||||
assertThat(c1).isNotEqualTo(c3).isNotEqualTo(c4);
|
||||
}
|
||||
|
||||
private Object[] findRepeatedAnnotationValues(RepeatableContainers containers,
|
||||
Class<?> element, Class<? extends Annotation> annotationType) {
|
||||
Annotation[] annotations = containers.findRepeatedAnnotations(
|
||||
element.getAnnotation(annotationType));
|
||||
return extractValues(annotations);
|
||||
}
|
||||
|
||||
private Object[] extractValues(Annotation[] annotations) {
|
||||
try {
|
||||
if (annotations == null) {
|
||||
return null;
|
||||
}
|
||||
Object[] result = new String[annotations.length];
|
||||
for (int i = 0; i < annotations.length; i++) {
|
||||
result[i] = annotations[i].annotationType().getMethod("value").invoke(
|
||||
annotations[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface NonRepeatable {
|
||||
|
||||
String value() default "";
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Repeatable(StandardContainer.class)
|
||||
static @interface StandardRepeatable {
|
||||
|
||||
String value() default "";
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface StandardContainer {
|
||||
|
||||
StandardRepeatable[] value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface ExplicitRepeatable {
|
||||
|
||||
String value() default "";
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface ExplicitContainer {
|
||||
|
||||
ExplicitRepeatable[] value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface InvalidNoValue {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface InvalidNotArray {
|
||||
|
||||
int value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface InvalidWrongArrayType {
|
||||
|
||||
StandardRepeatable[] value();
|
||||
|
||||
}
|
||||
|
||||
@NonRepeatable("a")
|
||||
static class WithNonRepeatable {
|
||||
|
||||
}
|
||||
|
||||
@StandardRepeatable("a")
|
||||
static class WithSingleStandardRepeatable {
|
||||
|
||||
}
|
||||
|
||||
@StandardRepeatable("a")
|
||||
@StandardRepeatable("b")
|
||||
static class WithStandardRepeatables {
|
||||
|
||||
}
|
||||
|
||||
@ExplicitContainer({ @ExplicitRepeatable("a"), @ExplicitRepeatable("b") })
|
||||
static class WithExplicitRepeatables {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Tests for {@link TypeMappedAnnotation}. See also
|
||||
* {@link MergedAnnotationsTests} for a much more extensive collection of tests.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class TypeMappedAnnotationTests {
|
||||
|
||||
@Test
|
||||
public void mappingWhenMirroredReturnsMirroredValues() {
|
||||
testExplicitMirror(WithExplicitMirrorA.class);
|
||||
testExplicitMirror(WithExplicitMirrorB.class);
|
||||
}
|
||||
|
||||
private void testExplicitMirror(Class<?> annotatedClass) {
|
||||
TypeMappedAnnotation<ExplicitMirror> annotation = getTypeMappedAnnotation(
|
||||
annotatedClass, ExplicitMirror.class);
|
||||
assertThat(annotation.getString("a")).isEqualTo("test");
|
||||
assertThat(annotation.getString("b")).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mappingExplicitAliasToMetaAnnotationReturnsMappedValues() {
|
||||
TypeMappedAnnotation<?> annotation = getTypeMappedAnnotation(
|
||||
WithExplicitAliasToMetaAnnotation.class,
|
||||
ExplicitAliasToMetaAnnotation.class,
|
||||
ExplicitAliasMetaAnnotationTarget.class);
|
||||
assertThat(annotation.getString("aliased")).isEqualTo("aliased");
|
||||
assertThat(annotation.getString("nonAliased")).isEqualTo("nonAliased");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mappingConventionAliasToMetaAnnotationReturnsMappedValues() {
|
||||
TypeMappedAnnotation<?> annotation = getTypeMappedAnnotation(
|
||||
WithConventionAliasToMetaAnnotation.class,
|
||||
ConventionAliasToMetaAnnotation.class,
|
||||
ConventionAliasMetaAnnotationTarget.class);
|
||||
assertThat(annotation.getString("value")).isEqualTo("");
|
||||
assertThat(annotation.getString("convention")).isEqualTo("convention");
|
||||
}
|
||||
|
||||
private <A extends Annotation> TypeMappedAnnotation<A> getTypeMappedAnnotation(
|
||||
Class<?> source, Class<A> annotationType) {
|
||||
return getTypeMappedAnnotation(source, annotationType, annotationType);
|
||||
}
|
||||
|
||||
private <A extends Annotation> TypeMappedAnnotation<A> getTypeMappedAnnotation(
|
||||
Class<?> source, Class<? extends Annotation> rootAnnotationType,
|
||||
Class<A> annotationType) {
|
||||
Annotation rootAnnotation = source.getAnnotation(rootAnnotationType);
|
||||
AnnotationTypeMapping mapping = getMapping(rootAnnotation, annotationType);
|
||||
return TypeMappedAnnotation.createIfPossible(mapping, source, rootAnnotation, 0, IntrospectionFailureLogger.INFO);
|
||||
}
|
||||
|
||||
private AnnotationTypeMapping getMapping(Annotation annotation,
|
||||
Class<? extends Annotation> mappedAnnotationType) {
|
||||
AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(
|
||||
annotation.annotationType());
|
||||
for (int i = 0; i < mappings.size(); i++) {
|
||||
AnnotationTypeMapping candidate = mappings.get(i);
|
||||
if (candidate.getAnnotationType().equals(mappedAnnotationType)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
"No mapping from " + annotation + " to " + mappedAnnotationType);
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface ExplicitMirror {
|
||||
|
||||
@AliasFor("b")
|
||||
String a() default "";
|
||||
|
||||
@AliasFor("a")
|
||||
String b() default "";
|
||||
|
||||
}
|
||||
|
||||
@ExplicitMirror(a = "test")
|
||||
static class WithExplicitMirrorA {
|
||||
|
||||
}
|
||||
|
||||
@ExplicitMirror(b = "test")
|
||||
static class WithExplicitMirrorB {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@ExplicitAliasMetaAnnotationTarget(nonAliased = "nonAliased")
|
||||
static @interface ExplicitAliasToMetaAnnotation {
|
||||
|
||||
@AliasFor(annotation = ExplicitAliasMetaAnnotationTarget.class)
|
||||
String aliased() default "";
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface ExplicitAliasMetaAnnotationTarget {
|
||||
|
||||
String aliased() default "";
|
||||
|
||||
String nonAliased() default "";
|
||||
|
||||
}
|
||||
|
||||
@ExplicitAliasToMetaAnnotation(aliased = "aliased")
|
||||
private static class WithExplicitAliasToMetaAnnotation {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@ConventionAliasMetaAnnotationTarget
|
||||
static @interface ConventionAliasToMetaAnnotation {
|
||||
|
||||
String value() default "";
|
||||
|
||||
String convention() default "";
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
static @interface ConventionAliasMetaAnnotationTarget {
|
||||
|
||||
String value() default "";
|
||||
|
||||
String convention() default "";
|
||||
|
||||
}
|
||||
|
||||
@ConventionAliasToMetaAnnotation(value = "value", convention = "convention")
|
||||
private static class WithConventionAliasToMetaAnnotation {
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue