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:
Phillip Webb 2019-01-31 11:03:57 -08:00 committed by Juergen Hoeller
parent fdacda8b01
commit 4972d85ae0
35 changed files with 13436 additions and 0 deletions

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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];
}
}
}
}

View File

@ -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 &lt; 0 || index &gt;= 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);
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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 &lt; 0 || index &gt;= 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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -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;
}
}
}

View File

@ -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">
* &#064;Retention(RetentionPolicy.RUNTIME)
* &#064;RequestMapping(method = RequestMethod.POST)
* public &#064;interface PostMapping {
*
* &#064;AliasFor(attribute = "path")
* String[] value() default {};
*
* &#064;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
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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
}
}

View File

@ -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 {
}
}

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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";
}
}

View File

@ -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
}
}

View File

@ -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 {
}
}

View File

@ -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 {
}
}

View File

@ -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
}

View File

@ -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 {
}
}

View File

@ -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);
}
}

View File

@ -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 {
}
}

View File

@ -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 {
}
}