Browse Source
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
pull/22645/head
35 changed files with 13436 additions and 0 deletions
@ -0,0 +1,226 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.reflect.Array; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.NoSuchElementException; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.function.Predicate; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
|
||||||
|
/** |
||||||
|
* Abstract base class for {@link MergedAnnotation} implementations. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
* @param <A> the annotation type |
||||||
|
*/ |
||||||
|
abstract class AbstractMergedAnnotation<A extends Annotation> |
||||||
|
implements MergedAnnotation<A> { |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private volatile A synthesizedAnnotation; |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isDirectlyPresent() { |
||||||
|
return isPresent() && getDepth() == 0; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isMetaPresent() { |
||||||
|
return isPresent() && getDepth() > 0; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean hasNonDefaultValue(String attributeName) { |
||||||
|
return !hasDefaultValue(attributeName); |
||||||
|
} |
||||||
|
|
||||||
|
public byte getByte(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, Byte.class); |
||||||
|
} |
||||||
|
|
||||||
|
public byte[] getByteArray(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, byte[].class); |
||||||
|
} |
||||||
|
|
||||||
|
public boolean getBoolean(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, Boolean.class); |
||||||
|
} |
||||||
|
|
||||||
|
public boolean[] getBooleanArray(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, boolean[].class); |
||||||
|
} |
||||||
|
|
||||||
|
public char getChar(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, Character.class); |
||||||
|
} |
||||||
|
|
||||||
|
public char[] getCharArray(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, char[].class); |
||||||
|
} |
||||||
|
|
||||||
|
public short getShort(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, Short.class); |
||||||
|
} |
||||||
|
|
||||||
|
public short[] getShortArray(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, short[].class); |
||||||
|
} |
||||||
|
|
||||||
|
public int getInt(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, Integer.class); |
||||||
|
} |
||||||
|
|
||||||
|
public int[] getIntArray(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, int[].class); |
||||||
|
} |
||||||
|
|
||||||
|
public long getLong(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, Long.class); |
||||||
|
} |
||||||
|
|
||||||
|
public long[] getLongArray(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, long[].class); |
||||||
|
} |
||||||
|
|
||||||
|
public double getDouble(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, Double.class); |
||||||
|
} |
||||||
|
|
||||||
|
public double[] getDoubleArray(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, double[].class); |
||||||
|
} |
||||||
|
|
||||||
|
public float getFloat(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, Float.class); |
||||||
|
} |
||||||
|
|
||||||
|
public float[] getFloatArray(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, float[].class); |
||||||
|
} |
||||||
|
|
||||||
|
public String getString(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, String.class); |
||||||
|
} |
||||||
|
|
||||||
|
public String[] getStringArray(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, String[].class); |
||||||
|
} |
||||||
|
|
||||||
|
public Class<?> getClass(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, Class.class); |
||||||
|
} |
||||||
|
|
||||||
|
public Class<?>[] getClassArray(String attributeName) { |
||||||
|
return getRequiredAttributeValue(attributeName, Class[].class); |
||||||
|
} |
||||||
|
|
||||||
|
public <E extends Enum<E>> E getEnum(String attributeName, Class<E> type) { |
||||||
|
Assert.notNull(type, "Type must not be null"); |
||||||
|
return getRequiredAttributeValue(attributeName, type); |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
public <E extends Enum<E>> E[] getEnumArray(String attributeName, Class<E> type) { |
||||||
|
Assert.notNull(type, "Type must not be null"); |
||||||
|
Class<?> arrayType = Array.newInstance(type, 0).getClass(); |
||||||
|
return (E[]) getRequiredAttributeValue(attributeName, arrayType); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Optional<Object> getValue(String attributeName) { |
||||||
|
return getValue(attributeName, Object.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public <T> Optional<T> getValue(String attributeName, Class<T> type) { |
||||||
|
return Optional.ofNullable(getAttributeValue(attributeName, type)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Optional<Object> getDefaultValue(String attributeName) { |
||||||
|
return getDefaultValue(attributeName, Object.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public MergedAnnotation<A> filterDefaultValues() { |
||||||
|
return filterAttributes(this::hasNonDefaultValue); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Map<String, Object> asMap(MapValues... options) { |
||||||
|
return asMap(null, options); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Optional<A> synthesize( |
||||||
|
@Nullable Predicate<? super MergedAnnotation<A>> condition) |
||||||
|
throws NoSuchElementException { |
||||||
|
|
||||||
|
if (condition == null || condition.test(this)) { |
||||||
|
return Optional.of(synthesize()); |
||||||
|
} |
||||||
|
return Optional.empty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public A synthesize() { |
||||||
|
if (!isPresent()) { |
||||||
|
throw new NoSuchElementException("Unable to synthesize missing annotation"); |
||||||
|
} |
||||||
|
A synthesized = this.synthesizedAnnotation; |
||||||
|
if (synthesized == null) { |
||||||
|
synthesized = createSynthesized(); |
||||||
|
this.synthesizedAnnotation = synthesized; |
||||||
|
} |
||||||
|
return synthesized; |
||||||
|
} |
||||||
|
|
||||||
|
private <T> T getRequiredAttributeValue(String attributeName, Class<T> type) { |
||||||
|
T value = getAttributeValue(attributeName, type); |
||||||
|
if (value == null) { |
||||||
|
throw new NoSuchElementException("No attribute named '" + attributeName + |
||||||
|
"' present in merged annotation " + getType()); |
||||||
|
} |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the underlying attribute value. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @param type the type to return (see {@link MergedAnnotation} class
|
||||||
|
* documentation for details). |
||||||
|
* @return the attribute value or {@code null} if the value is not found and |
||||||
|
* is not required |
||||||
|
* @throws IllegalArgumentException if the source type is not compatible |
||||||
|
* @throws NoSuchElementException if the value is required but not found |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
protected abstract <T> T getAttributeValue(String attributeName, Class<T> type); |
||||||
|
|
||||||
|
/** |
||||||
|
* Factory method used to create the synthesized annotation. |
||||||
|
*/ |
||||||
|
protected abstract A createSynthesized(); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,179 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Collection; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
|
||||||
|
/** |
||||||
|
* Callback interface that can be used to filter specific annotation types. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
@FunctionalInterface |
||||||
|
public interface AnnotationFilter { |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link AnnotationFilter} that matches annotations is in the |
||||||
|
* {@code java.lang.*} or in the |
||||||
|
* {@code org.springframework.lang.*} package. |
||||||
|
*/ |
||||||
|
static final AnnotationFilter PLAIN = packages("java.lang", |
||||||
|
"org.springframework.lang"); |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link AnnotationFilter} that matches annotations in the |
||||||
|
* {@code java.lang.*} package. |
||||||
|
*/ |
||||||
|
static final AnnotationFilter JAVA = packages("java.lang"); |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link AnnotationFilter} that never matches and can be used when no |
||||||
|
* filtering is needed. |
||||||
|
*/ |
||||||
|
static final AnnotationFilter NONE = new AnnotationFilter() { |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean matches(@Nullable String typeName) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
return "No annotation filtering"; |
||||||
|
} |
||||||
|
|
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Test if the given annotation matches the filter. |
||||||
|
* @param annotation the annotation to test |
||||||
|
* @return {@code true} if the annotation matches |
||||||
|
*/ |
||||||
|
default boolean matches(@Nullable Annotation annotation) { |
||||||
|
return matches(annotation != null ? annotation.annotationType() : null); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Test if the given type matches the filter. |
||||||
|
* @param type the annotation type to test |
||||||
|
* @return {@code true} if the annotation matches |
||||||
|
*/ |
||||||
|
default boolean matches(@Nullable Class<?> type) { |
||||||
|
return matches(type != null ? type.getName() : null); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Test if the given type name matches the filter. |
||||||
|
* @param typeName the annotation type to test |
||||||
|
* @return {@code true} if the annotation matches |
||||||
|
*/ |
||||||
|
boolean matches(@Nullable String typeName); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a new {@link AnnotationFilter} that matches annotations in the |
||||||
|
* specified packages. |
||||||
|
* @param packages the annotation packages that should match |
||||||
|
* @return a new {@link AnnotationFilter} instance |
||||||
|
*/ |
||||||
|
static AnnotationFilter packages(String... packages) { |
||||||
|
return new PackagesAnnotationFilter(packages); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return an {@link AnnotationFilter} that is the most appropriate for, and |
||||||
|
* will always match the given annotation type. Whenever possible, |
||||||
|
* {@link AnnotationFilter#PLAIN} will be returned. |
||||||
|
* @param annotationType the annotation type to check |
||||||
|
* @return the most appropriate annotation filter |
||||||
|
*/ |
||||||
|
static AnnotationFilter mostAppropriateFor(@Nullable Class<?> annotationType) { |
||||||
|
return PLAIN.matches(annotationType) ? NONE : PLAIN; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return an {@link AnnotationFilter} that is the most appropriate for, and |
||||||
|
* will always match all the given annotation types. Whenever possible, |
||||||
|
* {@link AnnotationFilter#PLAIN} will be returned. |
||||||
|
* @param annotationTypes the annotation types to check |
||||||
|
* @return the most appropriate annotation filter |
||||||
|
*/ |
||||||
|
static AnnotationFilter mostAppropriateFor(Class<?>... annotationTypes) { |
||||||
|
Assert.notNull(annotationTypes, "AnnotationTypes must not be null"); |
||||||
|
return mostAppropriateFor(Arrays.asList(annotationTypes)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return an {@link AnnotationFilter} that is the most appropriate for, and |
||||||
|
* will always match all the given annotation type. Whenever possible, |
||||||
|
* {@link AnnotationFilter#PLAIN} will be returned. |
||||||
|
* @param annotationType the annotation type to check |
||||||
|
* @return the most appropriate annotation filter |
||||||
|
*/ |
||||||
|
static AnnotationFilter mostAppropriateFor(@Nullable String annotationType) { |
||||||
|
return PLAIN.matches(annotationType) ? NONE : PLAIN; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return an {@link AnnotationFilter} that is the most appropriate for, and |
||||||
|
* will always match all the given annotation types. Whenever possible, |
||||||
|
* {@link AnnotationFilter#PLAIN} will be returned. |
||||||
|
* @param annotationTypes the annotation types to check |
||||||
|
* @return the most appropriate annotation filter |
||||||
|
*/ |
||||||
|
static AnnotationFilter mostAppropriateFor(String... annotationTypes) { |
||||||
|
Assert.notNull(annotationTypes, "AnnotationTypes must not be null"); |
||||||
|
return mostAppropriateFor(Arrays.asList(annotationTypes)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return an {@link AnnotationFilter} that is the most appropriate for, and |
||||||
|
* will always match all the given annotation types. Whenever possible, |
||||||
|
* {@link AnnotationFilter#PLAIN} will be returned. |
||||||
|
* @param annotationTypes the annotation types to check (may be class names |
||||||
|
* or class types) |
||||||
|
* @return the most appropriate annotation filter |
||||||
|
*/ |
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
static AnnotationFilter mostAppropriateFor(Collection<?> annotationTypes) { |
||||||
|
Assert.notNull(annotationTypes, "AnnotationTypes must not be null"); |
||||||
|
for (Object annotationType : annotationTypes) { |
||||||
|
if (annotationType == null) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
Assert.isTrue( |
||||||
|
annotationType instanceof Class || annotationType instanceof String, |
||||||
|
"AnnotationType must be a Class or String"); |
||||||
|
if (annotationType instanceof Class |
||||||
|
&& PLAIN.matches((Class<Annotation>) annotationType)) { |
||||||
|
return NONE; |
||||||
|
} |
||||||
|
if (annotationType instanceof String |
||||||
|
&& PLAIN.matches((String) annotationType)) { |
||||||
|
return NONE; |
||||||
|
} |
||||||
|
} |
||||||
|
return PLAIN; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,619 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Collection; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.HashSet; |
||||||
|
import java.util.LinkedHashSet; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Objects; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.function.BiFunction; |
||||||
|
|
||||||
|
import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.ObjectUtils; |
||||||
|
import org.springframework.util.ReflectionUtils; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* Provides mapping information for a single annotation (or meta-annotation) in |
||||||
|
* the context of a root annotation type. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
* @see AnnotationTypeMappings |
||||||
|
*/ |
||||||
|
class AnnotationTypeMapping { |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private final AnnotationTypeMapping parent; |
||||||
|
|
||||||
|
private final AnnotationTypeMapping root; |
||||||
|
|
||||||
|
private final int depth; |
||||||
|
|
||||||
|
private final Class<? extends Annotation> annotationType; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private final Annotation annotation; |
||||||
|
|
||||||
|
private final AttributeMethods attributes; |
||||||
|
|
||||||
|
private final MirrorSets mirrorSets; |
||||||
|
|
||||||
|
private final int[] aliasMappings; |
||||||
|
|
||||||
|
private final int[] conventionMappings; |
||||||
|
|
||||||
|
private final Map<Method, List<Method>> aliasedBy; |
||||||
|
|
||||||
|
private final Set<Method> claimedAliases = new HashSet<>(); |
||||||
|
|
||||||
|
|
||||||
|
AnnotationTypeMapping(Class<? extends Annotation> annotationType) { |
||||||
|
this(null, annotationType, null); |
||||||
|
} |
||||||
|
|
||||||
|
AnnotationTypeMapping(AnnotationTypeMapping parent, Annotation annotation) { |
||||||
|
this(parent, annotation.annotationType(), annotation); |
||||||
|
} |
||||||
|
|
||||||
|
AnnotationTypeMapping(@Nullable AnnotationTypeMapping parent, |
||||||
|
Class<? extends Annotation> annotationType, @Nullable Annotation annotation) { |
||||||
|
|
||||||
|
this.parent = parent; |
||||||
|
this.root = parent != null ? parent.getRoot() : this; |
||||||
|
this.depth = parent == null ? 0 : parent.getDepth() + 1; |
||||||
|
this.annotationType = annotationType; |
||||||
|
this.annotation = annotation; |
||||||
|
this.attributes = AttributeMethods.forAnnotationType(annotationType); |
||||||
|
this.mirrorSets = new MirrorSets(); |
||||||
|
this.aliasMappings = filledIntArray(this.attributes.size(), -1); |
||||||
|
this.conventionMappings = filledIntArray(this.attributes.size(), -1); |
||||||
|
this.aliasedBy = resolveAliasedForTargets(); |
||||||
|
processAliases(); |
||||||
|
addConventionMappings(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private Map<Method, List<Method>> resolveAliasedForTargets() { |
||||||
|
Map<Method, List<Method>> aliasedBy = new HashMap<>(); |
||||||
|
for (int i = 0; i < this.attributes.size(); i++) { |
||||||
|
Method attribute = this.attributes.get(i); |
||||||
|
AliasFor aliasFor = AnnotationsScanner.getDeclaredAnnotation(attribute, |
||||||
|
AliasFor.class); |
||||||
|
if (aliasFor != null) { |
||||||
|
Method target = resolveAliasTarget(attribute, aliasFor); |
||||||
|
aliasedBy.computeIfAbsent(target, key -> new ArrayList<>()).add( |
||||||
|
attribute); |
||||||
|
} |
||||||
|
} |
||||||
|
return Collections.unmodifiableMap(aliasedBy); |
||||||
|
} |
||||||
|
|
||||||
|
private Method resolveAliasTarget(Method attribute, AliasFor aliasFor) { |
||||||
|
return resolveAliasTarget(attribute, aliasFor, true); |
||||||
|
} |
||||||
|
|
||||||
|
private Method resolveAliasTarget(Method attribute, AliasFor aliasFor, boolean checkAliasPair) { |
||||||
|
if (StringUtils.hasText(aliasFor.value()) && |
||||||
|
StringUtils.hasText(aliasFor.attribute())) { |
||||||
|
throw new AnnotationConfigurationException(String.format( |
||||||
|
"In @AliasFor declared on %s, attribute 'attribute' and its alias " |
||||||
|
+ "'value' are present with values of '%s' and '%s', but " |
||||||
|
+ "only one is permitted.", |
||||||
|
AttributeMethods.describe(attribute), aliasFor.attribute(), |
||||||
|
aliasFor.value())); |
||||||
|
} |
||||||
|
Class<? extends Annotation> targetAnnotation = aliasFor.annotation(); |
||||||
|
if (targetAnnotation == Annotation.class) { |
||||||
|
targetAnnotation = this.annotationType; |
||||||
|
} |
||||||
|
String targetAttributeName = aliasFor.attribute(); |
||||||
|
if (!StringUtils.hasLength(targetAttributeName)) { |
||||||
|
targetAttributeName = aliasFor.value(); |
||||||
|
} |
||||||
|
if (!StringUtils.hasLength(targetAttributeName)) { |
||||||
|
targetAttributeName = attribute.getName(); |
||||||
|
} |
||||||
|
Method target = AttributeMethods.forAnnotationType(targetAnnotation) |
||||||
|
.get(targetAttributeName); |
||||||
|
if (target == null) { |
||||||
|
if (targetAnnotation == this.annotationType) { |
||||||
|
throw new AnnotationConfigurationException(String.format( |
||||||
|
"@AliasFor declaration on %s declares an " |
||||||
|
+ "alias for '%s' which is not present.", |
||||||
|
AttributeMethods.describe(attribute), targetAttributeName)); |
||||||
|
} |
||||||
|
throw new AnnotationConfigurationException(String.format( |
||||||
|
"%s is declared as an @AliasFor nonexistent %s.", |
||||||
|
StringUtils.capitalize(AttributeMethods.describe(attribute)), |
||||||
|
AttributeMethods.describe(targetAnnotation, targetAttributeName))); |
||||||
|
} |
||||||
|
if (target == attribute) { |
||||||
|
throw new AnnotationConfigurationException(String.format( |
||||||
|
"@AliasFor declaration on %s points to itself. " |
||||||
|
+ "Specify 'annotation' to point to a same-named " |
||||||
|
+ "attribute on a meta-annotation.", |
||||||
|
AttributeMethods.describe(attribute))); |
||||||
|
} |
||||||
|
if (!isCompatibleReturnType(attribute.getReturnType(), target.getReturnType())) { |
||||||
|
throw new AnnotationConfigurationException(String.format( |
||||||
|
"Misconfigured aliases: %s and %s must declare the same return type.", |
||||||
|
AttributeMethods.describe(attribute), |
||||||
|
AttributeMethods.describe(target))); |
||||||
|
} |
||||||
|
if (isAliasPair(target) && checkAliasPair) { |
||||||
|
AliasFor targetAliasFor = target.getAnnotation(AliasFor.class); |
||||||
|
if (targetAliasFor == null) { |
||||||
|
throw new AnnotationConfigurationException( |
||||||
|
String.format("%s must be declared as an @AliasFor '%s'.", |
||||||
|
StringUtils.capitalize(AttributeMethods.describe(target)), |
||||||
|
attribute.getName())); |
||||||
|
} |
||||||
|
Method mirror = resolveAliasTarget(target, targetAliasFor, false); |
||||||
|
if (mirror != attribute) { |
||||||
|
throw new AnnotationConfigurationException(String.format( |
||||||
|
"%s must be declared as an @AliasFor '%s', not '%s'.", |
||||||
|
StringUtils.capitalize(AttributeMethods.describe(target)), |
||||||
|
attribute.getName(), mirror.getName())); |
||||||
|
} |
||||||
|
} |
||||||
|
return target; |
||||||
|
} |
||||||
|
|
||||||
|
private boolean isAliasPair(Method target) { |
||||||
|
return target.getDeclaringClass().equals(this.annotationType); |
||||||
|
} |
||||||
|
|
||||||
|
private boolean isCompatibleReturnType(Class<?> attributeType, Class<?> targetType) { |
||||||
|
return Objects.equals(attributeType, targetType) || |
||||||
|
Objects.equals(attributeType, targetType.getComponentType()); |
||||||
|
} |
||||||
|
|
||||||
|
private void processAliases() { |
||||||
|
List<Method> aliases = new ArrayList<>(); |
||||||
|
for (int i = 0; i < this.attributes.size(); i++) { |
||||||
|
aliases.clear(); |
||||||
|
aliases.add(this.attributes.get(i)); |
||||||
|
collectAliases(aliases); |
||||||
|
if (aliases.size() > 1) { |
||||||
|
processAliases(aliases); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void collectAliases(List<Method> aliases) { |
||||||
|
AnnotationTypeMapping mapping = this; |
||||||
|
while (mapping != null) { |
||||||
|
int size = aliases.size(); |
||||||
|
for (int j = 0; j < size; j++) { |
||||||
|
List<Method> additional = mapping.aliasedBy.get(aliases.get(j)); |
||||||
|
if (additional != null) { |
||||||
|
aliases.addAll(additional); |
||||||
|
} |
||||||
|
} |
||||||
|
mapping = mapping.parent; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void processAliases(List<Method> aliases) { |
||||||
|
int rootAttributeIndex = getFirstRootAttributeIndex(aliases); |
||||||
|
AnnotationTypeMapping mapping = this; |
||||||
|
while (mapping != null) { |
||||||
|
if (rootAttributeIndex != -1 && mapping != this.root) { |
||||||
|
for (int i = 0; i < mapping.attributes.size(); i++) { |
||||||
|
if (aliases.contains(mapping.attributes.get(i))) { |
||||||
|
mapping.aliasMappings[i] = rootAttributeIndex; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
mapping.mirrorSets.updateFrom(aliases); |
||||||
|
mapping.claimedAliases.addAll(aliases); |
||||||
|
mapping = mapping.parent; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private int getFirstRootAttributeIndex(Collection<Method> aliases) { |
||||||
|
AttributeMethods rootAttributes = this.root.getAttributes(); |
||||||
|
for (int i = 0; i < rootAttributes.size(); i++) { |
||||||
|
if (aliases.contains(rootAttributes.get(i))) { |
||||||
|
return i; |
||||||
|
} |
||||||
|
} |
||||||
|
return -1; |
||||||
|
} |
||||||
|
|
||||||
|
private void addConventionMappings() { |
||||||
|
if (this.depth == 0) { |
||||||
|
return; |
||||||
|
} |
||||||
|
AttributeMethods rootAttributes = this.root.getAttributes(); |
||||||
|
int[] mappings = this.conventionMappings; |
||||||
|
for (int i = 0; i < mappings.length; i++) { |
||||||
|
String name = this.attributes.get(i).getName(); |
||||||
|
MirrorSet mirrors = getMirrorSets().getAssigned(i); |
||||||
|
int mapped = rootAttributes.indexOf(name); |
||||||
|
if (!MergedAnnotation.VALUE.equals(name) && mapped != -1) { |
||||||
|
mappings[i] = mapped; |
||||||
|
if (mirrors != null) { |
||||||
|
for (int j = 0; j < mirrors.size(); j++) { |
||||||
|
mappings[mirrors.getAttributeIndex(j)] = mapped; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Method called after all mappings have been set. At this point no further |
||||||
|
* lookups from child mappings will occur. |
||||||
|
*/ |
||||||
|
void afterAllMappingsSet() { |
||||||
|
validateAllAliasesClaimed(); |
||||||
|
for (int i = 0; i < this.mirrorSets.size(); i++) { |
||||||
|
validateMirrorSet(this.mirrorSets.get(i)); |
||||||
|
} |
||||||
|
this.claimedAliases.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
private void validateAllAliasesClaimed() { |
||||||
|
for (int i = 0; i < this.attributes.size(); i++) { |
||||||
|
Method attribute = this.attributes.get(i); |
||||||
|
AliasFor aliasFor = AnnotationsScanner.getDeclaredAnnotation(attribute, AliasFor.class); |
||||||
|
if (aliasFor != null && !this.claimedAliases.contains(attribute)) { |
||||||
|
Method target = resolveAliasTarget(attribute, aliasFor); |
||||||
|
throw new AnnotationConfigurationException(String.format( |
||||||
|
"@AliasFor declaration on %s declares an alias for %s which is not meta-present.", |
||||||
|
AttributeMethods.describe(attribute), |
||||||
|
AttributeMethods.describe(target))); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void validateMirrorSet(MirrorSet mirrorSet) { |
||||||
|
Method firstAttribute = mirrorSet.get(0); |
||||||
|
Object firstDefaultValue = firstAttribute.getDefaultValue(); |
||||||
|
for (int i = 1; i <= mirrorSet.size() - 1; i++) { |
||||||
|
Method mirrorAttribute = mirrorSet.get(i); |
||||||
|
Object mirrorDefaultValue = mirrorAttribute.getDefaultValue(); |
||||||
|
if (firstDefaultValue == null || mirrorDefaultValue == null) { |
||||||
|
throw new AnnotationConfigurationException(String.format( |
||||||
|
"Misconfigured aliases: %s and %s must declare default values.", |
||||||
|
AttributeMethods.describe(firstAttribute), |
||||||
|
AttributeMethods.describe(mirrorAttribute))); |
||||||
|
} |
||||||
|
if (!ObjectUtils.nullSafeEquals(firstDefaultValue, mirrorDefaultValue)) { |
||||||
|
throw new AnnotationConfigurationException(String.format( |
||||||
|
"Misconfigured aliases: %s and %s must declare the same default value.", |
||||||
|
AttributeMethods.describe(firstAttribute), |
||||||
|
AttributeMethods.describe(mirrorAttribute))); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the root mapping. |
||||||
|
* @return the root mapping |
||||||
|
*/ |
||||||
|
AnnotationTypeMapping getRoot() { |
||||||
|
return this.root; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the parent mapping or {@code null}. |
||||||
|
* @return the parent mapping |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
AnnotationTypeMapping getParent() { |
||||||
|
return this.parent; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the depth of this mapping. |
||||||
|
* @return the depth of the mapping |
||||||
|
*/ |
||||||
|
int getDepth() { |
||||||
|
return this.depth; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the type of the mapped annotation. |
||||||
|
* @return the annotation type |
||||||
|
*/ |
||||||
|
Class<? extends Annotation> getAnnotationType() { |
||||||
|
return this.annotationType; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the source annotation for this mapping. This will be the |
||||||
|
* meta-annotation, or {@code null} if this is the root mapping. |
||||||
|
* @return the source annotation of the mapping |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
Annotation getAnnotation() { |
||||||
|
return this.annotation; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the annotation attributes for the mapping annotation type. |
||||||
|
* @return the attribute methods |
||||||
|
*/ |
||||||
|
AttributeMethods getAttributes() { |
||||||
|
return this.attributes; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the related index of an alias mapped attribute, or {@code -1} if |
||||||
|
* there is no mapping. The resulting value is the index of the attribute on |
||||||
|
* the root annotation that can be invoked in order to obtain the actual |
||||||
|
* value. |
||||||
|
* @param attributeIndex the attribute index of the source attribute |
||||||
|
* @return the mapped attribute index or {@code -1} |
||||||
|
*/ |
||||||
|
int getAliasMapping(int attributeIndex) { |
||||||
|
return this.aliasMappings[attributeIndex]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the related index of a convention mapped attribute, or {@code -1} |
||||||
|
* if there is no mapping. The resulting value is the index of the attribute |
||||||
|
* on the root annotation that can be invoked in order to obtain the actual |
||||||
|
* value. |
||||||
|
* @param attributeIndex the attribute index of the source attribute |
||||||
|
* @return the mapped attribute index or {@code -1} |
||||||
|
*/ |
||||||
|
int getConventionMapping(int attributeIndex) { |
||||||
|
return this.conventionMappings[attributeIndex]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return if the specified value is equivalent to the default value of the |
||||||
|
* attribute at the given index. |
||||||
|
* @param attributeIndex the attribute index of the source attribute |
||||||
|
* @param value the value to check |
||||||
|
* @param valueExtractor the value extractor used to extract value from any |
||||||
|
* nested annotations |
||||||
|
* @return {@code true} if the value is equivalent to the default value |
||||||
|
*/ |
||||||
|
boolean isEquivalentToDefaultValue(int attributeIndex, Object value, |
||||||
|
BiFunction<Method, Object, Object> valueExtractor) { |
||||||
|
Method attribute = this.attributes.get(attributeIndex); |
||||||
|
return isEquivalentToDefaultValue(attribute, value, valueExtractor); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the mirror sets for this type mapping. |
||||||
|
* @return the mirrorSets the attribute mirror sets. |
||||||
|
*/ |
||||||
|
MirrorSets getMirrorSets() { |
||||||
|
return this.mirrorSets; |
||||||
|
} |
||||||
|
|
||||||
|
private static int[] filledIntArray(int size, int value) { |
||||||
|
int[] array = new int[size]; |
||||||
|
Arrays.fill(array, value); |
||||||
|
return array; |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean isEquivalentToDefaultValue(Method attribute, Object value, |
||||||
|
BiFunction<Method, Object, Object> valueExtractor) { |
||||||
|
return areEquivalent(attribute.getDefaultValue(), value, valueExtractor); |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean areEquivalent(@Nullable Object value, |
||||||
|
@Nullable Object extractedValue, |
||||||
|
BiFunction<Method, Object, Object> valueExtractor) { |
||||||
|
if (ObjectUtils.nullSafeEquals(value, extractedValue)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
if (value instanceof Class && extractedValue instanceof String) { |
||||||
|
return areEquivalent((Class<?>) value, (String) extractedValue); |
||||||
|
} |
||||||
|
if (value instanceof Class[] && extractedValue instanceof String[]) { |
||||||
|
return areEquivalent((Class[]) value, (String[]) extractedValue); |
||||||
|
} |
||||||
|
if (value instanceof Annotation) { |
||||||
|
return areEquivalent((Annotation) value, extractedValue, valueExtractor); |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean areEquivalent(Class<?>[] value, String[] extractedValue) { |
||||||
|
if (value.length != extractedValue.length) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
for (int i = 0; i < value.length; i++) { |
||||||
|
if (!areEquivalent(value[i], extractedValue[i])) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean areEquivalent(Class<?> value, String extractedValue) { |
||||||
|
return value.getName().equals(extractedValue); |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean areEquivalent(Annotation value, @Nullable Object extractedValue, |
||||||
|
BiFunction<Method, Object, Object> valueExtractor) { |
||||||
|
AttributeMethods attributes = AttributeMethods.forAnnotationType( |
||||||
|
value.annotationType()); |
||||||
|
for (int i = 0; i < attributes.size(); i++) { |
||||||
|
Method attribute = attributes.get(i); |
||||||
|
if (!areEquivalent(ReflectionUtils.invokeMethod(attribute, value), |
||||||
|
valueExtractor.apply(attribute, extractedValue), valueExtractor)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* A collection of {@link MirrorSet} instances that provides details of all |
||||||
|
* defined mirrors. |
||||||
|
*/ |
||||||
|
class MirrorSets { |
||||||
|
|
||||||
|
private MirrorSet[] mirrorSets; |
||||||
|
|
||||||
|
private final MirrorSet[] assigned; |
||||||
|
|
||||||
|
|
||||||
|
MirrorSets() { |
||||||
|
this.assigned = new MirrorSet[attributes.size()]; |
||||||
|
this.mirrorSets = new MirrorSet[0]; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
void updateFrom(Collection<Method> aliases) { |
||||||
|
MirrorSet mirrorSet = null; |
||||||
|
int size = 0; |
||||||
|
int last = -1; |
||||||
|
for (int i = 0; i < attributes.size(); i++) { |
||||||
|
Method attribute = attributes.get(i); |
||||||
|
if (aliases.contains(attribute)) { |
||||||
|
size++; |
||||||
|
if (size > 1) { |
||||||
|
if (mirrorSet == null) { |
||||||
|
mirrorSet = new MirrorSet(); |
||||||
|
this.assigned[last] = mirrorSet; |
||||||
|
} |
||||||
|
this.assigned[i] = mirrorSet; |
||||||
|
} |
||||||
|
last = i; |
||||||
|
} |
||||||
|
} |
||||||
|
if (mirrorSet != null) { |
||||||
|
mirrorSet.update(); |
||||||
|
LinkedHashSet<MirrorSet> unique = new LinkedHashSet<>( |
||||||
|
Arrays.asList(this.assigned)); |
||||||
|
unique.remove(null); |
||||||
|
this.mirrorSets = unique.toArray(new MirrorSet[0]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
int size() { |
||||||
|
return this.mirrorSets.length; |
||||||
|
} |
||||||
|
|
||||||
|
MirrorSet get(int index) { |
||||||
|
return this.mirrorSets[index]; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
MirrorSet getAssigned(int attributeIndex) { |
||||||
|
return this.assigned[attributeIndex]; |
||||||
|
} |
||||||
|
|
||||||
|
int[] resolve(@Nullable Object source, @Nullable Object annotation, |
||||||
|
BiFunction<Method, Object, Object> valueExtractor) { |
||||||
|
|
||||||
|
int[] result = new int[attributes.size()]; |
||||||
|
for (int i = 0; i < result.length; i++) { |
||||||
|
result[i] = i; |
||||||
|
} |
||||||
|
for (int i = 0; i < size(); i++) { |
||||||
|
MirrorSet mirrorSet = get(i); |
||||||
|
int resolved = mirrorSet.resolve(source, annotation, valueExtractor); |
||||||
|
for (int j = 0; j < mirrorSet.size; j++) { |
||||||
|
result[mirrorSet.indexes[j]] = resolved; |
||||||
|
} |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* A single set of mirror attributes. |
||||||
|
*/ |
||||||
|
class MirrorSet { |
||||||
|
|
||||||
|
private int size; |
||||||
|
|
||||||
|
private final int[] indexes = new int[attributes.size()]; |
||||||
|
|
||||||
|
|
||||||
|
void update() { |
||||||
|
this.size = 0; |
||||||
|
Arrays.fill(this.indexes, -1); |
||||||
|
for (int i = 0; i < MirrorSets.this.assigned.length; i++) { |
||||||
|
if (MirrorSets.this.assigned[i] == this) { |
||||||
|
this.indexes[this.size] = i; |
||||||
|
this.size++; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
<A> int resolve(@Nullable Object source, @Nullable A annotation, |
||||||
|
BiFunction<Method, Object, Object> valueExtractor) { |
||||||
|
|
||||||
|
int result = -1; |
||||||
|
Object lastValue = null; |
||||||
|
for (int i = 0; i < this.size; i++) { |
||||||
|
Method attribute = attributes.get(this.indexes[i]); |
||||||
|
Object value = valueExtractor.apply(attribute, annotation); |
||||||
|
boolean isDefaultValue = value == null || isEquivalentToDefaultValue( |
||||||
|
attribute, value, valueExtractor); |
||||||
|
if (isDefaultValue || ObjectUtils.nullSafeEquals(lastValue, value)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
if (lastValue != null && |
||||||
|
!ObjectUtils.nullSafeEquals(lastValue, value)) { |
||||||
|
String on = (source != null) ? " declared on " + source : ""; |
||||||
|
throw new AnnotationConfigurationException(String.format( |
||||||
|
"Different @AliasFor mirror values for annotation [%s]%s, " |
||||||
|
+ "attribute '%s' and its alias '%s' are declared with values of [%s] and [%s].", |
||||||
|
getAnnotationType().getName(), on, |
||||||
|
attributes.get(result).getName(), |
||||||
|
attribute.getName(), |
||||||
|
ObjectUtils.nullSafeToString(lastValue), |
||||||
|
ObjectUtils.nullSafeToString(value))); |
||||||
|
} |
||||||
|
result = this.indexes[i]; |
||||||
|
lastValue = value; |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
int size() { |
||||||
|
return this.size; |
||||||
|
} |
||||||
|
|
||||||
|
Method get(int index) { |
||||||
|
int attributeIndex = this.indexes[index]; |
||||||
|
return attributes.get(attributeIndex); |
||||||
|
} |
||||||
|
|
||||||
|
int getAttributeIndex(int index) { |
||||||
|
return this.indexes[index]; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,239 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.util.ArrayDeque; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Deque; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.util.ConcurrentReferenceHashMap; |
||||||
|
|
||||||
|
/** |
||||||
|
* Provides {@link AnnotationTypeMapping} information for a single source |
||||||
|
* annotation type. Performs a recursive breadth first crawl of all |
||||||
|
* meta-annotations to ultimately provide a quick way to map the attributes of |
||||||
|
* root {@link Annotation}. |
||||||
|
* |
||||||
|
* <p>Supports convention based merging of meta-annotations as well as implicit |
||||||
|
* and explicit {@link AliasFor @AliasFor} aliases. Also provide information |
||||||
|
* about mirrored attributes. |
||||||
|
* |
||||||
|
* <p>This class is designed to be cached so that meta-annotations only need to |
||||||
|
* be searched once, regardless of how many times they are actually used. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
* @see AnnotationTypeMapping |
||||||
|
*/ |
||||||
|
final class AnnotationTypeMappings { |
||||||
|
|
||||||
|
private static final IntrospectionFailureLogger failureLogger = IntrospectionFailureLogger.DEBUG; |
||||||
|
|
||||||
|
private static final Map<AnnotationFilter, Cache> cache = new ConcurrentReferenceHashMap<>(); |
||||||
|
|
||||||
|
|
||||||
|
private final AnnotationFilter filter; |
||||||
|
|
||||||
|
private final List<AnnotationTypeMapping> mappings; |
||||||
|
|
||||||
|
|
||||||
|
private AnnotationTypeMappings(AnnotationFilter filter, |
||||||
|
Class<? extends Annotation> annotationType) { |
||||||
|
this.filter = filter; |
||||||
|
this.mappings = new ArrayList<>(); |
||||||
|
addAllMappings(annotationType); |
||||||
|
this.mappings.forEach(AnnotationTypeMapping::afterAllMappingsSet); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private void addAllMappings(Class<? extends Annotation> annotationType) { |
||||||
|
Deque<AnnotationTypeMapping> queue = new ArrayDeque<>(); |
||||||
|
addIfPossible(queue, null, annotationType, null); |
||||||
|
while (!queue.isEmpty()) { |
||||||
|
AnnotationTypeMapping mapping = queue.removeFirst(); |
||||||
|
this.mappings.add(mapping); |
||||||
|
addMetaAnnotationsToQueue(queue, mapping); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void addMetaAnnotationsToQueue(Deque<AnnotationTypeMapping> queue, |
||||||
|
AnnotationTypeMapping parent) { |
||||||
|
|
||||||
|
Annotation[] metaAnnotations = AnnotationsScanner.getDeclaredAnnotations( |
||||||
|
parent.getAnnotationType(), false); |
||||||
|
for (Annotation metaAnnotation : metaAnnotations) { |
||||||
|
if (!isMappable(parent, metaAnnotation)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
Annotation[] repeatedAnnotations = RepeatableContainers.standardRepeatables() |
||||||
|
.findRepeatedAnnotations(metaAnnotation); |
||||||
|
if (repeatedAnnotations != null) { |
||||||
|
for (Annotation repeatedAnnotation : repeatedAnnotations) { |
||||||
|
if (!isMappable(parent, metaAnnotation)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
addIfPossible(queue, parent, repeatedAnnotation); |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
addIfPossible(queue, parent, metaAnnotation); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void addIfPossible(Deque<AnnotationTypeMapping> queue, |
||||||
|
AnnotationTypeMapping parent, Annotation annotation) { |
||||||
|
addIfPossible(queue, parent, annotation.annotationType(), annotation); |
||||||
|
} |
||||||
|
|
||||||
|
private void addIfPossible(Deque<AnnotationTypeMapping> queue, |
||||||
|
@Nullable AnnotationTypeMapping parent, |
||||||
|
Class<? extends Annotation> annotationType, @Nullable Annotation annotation) { |
||||||
|
|
||||||
|
try { |
||||||
|
queue.addLast(new AnnotationTypeMapping(parent, annotationType, annotation)); |
||||||
|
} |
||||||
|
catch (Exception ex) { |
||||||
|
if (ex instanceof AnnotationConfigurationException) { |
||||||
|
throw (AnnotationConfigurationException) ex; |
||||||
|
} |
||||||
|
if (failureLogger.isEnabled()) { |
||||||
|
failureLogger.log( |
||||||
|
"Failed to introspect meta-annotation " |
||||||
|
+ annotationType.getName(), |
||||||
|
(parent != null) ? parent.getAnnotationType() : null, ex); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private boolean isMappable(AnnotationTypeMapping parent, Annotation metaAnnotation) { |
||||||
|
return !this.filter.matches(metaAnnotation) && |
||||||
|
!AnnotationFilter.PLAIN.matches(parent.getAnnotationType()) && |
||||||
|
!isAlreadyMapped(parent, metaAnnotation); |
||||||
|
} |
||||||
|
|
||||||
|
private boolean isAlreadyMapped(AnnotationTypeMapping parent, |
||||||
|
Annotation metaAnnotation) { |
||||||
|
|
||||||
|
Class<? extends Annotation> annotationType = metaAnnotation.annotationType(); |
||||||
|
AnnotationTypeMapping mapping = parent; |
||||||
|
while (mapping != null) { |
||||||
|
if (mapping.getAnnotationType().equals(annotationType)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
mapping = mapping.getParent(); |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the total number of contained mappings. |
||||||
|
* @return the total number of mappings |
||||||
|
*/ |
||||||
|
int size() { |
||||||
|
return this.mappings.size(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return an individual mapping from this instance. Index {@code 0} will |
||||||
|
* always be return the root mapping, higer indexes will return |
||||||
|
* meta-annotation mappings. |
||||||
|
* @param index the index to return |
||||||
|
* @return the {@link AnnotationTypeMapping} |
||||||
|
* @throws IndexOutOfBoundsException if the index is out of range |
||||||
|
* (<tt>index < 0 || index >= size()</tt>) |
||||||
|
*/ |
||||||
|
AnnotationTypeMapping get(int index) { |
||||||
|
return this.mappings.get(index); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return {@link AnnotationTypeMappings} for the specified annotation type. |
||||||
|
* @param annotationType the source annotation type |
||||||
|
* @return type mappings for the annotation type |
||||||
|
*/ |
||||||
|
static AnnotationTypeMappings forAnnotationType( |
||||||
|
Class<? extends Annotation> annotationType) { |
||||||
|
|
||||||
|
return forAnnotationType(annotationType, |
||||||
|
AnnotationFilter.mostAppropriateFor(annotationType)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return {@link AnnotationTypeMappings} for the specified annotation type. |
||||||
|
* @param annotationType the source annotation type |
||||||
|
* @param annotationFilter the annotation filter used to limit which |
||||||
|
* annotations are considered |
||||||
|
* @return type mappings for the annotation type |
||||||
|
*/ |
||||||
|
static AnnotationTypeMappings forAnnotationType( |
||||||
|
Class<? extends Annotation> annotationType, |
||||||
|
AnnotationFilter annotationFilter) { |
||||||
|
|
||||||
|
Assert.notNull(annotationType, "AnnotationType must not be null"); |
||||||
|
Assert.notNull(annotationFilter, "AnnotationFilter must not be null"); |
||||||
|
return cache.computeIfAbsent(annotationFilter, Cache::new).get(annotationType); |
||||||
|
} |
||||||
|
|
||||||
|
static void clearCache() { |
||||||
|
cache.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Cache created per {@link AnnotationFilter}. |
||||||
|
*/ |
||||||
|
private static class Cache { |
||||||
|
|
||||||
|
private final AnnotationFilter filter; |
||||||
|
|
||||||
|
private final Map<Class<? extends Annotation>, AnnotationTypeMappings> mappings; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Create a cache instance with the specified filter. |
||||||
|
* @param filter the annotation filter |
||||||
|
*/ |
||||||
|
Cache(AnnotationFilter filter) { |
||||||
|
this.filter = filter; |
||||||
|
this.mappings = new ConcurrentReferenceHashMap<>(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Return or create {@link AnnotationTypeMappings} for the specified |
||||||
|
* annotation type. |
||||||
|
* @param annotationType the annotation type |
||||||
|
* @return a new or existing {@link AnnotationTypeMapping} instance |
||||||
|
*/ |
||||||
|
AnnotationTypeMappings get(Class<? extends Annotation> annotationType) { |
||||||
|
return this.mappings.computeIfAbsent(annotationType, this::createMappings); |
||||||
|
} |
||||||
|
|
||||||
|
AnnotationTypeMappings createMappings( |
||||||
|
Class<? extends Annotation> annotationType) { |
||||||
|
return new AnnotationTypeMappings(this.filter, annotationType); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,72 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
|
||||||
|
/** |
||||||
|
* Callback interface used to process annotations. |
||||||
|
* |
||||||
|
* @param <C> the context type |
||||||
|
* @param <R> the result type |
||||||
|
* @author Phillip Webb |
||||||
|
* @see AnnotationsScanner |
||||||
|
* @see TypeMappedAnnotations |
||||||
|
*/ |
||||||
|
@FunctionalInterface |
||||||
|
interface AnnotationsProcessor<C, R> { |
||||||
|
|
||||||
|
/** |
||||||
|
* Called when an aggregate is about to be processed. This method may return |
||||||
|
* a {@code non-null} result to short-circuit any further processing. |
||||||
|
* @param context context information relevant to the processor |
||||||
|
* @param aggregateIndex the aggregate index about to be processed |
||||||
|
* @return a {@code non-null} result if no further processing is required |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
default R doWithAggregate(C context, int aggregateIndex) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Called when an array of annotations can be processed. This method may |
||||||
|
* return a {@code non-null} result to short-circuit any further processing. |
||||||
|
* @param context context information relevant to the processor |
||||||
|
* @param aggregateIndex the aggregate index of the provided annotations |
||||||
|
* @param source the original source of the annotations, if known |
||||||
|
* @param annotations the annotations to process (this array may contain |
||||||
|
* {@code null} elements) |
||||||
|
* @return a {@code non-null} result if no further processing is required |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
R doWithAnnotations(C context, int aggregateIndex, @Nullable Object source, |
||||||
|
Annotation[] annotations); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the final result to be returned. By default this method returns |
||||||
|
* the last process result. |
||||||
|
* @param result the last early exit result, or {@code null}. |
||||||
|
* @return the final result to be returned to the caller |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
default R finish(@Nullable R result) { |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,552 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.reflect.AnnotatedElement; |
||||||
|
import java.lang.reflect.Member; |
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.lang.reflect.Modifier; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.function.BiPredicate; |
||||||
|
|
||||||
|
import org.springframework.core.BridgeMethodResolver; |
||||||
|
import org.springframework.core.Ordered; |
||||||
|
import org.springframework.core.ResolvableType; |
||||||
|
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.ConcurrentReferenceHashMap; |
||||||
|
import org.springframework.util.ObjectUtils; |
||||||
|
import org.springframework.util.ReflectionUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* Scanner to search for relevant annotations on the hierarchy of an |
||||||
|
* {@link AnnotatedElement}. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
* @see AnnotationsProcessor |
||||||
|
*/ |
||||||
|
abstract class AnnotationsScanner { |
||||||
|
|
||||||
|
private static final Annotation[] NO_ANNOTATIONS = {}; |
||||||
|
|
||||||
|
private static final Method[] NO_METHODS = {}; |
||||||
|
|
||||||
|
|
||||||
|
private static final Map<AnnotatedElement, Annotation[]> declaredAnnotationCache = |
||||||
|
new ConcurrentReferenceHashMap<>(256); |
||||||
|
|
||||||
|
private static final Map<Class<?>, Method[]> baseTypeMethodsCache = |
||||||
|
new ConcurrentReferenceHashMap<>(256); |
||||||
|
|
||||||
|
|
||||||
|
private AnnotationsScanner() { |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Scan the hierarchy of the specified element for relevant annotations and |
||||||
|
* call the processor as required. |
||||||
|
* @param context an optional context object that will be passed back to the |
||||||
|
* processor |
||||||
|
* @param source the source element to scan |
||||||
|
* @param searchStrategy the search strategy to use |
||||||
|
* @param processor the processor that receives the annotations |
||||||
|
* @return the result of {@link AnnotationsProcessor#finish(Object)} |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
static <C, R> R scan(C context, AnnotatedElement source, |
||||||
|
SearchStrategy searchStrategy, AnnotationsProcessor<C, R> processor) { |
||||||
|
return scan(context, source, searchStrategy, processor, null); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Scan the hierarchy of the specified element for relevant annotations and |
||||||
|
* call the processor as required. |
||||||
|
* @param context an optional context object that will be passed back to the |
||||||
|
* processor |
||||||
|
* @param source the source element to scan |
||||||
|
* @param searchStrategy the search strategy to use |
||||||
|
* @param processor the processor that receives the annotations |
||||||
|
* @param classFilter an optional filter that can be used to entirely filter |
||||||
|
* out a specific class from the hierarchy |
||||||
|
* @return the result of {@link AnnotationsProcessor#finish(Object)} |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
static <C, R> R scan(C context, AnnotatedElement source, |
||||||
|
SearchStrategy searchStrategy, AnnotationsProcessor<C, R> processor, |
||||||
|
@Nullable BiPredicate<C, Class<?>> classFilter) { |
||||||
|
|
||||||
|
R result = process(context, source, searchStrategy, processor, classFilter); |
||||||
|
return processor.finish(result); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private static <C, R> R process(C context, AnnotatedElement source, |
||||||
|
SearchStrategy searchStrategy, AnnotationsProcessor<C, R> processor, |
||||||
|
@Nullable BiPredicate<C, Class<?>> classFilter) { |
||||||
|
|
||||||
|
if (source instanceof Class) { |
||||||
|
return processClass(context, (Class<?>) source, searchStrategy, processor, classFilter); |
||||||
|
} |
||||||
|
if (source instanceof Method) { |
||||||
|
return processMethod(context, (Method) source, searchStrategy, processor, classFilter); |
||||||
|
} |
||||||
|
return processElement(context, source, processor, classFilter); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private static <C, R> R processClass(C context, Class<?> source, |
||||||
|
SearchStrategy searchStrategy, AnnotationsProcessor<C, R> processor, |
||||||
|
@Nullable BiPredicate<C, Class<?>> classFilter) { |
||||||
|
|
||||||
|
switch (searchStrategy) { |
||||||
|
case DIRECT: |
||||||
|
return processElement(context, source, |
||||||
|
processor, classFilter); |
||||||
|
case INHERITED_ANNOTATIONS: |
||||||
|
return processClassInheritedAnnotations(context, source, |
||||||
|
processor, classFilter); |
||||||
|
case SUPER_CLASS: |
||||||
|
return processClassHierarchy(context, new int[] { 0 }, source, |
||||||
|
processor, classFilter, false); |
||||||
|
case EXHAUSTIVE: |
||||||
|
return processClassHierarchy(context, new int[] { 0 }, source, |
||||||
|
processor, classFilter, true); |
||||||
|
} |
||||||
|
throw new IllegalStateException("Unsupported search strategy " + searchStrategy); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private static <C, R> R processClassInheritedAnnotations(C context, Class<?> source, |
||||||
|
AnnotationsProcessor<C, R> processor, |
||||||
|
@Nullable BiPredicate<C, Class<?>> classFilter) { |
||||||
|
|
||||||
|
if (isWithoutHierarchy(source)) { |
||||||
|
return processElement(context, source, processor, classFilter); |
||||||
|
} |
||||||
|
Annotation[] relevant = null; |
||||||
|
int remaining = Integer.MAX_VALUE; |
||||||
|
int aggregateIndex = 0; |
||||||
|
Class<?> root = source; |
||||||
|
while (source != null && source != Object.class |
||||||
|
&& !hasPlainJavaAnnotationsOnly(source) && remaining > 0) { |
||||||
|
R result = processor.doWithAggregate(context, aggregateIndex); |
||||||
|
if (result != null) { |
||||||
|
return result; |
||||||
|
} |
||||||
|
if (isFiltered(source, context, classFilter)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
Annotation[] declaredAnnotations = |
||||||
|
getDeclaredAnnotations(context, source, classFilter, true); |
||||||
|
if (relevant == null && declaredAnnotations.length > 0) { |
||||||
|
relevant = root.getAnnotations(); |
||||||
|
remaining = relevant.length; |
||||||
|
} |
||||||
|
for (int i = 0; i < declaredAnnotations.length; i++) { |
||||||
|
if (declaredAnnotations[i] != null) { |
||||||
|
boolean isRelevant = false; |
||||||
|
for (int relevantIndex = 0; relevantIndex < relevant.length; relevantIndex++) { |
||||||
|
if (relevant[relevantIndex] != null && |
||||||
|
declaredAnnotations[i].annotationType() == relevant[relevantIndex].annotationType()) { |
||||||
|
isRelevant = true; |
||||||
|
relevant[relevantIndex] = null; |
||||||
|
remaining--; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
if (!isRelevant) { |
||||||
|
declaredAnnotations[i] = null; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
result = processor.doWithAnnotations(context, aggregateIndex, source, declaredAnnotations); |
||||||
|
if (result != null) { |
||||||
|
return result; |
||||||
|
} |
||||||
|
source = source.getSuperclass(); |
||||||
|
aggregateIndex++; |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private static <C, R> R processClassHierarchy(C context, int[] aggregateIndex, |
||||||
|
Class<?> source, AnnotationsProcessor<C, R> processor, |
||||||
|
@Nullable BiPredicate<C, Class<?>> classFilter, boolean includeInterfaces) { |
||||||
|
|
||||||
|
R result = processor.doWithAggregate(context, aggregateIndex[0]); |
||||||
|
if (result != null) { |
||||||
|
return result; |
||||||
|
} |
||||||
|
if (hasPlainJavaAnnotationsOnly(source)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
Annotation[] annotations = getDeclaredAnnotations(context, source, classFilter, false); |
||||||
|
result = processor.doWithAnnotations(context, aggregateIndex[0], source, annotations); |
||||||
|
if (result != null) { |
||||||
|
return result; |
||||||
|
} |
||||||
|
aggregateIndex[0]++; |
||||||
|
if (includeInterfaces) { |
||||||
|
for (Class<?> interfaceType : source.getInterfaces()) { |
||||||
|
R interfacesResult = processClassHierarchy(context, aggregateIndex, |
||||||
|
interfaceType, processor, classFilter, includeInterfaces); |
||||||
|
if (interfacesResult != null) { |
||||||
|
return interfacesResult; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
Class<?> superclass = source.getSuperclass(); |
||||||
|
if (superclass != Object.class && superclass != null) { |
||||||
|
R superclassResult = processClassHierarchy(context, aggregateIndex, |
||||||
|
superclass, processor, classFilter, includeInterfaces); |
||||||
|
if (superclassResult != null) { |
||||||
|
return superclassResult; |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private static <C, R> R processMethod(C context, Method source, |
||||||
|
SearchStrategy searchStrategy, AnnotationsProcessor<C, R> processor, |
||||||
|
@Nullable BiPredicate<C, Class<?>> classFilter) { |
||||||
|
|
||||||
|
switch (searchStrategy) { |
||||||
|
case DIRECT: |
||||||
|
case INHERITED_ANNOTATIONS: |
||||||
|
return processMethodInheritedAnnotations(context, source, |
||||||
|
processor, classFilter); |
||||||
|
case SUPER_CLASS: |
||||||
|
return processMethodHierarchy(context, new int[] { 0 }, source.getDeclaringClass(), |
||||||
|
processor, classFilter, source, false); |
||||||
|
case EXHAUSTIVE: |
||||||
|
return processMethodHierarchy(context, new int[] { 0 }, source.getDeclaringClass(), |
||||||
|
processor, classFilter, source, true); |
||||||
|
} |
||||||
|
throw new IllegalStateException("Unsupported search strategy " + searchStrategy); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private static <C, R> R processMethodInheritedAnnotations(C context, Method source, |
||||||
|
AnnotationsProcessor<C, R> processor, |
||||||
|
@Nullable BiPredicate<C, Class<?>> classFilter) { |
||||||
|
|
||||||
|
R result = processor.doWithAggregate(context, 0); |
||||||
|
return result != null ? result : |
||||||
|
processMethodAnnotations(context, 0, source, processor, classFilter); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private static <C, R> R processMethodHierarchy(C context, int[] aggregateIndex, |
||||||
|
Class<?> sourceClass, AnnotationsProcessor<C, R> processor, |
||||||
|
@Nullable BiPredicate<C, Class<?>> classFilter, Method rootMethod, |
||||||
|
boolean includeInterfaces) { |
||||||
|
|
||||||
|
R result = processor.doWithAggregate(context, aggregateIndex[0]); |
||||||
|
if (result != null) { |
||||||
|
return result; |
||||||
|
} |
||||||
|
if (hasPlainJavaAnnotationsOnly(sourceClass)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
boolean calledProcessor = false; |
||||||
|
if (sourceClass == rootMethod.getDeclaringClass()) { |
||||||
|
result = processMethodAnnotations(context, aggregateIndex[0], |
||||||
|
rootMethod, processor, classFilter); |
||||||
|
calledProcessor = true; |
||||||
|
if (result != null) { |
||||||
|
return result; |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
for (Method candidateMethod : getBaseTypeMethods(context, sourceClass, classFilter)) { |
||||||
|
if (candidateMethod != null && isOverride(rootMethod, candidateMethod)) { |
||||||
|
result = processMethodAnnotations(context, aggregateIndex[0], |
||||||
|
candidateMethod, processor, classFilter); |
||||||
|
calledProcessor = true; |
||||||
|
if (result != null) { |
||||||
|
return result; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if (Modifier.isPrivate(rootMethod.getModifiers())) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
if (calledProcessor) { |
||||||
|
aggregateIndex[0]++; |
||||||
|
} |
||||||
|
if (includeInterfaces) { |
||||||
|
for (Class<?> interfaceType : sourceClass.getInterfaces()) { |
||||||
|
R interfacesResult = processMethodHierarchy(context, aggregateIndex, |
||||||
|
interfaceType, processor, classFilter, rootMethod, includeInterfaces); |
||||||
|
if (interfacesResult != null) { |
||||||
|
return interfacesResult; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
Class<?> superclass = sourceClass.getSuperclass(); |
||||||
|
if (superclass != Object.class && superclass != null) { |
||||||
|
R superclassResult = processMethodHierarchy(context, aggregateIndex, |
||||||
|
superclass, processor, classFilter, rootMethod, includeInterfaces); |
||||||
|
if (superclassResult != null) { |
||||||
|
return superclassResult; |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
private static <C> Method[] getBaseTypeMethods(C context, Class<?> baseType, |
||||||
|
@Nullable BiPredicate<C, Class<?>> classFilter) { |
||||||
|
|
||||||
|
if (baseType == Object.class || hasPlainJavaAnnotationsOnly(baseType) || |
||||||
|
isFiltered(baseType, context, classFilter)) { |
||||||
|
return NO_METHODS; |
||||||
|
} |
||||||
|
|
||||||
|
Method[] methods = baseTypeMethodsCache.get(baseType); |
||||||
|
if (methods == null) { |
||||||
|
boolean isInterface = baseType.isInterface(); |
||||||
|
methods = isInterface ? baseType.getMethods() |
||||||
|
: ReflectionUtils.getDeclaredMethods(baseType); |
||||||
|
int cleared = 0; |
||||||
|
for (int i = 0; i < methods.length; i++) { |
||||||
|
if ((!isInterface && Modifier.isPrivate(methods[i].getModifiers())) || |
||||||
|
hasPlainJavaAnnotationsOnly(methods[i]) || |
||||||
|
getDeclaredAnnotations(methods[i], false).length == 0) { |
||||||
|
methods[i] = null; |
||||||
|
cleared++; |
||||||
|
} |
||||||
|
} |
||||||
|
if (cleared == methods.length) { |
||||||
|
methods = NO_METHODS; |
||||||
|
} |
||||||
|
baseTypeMethodsCache.put(baseType, methods); |
||||||
|
} |
||||||
|
return methods; |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean isOverride(Method rootMethod, Method candidateMethod) { |
||||||
|
return !Modifier.isPrivate(candidateMethod.getModifiers()) && |
||||||
|
candidateMethod.getName().equals(rootMethod.getName()) && |
||||||
|
hasSameParameterTypes(rootMethod, candidateMethod); |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean hasSameParameterTypes(Method rootMethod, |
||||||
|
Method candidateMethod) { |
||||||
|
|
||||||
|
if (candidateMethod.getParameterCount() != rootMethod.getParameterCount()) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
Class<?>[] rootParameterTypes = rootMethod.getParameterTypes(); |
||||||
|
Class<?>[] candidateParameterTypes = candidateMethod.getParameterTypes(); |
||||||
|
if (Arrays.equals(candidateParameterTypes, rootParameterTypes)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
return hasSameGenericTypeParameters(rootMethod, candidateMethod, |
||||||
|
rootParameterTypes); |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean hasSameGenericTypeParameters(Method rootMethod, |
||||||
|
Method candidateMethod, Class<?>[] rootParameterTypes) { |
||||||
|
|
||||||
|
Class<?> sourceDeclaringClass = rootMethod.getDeclaringClass(); |
||||||
|
Class<?> candidateDeclaringClass = candidateMethod.getDeclaringClass(); |
||||||
|
if (!candidateDeclaringClass.isAssignableFrom(sourceDeclaringClass)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
for (int i = 0; i < rootParameterTypes.length; i++) { |
||||||
|
Class<?> resolvedParameterType = ResolvableType.forMethodParameter( |
||||||
|
candidateMethod, i, sourceDeclaringClass).resolve(); |
||||||
|
if (rootParameterTypes[i] != resolvedParameterType) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private static <C, R> R processMethodAnnotations(C context, int aggregateIndex, |
||||||
|
Method source, AnnotationsProcessor<C, R> processor, |
||||||
|
@Nullable BiPredicate<C, Class<?>> classFilter) { |
||||||
|
|
||||||
|
Annotation[] annotations = getDeclaredAnnotations(context, source, classFilter, |
||||||
|
false); |
||||||
|
R result = processor.doWithAnnotations(context, aggregateIndex, source, |
||||||
|
annotations); |
||||||
|
if (result != null) { |
||||||
|
return result; |
||||||
|
} |
||||||
|
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(source); |
||||||
|
if (bridgedMethod != source) { |
||||||
|
Annotation[] bridgedAnnotations = getDeclaredAnnotations(context, |
||||||
|
bridgedMethod, classFilter, true); |
||||||
|
for (int i = 0; i < bridgedAnnotations.length; i++) { |
||||||
|
if (ObjectUtils.containsElement(annotations, bridgedAnnotations[i])) { |
||||||
|
bridgedAnnotations[i] = null; |
||||||
|
} |
||||||
|
} |
||||||
|
return processor.doWithAnnotations(context, aggregateIndex, source, |
||||||
|
bridgedAnnotations); |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private static <C, R> R processElement(C context, AnnotatedElement source, |
||||||
|
AnnotationsProcessor<C, R> processor, |
||||||
|
@Nullable BiPredicate<C, Class<?>> classFilter) { |
||||||
|
|
||||||
|
R result = processor.doWithAggregate(context, 0); |
||||||
|
return result != null ? result |
||||||
|
: processor.doWithAnnotations(context, 0, source, |
||||||
|
getDeclaredAnnotations(context, source, classFilter, false)); |
||||||
|
} |
||||||
|
|
||||||
|
private static <C, R> Annotation[] getDeclaredAnnotations(C context, |
||||||
|
AnnotatedElement source, @Nullable BiPredicate<C, Class<?>> classFilter, |
||||||
|
boolean copy) { |
||||||
|
|
||||||
|
if (source instanceof Class && |
||||||
|
isFiltered((Class<?>) source, context, classFilter)) { |
||||||
|
return NO_ANNOTATIONS; |
||||||
|
} |
||||||
|
if (source instanceof Method && |
||||||
|
isFiltered(((Method) source).getDeclaringClass(), context, classFilter)) { |
||||||
|
return NO_ANNOTATIONS; |
||||||
|
} |
||||||
|
return getDeclaredAnnotations(source, copy); |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
@Nullable |
||||||
|
static <A extends Annotation> A getDeclaredAnnotation(AnnotatedElement source, |
||||||
|
Class<A> annotationType) { |
||||||
|
|
||||||
|
Annotation[] annotations = getDeclaredAnnotations(source, false); |
||||||
|
for (int i = 0; i < annotations.length; i++) { |
||||||
|
if (annotations[i] != null && |
||||||
|
annotationType.equals(annotations[i].annotationType())) { |
||||||
|
return (A) annotations[i]; |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
static Annotation[] getDeclaredAnnotations(AnnotatedElement source, |
||||||
|
boolean defensive) { |
||||||
|
|
||||||
|
boolean cached = true; |
||||||
|
Annotation[] annotations = declaredAnnotationCache.get(source); |
||||||
|
if (annotations == null) { |
||||||
|
annotations = source.getDeclaredAnnotations(); |
||||||
|
if (annotations.length != 0) { |
||||||
|
boolean allIgnored = true; |
||||||
|
for (int i = 0; i < annotations.length; i++) { |
||||||
|
Annotation annotation = annotations[i]; |
||||||
|
if (isIgnorable(annotation.annotationType()) || |
||||||
|
!AttributeMethods.forAnnotationType( |
||||||
|
annotation.annotationType()).isValid(annotation)) { |
||||||
|
annotations[i] = null; |
||||||
|
} |
||||||
|
else { |
||||||
|
allIgnored = false; |
||||||
|
} |
||||||
|
} |
||||||
|
annotations = allIgnored ? NO_ANNOTATIONS : annotations; |
||||||
|
if (source instanceof Class || source instanceof Member) { |
||||||
|
declaredAnnotationCache.put(source, annotations); |
||||||
|
cached = true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if (!defensive || annotations.length == 0 || !cached) { |
||||||
|
return annotations; |
||||||
|
} |
||||||
|
return annotations.clone(); |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean isIgnorable(Class<?> annotationType) { |
||||||
|
return (annotationType == Nullable.class || |
||||||
|
annotationType == FunctionalInterface.class); |
||||||
|
} |
||||||
|
|
||||||
|
private static <C> boolean isFiltered(Class<?> sourceClass, C context, |
||||||
|
@Nullable BiPredicate<C, Class<?>> classFilter) { |
||||||
|
return classFilter != null && classFilter.test(context, sourceClass); |
||||||
|
} |
||||||
|
|
||||||
|
static boolean isKnownEmpty(AnnotatedElement source, |
||||||
|
SearchStrategy searchStrategy, AnnotationFilter annotationFilter) { |
||||||
|
if (annotationFilter == AnnotationFilter.PLAIN && |
||||||
|
hasPlainJavaAnnotationsOnly(source)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
if (searchStrategy == SearchStrategy.DIRECT || isWithoutHierarchy(source)) { |
||||||
|
if (source instanceof Method && ((Method) source).isBridge()) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
return getDeclaredAnnotations(source, false).length == 0; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
static boolean hasPlainJavaAnnotationsOnly(@Nullable Object annotatedElement) { |
||||||
|
Class<?> type = null; |
||||||
|
if (annotatedElement instanceof Class) { |
||||||
|
type = (Class<?>) annotatedElement; |
||||||
|
} |
||||||
|
else if (annotatedElement instanceof Member) { |
||||||
|
type = ((Member) annotatedElement).getDeclaringClass(); |
||||||
|
} |
||||||
|
else { |
||||||
|
return false; |
||||||
|
} |
||||||
|
String name = type.getName(); |
||||||
|
return type.equals(Ordered.class) || |
||||||
|
name.startsWith("java") || |
||||||
|
name.startsWith("org.springframework.lang."); |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean isWithoutHierarchy(AnnotatedElement source) { |
||||||
|
if (source == Object.class) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
if (source instanceof Class) { |
||||||
|
Class<?> sourceClass = (Class<?>) source; |
||||||
|
return sourceClass.getSuperclass() == Object.class && |
||||||
|
sourceClass.getInterfaces().length == 0; |
||||||
|
} |
||||||
|
if (source instanceof Method) { |
||||||
|
Method sourceMethod = (Method) source; |
||||||
|
return Modifier.isPrivate(sourceMethod.getModifiers()) || |
||||||
|
isWithoutHierarchy(sourceMethod.getDeclaringClass()); |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
static void clearCache() { |
||||||
|
declaredAnnotationCache.clear(); |
||||||
|
baseTypeMethodsCache.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,316 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Comparator; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.util.ConcurrentReferenceHashMap; |
||||||
|
|
||||||
|
/** |
||||||
|
* Provides a quick way to access the attribute methods of an {@link Annotation} |
||||||
|
* with consistent ordering as well as a few useful utility methods. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
final class AttributeMethods { |
||||||
|
|
||||||
|
static final AttributeMethods NONE = new AttributeMethods(null, new Method[0]); |
||||||
|
|
||||||
|
|
||||||
|
private static final Map<Class<? extends Annotation>, AttributeMethods> cache = |
||||||
|
new ConcurrentReferenceHashMap<>(); |
||||||
|
|
||||||
|
private static final Comparator<Method> methodComparator = (m1, m2) -> { |
||||||
|
if (m1 != null && m2 != null) { |
||||||
|
return m1.getName().compareTo(m2.getName()); |
||||||
|
} |
||||||
|
return m1 != null ? -1 : 1; |
||||||
|
}; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private final Class<? extends Annotation> annotationType; |
||||||
|
|
||||||
|
private final Method[] attributeMethods; |
||||||
|
|
||||||
|
private final boolean[] canThrowTypeNotPresentException; |
||||||
|
|
||||||
|
private final boolean hasDefaultValueMethod; |
||||||
|
|
||||||
|
private final boolean hasNestedAnnotation; |
||||||
|
|
||||||
|
|
||||||
|
private AttributeMethods(@Nullable Class<? extends Annotation> annotationType, |
||||||
|
Method[] attributeMethods) { |
||||||
|
this.annotationType = annotationType; |
||||||
|
this.attributeMethods = attributeMethods; |
||||||
|
this.canThrowTypeNotPresentException = new boolean[attributeMethods.length]; |
||||||
|
boolean foundDefaultValueMethod = false; |
||||||
|
boolean foundNestedAnnotation = false; |
||||||
|
for (int i = 0; i < attributeMethods.length; i++) { |
||||||
|
Method method = this.attributeMethods[i]; |
||||||
|
Class<?> type = method.getReturnType(); |
||||||
|
if (method.getDefaultValue() != null) { |
||||||
|
foundDefaultValueMethod = true; |
||||||
|
} |
||||||
|
if (type.isAnnotation() || |
||||||
|
(type.isArray() && type.getComponentType().isAnnotation())) { |
||||||
|
foundNestedAnnotation = true; |
||||||
|
} |
||||||
|
method.setAccessible(true); |
||||||
|
this.canThrowTypeNotPresentException[i] = |
||||||
|
type == Class.class || |
||||||
|
type == Class[].class || |
||||||
|
type.isEnum(); |
||||||
|
} |
||||||
|
this.hasDefaultValueMethod = foundDefaultValueMethod; |
||||||
|
this.hasNestedAnnotation = foundNestedAnnotation; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Return if this instance only contains only a single attribute named |
||||||
|
* {@code value}. |
||||||
|
* @return {@code true} if this is only a value attribute |
||||||
|
*/ |
||||||
|
boolean isOnlyValueAttribute() { |
||||||
|
return this.attributeMethods.length == 1 && |
||||||
|
MergedAnnotation.VALUE.equals(this.attributeMethods[0].getName()); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Returns {@code true} if values from the given annotation can be safely |
||||||
|
* accessed without causing any {@link TypeNotPresentException |
||||||
|
* TypeNotPresentExceptions}. |
||||||
|
* @param annotation the annotation to check |
||||||
|
* @return {@true} if all values are present |
||||||
|
* @see #validate(Annotation) |
||||||
|
*/ |
||||||
|
boolean isValid(Annotation annotation) { |
||||||
|
assertAnnotation(annotation); |
||||||
|
for (int i = 0; i < size(); i++) { |
||||||
|
if (canThrowTypeNotPresentException(i)) { |
||||||
|
try { |
||||||
|
get(i).invoke(annotation); |
||||||
|
} |
||||||
|
catch (Throwable ex) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks if values from the given annotation can be safely accessed without |
||||||
|
* causing any {@link TypeNotPresentException TypeNotPresentExceptions}. In |
||||||
|
* particular, this method is designed to cover Google App Engine's late |
||||||
|
* arrival of such exceptions for {@code Class} values (instead of the more |
||||||
|
* typical early {@code Class.getAnnotations() failure}. |
||||||
|
* @param annotation the annotation to validate |
||||||
|
* @throws IllegalStateException if a declared {@code Class} attribute could |
||||||
|
* not be read |
||||||
|
* @see #isValid(Annotation) |
||||||
|
*/ |
||||||
|
void validate(Annotation annotation) { |
||||||
|
assertAnnotation(annotation); |
||||||
|
for (int i = 0; i < size(); i++) { |
||||||
|
if (canThrowTypeNotPresentException(i)) { |
||||||
|
try { |
||||||
|
get(i).invoke(annotation); |
||||||
|
} |
||||||
|
catch (Throwable ex) { |
||||||
|
throw new IllegalStateException( |
||||||
|
"Could not obtain annotation attribute value for " |
||||||
|
+ get(i).getName() + " declared on " |
||||||
|
+ annotation.annotationType(), |
||||||
|
ex); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void assertAnnotation(Annotation annotation) { |
||||||
|
Assert.notNull(annotation, "Annotation must not be null"); |
||||||
|
if (this.annotationType != null) { |
||||||
|
Assert.isInstanceOf(this.annotationType, annotation); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the attribute with the specified name or {@code null} if no |
||||||
|
* matching attribute exists. |
||||||
|
* @param name the attribute name to find |
||||||
|
* @return the attribute method or {@code null} |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
Method get(String name) { |
||||||
|
int index = indexOf(name); |
||||||
|
return index != -1 ? this.attributeMethods[index] : null; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the attribute at the specified index. |
||||||
|
* @param index the index of the attribute to return |
||||||
|
* @return the attribute method |
||||||
|
* @throws IndexOutOfBoundsException if the index is out of range |
||||||
|
* (<tt>index < 0 || index >= size()</tt>) |
||||||
|
*/ |
||||||
|
Method get(int index) { |
||||||
|
return this.attributeMethods[index]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return {@code true} if the attribute at the specified index could throw a |
||||||
|
* {@link TypeNotPresentException} when accessed. |
||||||
|
* @param index the index of the attribute to check |
||||||
|
* @return {@code true} if the attribute can throw a |
||||||
|
* {@link TypeNotPresentException} |
||||||
|
*/ |
||||||
|
boolean canThrowTypeNotPresentException(int index) { |
||||||
|
return this.canThrowTypeNotPresentException[index]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the index of the attribute with the specified name, or {@code -1} |
||||||
|
* if there is no attribute with the name. |
||||||
|
* @param name the name to find |
||||||
|
* @return the index of the attribute, or {@code -1} |
||||||
|
*/ |
||||||
|
int indexOf(String name) { |
||||||
|
for (int i = 0; i < this.attributeMethods.length; i++) { |
||||||
|
if (this.attributeMethods[i].getName().equals(name)) { |
||||||
|
return i; |
||||||
|
} |
||||||
|
} |
||||||
|
return -1; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the index of the specified attribute , or {@code -1} if the |
||||||
|
* attribute is not not in this collection. |
||||||
|
* @param attribute the attribute to find |
||||||
|
* @return the index of the attribute, or {@code -1} |
||||||
|
*/ |
||||||
|
int indexOf(Method attribute) { |
||||||
|
for (int i = 0; i < this.attributeMethods.length; i++) { |
||||||
|
if (this.attributeMethods[i].equals(attribute)) { |
||||||
|
return i; |
||||||
|
} |
||||||
|
} |
||||||
|
return -1; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the number of attributes in this collection. |
||||||
|
* @return the number of attributes |
||||||
|
*/ |
||||||
|
int size() { |
||||||
|
return this.attributeMethods.length; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return if at least one of the attribute methods has a default value. |
||||||
|
* @return if there is at least one attribute method with a default value |
||||||
|
*/ |
||||||
|
boolean hasDefaultValueMethod() { |
||||||
|
return this.hasDefaultValueMethod; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return if at least on of the attribute methods is a nested annotation. |
||||||
|
* @return if there is at least one attribute method with a annotation type |
||||||
|
*/ |
||||||
|
boolean hasNestedAnnotation() { |
||||||
|
return this.hasNestedAnnotation; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the attribute methods for the given annotation type. |
||||||
|
* @param annotationType the annotation type |
||||||
|
* @return the attribute methods for the annotation |
||||||
|
*/ |
||||||
|
static AttributeMethods forAnnotationType( |
||||||
|
@Nullable Class<? extends Annotation> annotationType) { |
||||||
|
|
||||||
|
if (annotationType == null) { |
||||||
|
return NONE; |
||||||
|
} |
||||||
|
return cache.computeIfAbsent(annotationType, AttributeMethods::compute); |
||||||
|
} |
||||||
|
|
||||||
|
private static AttributeMethods compute(Class<? extends Annotation> annotationType) { |
||||||
|
Method[] methods = annotationType.getDeclaredMethods(); |
||||||
|
int size = methods.length; |
||||||
|
for (int i = 0; i < methods.length; i++) { |
||||||
|
if (!isAttributeMethod(methods[i])) { |
||||||
|
methods[i] = null; |
||||||
|
size--; |
||||||
|
} |
||||||
|
} |
||||||
|
if (size == 0) { |
||||||
|
return NONE; |
||||||
|
} |
||||||
|
Arrays.sort(methods, methodComparator); |
||||||
|
Method[] attributeMethods = new Method[size]; |
||||||
|
System.arraycopy(methods, 0, attributeMethods, 0, size); |
||||||
|
return new AttributeMethods(annotationType, attributeMethods); |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean isAttributeMethod(Method method) { |
||||||
|
return method.getParameterCount() == 0 && method.getReturnType() != void.class; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a description for the given attribute method suitable to use in |
||||||
|
* exception messages and logs. |
||||||
|
* @param attribute the attribute to describe |
||||||
|
* @return a description of the attribute |
||||||
|
*/ |
||||||
|
static String describe(@Nullable Method attribute) { |
||||||
|
if (attribute == null) { |
||||||
|
return "(none)"; |
||||||
|
} |
||||||
|
return describe(attribute.getDeclaringClass(), attribute.getName()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a description for the given attribute method suitable to use in |
||||||
|
* exception messages and logs. |
||||||
|
* @param annotationType the annotation type |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return a description of the attribute |
||||||
|
*/ |
||||||
|
static String describe(@Nullable Class<?> annotationType, |
||||||
|
@Nullable String attributeName) { |
||||||
|
if (attributeName == null) { |
||||||
|
return "(none)"; |
||||||
|
} |
||||||
|
String in = annotationType != null ? |
||||||
|
" in annotation [" + annotationType.getName() + "]" : |
||||||
|
""; |
||||||
|
return "attribute '" + attributeName + "'" + in; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,84 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import org.apache.commons.logging.Log; |
||||||
|
import org.apache.commons.logging.LogFactory; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
|
||||||
|
/** |
||||||
|
* Log facade used to handle annotation introspection failures (in particular |
||||||
|
* {@code TypeNotPresentExceptions}). Allows annotation processing to continue, |
||||||
|
* assuming that when Class attribute values are not resolvable the annotation |
||||||
|
* should effectively disappear. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
enum IntrospectionFailureLogger { |
||||||
|
|
||||||
|
DEBUG { |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isEnabled() { |
||||||
|
return getLogger().isDebugEnabled(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void log(String message) { |
||||||
|
getLogger().debug(message); |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
INFO { |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isEnabled() { |
||||||
|
return getLogger().isInfoEnabled(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void log(String message) { |
||||||
|
getLogger().info(message); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
@Nullable |
||||||
|
private static Log logger; |
||||||
|
|
||||||
|
|
||||||
|
abstract boolean isEnabled(); |
||||||
|
|
||||||
|
void log(String message, @Nullable Object source, Exception ex) { |
||||||
|
String on = source != null ? " on " + source : ""; |
||||||
|
log(message + on + ": " + ex); |
||||||
|
} |
||||||
|
|
||||||
|
abstract void log(String message); |
||||||
|
|
||||||
|
private static Log getLogger() { |
||||||
|
Log logger = IntrospectionFailureLogger.logger; |
||||||
|
if (logger == null) { |
||||||
|
logger = LogFactory.getLog(MergedAnnotation.class); |
||||||
|
IntrospectionFailureLogger.logger = logger; |
||||||
|
} |
||||||
|
return logger; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,597 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.annotation.Inherited; |
||||||
|
import java.lang.reflect.AnnotatedElement; |
||||||
|
import java.lang.reflect.Proxy; |
||||||
|
import java.util.EnumSet; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.NoSuchElementException; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.function.Function; |
||||||
|
import java.util.function.Predicate; |
||||||
|
|
||||||
|
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
|
||||||
|
/** |
||||||
|
* A single merged annotation returned from a {@link MergedAnnotations} |
||||||
|
* collection. Presents a view onto an annotation where attribute values may |
||||||
|
* have been "merged" from different source values. |
||||||
|
* |
||||||
|
* <p>Attribute values may be accessed using the various {@code get} methods. |
||||||
|
* For example, to access an {@code int} attribute the {@link #getInt(String)} |
||||||
|
* method would be used. |
||||||
|
* |
||||||
|
* <p>Note that attribute values are <b>not</b> converted when accessed. For |
||||||
|
* example, it is not possible to call {@link #getString(String)} if the |
||||||
|
* underlying attribute is an {@code int}. The only exception to this rule is |
||||||
|
* {@code Class} and {@code Class[]} values which may be accessed as |
||||||
|
* {@code String} and {@code String[]} respectively to prevent potential early |
||||||
|
* class initialization. |
||||||
|
* |
||||||
|
* <p>If necessary, a {@link MergedAnnotation} can be {@link #synthesize() |
||||||
|
* synthesized} back into an actual {@link java.lang.annotation.Annotation}. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
* @param <A> the annotation type |
||||||
|
* @see MergedAnnotations |
||||||
|
* @see MergedAnnotationPredicates |
||||||
|
*/ |
||||||
|
public interface MergedAnnotation<A extends Annotation> { |
||||||
|
|
||||||
|
/** |
||||||
|
* The attribute name for annotations with a single element. |
||||||
|
*/ |
||||||
|
String VALUE = "value"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the class name of the actual annotation type. |
||||||
|
* @return the annotation type |
||||||
|
*/ |
||||||
|
String getType(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return if the annotation is present on the source. Considers |
||||||
|
* {@link #isDirectlyPresent() direct annotations}, and |
||||||
|
* {@link #isMetaPresent() meta-annotation} annotations within the context |
||||||
|
* of the {@link SearchStrategy} used. |
||||||
|
* @return {@code true} if the annotation is present |
||||||
|
*/ |
||||||
|
boolean isPresent(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return if the annotation is directly present on the source. A directly |
||||||
|
* present annotation is one that the user has explicitly defined and not |
||||||
|
* one that is {@link #isMetaPresent() meta-present} or |
||||||
|
* {@link Inherited @Inherited}. |
||||||
|
* @return {@code true} if the annotation is directly present |
||||||
|
*/ |
||||||
|
boolean isDirectlyPresent(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return if the annotation is meta-present on the source. A meta-present |
||||||
|
* annotation is an annotation that the user hasn't explicitly defined, but |
||||||
|
* has been used as a meta-annotation somewhere in the annotation hierarchy. |
||||||
|
* @return {@code true} if the annotation is meta-present |
||||||
|
*/ |
||||||
|
boolean isMetaPresent(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the depth of this annotation related to its use as a |
||||||
|
* meta-annotation. A directly declared annotation has a depth of {@code 0}, |
||||||
|
* a meta-annotation has a depth of {@code 1}, a meta-annotation on a |
||||||
|
* meta-annotation has a depth of {@code 2}, etc. A {@link #missing() |
||||||
|
* missing} annotation will always return a depth of {@code -1}. |
||||||
|
* @return the annotation depth or {@code -1} if the annotation is missing |
||||||
|
*/ |
||||||
|
int getDepth(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the index of the aggregate collection containing this annotation. |
||||||
|
* Can be used to reorder a stream of annotations, for example, to give a |
||||||
|
* higher priority to annotations declared on a superclass or interface. A |
||||||
|
* {@link #missing() missing} annotation will always return an aggregate |
||||||
|
* index of {@code -1}. |
||||||
|
* @return the aggregate index (starting at {@code 0}) or {@code -1} if the |
||||||
|
* annotation is missing |
||||||
|
*/ |
||||||
|
int getAggregateIndex(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the source that ultimately declared the annotation, or |
||||||
|
* {@code null} if the source is not known. If this merged annotation was |
||||||
|
* created {@link MergedAnnotations#from(java.lang.reflect.AnnotatedElement) |
||||||
|
* from} an {@link AnnotatedElement} then this source will be an element of |
||||||
|
* the same type. If the annotation was loaded without using reflection, the |
||||||
|
* source can be of any type, but should have a sensible {@code toString()}. |
||||||
|
* Meta-annotations will return the same source as the {@link #getParent()}. |
||||||
|
* @return the source, or {@code null} |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
Object getSource(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the parent of the meta-annotation, or {@code null} if the |
||||||
|
* annotation is not {@link #isMetaPresent() meta-present}. |
||||||
|
* @return the parent annotation or {@code null} |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
MergedAnnotation<?> getParent(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return if the specified attribute name as a non-default value when |
||||||
|
* compared to the annotation declaration. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return {@code true} if the attribute value is different from the default |
||||||
|
* value |
||||||
|
*/ |
||||||
|
boolean hasNonDefaultValue(String attributeName); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return if the specified attribute name as a default value when compared |
||||||
|
* to the annotation declaration. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return {@code true} if the attribute value is the same as the default |
||||||
|
* value |
||||||
|
*/ |
||||||
|
boolean hasDefaultValue(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required byte attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as a byte |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
byte getByte(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required byte array attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as a byte array |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
byte[] getByteArray(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required boolean attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as a boolean |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
boolean getBoolean(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required boolean array attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as a boolean array |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
boolean[] getBooleanArray(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required char attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as a char |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
char getChar(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required char array attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as a char array |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
char[] getCharArray(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required short attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as a short |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
short getShort(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required short array attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as a short array |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
short[] getShortArray(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required int attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as an int |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
int getInt(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required int array attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as an int array |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
int[] getIntArray(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required long attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as a long |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
long getLong(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required long array attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as a long array |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
long[] getLongArray(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required double attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as a double |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
double getDouble(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required double array attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as a double array |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
double[] getDoubleArray(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required float attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as a float |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
float getFloat(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required float array attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as a float array |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
float[] getFloatArray(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required string attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as a string |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
String getString(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required string array attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as a string array |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
String[] getStringArray(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required class attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as a class
|
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
Class<?> getClass(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required class array attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return the value as a class array |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
Class<?>[] getClassArray(String attributeName) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required enum attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @param type the enum type |
||||||
|
* @return the value as a enum |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
<E extends Enum<E>> E getEnum(String attributeName, Class<E> type) |
||||||
|
throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required enum array attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @param type the enum type |
||||||
|
* @return the value as a enum array |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
<E extends Enum<E>> E[] getEnumArray(String attributeName, Class<E> type) |
||||||
|
throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required annotation attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @param type the annotation type |
||||||
|
* @return the value as a {@link MergedAnnotation} |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
<T extends Annotation> MergedAnnotation<T> getAnnotation(String attributeName, |
||||||
|
Class<T> type) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a required annotation array attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @param type the annotation type |
||||||
|
* @return the value as a {@link MergedAnnotation} array |
||||||
|
* @throws NoSuchElementException if there is no matching attribute |
||||||
|
*/ |
||||||
|
<T extends Annotation> MergedAnnotation<T>[] getAnnotationArray(String attributeName, |
||||||
|
Class<T> type) throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return an optional attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return an optional value or {@link Optional#empty()} if there is no |
||||||
|
* matching attribute |
||||||
|
*/ |
||||||
|
Optional<Object> getValue(String attributeName); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return an optional attribute value from the annotation. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @param type the attribute type. Must be compatible with the underlying |
||||||
|
* attribute type or {@code Object.class}. |
||||||
|
* @return an optional value or {@link Optional#empty()} if there is no |
||||||
|
* matching attribute |
||||||
|
*/ |
||||||
|
<T> Optional<T> getValue(String attributeName, Class<T> type); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the default attribute value from the annotation as specified in |
||||||
|
* the annotation declaration. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @return an optional of the default value or {@link Optional#empty()} if |
||||||
|
* there is no matching attribute or no defined default |
||||||
|
*/ |
||||||
|
Optional<Object> getDefaultValue(String attributeName); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the default attribute value from the annotation as specified in |
||||||
|
* the annotation declaration. |
||||||
|
* @param attributeName the attribute name |
||||||
|
* @param type the attribute type. Must be compatible with the underlying |
||||||
|
* attribute type or {@code Object.class}. |
||||||
|
* @return an optional of the default value or {@link Optional#empty()} if |
||||||
|
* there is no matching attribute or no defined default |
||||||
|
*/ |
||||||
|
<T> Optional<T> getDefaultValue(String attributeName, Class<T> type); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a new view of the annotation with all attributes that have default |
||||||
|
* values removed. |
||||||
|
* @return a filtered view of the annotation without any attributes that |
||||||
|
* have a default value |
||||||
|
* @see #filterAttributes(Predicate) |
||||||
|
*/ |
||||||
|
MergedAnnotation<A> filterDefaultValues(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a new view of the annotation with only attributes that match the |
||||||
|
* given predicate. |
||||||
|
* @param predicate a predicate used to filter attribute names |
||||||
|
* @return a filtered view of the annotation |
||||||
|
* @see #filterDefaultValues() |
||||||
|
* @see MergedAnnotationPredicates |
||||||
|
*/ |
||||||
|
MergedAnnotation<A> filterAttributes(Predicate<String> predicate); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a new view of the annotation that exposes non-merged attribute |
||||||
|
* values. Methods from this view will return attribute values with only |
||||||
|
* alias mirroring rules applied. Aliases to parent attributes will not be |
||||||
|
* applied. |
||||||
|
* @return a non-merged view of the annotation |
||||||
|
*/ |
||||||
|
MergedAnnotation<A> withNonMergedAttributes(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return an immutable {@link Map} that contains all the annotation |
||||||
|
* attributes. The {@link MapValues} options may be used to change the way |
||||||
|
* that values are added. |
||||||
|
* @param options map value options |
||||||
|
* @return a map containing the attributes and values |
||||||
|
*/ |
||||||
|
Map<String, Object> asMap(MapValues... options); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a {@link Map} of the supplied type that contains all the |
||||||
|
* annotation attributes. The {@link MapValues} options may be used to |
||||||
|
* change the way that values are added. |
||||||
|
* @param factory a map factory or {@code null} to return an immutable map. |
||||||
|
* @param options map value options |
||||||
|
* @return a map containing the attributes and values |
||||||
|
*/ |
||||||
|
<T extends Map<String, Object>> T asMap( |
||||||
|
@Nullable Function<MergedAnnotation<?>, T> factory, MapValues... options); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a type-safe synthesized version of this annotation that can be |
||||||
|
* used directly in code. The result is synthesized using a JDK |
||||||
|
* {@link Proxy} and as a result may incur a computational cost when first |
||||||
|
* invoked. |
||||||
|
* @return a sythesized version of the annotation. |
||||||
|
* @throws NoSuchElementException on a missing annotation |
||||||
|
*/ |
||||||
|
A synthesize() throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Optionally return type-safe synthesized version of this annotation based |
||||||
|
* on a condition predicate. The result is synthesized using a JDK |
||||||
|
* {@link Proxy} and as a result may incur a computational cost when first |
||||||
|
* invoked. |
||||||
|
* @param condition the test to determine if the annotation can be |
||||||
|
* sythesized |
||||||
|
* @return a optional containing the sythesized version of the annotation or |
||||||
|
* an empty optional if the condition doesn't match |
||||||
|
* @throws NoSuchElementException on a missing annotation |
||||||
|
* @see MergedAnnotationPredicates |
||||||
|
*/ |
||||||
|
Optional<A> synthesize(@Nullable Predicate<? super MergedAnnotation<A>> condition) |
||||||
|
throws NoSuchElementException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return an {@link MergedAnnotation} that represents a missing annotation |
||||||
|
* (i.e. one that is not present). |
||||||
|
* @return an instance representing a missing annotation |
||||||
|
*/ |
||||||
|
static <A extends Annotation> MergedAnnotation<A> missing() { |
||||||
|
return MissingMergedAnnotation.getInstance(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new {@link MergedAnnotation} instance from the specified |
||||||
|
* annotation. |
||||||
|
* @param annotation the annotation to include |
||||||
|
* @return a {@link MergedAnnotation} instance containing the annotation |
||||||
|
*/ |
||||||
|
static <A extends Annotation> MergedAnnotation<A> from(A annotation) { |
||||||
|
return from(null, annotation); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new {@link MergedAnnotations} instance from the specified |
||||||
|
* annotation. |
||||||
|
* @param source the source for the annotation. This source is used only for |
||||||
|
* information and logging. It does not need to <em>actually</em> contain |
||||||
|
* the specified annotations and it will not be searched. |
||||||
|
* @param annotation the annotation to include |
||||||
|
* @return a {@link MergedAnnotation} instance for the annotation |
||||||
|
*/ |
||||||
|
static <A extends Annotation> MergedAnnotation<A> from(@Nullable Object source, |
||||||
|
A annotation) { |
||||||
|
return TypeMappedAnnotation.from(source, annotation); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new {@link MergedAnnotations} instance from the specified |
||||||
|
* annotation type. The resulting annotation will not have any attribute |
||||||
|
* values, but may still be used to query default values. |
||||||
|
* @param annotationType the annotation type |
||||||
|
* @return a {@link MergedAnnotation} instance for the annotation |
||||||
|
*/ |
||||||
|
static <A extends Annotation> MergedAnnotation<A> from(Class<A> annotationType) { |
||||||
|
return from(null, annotationType, null); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new {@link MergedAnnotations} instance from the specified |
||||||
|
* annotation type and attributes maps. |
||||||
|
* @param annotationType the annotation type |
||||||
|
* @param attributes the annotation attributes or {@code null} if just |
||||||
|
* default values should be used |
||||||
|
* @return a {@link MergedAnnotation} instance for the annotation and |
||||||
|
* attributes |
||||||
|
* @see #from(AnnotatedElement, Class, Map) |
||||||
|
*/ |
||||||
|
static <A extends Annotation> MergedAnnotation<A> from(Class<A> annotationType, |
||||||
|
@Nullable Map<String, ?> attributes) { |
||||||
|
return from(null, annotationType, attributes); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new {@link MergedAnnotations} instance from the specified |
||||||
|
* annotation type and attributes maps. |
||||||
|
* @param source the source for the annotation. This source is used only for |
||||||
|
* information and logging. It does not need to <em>actually</em> contain |
||||||
|
* the specified annotations and it will not be searched. |
||||||
|
* @param annotationType the annotation type |
||||||
|
* @param attributes the annotation attributes or {@code null} if just |
||||||
|
* default values should be used |
||||||
|
* @return a {@link MergedAnnotation} instance for the annotation and |
||||||
|
* attributes |
||||||
|
*/ |
||||||
|
static <A extends Annotation> MergedAnnotation<A> from( |
||||||
|
@Nullable AnnotatedElement source, Class<A> annotationType, |
||||||
|
@Nullable Map<String, ?> attributes) { |
||||||
|
return TypeMappedAnnotation.from(source, annotationType, attributes); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Options that effect the way map values are |
||||||
|
* {@link MergedAnnotation#asMap(MapValues...) converted}. |
||||||
|
*/ |
||||||
|
enum MapValues { |
||||||
|
|
||||||
|
/** |
||||||
|
* Add class or class array attributes as strings. |
||||||
|
*/ |
||||||
|
CLASS_TO_STRING, |
||||||
|
|
||||||
|
/** |
||||||
|
* Convert any nested annotation or annotation arrays to maps rather |
||||||
|
* than synthesizing the values. |
||||||
|
*/ |
||||||
|
ANNOTATION_TO_MAP; |
||||||
|
|
||||||
|
|
||||||
|
protected final boolean isIn(MapValues... options) { |
||||||
|
for (MapValues candidate : options) { |
||||||
|
if (candidate == this) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Factory method to create a {@link MapValues} array from a set of |
||||||
|
* boolean flags. |
||||||
|
* @param classToString if {@link MapValues#CLASS_TO_STRING} is included |
||||||
|
* @param annotationsToMap if {@link MapValues#ANNOTATION_TO_MAP} is |
||||||
|
* included |
||||||
|
* @return a new {@link MapValues} array |
||||||
|
*/ |
||||||
|
public static MapValues[] of(boolean classToString, boolean annotationsToMap) { |
||||||
|
EnumSet<MapValues> result = EnumSet.noneOf(MapValues.class); |
||||||
|
addIfTrue(result, MapValues.CLASS_TO_STRING, classToString); |
||||||
|
addIfTrue(result, MapValues.ANNOTATION_TO_MAP, annotationsToMap); |
||||||
|
return result.toArray(new MapValues[0]); |
||||||
|
} |
||||||
|
|
||||||
|
private static <T> void addIfTrue(Set<T> result, T value, boolean test) { |
||||||
|
if (test) { |
||||||
|
result.add(value); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,155 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.LinkedHashSet; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.function.Function; |
||||||
|
import java.util.function.IntFunction; |
||||||
|
import java.util.stream.Collector; |
||||||
|
import java.util.stream.Collector.Characteristics; |
||||||
|
|
||||||
|
import org.springframework.core.annotation.MergedAnnotation.MapValues; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.util.LinkedMultiValueMap; |
||||||
|
import org.springframework.util.MultiValueMap; |
||||||
|
|
||||||
|
/** |
||||||
|
* Collector implementations that provide various reduction operations for |
||||||
|
* {@link MergedAnnotation MergedAnnotations}. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
public abstract class MergedAnnotationCollectors { |
||||||
|
|
||||||
|
private static final Characteristics[] NO_CHARACTERISTICS = {}; |
||||||
|
|
||||||
|
private static final Characteristics[] IDENTITY_FINISH_CHARACTERISTICS = { |
||||||
|
Characteristics.IDENTITY_FINISH }; |
||||||
|
|
||||||
|
|
||||||
|
private MergedAnnotationCollectors() { |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a new {@link Collector} that accumulates merged annotations to a |
||||||
|
* {@link LinkedHashSet} containing {@link MergedAnnotation#synthesize() |
||||||
|
* synthesized} versions. |
||||||
|
* @param <A> the annotation type |
||||||
|
* @return a {@link Collector} which collects and synthesizes the |
||||||
|
* annotations into a {@link Set} |
||||||
|
*/ |
||||||
|
public static <A extends Annotation> Collector<MergedAnnotation<A>, ?, Set<A>> toAnnotationSet() { |
||||||
|
return Collector.of(ArrayList<A>::new, (list, annotation) -> list.add(annotation.synthesize()), |
||||||
|
MergedAnnotationCollectors::addAll, LinkedHashSet::new); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a new {@link Collector} that accumulates merged annotations to an |
||||||
|
* {@link Annotation} array containing {@link MergedAnnotation#synthesize() |
||||||
|
* synthesized} versions. |
||||||
|
* @param <A> the annotation type |
||||||
|
* @return a {@link Collector} which collects and synthesizes the |
||||||
|
* annotations into an {@code Annotation[]} |
||||||
|
* @see #toAnnotationArray(IntFunction) |
||||||
|
*/ |
||||||
|
public static <A extends Annotation> Collector<MergedAnnotation<A>, ?, Annotation[]> toAnnotationArray() { |
||||||
|
return toAnnotationArray(Annotation[]::new); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a new {@link Collector} that accumulates merged annotations to an |
||||||
|
* {@link Annotation} array containing {@link MergedAnnotation#synthesize() |
||||||
|
* synthesized} versions. |
||||||
|
* @param <A> the annotation type |
||||||
|
* @param <R> the resulting array type |
||||||
|
* @param generator a function which produces a new array of the desired |
||||||
|
* type and the provided length |
||||||
|
* @return a {@link Collector} which collects and synthesizes the |
||||||
|
* annotations into an annotation array |
||||||
|
* @see #toAnnotationArray |
||||||
|
*/ |
||||||
|
public static <R extends Annotation, A extends R> Collector<MergedAnnotation<A>, ?, R[]> toAnnotationArray( |
||||||
|
IntFunction<R[]> generator) { |
||||||
|
|
||||||
|
return Collector.of(ArrayList::new, (list, annotation) -> list.add(annotation.synthesize()), |
||||||
|
MergedAnnotationCollectors::addAll, list -> list.toArray(generator.apply(list.size()))); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a new {@link Collector} that accumulates merged annotations to an |
||||||
|
* {@link MultiValueMap} with items {@link MultiValueMap#add(Object, Object) |
||||||
|
* added} from each merged annotation |
||||||
|
* {@link MergedAnnotation#asMap(MapValues...) as a map}. |
||||||
|
* @param <A> the annotation type |
||||||
|
* @param options the map conversion options |
||||||
|
* @return a {@link Collector} which collects and synthesizes the |
||||||
|
* annotations into a {@link LinkedMultiValueMap} |
||||||
|
* @see #toMultiValueMap(Function, MapValues...) |
||||||
|
*/ |
||||||
|
public static <A extends Annotation> Collector<MergedAnnotation<A>, ?, MultiValueMap<String, Object>> toMultiValueMap( |
||||||
|
MapValues... options) { |
||||||
|
|
||||||
|
return toMultiValueMap(Function.identity(), options); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a new {@link Collector} that accumulates merged annotations to an |
||||||
|
* {@link MultiValueMap} with items {@link MultiValueMap#add(Object, Object) |
||||||
|
* added} from each merged annotation |
||||||
|
* {@link MergedAnnotation#asMap(MapValues...) as a map}. |
||||||
|
* @param <A> the annotation type |
||||||
|
* @param options the map conversion options |
||||||
|
* @param finisher the finisher function for the new {@link MultiValueMap} |
||||||
|
* @return a {@link Collector} which collects and synthesizes the |
||||||
|
* annotations into a {@link LinkedMultiValueMap} |
||||||
|
* @see #toMultiValueMap(MapValues...) |
||||||
|
*/ |
||||||
|
public static <A extends Annotation> Collector<MergedAnnotation<A>, ?, MultiValueMap<String, Object>> toMultiValueMap( |
||||||
|
Function<MultiValueMap<String, Object>, MultiValueMap<String, Object>> finisher, |
||||||
|
MapValues... options) { |
||||||
|
|
||||||
|
Assert.notNull(finisher, "Finisher must not be null"); |
||||||
|
Characteristics[] characteristics = isSameInstance(finisher, Function.identity()) ? |
||||||
|
IDENTITY_FINISH_CHARACTERISTICS : |
||||||
|
NO_CHARACTERISTICS; |
||||||
|
return Collector.of(LinkedMultiValueMap::new, |
||||||
|
(map, annotation) -> annotation.asMap(options).forEach(map::add), |
||||||
|
MergedAnnotationCollectors::merge, finisher, characteristics); |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean isSameInstance(Object instance, Object candidate) { |
||||||
|
return instance == candidate; |
||||||
|
} |
||||||
|
|
||||||
|
private static <E, L extends List<E>> L addAll(L list, L additions) { |
||||||
|
list.addAll(additions); |
||||||
|
return list; |
||||||
|
} |
||||||
|
|
||||||
|
private static <K, V> MultiValueMap<K, V> merge(MultiValueMap<K, V> map, |
||||||
|
MultiValueMap<K, V> additions) { |
||||||
|
map.addAll(additions); |
||||||
|
return map; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,188 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Collection; |
||||||
|
import java.util.HashSet; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.function.Function; |
||||||
|
import java.util.function.Predicate; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.util.ObjectUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* Predicate implementations that provide various test operations for |
||||||
|
* {@link MergedAnnotation MergedAnnotations}. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
public abstract class MergedAnnotationPredicates { |
||||||
|
|
||||||
|
private MergedAnnotationPredicates() { |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a new {@link Predicate} that evaluates {@code true} if the |
||||||
|
* {@link MergedAnnotation#getType() merged annotation type} is contained in |
||||||
|
* the specified array. |
||||||
|
* @param <A> the annotation type |
||||||
|
* @param typeNames the names that should be matched |
||||||
|
* @return a {@link Predicate} to test the annotation type |
||||||
|
*/ |
||||||
|
public static <A extends Annotation> Predicate<MergedAnnotation<? extends A>> typeIn( |
||||||
|
String... typeNames) { |
||||||
|
|
||||||
|
Assert.notNull(typeNames, "TypeNames must not be null"); |
||||||
|
return annotation -> ObjectUtils.containsElement(typeNames, annotation.getType()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a new {@link Predicate} that evaluates {@code true} if the |
||||||
|
* {@link MergedAnnotation#getType() merged annotation type} is contained in |
||||||
|
* the specified array. |
||||||
|
* @param <A> the annotation type |
||||||
|
* @param types the types that should be matched |
||||||
|
* @return a {@link Predicate} to test the annotation type |
||||||
|
*/ |
||||||
|
public static <A extends Annotation> Predicate<MergedAnnotation<? extends A>> typeIn( |
||||||
|
Class<?>... types) { |
||||||
|
|
||||||
|
Assert.notNull(types, "Types must not be null"); |
||||||
|
return annotation -> Arrays.stream(types) |
||||||
|
.anyMatch(type -> type.getName().equals(annotation.getType())); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a new {@link Predicate} that evaluates {@code true} if the |
||||||
|
* {@link MergedAnnotation#getType() merged annotation type} is contained in |
||||||
|
* the collection. |
||||||
|
* @param <A> the annotation type |
||||||
|
* @param types the type names or classes that should be matched |
||||||
|
* @return a {@link Predicate} to test the annotation type |
||||||
|
*/ |
||||||
|
public static <A extends Annotation> Predicate<MergedAnnotation<? extends A>> typeIn( |
||||||
|
Collection<?> types) { |
||||||
|
|
||||||
|
Assert.notNull(types, "Types must not be null"); |
||||||
|
return annotation -> types.stream() |
||||||
|
.map(type -> type instanceof Class ? ((Class<?>) type).getName() : type.toString()) |
||||||
|
.anyMatch(typeName -> typeName.equals(annotation.getType())); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a new stateful, single use {@link Predicate} that matches only |
||||||
|
* the first run of an extracted value. For example, |
||||||
|
* {@code MergedAnnotationPredicates.firstRunOf(MergedAnnotation::depth)} |
||||||
|
* will return the first annotation and a subsequent run of the same depth. |
||||||
|
* NOTE: this predicate only matches the first first run, once the extracted |
||||||
|
* value changes the predicate always returns {@code false}. |
||||||
|
* @param valueExtractor function used to extract the value to check |
||||||
|
* @return a {@link Predicate} that matches the first run of the extracted |
||||||
|
* values |
||||||
|
*/ |
||||||
|
public static <A extends Annotation> Predicate<MergedAnnotation<A>> firstRunOf( |
||||||
|
Function<? super MergedAnnotation<A>, ?> valueExtractor) { |
||||||
|
|
||||||
|
Assert.notNull(valueExtractor, "ValueExtractor must not be null"); |
||||||
|
return new FirstRunOfPredicate<>(valueExtractor); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a new stateful, single use {@link Predicate} that matches |
||||||
|
* annotations that are unique based on extracted key. For example |
||||||
|
* {@code MergedAnnotationPredicates.unique(MergedAnnotation::type)} will |
||||||
|
* match the first time a unique type is seen. |
||||||
|
* @param keyExtractor function used to extract the key used to test for |
||||||
|
* uniqueness |
||||||
|
* @return a {@link Predicate} that matches unique annotation based on the |
||||||
|
* extracted key |
||||||
|
*/ |
||||||
|
public static <A extends Annotation, K> Predicate<MergedAnnotation<A>> unique( |
||||||
|
Function<? super MergedAnnotation<A>, K> keyExtractor) { |
||||||
|
|
||||||
|
Assert.notNull(keyExtractor, "KeyExtractor must not be null"); |
||||||
|
return new UniquePredicate<>(keyExtractor); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* {@link Predicate} implementation used for |
||||||
|
* {@link MergedAnnotationPredicates#firstRunOf(Function)}. |
||||||
|
*/ |
||||||
|
private static class FirstRunOfPredicate<A extends Annotation> |
||||||
|
implements Predicate<MergedAnnotation<A>> { |
||||||
|
|
||||||
|
private final Function<? super MergedAnnotation<A>, ?> valueExtractor; |
||||||
|
|
||||||
|
private boolean hasLastValue; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private Object lastValue; |
||||||
|
|
||||||
|
|
||||||
|
FirstRunOfPredicate( |
||||||
|
Function<? super MergedAnnotation<A>, ?> valueExtractor) { |
||||||
|
this.valueExtractor = valueExtractor; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean test(@Nullable MergedAnnotation<A> annotation) { |
||||||
|
if (!this.hasLastValue) { |
||||||
|
this.hasLastValue = true; |
||||||
|
this.lastValue = this.valueExtractor.apply(annotation); |
||||||
|
} |
||||||
|
Object value = this.valueExtractor.apply(annotation); |
||||||
|
return ObjectUtils.nullSafeEquals(value, this.lastValue); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* {@link Predicate} implementation used for |
||||||
|
* {@link MergedAnnotationPredicates#unique(Function)}. |
||||||
|
*/ |
||||||
|
private static class UniquePredicate<A extends Annotation, K> |
||||||
|
implements Predicate<MergedAnnotation<A>> { |
||||||
|
|
||||||
|
private final Function<? super MergedAnnotation<A>, K> keyExtractor; |
||||||
|
|
||||||
|
private final Set<K> seen = new HashSet<>(); |
||||||
|
|
||||||
|
|
||||||
|
UniquePredicate(Function<? super MergedAnnotation<A>, K> keyExtractor) { |
||||||
|
this.keyExtractor = keyExtractor; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean test(@Nullable MergedAnnotation<A> annotation) { |
||||||
|
K key = this.keyExtractor.apply(annotation); |
||||||
|
return this.seen.add(key); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
|
||||||
|
/** |
||||||
|
* Strategy interface used to select between two {@link MergedAnnotation} |
||||||
|
* instances. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
* @param <A> the annotation type |
||||||
|
* @see MergedAnnotationSelectors |
||||||
|
*/ |
||||||
|
@FunctionalInterface |
||||||
|
public interface MergedAnnotationSelector<A extends Annotation> { |
||||||
|
|
||||||
|
/** |
||||||
|
* Return {@code true} if the existing annotation is known to be the best |
||||||
|
* candidate and any subsequent selections may be skipped. |
||||||
|
* @param annotation the annotation to check |
||||||
|
* @return {@code true} if the annotation is known to be the best candidate |
||||||
|
*/ |
||||||
|
default boolean isBestCandidate(MergedAnnotation<A> annotation) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Select the annotation that should be used. |
||||||
|
* @param existing an existing annotation returned from an earlier result |
||||||
|
* @param candidate a candidate annotation that may be better suited |
||||||
|
* @return the most appropriate annotation from the {@code existing} or |
||||||
|
* {@code candidate} |
||||||
|
*/ |
||||||
|
MergedAnnotation<A> select(MergedAnnotation<A> existing, |
||||||
|
MergedAnnotation<A> candidate); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,106 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.util.function.Predicate; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link MergedAnnotationSelector} implementations that provide various options |
||||||
|
* for {@link MergedAnnotation MergedAnnotations}. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
* @see MergedAnnotations#get(Class, Predicate, MergedAnnotationSelector) |
||||||
|
* @see MergedAnnotations#get(String, Predicate, MergedAnnotationSelector) |
||||||
|
*/ |
||||||
|
public abstract class MergedAnnotationSelectors { |
||||||
|
|
||||||
|
private static final MergedAnnotationSelector<?> NEAREST = new Nearest(); |
||||||
|
|
||||||
|
private static final MergedAnnotationSelector<?> FIRST_DIRECTLY_DECLARED = new FirstDirectlyDeclared(); |
||||||
|
|
||||||
|
|
||||||
|
private MergedAnnotationSelectors() { |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Select the nearest annotation, i.e. the one with the lowest depth. |
||||||
|
* @return a selector that picks the annotation with the lowest depth |
||||||
|
*/ |
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
public static <A extends Annotation> MergedAnnotationSelector<A> nearest() { |
||||||
|
return (MergedAnnotationSelector<A>) NEAREST; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Select the first directly declared annotation when possible. If not direct |
||||||
|
* annotations are declared then the earliest annotation is selected. |
||||||
|
* @return a selector that picks the first directly declared annotation whenever possible |
||||||
|
*/ |
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
public static <A extends Annotation> MergedAnnotationSelector<A> firstDirectlyDeclared() { |
||||||
|
return (MergedAnnotationSelector<A>) FIRST_DIRECTLY_DECLARED; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* {@link MergedAnnotationSelector} to select the nearest annotation. |
||||||
|
*/ |
||||||
|
private static class Nearest implements MergedAnnotationSelector<Annotation> { |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isBestCandidate(MergedAnnotation<Annotation> annotation) { |
||||||
|
return annotation.getDepth() == 0; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public MergedAnnotation<Annotation> select(MergedAnnotation<Annotation> existing, |
||||||
|
MergedAnnotation<Annotation> candidate) { |
||||||
|
if (candidate.getDepth() < existing.getDepth()) { |
||||||
|
return candidate; |
||||||
|
} |
||||||
|
return existing; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* {@link MergedAnnotationSelector} to select the first directly declared |
||||||
|
* annotation. |
||||||
|
*/ |
||||||
|
private static class FirstDirectlyDeclared implements MergedAnnotationSelector<Annotation> { |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isBestCandidate(MergedAnnotation<Annotation> annotation) { |
||||||
|
return annotation.getDepth() == 0; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public MergedAnnotation<Annotation> select(MergedAnnotation<Annotation> existing, |
||||||
|
MergedAnnotation<Annotation> candidate) { |
||||||
|
if (existing.getDepth() > 0 && candidate.getDepth() == 0) { |
||||||
|
return candidate; |
||||||
|
} |
||||||
|
return existing; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,412 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.annotation.Inherited; |
||||||
|
import java.lang.reflect.AnnotatedElement; |
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.util.function.Predicate; |
||||||
|
import java.util.stream.Stream; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
|
||||||
|
/** |
||||||
|
* Provides access to a collection of merged annotations, usually obtained from |
||||||
|
* a from a source such as a {@link Class} or {@link Method}. Each merged |
||||||
|
* annotation represent a view where the attribute values may be "merged" from |
||||||
|
* different source values, typically: |
||||||
|
* |
||||||
|
* <ul> |
||||||
|
* <li>Explicit and Implicit {@link AliasFor @AliasFor} declarations on one or |
||||||
|
* attributes within the annotation.</li> |
||||||
|
* <li>Explicit {@link AliasFor @AliasFor} declarations for a |
||||||
|
* meta-annotation.</li> |
||||||
|
* <li>Convention based attribute aliases for a meta-annotation</li> |
||||||
|
* <li>From a meta-annotation declaration.</li> |
||||||
|
* </ul> |
||||||
|
* |
||||||
|
* <p>For example, a {@code @PostMapping} annotation might be defined as follows: |
||||||
|
* |
||||||
|
* <pre class="code"> |
||||||
|
* @Retention(RetentionPolicy.RUNTIME) |
||||||
|
* @RequestMapping(method = RequestMethod.POST) |
||||||
|
* public @interface PostMapping { |
||||||
|
* |
||||||
|
* @AliasFor(attribute = "path") |
||||||
|
* String[] value() default {}; |
||||||
|
* |
||||||
|
* @AliasFor(attribute = "value") |
||||||
|
* String[] path() default {}; |
||||||
|
* |
||||||
|
* } |
||||||
|
* </pre> |
||||||
|
* |
||||||
|
* If a method is annotated with {@code @PostMapping("/home")} it will contain |
||||||
|
* merged annotations for both {@code @PostMapping} and the meta-annotation |
||||||
|
* {@code @RequestMapping}. The merged view of the {@code @RequestMapping} |
||||||
|
* annotation will contain the following attributes: |
||||||
|
* |
||||||
|
* <p><table> |
||||||
|
* <tr> |
||||||
|
* <th>Name</th> |
||||||
|
* <th>Value</th> |
||||||
|
* <th>Source</th> |
||||||
|
* </tr> |
||||||
|
* <tr> |
||||||
|
* <td>value</td> |
||||||
|
* <td>"/home"</td> |
||||||
|
* <td>Declared {@code @PostMapping}</td> |
||||||
|
* </tr> |
||||||
|
* <tr> |
||||||
|
* <td>path</td> |
||||||
|
* <td>"/home"</td> |
||||||
|
* <td>Explicit {@code @AliasFor}</td> |
||||||
|
* </tr> |
||||||
|
* <tr> |
||||||
|
* <td>method</td> |
||||||
|
* <td>RequestMethod.POST</td> |
||||||
|
* <td>Declared meta-annotation</td> |
||||||
|
* </tr> |
||||||
|
* </table> |
||||||
|
* |
||||||
|
* <p>{@link MergedAnnotations} can be obtained {@link #from(AnnotatedElement) |
||||||
|
* from} any Java {@link AnnotatedElement}. They may also used for sources that |
||||||
|
* don't use reflection (such as those that directly parse bytecode). |
||||||
|
* |
||||||
|
* <p>Different {@link SearchStrategy search strategies} can be used to locate |
||||||
|
* related source elements that contain the annotations to be aggregated |
||||||
|
* together. For example, {@link SearchStrategy#EXHAUSTIVE} will search both |
||||||
|
* superclasses and implemented interfaces. |
||||||
|
* |
||||||
|
* <p>From a {@link MergedAnnotations} instance you can either {@link #get(String)} |
||||||
|
* a single annotation, or {@link #stream() stream all annotations} or just |
||||||
|
* those that match {@link #stream(String) a specific type}. You can also |
||||||
|
* quickly tell if an annotation {@link #isPresent(String) is present}. |
||||||
|
* |
||||||
|
* <p>Here are some typical examples: |
||||||
|
* |
||||||
|
* <pre class="code"> |
||||||
|
* // is an annotation present or meta-present
|
||||||
|
* mergedAnnotations.isPresent(ExampleAnnotation.class); |
||||||
|
* |
||||||
|
* // get the merged "value" attribute of ExampleAnnotation (either direct or
|
||||||
|
* // meta-present)
|
||||||
|
* mergedAnnotations.get(ExampleAnnotation.class).getString("value"); |
||||||
|
* |
||||||
|
* // get all meta-annotations but no direct annotations
|
||||||
|
* mergedAnnotations.stream().anyMatch(MergedAnnotation::isMetaPresent); |
||||||
|
* |
||||||
|
* // get all ExampleAnnotation declarations (include any meta-annotations) and
|
||||||
|
* // print the merged "value" attributes
|
||||||
|
* mergedAnnotations.stream(ExampleAnnotation.class).map( |
||||||
|
* a -> a.getString("value")).forEach(System.out::println); |
||||||
|
* </pre> |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
* @see MergedAnnotation |
||||||
|
* @see MergedAnnotationCollectors |
||||||
|
* @see MergedAnnotationPredicates |
||||||
|
* @see MergedAnnotationSelectors |
||||||
|
*/ |
||||||
|
public interface MergedAnnotations extends Iterable<MergedAnnotation<Annotation>> { |
||||||
|
|
||||||
|
/** |
||||||
|
* Return if the specified annotation is either directly present, or |
||||||
|
* meta-present. Equivalent to calling |
||||||
|
* {@code get(annotationType).isPresent()}. |
||||||
|
* @param annotationType the annotation type to check |
||||||
|
* @return {@code true} if the annotation is present |
||||||
|
*/ |
||||||
|
<A extends Annotation> boolean isPresent(@Nullable Class<A> annotationType); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return if the specified annotation is directly present. Equivalent to |
||||||
|
* calling {@code get(annotationType).isDirectlyPresent()}. |
||||||
|
* @param annotationType the annotation type to check |
||||||
|
* @return {@code true} if the annotation is present |
||||||
|
*/ |
||||||
|
boolean isPresent(@Nullable String annotationType); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return if the specified annotation is directly present. Equivalent to |
||||||
|
* calling {@code get(annotationType).isDirectlyPresent()}. |
||||||
|
* @param annotationType the annotation type to check |
||||||
|
* @return {@code true} if the annotation is present |
||||||
|
*/ |
||||||
|
<A extends Annotation> boolean isDirectlyPresent(@Nullable Class<A> annotationType); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return if the specified annotation is either directly present, or |
||||||
|
* meta-present. Equivalent to calling |
||||||
|
* {@code get(annotationType).isPresent()}. |
||||||
|
* @param annotationType the annotation type to check |
||||||
|
* @return {@code true} if the annotation is present |
||||||
|
*/ |
||||||
|
boolean isDirectlyPresent(@Nullable String annotationType); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the {@link MergedAnnotationSelectors#nearest() nearest} matching |
||||||
|
* annotation or meta-annotation of the specified type, or |
||||||
|
* {@link MergedAnnotation#missing()} if none is present. |
||||||
|
* @param annotationType the annotation type to get |
||||||
|
* @return a {@link MergedAnnotation} instance |
||||||
|
*/ |
||||||
|
<A extends Annotation> MergedAnnotation<A> get(@Nullable Class<A> annotationType); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the {@link MergedAnnotationSelectors#nearest() nearest} matching |
||||||
|
* annotation or meta-annotation of the specified type, or |
||||||
|
* {@link MergedAnnotation#missing()} if none is present. |
||||||
|
* @param annotationType the annotation type to get |
||||||
|
* @param predicate a predicate that must match, or {@code null} if only |
||||||
|
* type matching is required |
||||||
|
* @return a {@link MergedAnnotation} instance |
||||||
|
* @see MergedAnnotationPredicates |
||||||
|
*/ |
||||||
|
<A extends Annotation> MergedAnnotation<A> get(@Nullable Class<A> annotationType, |
||||||
|
@Nullable Predicate<? super MergedAnnotation<A>> predicate); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a matching annotation or meta-annotation of the specified type, or |
||||||
|
* {@link MergedAnnotation#missing()} if none is present. |
||||||
|
* @param annotationType the annotation type to get |
||||||
|
* @param predicate a predicate that must match, or {@code null} if only |
||||||
|
* type matching is required |
||||||
|
* @param selector a selector used to choose the most appropriate annotation |
||||||
|
* within an aggregate, or {@code null} to select the |
||||||
|
* {@link MergedAnnotationSelectors#nearest() nearest}. |
||||||
|
* @return a {@link MergedAnnotation} instance |
||||||
|
* @see MergedAnnotationPredicates |
||||||
|
* @see MergedAnnotationSelectors |
||||||
|
*/ |
||||||
|
<A extends Annotation> MergedAnnotation<A> get(@Nullable Class<A> annotationType, |
||||||
|
@Nullable Predicate<? super MergedAnnotation<A>> predicate, |
||||||
|
@Nullable MergedAnnotationSelector<A> selector); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the {@link MergedAnnotationSelectors#nearest() nearest} matching |
||||||
|
* annotation or meta-annotation of the specified type, or |
||||||
|
* {@link MergedAnnotation#missing()} if none is present. |
||||||
|
* @param annotationType the annotation type to get |
||||||
|
* @return a {@link MergedAnnotation} instance |
||||||
|
*/ |
||||||
|
<A extends Annotation> MergedAnnotation<A> get(@Nullable String annotationType); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the {@link MergedAnnotationSelectors#nearest() nearest} matching |
||||||
|
* annotation or meta-annotation of the specified type, or |
||||||
|
* {@link MergedAnnotation#missing()} if none is present. |
||||||
|
* @param annotationType the annotation type to get |
||||||
|
* @param predicate a predicate that must match, or {@code null} if only |
||||||
|
* type matching is required |
||||||
|
* @return a {@link MergedAnnotation} instance |
||||||
|
* @see MergedAnnotationPredicates |
||||||
|
*/ |
||||||
|
<A extends Annotation> MergedAnnotation<A> get(@Nullable String annotationType, |
||||||
|
@Nullable Predicate<? super MergedAnnotation<A>> predicate); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a matching annotation or meta-annotation of the specified type, or |
||||||
|
* {@link MergedAnnotation#missing()} if none is present. |
||||||
|
* @param annotationType the annotation type to get |
||||||
|
* @param predicate a predicate that must match, or {@code null} if only |
||||||
|
* type matching is required |
||||||
|
* @param selector a selector used to choose the most appropriate annotation |
||||||
|
* within an aggregate, or {@code null} to select the |
||||||
|
* {@link MergedAnnotationSelectors#nearest() nearest}. |
||||||
|
* @return a {@link MergedAnnotation} instance |
||||||
|
* @see MergedAnnotationPredicates |
||||||
|
* @see MergedAnnotationSelectors |
||||||
|
*/ |
||||||
|
<A extends Annotation> MergedAnnotation<A> get(@Nullable String annotationType, |
||||||
|
@Nullable Predicate<? super MergedAnnotation<A>> predicate, |
||||||
|
@Nullable MergedAnnotationSelector<A> selector); |
||||||
|
|
||||||
|
/** |
||||||
|
* Stream all annotations and meta-annotations that match the specified |
||||||
|
* type. The resulting stream follows the same ordering rules are |
||||||
|
* {@link #stream()}. |
||||||
|
* @param annotationType the annotation type to match |
||||||
|
* @return a stream of matching annotations |
||||||
|
*/ |
||||||
|
<A extends Annotation> Stream<MergedAnnotation<A>> stream( |
||||||
|
@Nullable Class<A> annotationType); |
||||||
|
|
||||||
|
/** |
||||||
|
* Stream all annotations and meta-annotations that match the specified |
||||||
|
* type.The resulting stream follows the same ordering rules are |
||||||
|
* {@link #stream()}. |
||||||
|
* @param annotationType the annotation type to match |
||||||
|
* @return a stream of matching annotations |
||||||
|
*/ |
||||||
|
<A extends Annotation> Stream<MergedAnnotation<A>> stream( |
||||||
|
@Nullable String annotationType); |
||||||
|
|
||||||
|
/** |
||||||
|
* Stream all contained annotations and meta-annotations contained in this |
||||||
|
* collection. The resulting stream is ordered first by the |
||||||
|
* {@link MergedAnnotation#getAggregateIndex() aggregate index}, and then by |
||||||
|
* the annotation depth (with the closest annotations first). This ordering |
||||||
|
* means that, for most use-cases, the most suitable annotations appear |
||||||
|
* earliest in the stream. |
||||||
|
* @return a stream of annotations |
||||||
|
*/ |
||||||
|
Stream<MergedAnnotation<Annotation>> stream(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new {@link MergedAnnotations} instance containing all |
||||||
|
* annotations and meta-annotations from the specified element. The |
||||||
|
* resulting instance will not include any inherited annotations, if you |
||||||
|
* want to include those as well you should use |
||||||
|
* {@link #from(AnnotatedElement, SearchStrategy)} with an appropriate |
||||||
|
* {@link SearchStrategy}. |
||||||
|
* @param element the source element |
||||||
|
* @return a {@link MergedAnnotations} instance containing the element |
||||||
|
* annotations |
||||||
|
*/ |
||||||
|
static MergedAnnotations from(@Nullable AnnotatedElement element) { |
||||||
|
return from(element, SearchStrategy.DIRECT); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new {@link MergedAnnotations} instance containing all |
||||||
|
* annotations and meta-annotations from the specified element and, |
||||||
|
* depending on the {@link SearchStrategy}, related inherited elements. |
||||||
|
* @param element the source element |
||||||
|
* @param searchStrategy the search strategy to use |
||||||
|
* @return a {@link MergedAnnotations} instance containing the merged |
||||||
|
* element annotations |
||||||
|
*/ |
||||||
|
static MergedAnnotations from(@Nullable AnnotatedElement element, |
||||||
|
SearchStrategy searchStrategy) { |
||||||
|
return from(element, searchStrategy, RepeatableContainers.standardRepeatables(), |
||||||
|
AnnotationFilter.PLAIN); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new {@link MergedAnnotations} instance containing all |
||||||
|
* annotations and meta-annotations from the specified element and, |
||||||
|
* depending on the {@link SearchStrategy}, related inherited elements. |
||||||
|
* @param element the source element |
||||||
|
* @param searchStrategy the search strategy to use |
||||||
|
* @param repeatableContainers the repeatable containers that may be used by |
||||||
|
* the element annotations or the meta-annotations |
||||||
|
* @param annotationFilter an annotation filter used to restrict the |
||||||
|
* annotations considered |
||||||
|
* @return a {@link MergedAnnotations} instance containing the merged |
||||||
|
* element annotations |
||||||
|
*/ |
||||||
|
static MergedAnnotations from(@Nullable AnnotatedElement element, |
||||||
|
SearchStrategy searchStrategy, RepeatableContainers repeatableContainers, |
||||||
|
AnnotationFilter annotationFilter) { |
||||||
|
return TypeMappedAnnotations.from(element, searchStrategy, repeatableContainers, |
||||||
|
annotationFilter); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new {@link MergedAnnotations} instance from the specified |
||||||
|
* annotations. |
||||||
|
* @param annotations the annotations to include |
||||||
|
* @return a {@link MergedAnnotations} instance containing the annotations |
||||||
|
* @see #from(Object, Annotation...) |
||||||
|
*/ |
||||||
|
static MergedAnnotations from(Annotation... annotations) { |
||||||
|
return from(null, annotations); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new {@link MergedAnnotations} instance from the specified |
||||||
|
* annotations. |
||||||
|
* @param source the source for the annotations. This source is used only |
||||||
|
* for information and logging. It does not need to <em>actually</em> |
||||||
|
* contain the specified annotations and it will not be searched. |
||||||
|
* @param annotations the annotations to include |
||||||
|
* @return a {@link MergedAnnotations} instance containing the annotations |
||||||
|
* @see #from(Annotation...) |
||||||
|
* @see #from(AnnotatedElement) |
||||||
|
*/ |
||||||
|
static MergedAnnotations from(@Nullable Object source, Annotation... annotations) { |
||||||
|
return from(source, annotations, RepeatableContainers.standardRepeatables(), |
||||||
|
AnnotationFilter.PLAIN); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new {@link MergedAnnotations} instance from the specified |
||||||
|
* annotations. |
||||||
|
* @param source the source for the annotations. This source is used only |
||||||
|
* for information and logging. It does not need to <em>actually</em> |
||||||
|
* contain the specified annotations and it will not be searched. |
||||||
|
* @param annotations the annotations to include |
||||||
|
* @param repeatableContainers the repeatable containers that may be used by |
||||||
|
* meta-annotations |
||||||
|
* @param annotationFilter an annotation filter used to restrict the |
||||||
|
* annotations considered |
||||||
|
* @return a {@link MergedAnnotations} instance containing the annotations |
||||||
|
*/ |
||||||
|
static MergedAnnotations from(@Nullable Object source, Annotation[] annotations, |
||||||
|
RepeatableContainers repeatableContainers, |
||||||
|
AnnotationFilter annotationFilter) { |
||||||
|
return TypeMappedAnnotations.from(source, annotations, repeatableContainers, |
||||||
|
annotationFilter); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Search strategies supported by |
||||||
|
* {@link MergedAnnotations#from(AnnotatedElement, SearchStrategy)}. Each |
||||||
|
* strategy creates a different set of aggregates that will be combined to |
||||||
|
* create the final {@link MergedAnnotations}. |
||||||
|
*/ |
||||||
|
enum SearchStrategy { |
||||||
|
|
||||||
|
/** |
||||||
|
* Find only directly declared annotations, without considering |
||||||
|
* {@link Inherited @Inherited} annotations and without searching |
||||||
|
* super-classes or implemented interfaces. |
||||||
|
*/ |
||||||
|
DIRECT, |
||||||
|
|
||||||
|
/** |
||||||
|
* Find all directly declared annotations as well any |
||||||
|
* {@link Inherited @Inherited} super-class annotations. This strategy |
||||||
|
* is only really useful when used with {@link Class} types since the |
||||||
|
* {@link Inherited @Inherited} annotation is ignored for all other |
||||||
|
* {@link AnnotatedElement annotated elements}. This strategy does not |
||||||
|
* search implemented interfaces. |
||||||
|
*/ |
||||||
|
INHERITED_ANNOTATIONS, |
||||||
|
|
||||||
|
/** |
||||||
|
* Find all directly declared and super-class annotations. This strategy |
||||||
|
* is similar to {@link #INHERITED_ANNOTATIONS} except the annotations |
||||||
|
* do not need to be meta-annotated with {@link Inherited @Inherited}. |
||||||
|
* This strategy does not search implemented interfaces. |
||||||
|
*/ |
||||||
|
SUPER_CLASS, |
||||||
|
|
||||||
|
/** |
||||||
|
* Perform a full search of all related elements, include those on any |
||||||
|
* super-classes or implemented interfaces. Superclass annotations do |
||||||
|
* not need to be meta-annotated with {@link Inherited @Inherited}. |
||||||
|
*/ |
||||||
|
EXHAUSTIVE |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,161 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.NoSuchElementException; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.function.Function; |
||||||
|
import java.util.function.Predicate; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
|
||||||
|
/** |
||||||
|
* A {@link AbstractMergedAnnotation} used as the implementation of |
||||||
|
* {@link MergedAnnotation#missing()}. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
* @param <A> the annotation type |
||||||
|
*/ |
||||||
|
final class MissingMergedAnnotation<A extends Annotation> |
||||||
|
extends AbstractMergedAnnotation<A> { |
||||||
|
|
||||||
|
private static final MissingMergedAnnotation<?> INSTANCE = new MissingMergedAnnotation<>(); |
||||||
|
|
||||||
|
|
||||||
|
private MissingMergedAnnotation() { |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public String getType() { |
||||||
|
throw new NoSuchElementException("Unable to get type for missing annotation"); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isPresent() { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public Object getSource() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public MergedAnnotation<?> getParent() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getDepth() { |
||||||
|
return -1; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getAggregateIndex() { |
||||||
|
return -1; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean hasNonDefaultValue(String attributeName) { |
||||||
|
throw new NoSuchElementException( |
||||||
|
"Unable to check non-default value for missing annotation"); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean hasDefaultValue(String attributeName) { |
||||||
|
throw new NoSuchElementException( |
||||||
|
"Unable to check default value for missing annotation"); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public <T> Optional<T> getValue(String attributeName, Class<T> type) { |
||||||
|
return Optional.empty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public <T> Optional<T> getDefaultValue(@Nullable String attributeName, Class<T> type) { |
||||||
|
return Optional.empty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public MergedAnnotation<A> filterAttributes(Predicate<String> predicate) { |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public MergedAnnotation<A> withNonMergedAttributes() { |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Map<String, Object> asMap(MapValues... options) { |
||||||
|
return Collections.emptyMap(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@SuppressWarnings({ "unchecked", "rawtypes" }) |
||||||
|
public <T extends Map<String, Object>> T asMap( |
||||||
|
@Nullable Function<MergedAnnotation<?>, T> factory, MapValues... options) { |
||||||
|
if (factory != null) { |
||||||
|
return factory.apply(this); |
||||||
|
} |
||||||
|
return (T) ((Map) Collections.emptyMap()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
return "(missing)"; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public <T extends Annotation> MergedAnnotation<T> getAnnotation(String attributeName, |
||||||
|
Class<T> type) throws NoSuchElementException { |
||||||
|
|
||||||
|
throw new NoSuchElementException( |
||||||
|
"Unable to get attribute value for missing annotation"); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public <T extends Annotation> MergedAnnotation<T>[] getAnnotationArray( |
||||||
|
String attributeName, Class<T> type) throws NoSuchElementException { |
||||||
|
|
||||||
|
throw new NoSuchElementException( |
||||||
|
"Unable to get attribute value for missing annotation"); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected <T> T getAttributeValue(String attributeName, Class<T> type) { |
||||||
|
throw new NoSuchElementException( |
||||||
|
"Unable to get attribute value for missing annotation"); |
||||||
|
} |
||||||
|
|
||||||
|
protected A createSynthesized() { |
||||||
|
throw new NoSuchElementException("Unable to synthesize missing annotation"); |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
static <A extends Annotation> MergedAnnotation<A> getInstance() { |
||||||
|
return (MergedAnnotation<A>) INSTANCE; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,86 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.util.Arrays; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link AnnotationFilter} implementation used for |
||||||
|
* {@link AnnotationFilter#packages(String...)}. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
class PackagesAnnotationFilter implements AnnotationFilter { |
||||||
|
|
||||||
|
private final String[] prefixes; |
||||||
|
|
||||||
|
private final int hashCode; |
||||||
|
|
||||||
|
|
||||||
|
PackagesAnnotationFilter(String... packages) { |
||||||
|
Assert.notNull(packages, "Packages must not be null"); |
||||||
|
this.prefixes = new String[packages.length]; |
||||||
|
for (int i = 0; i < packages.length; i++) { |
||||||
|
Assert.hasText(packages[i], "Package must not have empty elements"); |
||||||
|
this.prefixes[i] = packages[i] + "."; |
||||||
|
} |
||||||
|
Arrays.sort(this.prefixes); |
||||||
|
this.hashCode = Arrays.hashCode(this.prefixes); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean matches(@Nullable String annotationType) { |
||||||
|
if (annotationType != null) { |
||||||
|
for (String prefix : this.prefixes) { |
||||||
|
if (annotationType.startsWith(prefix)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean equals(@Nullable Object obj) { |
||||||
|
if (this == obj) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
if (obj == null || getClass() != obj.getClass()) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
PackagesAnnotationFilter other = (PackagesAnnotationFilter) obj; |
||||||
|
return Arrays.equals(this.prefixes, other.prefixes); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int hashCode() { |
||||||
|
return this.hashCode; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
return "Packages annotation filter: " + |
||||||
|
StringUtils.arrayToCommaDelimitedString(this.prefixes); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,290 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.annotation.Repeatable; |
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Objects; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.util.ConcurrentReferenceHashMap; |
||||||
|
import org.springframework.util.ObjectUtils; |
||||||
|
import org.springframework.util.ReflectionUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* Strategy used to determine annotations that act as containers for other |
||||||
|
* annotations. The {@link #standardRepeatables()} method provides a default |
||||||
|
* strategy that respects Java's {@link Repeatable @Repeatable} support and |
||||||
|
* should be suitable for most situations. |
||||||
|
* <p> The {@link #of} method can be used to register relationships for |
||||||
|
* annotations that do not wish to use {@link Repeatable @Repeatable}. |
||||||
|
* |
||||||
|
* <p>To completely disable repeatable support use {@link #none()}. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
public abstract class RepeatableContainers { |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private final RepeatableContainers parent; |
||||||
|
|
||||||
|
|
||||||
|
private RepeatableContainers(@Nullable RepeatableContainers parent) { |
||||||
|
this.parent = parent; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Add an additional explicit relationship between a contained and |
||||||
|
* repeatable annotation. |
||||||
|
* @param container the container type |
||||||
|
* @param repeatable the contained repeatable type |
||||||
|
* @return a new {@link RepeatableContainers} instance |
||||||
|
*/ |
||||||
|
public RepeatableContainers and(Class<? extends Annotation> container, |
||||||
|
Class<? extends Annotation> repeatable) { |
||||||
|
|
||||||
|
return new ExplicitRepeatableContainer(this, repeatable, container); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
Annotation[] findRepeatedAnnotations(Annotation annotation) { |
||||||
|
if (this.parent == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return this.parent.findRepeatedAnnotations(annotation); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean equals(@Nullable Object obj) { |
||||||
|
if (obj == this) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
if (obj == null || getClass() != obj.getClass()) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
RepeatableContainers other = (RepeatableContainers) obj; |
||||||
|
return Objects.equals(this.parent, other.parent); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int hashCode() { |
||||||
|
return ObjectUtils.nullSafeHashCode(this.parent); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a {@link RepeatableContainers} instance that searches using Java's |
||||||
|
* {@link Repeatable @Repeatable} annotation. |
||||||
|
* @return a {@link RepeatableContainers} instance |
||||||
|
*/ |
||||||
|
public static RepeatableContainers standardRepeatables() { |
||||||
|
return StandardRepeatableContainers.INSTANCE; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a {@link RepeatableContainers} instance that uses a defined |
||||||
|
* container and repeatable type. |
||||||
|
* @param repeatable the contained repeatable annotation |
||||||
|
* @param container the container annotation or {@code null}. If specified, |
||||||
|
* this annotation must declare a {@code value} attribute returning an array |
||||||
|
* of repeatable annotations. If not specified, the container will be |
||||||
|
* deduced by inspecting the {@code @Repeatable} annotation on |
||||||
|
* {@code repeatable}. |
||||||
|
* @return a {@link RepeatableContainers} instance |
||||||
|
*/ |
||||||
|
public static RepeatableContainers of(Class<? extends Annotation> repeatable, |
||||||
|
@Nullable Class<? extends Annotation> container) { |
||||||
|
|
||||||
|
return new ExplicitRepeatableContainer(null, repeatable, container); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a {@link RepeatableContainers} instance that does not expand any |
||||||
|
* repeatable annotations. |
||||||
|
* @return a {@link RepeatableContainers} instance |
||||||
|
*/ |
||||||
|
public static RepeatableContainers none() { |
||||||
|
return NoRepeatableContainers.INSTANCE; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Standard {@link RepeatableContainers} implementation that searches using |
||||||
|
* Java's {@link Repeatable @Repeatable} annotation. |
||||||
|
*/ |
||||||
|
private static class StandardRepeatableContainers extends RepeatableContainers { |
||||||
|
|
||||||
|
private static final Map<Class<? extends Annotation>, Object> cache = |
||||||
|
new ConcurrentReferenceHashMap<>(); |
||||||
|
|
||||||
|
private static final Object NONE = new Object(); |
||||||
|
|
||||||
|
private static StandardRepeatableContainers INSTANCE = |
||||||
|
new StandardRepeatableContainers(); |
||||||
|
|
||||||
|
StandardRepeatableContainers() { |
||||||
|
super(null); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
Annotation[] findRepeatedAnnotations(Annotation annotation) { |
||||||
|
Method method = getRepeatedAnnotationsMethod(annotation.annotationType()); |
||||||
|
if (method != null) { |
||||||
|
return (Annotation[]) ReflectionUtils.invokeMethod(method, annotation); |
||||||
|
} |
||||||
|
return super.findRepeatedAnnotations(annotation); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private static Method getRepeatedAnnotationsMethod( |
||||||
|
Class<? extends Annotation> annotationType) { |
||||||
|
|
||||||
|
Object result = cache.computeIfAbsent(annotationType, |
||||||
|
StandardRepeatableContainers::computeRepeatedAnnotationsMethod); |
||||||
|
return result != NONE ? (Method) result : null; |
||||||
|
} |
||||||
|
|
||||||
|
private static Object computeRepeatedAnnotationsMethod( |
||||||
|
Class<? extends Annotation> annotationType) { |
||||||
|
|
||||||
|
AttributeMethods methods = AttributeMethods.forAnnotationType(annotationType); |
||||||
|
if (methods.isOnlyValueAttribute()) { |
||||||
|
Method method = methods.get("value"); |
||||||
|
if (method == null) { |
||||||
|
return NONE; |
||||||
|
} |
||||||
|
Class<?> returnType = method.getReturnType(); |
||||||
|
if (returnType.isArray()) { |
||||||
|
Class<?> componentType = returnType.getComponentType(); |
||||||
|
if (Annotation.class.isAssignableFrom(componentType) |
||||||
|
&& componentType.isAnnotationPresent(Repeatable.class)) { |
||||||
|
return method; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return NONE; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* A single explicit mapping. |
||||||
|
*/ |
||||||
|
private static class ExplicitRepeatableContainer extends RepeatableContainers { |
||||||
|
|
||||||
|
private final Class<? extends Annotation> repeatable; |
||||||
|
|
||||||
|
private final Class<? extends Annotation> container; |
||||||
|
|
||||||
|
private final Method valueMethod; |
||||||
|
|
||||||
|
|
||||||
|
ExplicitRepeatableContainer(@Nullable RepeatableContainers parent, |
||||||
|
Class<? extends Annotation> repeatable, |
||||||
|
@Nullable Class<? extends Annotation> container) { |
||||||
|
|
||||||
|
super(parent); |
||||||
|
Assert.notNull(repeatable, "Repeatable must not be null"); |
||||||
|
if (container == null) { |
||||||
|
container = deduceContainer(repeatable); |
||||||
|
} |
||||||
|
Method valueMethod = AttributeMethods.forAnnotationType(container).get("value"); |
||||||
|
try { |
||||||
|
if (valueMethod == null) { |
||||||
|
throw new NoSuchMethodException("No value method found"); |
||||||
|
} |
||||||
|
Class<?> returnType = valueMethod.getReturnType(); |
||||||
|
if (!returnType.isArray() || returnType.getComponentType() != repeatable) { |
||||||
|
throw new AnnotationConfigurationException("Container type [" + |
||||||
|
container.getName() + |
||||||
|
"] must declare a 'value' attribute for an array of type [" + |
||||||
|
repeatable.getName() + "]"); |
||||||
|
} |
||||||
|
} |
||||||
|
catch (AnnotationConfigurationException ex) { |
||||||
|
throw ex; |
||||||
|
} |
||||||
|
catch (Throwable ex) { |
||||||
|
throw new AnnotationConfigurationException( |
||||||
|
"Invalid declaration of container type [" + container.getName() + |
||||||
|
"] for repeatable annotation [" + repeatable.getName() + "]", |
||||||
|
ex); |
||||||
|
} |
||||||
|
this.repeatable = repeatable; |
||||||
|
this.container = container; |
||||||
|
this.valueMethod = valueMethod; |
||||||
|
} |
||||||
|
|
||||||
|
private Class<? extends Annotation> deduceContainer( |
||||||
|
Class<? extends Annotation> repeatable) { |
||||||
|
|
||||||
|
Repeatable annotation = repeatable.getAnnotation(Repeatable.class); |
||||||
|
Assert.notNull(annotation, "Annotation type must be a repeatable annotation: " + |
||||||
|
"failed to resolve container type for " + repeatable.getName()); |
||||||
|
return annotation.value(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
Annotation[] findRepeatedAnnotations(Annotation annotation) { |
||||||
|
if (this.container.isAssignableFrom(annotation.annotationType())) { |
||||||
|
return (Annotation[]) ReflectionUtils.invokeMethod(this.valueMethod, annotation); |
||||||
|
} |
||||||
|
return super.findRepeatedAnnotations(annotation); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean equals(@Nullable Object obj) { |
||||||
|
if (super.equals(obj)) { |
||||||
|
ExplicitRepeatableContainer other = (ExplicitRepeatableContainer) obj; |
||||||
|
return this.container.equals(other.container) && |
||||||
|
this.repeatable.equals(other.repeatable); |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int hashCode() { |
||||||
|
int hashCode = super.hashCode(); |
||||||
|
hashCode = 31 * hashCode + this.container.hashCode(); |
||||||
|
hashCode = 31 * hashCode + this.repeatable.hashCode(); |
||||||
|
return hashCode; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* No repeatable containers. |
||||||
|
*/ |
||||||
|
private static class NoRepeatableContainers extends RepeatableContainers { |
||||||
|
|
||||||
|
private static NoRepeatableContainers INSTANCE = new NoRepeatableContainers(); |
||||||
|
|
||||||
|
NoRepeatableContainers() { |
||||||
|
super(null); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,211 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.reflect.AnnotatedElement; |
||||||
|
import java.lang.reflect.InvocationHandler; |
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.lang.reflect.Proxy; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.NoSuchElementException; |
||||||
|
import java.util.Objects; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.util.ClassUtils; |
||||||
|
import org.springframework.util.ObjectUtils; |
||||||
|
import org.springframework.util.ReflectionUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link InvocationHandler} for an {@link Annotation} that Spring has |
||||||
|
* <em>synthesized</em> (i.e., wrapped in a dynamic proxy) with additional |
||||||
|
* functionality. |
||||||
|
* |
||||||
|
* @author Sam Brannen |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
* @param <A> the annotation type |
||||||
|
* @see Annotation |
||||||
|
* @see AnnotationAttributeExtractor |
||||||
|
* @see AnnotationUtils#synthesizeAnnotation(Annotation, AnnotatedElement) |
||||||
|
*/ |
||||||
|
class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> |
||||||
|
implements InvocationHandler { |
||||||
|
|
||||||
|
private final MergedAnnotation<?> annotation; |
||||||
|
|
||||||
|
private final Class<A> type; |
||||||
|
|
||||||
|
private final AttributeMethods attributes; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private volatile Integer hashCode; |
||||||
|
|
||||||
|
|
||||||
|
private SynthesizedMergedAnnotationInvocationHandler(MergedAnnotation<A> annotation, |
||||||
|
Class<A> type) { |
||||||
|
|
||||||
|
Assert.notNull(annotation, "Annotation must not be null"); |
||||||
|
Assert.notNull(type, "Type must not be null"); |
||||||
|
Assert.isTrue(type.isAnnotation(), "Type must be an annotation"); |
||||||
|
this.annotation = annotation; |
||||||
|
this.type = type; |
||||||
|
this.attributes = AttributeMethods.forAnnotationType(type); |
||||||
|
for (int i = 0; i < this.attributes.size(); i++) { |
||||||
|
getAttributeValue(this.attributes.get(i)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public Object invoke(Object proxy, Method method, Object[] args) { |
||||||
|
if (ReflectionUtils.isEqualsMethod(method)) { |
||||||
|
return annotationEquals(args[0]); |
||||||
|
} |
||||||
|
if (ReflectionUtils.isHashCodeMethod(method)) { |
||||||
|
return annotationHashCode(); |
||||||
|
} |
||||||
|
if (ReflectionUtils.isToStringMethod(method)) { |
||||||
|
return this.annotation.toString(); |
||||||
|
} |
||||||
|
if (isAnnotationTypeMethod(method)) { |
||||||
|
return this.type; |
||||||
|
} |
||||||
|
if (this.attributes.indexOf(method.getName()) != -1) { |
||||||
|
return getAttributeValue(method); |
||||||
|
} |
||||||
|
throw new AnnotationConfigurationException(String.format( |
||||||
|
"Method [%s] is unsupported for synthesized annotation type [%s]", method, |
||||||
|
this.type)); |
||||||
|
} |
||||||
|
|
||||||
|
private boolean isAnnotationTypeMethod(Method method) { |
||||||
|
return Objects.equals(method.getName(), "annotationType") |
||||||
|
&& method.getParameterCount() == 0; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* See {@link Annotation#equals(Object)} for a definition of the required |
||||||
|
* algorithm. |
||||||
|
* @param other the other object to compare against |
||||||
|
*/ |
||||||
|
private boolean annotationEquals(Object other) { |
||||||
|
if (this == other) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
if (!this.type.isInstance(other)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
for (int i = 0; i < this.attributes.size(); i++) { |
||||||
|
Method attribute = this.attributes.get(i); |
||||||
|
Object thisValue = getAttributeValue(attribute); |
||||||
|
Object otherValue = ReflectionUtils.invokeMethod(attribute, other); |
||||||
|
if (!ObjectUtils.nullSafeEquals(thisValue, otherValue)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* See {@link Annotation#hashCode()} for a definition of the required |
||||||
|
* algorithm. |
||||||
|
*/ |
||||||
|
private int annotationHashCode() { |
||||||
|
Integer hashCode = this.hashCode; |
||||||
|
if (hashCode == null) { |
||||||
|
hashCode = computeHashCode(); |
||||||
|
this.hashCode = hashCode; |
||||||
|
} |
||||||
|
return hashCode; |
||||||
|
} |
||||||
|
|
||||||
|
private Integer computeHashCode() { |
||||||
|
int hashCode = 0; |
||||||
|
for (int i = 0; i < this.attributes.size(); i++) { |
||||||
|
Method attribute = this.attributes.get(i); |
||||||
|
Object value = getAttributeValue(attribute); |
||||||
|
hashCode += (127 * attribute.getName().hashCode()) ^ getValueHashCode(value); |
||||||
|
} |
||||||
|
return hashCode; |
||||||
|
} |
||||||
|
|
||||||
|
private int getValueHashCode(Object value) { |
||||||
|
// Use Arrays.hashCode since ObjectUtils doesn't comply to to
|
||||||
|
// Annotation#hashCode()
|
||||||
|
if (value instanceof boolean[]) { |
||||||
|
return Arrays.hashCode((boolean[]) value); |
||||||
|
} |
||||||
|
if (value instanceof byte[]) { |
||||||
|
return Arrays.hashCode((byte[]) value); |
||||||
|
} |
||||||
|
if (value instanceof char[]) { |
||||||
|
return Arrays.hashCode((char[]) value); |
||||||
|
} |
||||||
|
if (value instanceof double[]) { |
||||||
|
return Arrays.hashCode((double[]) value); |
||||||
|
} |
||||||
|
if (value instanceof float[]) { |
||||||
|
return Arrays.hashCode((float[]) value); |
||||||
|
} |
||||||
|
if (value instanceof int[]) { |
||||||
|
return Arrays.hashCode((int[]) value); |
||||||
|
} |
||||||
|
if (value instanceof long[]) { |
||||||
|
return Arrays.hashCode((long[]) value); |
||||||
|
} |
||||||
|
if (value instanceof short[]) { |
||||||
|
return Arrays.hashCode((short[]) value); |
||||||
|
} |
||||||
|
if (value instanceof Object[]) { |
||||||
|
return Arrays.hashCode((Object[]) value); |
||||||
|
} |
||||||
|
return value.hashCode(); |
||||||
|
} |
||||||
|
|
||||||
|
private Object getAttributeValue(Method method) { |
||||||
|
String name = method.getName(); |
||||||
|
Class<?> type = ClassUtils.resolvePrimitiveIfNecessary(method.getReturnType()); |
||||||
|
return this.annotation.getValue(name, type).orElseThrow( |
||||||
|
() -> new NoSuchElementException("No value found for attribute named '" |
||||||
|
+ name + "' in merged annotation " + this.annotation.getType())); |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
static <A extends Annotation> A createProxy(MergedAnnotation<A> annotation, |
||||||
|
Class<A> type) { |
||||||
|
ClassLoader classLoader = type.getClassLoader(); |
||||||
|
InvocationHandler handler = new SynthesizedMergedAnnotationInvocationHandler<>( |
||||||
|
annotation, type); |
||||||
|
Class<?>[] interfaces = isVisible(classLoader, SynthesizedAnnotation.class) |
||||||
|
? new Class<?>[] { type, SynthesizedAnnotation.class } |
||||||
|
: new Class<?>[] { type }; |
||||||
|
return (A) Proxy.newProxyInstance(classLoader, interfaces, handler); |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean isVisible(ClassLoader classLoader, Class<?> interfaceClass) { |
||||||
|
try { |
||||||
|
return Class.forName(interfaceClass.getName(), false, |
||||||
|
classLoader) == interfaceClass; |
||||||
|
} |
||||||
|
catch (ClassNotFoundException ex) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,614 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.reflect.Array; |
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.LinkedHashMap; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.NoSuchElementException; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.function.BiFunction; |
||||||
|
import java.util.function.Function; |
||||||
|
import java.util.function.Predicate; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.util.ClassUtils; |
||||||
|
import org.springframework.util.ReflectionUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link MergedAnnotation} that adapts attributes from a root annotation by |
||||||
|
* applying the mapping and mirroring rules of an {@link AnnotationTypeMapping}. |
||||||
|
* |
||||||
|
* <p>Root attribute values are extracted from a source object using a supplied |
||||||
|
* {@code BiFunction}. This allows various different annotation models to be |
||||||
|
* supported by the same class. For example, the attributes source might be an |
||||||
|
* actual {@link Annotation} instance where methods on the annotation instance |
||||||
|
* are {@link ReflectionUtils#invokeMethod(Method, Object) invoked} to extract |
||||||
|
* values. Equally, the source could be a simple {@link Map} with values |
||||||
|
* extracted using {@link Map#get(Object)}. |
||||||
|
* |
||||||
|
* <p>Extracted root attribute values must be compatible with the attribute |
||||||
|
* return type, namely: |
||||||
|
* |
||||||
|
* <p><table border="1"> |
||||||
|
* <tr><th >Return Type</th><th >Extracted Type</th></tr> |
||||||
|
* <tr><td>Class</td><td>Class or String</td></tr> |
||||||
|
* <tr><td>Class[]</td><td>Class[] or String[]</td></tr> |
||||||
|
* <tr><td>Annotation</td><td>Annotation, Map or Object compatible with the value |
||||||
|
* extractor</td></tr> |
||||||
|
* <tr><td>Annotation[]</td><td>Annotation[], Map[] or Object[] where elements are |
||||||
|
* compatible with the value extractor</td></tr> |
||||||
|
* <tr><td>Other types</td><td>An exact match or the appropriate primitive wrapper</td></tr> |
||||||
|
* </table> |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
* @param <A> the annotation type |
||||||
|
* @see TypeMappedAnnotations |
||||||
|
*/ |
||||||
|
final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnnotation<A> { |
||||||
|
|
||||||
|
private final AnnotationTypeMapping mapping; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private final Object source; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private final Object rootAttributes; |
||||||
|
|
||||||
|
private final BiFunction<Method, Object, Object> valueExtractor; |
||||||
|
|
||||||
|
private final int aggregateIndex; |
||||||
|
|
||||||
|
private final boolean useMergedValues; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private final Predicate<String> attributeFilter; |
||||||
|
|
||||||
|
private final int[] resolvedRootMirrors; |
||||||
|
|
||||||
|
private final int[] resolvedMirrors; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private String string; |
||||||
|
|
||||||
|
|
||||||
|
private TypeMappedAnnotation(AnnotationTypeMapping mapping, |
||||||
|
@Nullable Object source, @Nullable Object rootAttributes, |
||||||
|
BiFunction<Method, Object, Object> valueExtractor, int aggregateIndex) { |
||||||
|
|
||||||
|
this(mapping, source, rootAttributes, valueExtractor, aggregateIndex, null); |
||||||
|
} |
||||||
|
|
||||||
|
private TypeMappedAnnotation(AnnotationTypeMapping mapping, |
||||||
|
@Nullable Object source, @Nullable Object rootAttributes, |
||||||
|
BiFunction<Method, Object, Object> valueExtractor, int aggregateIndex, |
||||||
|
@Nullable int[] resolvedRootMirrors) { |
||||||
|
|
||||||
|
this.source = source; |
||||||
|
this.rootAttributes = rootAttributes; |
||||||
|
this.valueExtractor = valueExtractor; |
||||||
|
this.mapping = mapping; |
||||||
|
this.aggregateIndex = aggregateIndex; |
||||||
|
this.useMergedValues = true; |
||||||
|
this.attributeFilter = null; |
||||||
|
this.resolvedRootMirrors = resolvedRootMirrors != null ? resolvedRootMirrors |
||||||
|
: mapping.getRoot().getMirrorSets().resolve(source, rootAttributes, |
||||||
|
this.valueExtractor); |
||||||
|
this.resolvedMirrors = getDepth() == 0 ? this.resolvedRootMirrors |
||||||
|
: mapping.getMirrorSets().resolve(source, this, |
||||||
|
this::getValueForMirrorResolution); |
||||||
|
} |
||||||
|
|
||||||
|
private TypeMappedAnnotation(AnnotationTypeMapping mapping, |
||||||
|
@Nullable Object source, @Nullable Object rootAnnotation, |
||||||
|
BiFunction<Method, Object, Object> valueExtractor, int aggregateIndex, |
||||||
|
boolean useMergedValues, @Nullable Predicate<String> attributeFilter, |
||||||
|
int[] resolvedRootMirrors, int[] resolvedMirrors) { |
||||||
|
|
||||||
|
this.source = source; |
||||||
|
this.rootAttributes = rootAnnotation; |
||||||
|
this.valueExtractor = valueExtractor; |
||||||
|
this.mapping = mapping; |
||||||
|
this.aggregateIndex = aggregateIndex; |
||||||
|
this.useMergedValues = useMergedValues; |
||||||
|
this.attributeFilter = attributeFilter; |
||||||
|
this.resolvedRootMirrors = resolvedRootMirrors; |
||||||
|
this.resolvedMirrors = resolvedMirrors; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Nullable |
||||||
|
private Object getValueForMirrorResolution(Method attribute, Object annotation) { |
||||||
|
int attributeIndex = this.mapping.getAttributes().indexOf(attribute); |
||||||
|
boolean valueAttribute = VALUE.equals(attribute.getName()); |
||||||
|
return getValue(attributeIndex, !valueAttribute, false); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getType() { |
||||||
|
return getAnnotationType().getName(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isPresent() { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getDepth() { |
||||||
|
return this.mapping.getDepth(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getAggregateIndex() { |
||||||
|
return this.aggregateIndex; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public Object getSource() { |
||||||
|
return this.source; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public MergedAnnotation<?> getParent() { |
||||||
|
AnnotationTypeMapping parentMapping = this.mapping.getParent(); |
||||||
|
if (parentMapping == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return new TypeMappedAnnotation<>(parentMapping, this.source, this.rootAttributes, |
||||||
|
this.valueExtractor, this.aggregateIndex, this.resolvedRootMirrors); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean hasDefaultValue(String attributeName) { |
||||||
|
int attributeIndex = getAttributeIndex(attributeName, true); |
||||||
|
Object value = getValue(attributeIndex, true, true); |
||||||
|
return value == null || this.mapping.isEquivalentToDefaultValue(attributeIndex, value, |
||||||
|
this.valueExtractor); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
public <T extends Annotation> MergedAnnotation<T> getAnnotation(String attributeName, |
||||||
|
Class<T> type) throws NoSuchElementException { |
||||||
|
|
||||||
|
Assert.notNull(attributeName, "AttributeName must not be null"); |
||||||
|
Assert.notNull(type, "Type must not be null"); |
||||||
|
int attributeIndex = getAttributeIndex(attributeName, true); |
||||||
|
Method attribute = this.mapping.getAttributes().get(attributeIndex); |
||||||
|
Assert.isAssignable(type, attribute.getReturnType(), |
||||||
|
"Attribute " + attributeName + " type mismatch:"); |
||||||
|
return (MergedAnnotation<T>) getRequiredValue(attributeIndex, Object.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
public <T extends Annotation> MergedAnnotation<T>[] getAnnotationArray( |
||||||
|
String attributeName, Class<T> type) throws NoSuchElementException { |
||||||
|
|
||||||
|
Assert.notNull(attributeName, "AttributeName must not be null"); |
||||||
|
Assert.notNull(type, "Type must not be null"); |
||||||
|
int attributeIndex = getAttributeIndex(attributeName, true); |
||||||
|
Method attribute = this.mapping.getAttributes().get(attributeIndex); |
||||||
|
Class<?> componentType = attribute.getReturnType().getComponentType(); |
||||||
|
Assert.notNull(componentType, () -> "Attribute " + attributeName + " is not an array"); |
||||||
|
Assert.isAssignable(type, componentType, "Attribute " + attributeName + " component type mismatch:"); |
||||||
|
return (MergedAnnotation<T>[]) getRequiredValue(attributeIndex, Object.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public <T> Optional<T> getDefaultValue(String attributeName, Class<T> type) { |
||||||
|
int attributeIndex = getAttributeIndex(attributeName, false); |
||||||
|
if (attributeIndex == -1) { |
||||||
|
return Optional.empty(); |
||||||
|
} |
||||||
|
Method attribute = this.mapping.getAttributes().get(attributeIndex); |
||||||
|
return Optional.ofNullable(adapt(attribute, attribute.getDefaultValue(), type)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public MergedAnnotation<A> filterAttributes(Predicate<String> predicate) { |
||||||
|
Assert.notNull(predicate, "Predicate must not be null"); |
||||||
|
if (this.attributeFilter != null) { |
||||||
|
predicate = this.attributeFilter.and(predicate); |
||||||
|
} |
||||||
|
return new TypeMappedAnnotation<>(this.mapping, this.source, this.rootAttributes, |
||||||
|
this.valueExtractor, this.aggregateIndex, this.useMergedValues, predicate, |
||||||
|
this.resolvedRootMirrors, this.resolvedMirrors); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public MergedAnnotation<A> withNonMergedAttributes() { |
||||||
|
return new TypeMappedAnnotation<>(this.mapping, this.source, this.rootAttributes, |
||||||
|
this.valueExtractor, this.aggregateIndex, false, this.attributeFilter, |
||||||
|
this.resolvedRootMirrors, this.resolvedMirrors); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
public <T extends Map<String, Object>> T asMap( |
||||||
|
@Nullable Function<MergedAnnotation<?>, T> factory, MapValues... options) { |
||||||
|
|
||||||
|
T map = factory != null ? factory.apply(this) : (T) new LinkedHashMap<String, Object>(); |
||||||
|
Assert.state(map != null, |
||||||
|
"Factory used to create MergedAnnotation Map must not return null;"); |
||||||
|
AttributeMethods attributes = this.mapping.getAttributes(); |
||||||
|
for (int i = 0; i < attributes.size(); i++) { |
||||||
|
Method attribute = attributes.get(i); |
||||||
|
Object value = isFiltered(attribute.getName()) ? |
||||||
|
null : |
||||||
|
getValue(i, getTypeForMapOptions(attribute, options)); |
||||||
|
if (value != null) { |
||||||
|
map.put(attribute.getName(), |
||||||
|
adaptValueForMapOptions(attribute, value, factory, options)); |
||||||
|
} |
||||||
|
} |
||||||
|
return (factory != null) ? map : (T) Collections.unmodifiableMap(map); |
||||||
|
} |
||||||
|
|
||||||
|
private Class<?> getTypeForMapOptions(Method attribute, MapValues[] options) { |
||||||
|
Class<?> attributeType = attribute.getReturnType(); |
||||||
|
Class<?> componentType = attributeType.isArray() ? |
||||||
|
attributeType.getComponentType() : |
||||||
|
attributeType; |
||||||
|
if (MapValues.CLASS_TO_STRING.isIn(options) && componentType == Class.class) { |
||||||
|
return attributeType.isArray() ? String[].class : String.class; |
||||||
|
} |
||||||
|
return Object.class; |
||||||
|
} |
||||||
|
|
||||||
|
private <T extends Map<String, Object>> Object adaptValueForMapOptions( |
||||||
|
Method attribute, Object value, |
||||||
|
@Nullable Function<MergedAnnotation<?>, T> factory, MapValues[] options) { |
||||||
|
|
||||||
|
if (value instanceof MergedAnnotation) { |
||||||
|
MergedAnnotation<?> annotation = (MergedAnnotation<?>) value; |
||||||
|
return MapValues.ANNOTATION_TO_MAP.isIn(options) ? |
||||||
|
annotation.asMap(factory, options) : |
||||||
|
annotation.synthesize(); |
||||||
|
} |
||||||
|
if (value instanceof MergedAnnotation[]) { |
||||||
|
MergedAnnotation<?>[] annotations = (MergedAnnotation<?>[]) value; |
||||||
|
if (MapValues.ANNOTATION_TO_MAP.isIn(options)) { |
||||||
|
Class<?> componentType = Map.class; |
||||||
|
if (factory != null) { |
||||||
|
componentType = factory.apply(this).getClass(); |
||||||
|
} |
||||||
|
Object result = Array.newInstance(componentType, annotations.length); |
||||||
|
for (int i = 0; i < annotations.length; i++) { |
||||||
|
Array.set(result, i, annotations[i].asMap(factory, options)); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
Object result = Array.newInstance( |
||||||
|
attribute.getReturnType().getComponentType(), annotations.length); |
||||||
|
for (int i = 0; i < annotations.length; i++) { |
||||||
|
Array.set(result, i, annotations[i].synthesize()); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected A createSynthesized() { |
||||||
|
return SynthesizedMergedAnnotationInvocationHandler.createProxy(this, getAnnotationType()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
String string = this.string; |
||||||
|
if (string == null) { |
||||||
|
StringBuilder builder = new StringBuilder(); |
||||||
|
builder.append("@"); |
||||||
|
builder.append(getType()); |
||||||
|
builder.append("("); |
||||||
|
for (int i = 0; i < this.mapping.getAttributes().size(); i++) { |
||||||
|
Method attribute = this.mapping.getAttributes().get(i); |
||||||
|
builder.append(i == 0 ? "" : ", "); |
||||||
|
builder.append(attribute.getName()); |
||||||
|
builder.append("="); |
||||||
|
builder.append(toString(getValue(i, Object.class))); |
||||||
|
} |
||||||
|
builder.append(")"); |
||||||
|
string = builder.toString(); |
||||||
|
this.string = string; |
||||||
|
} |
||||||
|
return string; |
||||||
|
} |
||||||
|
|
||||||
|
private Object toString(@Nullable Object value) { |
||||||
|
if (value == null) { |
||||||
|
return ""; |
||||||
|
} |
||||||
|
if (value instanceof Class) { |
||||||
|
return ((Class<?>) value).getName(); |
||||||
|
} |
||||||
|
if (value.getClass().isArray()) { |
||||||
|
StringBuilder builder = new StringBuilder(); |
||||||
|
builder.append("["); |
||||||
|
for (int i = 0; i < Array.getLength(value); i++) { |
||||||
|
builder.append(i == 0 ? "" : ", "); |
||||||
|
builder.append(toString(Array.get(value, i))); |
||||||
|
} |
||||||
|
builder.append("]"); |
||||||
|
return builder.toString(); |
||||||
|
} |
||||||
|
return String.valueOf(value); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
protected <T> T getAttributeValue(String attributeName, Class<T> type) { |
||||||
|
int attributeIndex = getAttributeIndex(attributeName, false); |
||||||
|
return attributeIndex != -1 ? getValue(attributeIndex, type) : null; |
||||||
|
} |
||||||
|
|
||||||
|
protected final <T> T getRequiredValue(int attributeIndex, Class<T> type) { |
||||||
|
T value = getValue(attributeIndex, type); |
||||||
|
if (value == null) { |
||||||
|
throw new NoSuchElementException( |
||||||
|
"No element at attribute index " + attributeIndex); |
||||||
|
} |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private <T> T getValue(int attributeIndex, Class<T> type) { |
||||||
|
Method attribute = this.mapping.getAttributes().get(attributeIndex); |
||||||
|
Object value = getValue(attributeIndex, true, true); |
||||||
|
if (value == null) { |
||||||
|
value = attribute.getDefaultValue(); |
||||||
|
} |
||||||
|
return adapt(attribute, value, type); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private Object getValue(int attributeIndex, boolean useConventionMapping, |
||||||
|
boolean resolveMirrors) { |
||||||
|
AnnotationTypeMapping mapping = this.mapping; |
||||||
|
if (this.useMergedValues) { |
||||||
|
int mappedIndex = this.mapping.getAliasMapping(attributeIndex); |
||||||
|
if (mappedIndex == -1 && useConventionMapping) { |
||||||
|
mappedIndex = this.mapping.getConventionMapping(attributeIndex); |
||||||
|
} |
||||||
|
if (mappedIndex != -1) { |
||||||
|
mapping = mapping.getRoot(); |
||||||
|
attributeIndex = mappedIndex; |
||||||
|
} |
||||||
|
} |
||||||
|
if (resolveMirrors) { |
||||||
|
attributeIndex = (mapping.getDepth() != 0 ? |
||||||
|
this.resolvedMirrors : |
||||||
|
this.resolvedRootMirrors)[attributeIndex]; |
||||||
|
} |
||||||
|
if (attributeIndex == -1) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
Method attribute = mapping.getAttributes().get(attributeIndex); |
||||||
|
if (mapping.getDepth() == 0) { |
||||||
|
return this.valueExtractor.apply(attribute, this.rootAttributes); |
||||||
|
} |
||||||
|
return getValueFromMetaAnnotation(attribute); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private Object getValueFromMetaAnnotation(Method attribute) { |
||||||
|
AnnotationTypeMapping mapping = this.mapping; |
||||||
|
if (this.useMergedValues && !VALUE.equals(attribute.getName())) { |
||||||
|
AnnotationTypeMapping candidate = mapping; |
||||||
|
while (candidate != null && candidate.getDepth() > 0) { |
||||||
|
int attributeIndex = candidate.getAttributes().indexOf(attribute.getName()); |
||||||
|
if (attributeIndex != -1) { |
||||||
|
Method candidateAttribute = candidate.getAttributes().get(attributeIndex); |
||||||
|
if (candidateAttribute.getReturnType().equals(attribute.getReturnType())) { |
||||||
|
mapping = candidate; |
||||||
|
attribute = candidateAttribute; |
||||||
|
} |
||||||
|
} |
||||||
|
candidate = candidate.getParent(); |
||||||
|
} |
||||||
|
} |
||||||
|
return ReflectionUtils.invokeMethod(attribute, mapping.getAnnotation()); |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
@Nullable |
||||||
|
private <T> T adapt(Method attribute, @Nullable Object value, Class<T> type) { |
||||||
|
if (value == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
value = adaptForAttribute(attribute, value); |
||||||
|
if (type == Object.class) { |
||||||
|
type = (Class<T>) getDefaultAdaptType(attribute); |
||||||
|
} |
||||||
|
else if (value instanceof Class && type == String.class) { |
||||||
|
value = ((Class<?>) value).getName(); |
||||||
|
} |
||||||
|
else if (value instanceof Class[] && type == String[].class) { |
||||||
|
Class<?>[] classes = (Class[]) value; |
||||||
|
String[] names = new String[classes.length]; |
||||||
|
for (int i = 0; i < classes.length; i++) { |
||||||
|
names[i] = classes[i].getName(); |
||||||
|
} |
||||||
|
value = names; |
||||||
|
} |
||||||
|
else if (value instanceof MergedAnnotation && type.isAnnotation()) { |
||||||
|
MergedAnnotation<?> annotation = (MergedAnnotation<?>) value; |
||||||
|
value = annotation.synthesize(); |
||||||
|
} |
||||||
|
else if (value instanceof MergedAnnotation[] && type.isArray() |
||||||
|
&& type.getComponentType().isAnnotation()) { |
||||||
|
MergedAnnotation<?>[] annotations = (MergedAnnotation<?>[]) value; |
||||||
|
Object array = Array.newInstance(type.getComponentType(), annotations.length); |
||||||
|
for (int i = 0; i < annotations.length; i++) { |
||||||
|
Array.set(array, i, annotations[i].synthesize()); |
||||||
|
} |
||||||
|
value = array; |
||||||
|
} |
||||||
|
if (!type.isInstance(value)) { |
||||||
|
throw new IllegalArgumentException("Unable to adapt value of type " + |
||||||
|
value.getClass().getName() + " to " + type.getName()); |
||||||
|
} |
||||||
|
return (T) value; |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
private Object adaptForAttribute(Method attribute, Object value) { |
||||||
|
Class<?> attributeType = ClassUtils.resolvePrimitiveIfNecessary(attribute.getReturnType()); |
||||||
|
if (attributeType.isArray() && !value.getClass().isArray()) { |
||||||
|
Object array = Array.newInstance(value.getClass(), 1); |
||||||
|
Array.set(array, 0, value); |
||||||
|
return adaptForAttribute(attribute, array); |
||||||
|
} |
||||||
|
if (attributeType.isAnnotation()) { |
||||||
|
return adaptToMergedAnnotation(value,(Class<? extends Annotation>) attributeType); |
||||||
|
} |
||||||
|
if (attributeType.isArray() && |
||||||
|
attributeType.getComponentType().isAnnotation() && |
||||||
|
value.getClass().isArray()) { |
||||||
|
MergedAnnotation<?>[] result = new MergedAnnotation<?>[Array.getLength(value)]; |
||||||
|
for (int i = 0; i < result.length; i++) { |
||||||
|
result[i] = adaptToMergedAnnotation(Array.get(value, i), |
||||||
|
(Class<? extends Annotation>) attributeType.getComponentType()); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
if ((attributeType == Class.class && value instanceof String) || |
||||||
|
(attributeType == Class[].class && value instanceof String[])) { |
||||||
|
return value; |
||||||
|
} |
||||||
|
if (!attributeType.isInstance(value)) { |
||||||
|
throw new IllegalStateException("Attribute '" + attribute.getName() + |
||||||
|
"' in annotation " + getType() + " should be compatible with " + |
||||||
|
attributeType.getName() + " but a " + value.getClass().getName() + |
||||||
|
" value was returned"); |
||||||
|
} |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
private MergedAnnotation<?> adaptToMergedAnnotation(Object value, |
||||||
|
Class<? extends Annotation> annotationType) { |
||||||
|
|
||||||
|
AnnotationFilter filter = AnnotationFilter.mostAppropriateFor(annotationType); |
||||||
|
AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType( |
||||||
|
annotationType, filter).get(0); |
||||||
|
return new TypeMappedAnnotation<>(mapping, this.source, value, |
||||||
|
this.getValueExtractor(value), this.aggregateIndex); |
||||||
|
} |
||||||
|
|
||||||
|
private BiFunction<Method, Object, Object> getValueExtractor(Object value) { |
||||||
|
if (value instanceof Annotation) { |
||||||
|
return ReflectionUtils::invokeMethod; |
||||||
|
} |
||||||
|
if (value instanceof Map) { |
||||||
|
return TypeMappedAnnotation::extractFromMap; |
||||||
|
} |
||||||
|
return this.valueExtractor; |
||||||
|
} |
||||||
|
|
||||||
|
private Class<?> getDefaultAdaptType(Method attribute) { |
||||||
|
Class<?> attributeType = attribute.getReturnType(); |
||||||
|
if (attributeType.isAnnotation()) { |
||||||
|
return MergedAnnotation.class; |
||||||
|
} |
||||||
|
if (attributeType.isArray() && attributeType.getComponentType().isAnnotation()) { |
||||||
|
return MergedAnnotation[].class; |
||||||
|
} |
||||||
|
return ClassUtils.resolvePrimitiveIfNecessary(attributeType); |
||||||
|
} |
||||||
|
|
||||||
|
private int getAttributeIndex(String attributeName, boolean required) { |
||||||
|
Assert.hasText(attributeName, "AttributeName must not be null"); |
||||||
|
int attributeIndex = isFiltered(attributeName) ? |
||||||
|
-1 : |
||||||
|
this.mapping.getAttributes().indexOf(attributeName); |
||||||
|
if (attributeIndex == -1 && required) { |
||||||
|
throw new NoSuchElementException("No attribute named '" + attributeName + |
||||||
|
"' present in merged annotation " + getType()); |
||||||
|
} |
||||||
|
return attributeIndex; |
||||||
|
} |
||||||
|
|
||||||
|
private boolean isFiltered(String attributeName) { |
||||||
|
if (this.attributeFilter != null) { |
||||||
|
return !this.attributeFilter.test(attributeName); |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
private Class<A> getAnnotationType() { |
||||||
|
return (Class<A>) this.mapping.getAnnotationType(); |
||||||
|
} |
||||||
|
|
||||||
|
static <A extends Annotation> MergedAnnotation<A> from(@Nullable Object source, |
||||||
|
A annotation) { |
||||||
|
|
||||||
|
Assert.notNull(annotation, "Annotation must not be null"); |
||||||
|
AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( |
||||||
|
annotation.annotationType()); |
||||||
|
return new TypeMappedAnnotation<>(mappings.get(0), source, annotation, |
||||||
|
ReflectionUtils::invokeMethod, 0); |
||||||
|
} |
||||||
|
|
||||||
|
static <A extends Annotation> MergedAnnotation<A> from(@Nullable Object source, |
||||||
|
Class<A> annotationType, @Nullable Map<String, ?> attributes) { |
||||||
|
|
||||||
|
Assert.notNull(annotationType, "AnnotationType must not be null"); |
||||||
|
AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(annotationType); |
||||||
|
return new TypeMappedAnnotation<>(mappings.get(0), source, attributes, |
||||||
|
TypeMappedAnnotation::extractFromMap, 0); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
static <A extends Annotation> TypeMappedAnnotation<A> createIfPossible( |
||||||
|
AnnotationTypeMapping mapping, @Nullable Object source, Annotation annotation, |
||||||
|
int aggregateIndex, IntrospectionFailureLogger logger) { |
||||||
|
|
||||||
|
try { |
||||||
|
return new TypeMappedAnnotation<>(mapping, source, annotation, |
||||||
|
ReflectionUtils::invokeMethod, aggregateIndex); |
||||||
|
} |
||||||
|
catch (Exception ex) { |
||||||
|
if (ex instanceof AnnotationConfigurationException) { |
||||||
|
throw (AnnotationConfigurationException) ex; |
||||||
|
} |
||||||
|
if (logger.isEnabled()) { |
||||||
|
String type = mapping.getAnnotationType().getName(); |
||||||
|
String item = mapping.getDepth() == 0 ? |
||||||
|
"annotation " + type : |
||||||
|
"meta-annotation " + type + " from " + mapping.getRoot().getAnnotationType().getName(); |
||||||
|
logger.log("Failed to introspect " + item, source, ex); |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
@Nullable |
||||||
|
private static Object extractFromMap(Method attribute, @Nullable Object map) { |
||||||
|
return map != null ? ((Map<String, ?>) map).get(attribute.getName()) : null; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,697 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.reflect.AnnotatedElement; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.Iterator; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Spliterator; |
||||||
|
import java.util.Spliterators; |
||||||
|
import java.util.function.Consumer; |
||||||
|
import java.util.function.Predicate; |
||||||
|
import java.util.stream.Stream; |
||||||
|
import java.util.stream.StreamSupport; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link MergedAnnotations} implementation that searches for and adapts |
||||||
|
* annotations and meta-annotations using {@link AnnotationTypeMappings}. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.1 |
||||||
|
*/ |
||||||
|
final class TypeMappedAnnotations implements MergedAnnotations { |
||||||
|
|
||||||
|
private static final AnnotationFilter FILTER_ALL = annotationType -> true; |
||||||
|
|
||||||
|
private static final MergedAnnotations NONE = new TypeMappedAnnotations(null, |
||||||
|
new Annotation[0], RepeatableContainers.none(), FILTER_ALL); |
||||||
|
|
||||||
|
|
||||||
|
@Nullable |
||||||
|
private final Object source; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private final AnnotatedElement element; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private final SearchStrategy searchStrategy; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private final Annotation[] annotations; |
||||||
|
|
||||||
|
private final RepeatableContainers repeatableContainers; |
||||||
|
|
||||||
|
private final AnnotationFilter annotationFilter; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private volatile List<Aggregate> aggregates; |
||||||
|
|
||||||
|
|
||||||
|
private TypeMappedAnnotations(AnnotatedElement element, SearchStrategy searchStrategy, |
||||||
|
RepeatableContainers repeatableContainers, |
||||||
|
AnnotationFilter annotationFilter) { |
||||||
|
|
||||||
|
this.source = element; |
||||||
|
this.element = element; |
||||||
|
this.searchStrategy = searchStrategy; |
||||||
|
this.annotations = null; |
||||||
|
this.repeatableContainers = repeatableContainers; |
||||||
|
this.annotationFilter = annotationFilter; |
||||||
|
} |
||||||
|
|
||||||
|
private TypeMappedAnnotations(@Nullable Object source, Annotation[] annotations, |
||||||
|
RepeatableContainers repeatableContainers, |
||||||
|
AnnotationFilter annotationFilter) { |
||||||
|
|
||||||
|
this.source = source; |
||||||
|
this.element = null; |
||||||
|
this.searchStrategy = null; |
||||||
|
this.annotations = annotations; |
||||||
|
this.repeatableContainers = repeatableContainers; |
||||||
|
this.annotationFilter = annotationFilter; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public <A extends Annotation> boolean isPresent(@Nullable Class<A> annotationType) { |
||||||
|
if (annotationType == null || this.annotationFilter.matches(annotationType)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
return Boolean.TRUE.equals(scan(annotationType, |
||||||
|
IsPresent.get(this.repeatableContainers, this.annotationFilter, false))); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isPresent(@Nullable String annotationType) { |
||||||
|
if (annotationType == null || this.annotationFilter.matches(annotationType)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
return Boolean.TRUE.equals(scan(annotationType, |
||||||
|
IsPresent.get(this.repeatableContainers, this.annotationFilter, false))); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public <A extends Annotation> boolean isDirectlyPresent(@Nullable Class<A> annotationType) { |
||||||
|
if (annotationType == null || this.annotationFilter.matches(annotationType)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
return Boolean.TRUE.equals(scan(annotationType, |
||||||
|
IsPresent.get(this.repeatableContainers, this.annotationFilter, true))); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isDirectlyPresent(@Nullable String annotationType) { |
||||||
|
if (annotationType == null || this.annotationFilter.matches(annotationType)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
return Boolean.TRUE.equals(scan(annotationType, |
||||||
|
IsPresent.get(this.repeatableContainers, this.annotationFilter, true))); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public <A extends Annotation> MergedAnnotation<A> get( |
||||||
|
@Nullable Class<A> annotationType) { |
||||||
|
|
||||||
|
return get(annotationType, null, null); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public <A extends Annotation> MergedAnnotation<A> get( |
||||||
|
@Nullable Class<A> annotationType, |
||||||
|
@Nullable Predicate<? super MergedAnnotation<A>> predicate) { |
||||||
|
|
||||||
|
return get(annotationType, predicate, null); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public <A extends Annotation> MergedAnnotation<A> get( |
||||||
|
@Nullable Class<A> annotationType, |
||||||
|
@Nullable Predicate<? super MergedAnnotation<A>> predicate, |
||||||
|
@Nullable MergedAnnotationSelector<A> selector) { |
||||||
|
|
||||||
|
if (annotationType == null || this.annotationFilter.matches(annotationType)) { |
||||||
|
return MergedAnnotation.missing(); |
||||||
|
} |
||||||
|
MergedAnnotation<A> result = scan(annotationType, |
||||||
|
new MergedAnnotationFinder<>(annotationType, predicate, selector)); |
||||||
|
return result != null ? result : MergedAnnotation.missing(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public <A extends Annotation> MergedAnnotation<A> get( |
||||||
|
@Nullable String annotationType) { |
||||||
|
return get(annotationType, null, null); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public <A extends Annotation> MergedAnnotation<A> get(@Nullable String annotationType, |
||||||
|
@Nullable Predicate<? super MergedAnnotation<A>> predicate) { |
||||||
|
return get(annotationType, predicate, null); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public <A extends Annotation> MergedAnnotation<A> get(@Nullable String annotationType, |
||||||
|
@Nullable Predicate<? super MergedAnnotation<A>> predicate, |
||||||
|
@Nullable MergedAnnotationSelector<A> selector) { |
||||||
|
|
||||||
|
if (annotationType == null || this.annotationFilter.matches(annotationType)) { |
||||||
|
return MergedAnnotation.missing(); |
||||||
|
} |
||||||
|
MergedAnnotation<A> result = scan(annotationType, |
||||||
|
new MergedAnnotationFinder<>(annotationType, predicate, selector)); |
||||||
|
return result != null ? result : MergedAnnotation.missing(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public <A extends Annotation> Stream<MergedAnnotation<A>> stream( |
||||||
|
@Nullable Class<A> annotationType) { |
||||||
|
|
||||||
|
if (this.annotationFilter == FILTER_ALL) { |
||||||
|
return Stream.empty(); |
||||||
|
} |
||||||
|
return StreamSupport.stream(spliterator(annotationType), false); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public <A extends Annotation> Stream<MergedAnnotation<A>> stream( |
||||||
|
@Nullable String annotationType) { |
||||||
|
|
||||||
|
if (this.annotationFilter == FILTER_ALL) { |
||||||
|
return Stream.empty(); |
||||||
|
} |
||||||
|
return StreamSupport.stream(spliterator(annotationType), false); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Stream<MergedAnnotation<Annotation>> stream() { |
||||||
|
if (this.annotationFilter == FILTER_ALL) { |
||||||
|
return Stream.empty(); |
||||||
|
} |
||||||
|
return StreamSupport.stream(spliterator(), false); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Iterator<MergedAnnotation<Annotation>> iterator() { |
||||||
|
if (this.annotationFilter == FILTER_ALL) { |
||||||
|
return Collections.emptyIterator(); |
||||||
|
} |
||||||
|
return Spliterators.iterator(spliterator()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Spliterator<MergedAnnotation<Annotation>> spliterator() { |
||||||
|
if (this.annotationFilter == FILTER_ALL) { |
||||||
|
return Collections.<MergedAnnotation<Annotation>> emptyList().spliterator(); |
||||||
|
} |
||||||
|
return spliterator(null); |
||||||
|
} |
||||||
|
|
||||||
|
private <A extends Annotation> Spliterator<MergedAnnotation<A>> spliterator( |
||||||
|
@Nullable Object annotationType) { |
||||||
|
return new AggregatesSpliterator<>(annotationType, getAggregates()); |
||||||
|
} |
||||||
|
|
||||||
|
private List<Aggregate> getAggregates() { |
||||||
|
List<Aggregate> aggregates = this.aggregates; |
||||||
|
if (aggregates == null) { |
||||||
|
aggregates = scan(this, new AggregatesCollector()); |
||||||
|
if (aggregates == null || aggregates.isEmpty()) { |
||||||
|
aggregates = Collections.emptyList(); |
||||||
|
} |
||||||
|
this.aggregates = aggregates; |
||||||
|
} |
||||||
|
return aggregates; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private <C, R> R scan(C criteria, AnnotationsProcessor<C, R> processor) { |
||||||
|
if (this.annotations != null) { |
||||||
|
R result = processor.doWithAnnotations(criteria, 0, this.source, this.annotations); |
||||||
|
return processor.finish(result); |
||||||
|
} |
||||||
|
if (this.element != null && this.searchStrategy != null) { |
||||||
|
return AnnotationsScanner.scan(criteria, this.element, this.searchStrategy, processor); |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
static MergedAnnotations from(@Nullable AnnotatedElement element, |
||||||
|
SearchStrategy searchStrategy, RepeatableContainers repeatableContainers, |
||||||
|
AnnotationFilter annotationFilter) { |
||||||
|
|
||||||
|
if (element == null || AnnotationsScanner.isKnownEmpty(element, searchStrategy, annotationFilter)) { |
||||||
|
return NONE; |
||||||
|
} |
||||||
|
return new TypeMappedAnnotations(element, searchStrategy, repeatableContainers, |
||||||
|
annotationFilter); |
||||||
|
} |
||||||
|
|
||||||
|
static MergedAnnotations from(@Nullable Object source, |
||||||
|
Annotation[] annotations, RepeatableContainers repeatableContainers, |
||||||
|
AnnotationFilter annotationFilter) { |
||||||
|
|
||||||
|
if (annotations.length == 0) { |
||||||
|
return NONE; |
||||||
|
} |
||||||
|
return new TypeMappedAnnotations(source, annotations, repeatableContainers, annotationFilter); |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean isMappingForType(@Nullable AnnotationTypeMapping mapping, |
||||||
|
AnnotationFilter annotationFilter, @Nullable Object requiredType) { |
||||||
|
|
||||||
|
if (mapping == null) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
Class<? extends Annotation> actualType = mapping.getAnnotationType(); |
||||||
|
return !annotationFilter.matches(actualType) && |
||||||
|
(requiredType == null || actualType == requiredType || actualType.getName().equals(requiredType)); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* {@link AnnotationsProcessor} used to detect if an annotation is directly |
||||||
|
* or meta-present. |
||||||
|
*/ |
||||||
|
private static final class IsPresent |
||||||
|
implements AnnotationsProcessor<Object, Boolean> { |
||||||
|
|
||||||
|
/** |
||||||
|
* Shared instances that save us needing to create a new processor for |
||||||
|
* the common combinations. |
||||||
|
*/ |
||||||
|
private static final IsPresent[] SHARED; |
||||||
|
static { |
||||||
|
SHARED = new IsPresent[4]; |
||||||
|
SHARED[0] = new IsPresent(RepeatableContainers.none(), AnnotationFilter.PLAIN, true); |
||||||
|
SHARED[1] = new IsPresent(RepeatableContainers.none(), AnnotationFilter.PLAIN, false); |
||||||
|
SHARED[2] = new IsPresent(RepeatableContainers.standardRepeatables(), AnnotationFilter.PLAIN, true); |
||||||
|
SHARED[3] = new IsPresent(RepeatableContainers.standardRepeatables(), AnnotationFilter.PLAIN, false); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private final RepeatableContainers repeatableContainers; |
||||||
|
|
||||||
|
private final AnnotationFilter annotationFilter; |
||||||
|
|
||||||
|
private final boolean directOnly; |
||||||
|
|
||||||
|
|
||||||
|
private IsPresent(RepeatableContainers repeatableContainers, |
||||||
|
AnnotationFilter annotationFilter, boolean directOnly) { |
||||||
|
|
||||||
|
this.repeatableContainers = repeatableContainers; |
||||||
|
this.annotationFilter = annotationFilter; |
||||||
|
this.directOnly = directOnly; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public Boolean doWithAnnotations(Object requiredType, int aggregateIndex, |
||||||
|
@Nullable Object source, Annotation[] annotations) { |
||||||
|
|
||||||
|
for (Annotation annotation : annotations) { |
||||||
|
if (annotation != null) { |
||||||
|
Class<? extends Annotation> type = annotation.annotationType(); |
||||||
|
if (type != null && !this.annotationFilter.matches(type)) { |
||||||
|
if (type == requiredType || type.getName().equals(requiredType)) { |
||||||
|
return Boolean.TRUE; |
||||||
|
} |
||||||
|
Annotation[] repeatedAnnotations = this.repeatableContainers |
||||||
|
.findRepeatedAnnotations(annotation); |
||||||
|
if (repeatedAnnotations != null) { |
||||||
|
Boolean result = doWithAnnotations(requiredType, aggregateIndex, |
||||||
|
source, repeatedAnnotations); |
||||||
|
if (result != null) { |
||||||
|
return result; |
||||||
|
} |
||||||
|
} |
||||||
|
if (!this.directOnly) { |
||||||
|
AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(type); |
||||||
|
for (int i = 0; i < mappings.size(); i++) { |
||||||
|
AnnotationTypeMapping mapping = mappings.get(i); |
||||||
|
if (isMappingForType(mapping, this.annotationFilter, requiredType)) { |
||||||
|
return Boolean.TRUE; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
static IsPresent get(RepeatableContainers repeatableContainers, |
||||||
|
AnnotationFilter annotationFilter, boolean directOnly) { |
||||||
|
|
||||||
|
// Use a single shared instance for common combinations
|
||||||
|
if (annotationFilter == AnnotationFilter.PLAIN) { |
||||||
|
if (repeatableContainers == RepeatableContainers.none()) { |
||||||
|
return SHARED[directOnly ? 0 : 1]; |
||||||
|
} |
||||||
|
if (repeatableContainers == RepeatableContainers.standardRepeatables()) { |
||||||
|
return SHARED[directOnly ? 2 : 3]; |
||||||
|
} |
||||||
|
} |
||||||
|
return new IsPresent(repeatableContainers, annotationFilter, directOnly); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* {@link AnnotationsProcessor} that finds a single |
||||||
|
* {@link MergedAnnotation}. |
||||||
|
*/ |
||||||
|
private class MergedAnnotationFinder<A extends Annotation> |
||||||
|
implements AnnotationsProcessor<Object, MergedAnnotation<A>> { |
||||||
|
|
||||||
|
private final Object requiredType; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private final Predicate<? super MergedAnnotation<A>> predicate; |
||||||
|
|
||||||
|
private final MergedAnnotationSelector<A> selector; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private MergedAnnotation<A> result; |
||||||
|
|
||||||
|
|
||||||
|
MergedAnnotationFinder(Object requiredType, |
||||||
|
@Nullable Predicate<? super MergedAnnotation<A>> predicate, |
||||||
|
@Nullable MergedAnnotationSelector<A> selector) { |
||||||
|
|
||||||
|
this.requiredType = requiredType; |
||||||
|
this.predicate = predicate; |
||||||
|
this.selector = selector != null ? selector |
||||||
|
: MergedAnnotationSelectors.nearest(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public MergedAnnotation<A> doWithAggregate(Object context, int aggregateIndex) { |
||||||
|
return this.result; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public MergedAnnotation<A> doWithAnnotations(Object type, int aggregateIndex, |
||||||
|
@Nullable Object source, Annotation[] annotations) { |
||||||
|
|
||||||
|
for (Annotation annotation : annotations) { |
||||||
|
if (annotation != null && |
||||||
|
!annotationFilter.matches(annotation)) { |
||||||
|
MergedAnnotation<A> result = process(type, aggregateIndex, source, annotation); |
||||||
|
if (result != null) { |
||||||
|
return result; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private MergedAnnotation<A> process(Object type, int aggregateIndex, |
||||||
|
@Nullable Object source, Annotation annotation) { |
||||||
|
Annotation[] repeatedAnnotations = repeatableContainers.findRepeatedAnnotations( |
||||||
|
annotation); |
||||||
|
if (repeatedAnnotations != null) { |
||||||
|
return doWithAnnotations(type, aggregateIndex, source, |
||||||
|
repeatedAnnotations); |
||||||
|
} |
||||||
|
AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( |
||||||
|
annotation.annotationType(), |
||||||
|
annotationFilter); |
||||||
|
for (int i = 0; i < mappings.size(); i++) { |
||||||
|
AnnotationTypeMapping mapping = mappings.get(i); |
||||||
|
if (isMappingForType(mapping, annotationFilter, |
||||||
|
this.requiredType)) { |
||||||
|
MergedAnnotation<A> candidate = TypeMappedAnnotation.createIfPossible( |
||||||
|
mapping, source, annotation, aggregateIndex, |
||||||
|
IntrospectionFailureLogger.INFO); |
||||||
|
if (candidate != null && (this.predicate == null |
||||||
|
|| this.predicate.test(candidate))) { |
||||||
|
if (this.selector.isBestCandidate(candidate)) { |
||||||
|
return candidate; |
||||||
|
} |
||||||
|
updateLastResult(candidate); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
private void updateLastResult(MergedAnnotation<A> candidate) { |
||||||
|
MergedAnnotation<A> lastResult = this.result; |
||||||
|
this.result = lastResult != null |
||||||
|
? this.selector.select(lastResult, candidate) |
||||||
|
: candidate; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public MergedAnnotation<A> finish(@Nullable MergedAnnotation<A> result) { |
||||||
|
return result != null ? result : this.result; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link AnnotationsProcessor} that collects {@link Aggregate} instances. |
||||||
|
*/ |
||||||
|
private class AggregatesCollector |
||||||
|
implements AnnotationsProcessor<Object, List<Aggregate>> { |
||||||
|
|
||||||
|
private final List<Aggregate> aggregates = new ArrayList<>(); |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public List<Aggregate> doWithAnnotations(Object criteria, int aggregateIndex, |
||||||
|
@Nullable Object source, Annotation[] annotations) { |
||||||
|
this.aggregates.add(createAggregate(aggregateIndex, source, annotations)); |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
private Aggregate createAggregate(int aggregateIndex, @Nullable Object source, |
||||||
|
Annotation[] annotations) { |
||||||
|
List<Annotation> aggregateAnnotations = getAggregateAnnotations(annotations); |
||||||
|
return new Aggregate(aggregateIndex, source, aggregateAnnotations); |
||||||
|
} |
||||||
|
|
||||||
|
private List<Annotation> getAggregateAnnotations(Annotation[] annotations) { |
||||||
|
List<Annotation> result = new ArrayList<>(annotations.length); |
||||||
|
addAggregateAnnotations(result, annotations); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
private void addAggregateAnnotations(List<Annotation> aggregateAnnotations, |
||||||
|
Annotation[] annotations) { |
||||||
|
for (Annotation annotation : annotations) { |
||||||
|
if (annotation != null |
||||||
|
&& !annotationFilter.matches( |
||||||
|
annotation)) { |
||||||
|
Annotation[] repeatedAnnotations = repeatableContainers.findRepeatedAnnotations( |
||||||
|
annotation); |
||||||
|
if (repeatedAnnotations != null) { |
||||||
|
addAggregateAnnotations(aggregateAnnotations, |
||||||
|
repeatedAnnotations); |
||||||
|
} |
||||||
|
else { |
||||||
|
aggregateAnnotations.add(annotation); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<Aggregate> finish(@Nullable List<Aggregate> processResult) { |
||||||
|
return this.aggregates; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
private static class Aggregate { |
||||||
|
|
||||||
|
private final int aggregateIndex; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private final Object source; |
||||||
|
|
||||||
|
private final List<Annotation> annotations; |
||||||
|
|
||||||
|
private final AnnotationTypeMappings[] mappings; |
||||||
|
|
||||||
|
Aggregate(int aggregateIndex, @Nullable Object source, |
||||||
|
List<Annotation> annotations) { |
||||||
|
this.aggregateIndex = aggregateIndex; |
||||||
|
this.source = source; |
||||||
|
this.annotations = annotations; |
||||||
|
this.mappings = new AnnotationTypeMappings[annotations.size()]; |
||||||
|
for (int i = 0; i < annotations.size(); i++) { |
||||||
|
this.mappings[i] = AnnotationTypeMappings.forAnnotationType( |
||||||
|
annotations.get(i).annotationType()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
int size() { |
||||||
|
return this.annotations.size(); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
AnnotationTypeMapping getMapping(int annotationIndex, int mappingIndex) { |
||||||
|
AnnotationTypeMappings mappings = getMappings(annotationIndex); |
||||||
|
return mappingIndex < mappings.size() ? mappings.get(mappingIndex) : null; |
||||||
|
} |
||||||
|
|
||||||
|
AnnotationTypeMappings getMappings(int annotationIndex) { |
||||||
|
return this.mappings[annotationIndex]; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
<A extends Annotation> MergedAnnotation<A> createMergedAnnotationIfPossible( |
||||||
|
int annotationIndex, int mappingIndex, |
||||||
|
IntrospectionFailureLogger logger) { |
||||||
|
return TypeMappedAnnotation.createIfPossible( |
||||||
|
this.mappings[annotationIndex].get(mappingIndex), this.source, |
||||||
|
this.annotations.get(annotationIndex), this.aggregateIndex, logger); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link Spliterator} used to consume merged annotations from the |
||||||
|
* aggregates in depth fist order. |
||||||
|
*/ |
||||||
|
private class AggregatesSpliterator<A extends Annotation> |
||||||
|
implements Spliterator<MergedAnnotation<A>> { |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private final Object requiredType; |
||||||
|
|
||||||
|
private final List<Aggregate> aggregates; |
||||||
|
|
||||||
|
private int aggregateCursor; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private int[] mappingCursors; |
||||||
|
|
||||||
|
AggregatesSpliterator(@Nullable Object requiredType, |
||||||
|
List<Aggregate> aggregates) { |
||||||
|
this.requiredType = requiredType; |
||||||
|
this.aggregates = aggregates; |
||||||
|
this.aggregateCursor = 0; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean tryAdvance(Consumer<? super MergedAnnotation<A>> action) { |
||||||
|
while (this.aggregateCursor < this.aggregates.size()) { |
||||||
|
Aggregate aggregate = this.aggregates.get(this.aggregateCursor); |
||||||
|
if (tryAdvance(aggregate, action)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
this.aggregateCursor++; |
||||||
|
this.mappingCursors = null; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
private boolean tryAdvance(Aggregate aggregate, |
||||||
|
Consumer<? super MergedAnnotation<A>> action) { |
||||||
|
if (this.mappingCursors == null) { |
||||||
|
this.mappingCursors = new int[aggregate.size()]; |
||||||
|
} |
||||||
|
int lowestDepth = Integer.MAX_VALUE; |
||||||
|
int annotationResult = -1; |
||||||
|
for (int annotationIndex = 0; annotationIndex < aggregate.size(); annotationIndex++) { |
||||||
|
AnnotationTypeMapping mapping = getNextSuitableMapping(aggregate, annotationIndex); |
||||||
|
if (mapping != null && mapping.getDepth() < lowestDepth) { |
||||||
|
annotationResult = annotationIndex; |
||||||
|
lowestDepth = mapping.getDepth(); |
||||||
|
} |
||||||
|
if (lowestDepth == 0) { |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
if (annotationResult != -1) { |
||||||
|
MergedAnnotation<A> mergedAnnotation = aggregate.createMergedAnnotationIfPossible( |
||||||
|
annotationResult, this.mappingCursors[annotationResult], |
||||||
|
this.requiredType != null ? IntrospectionFailureLogger.INFO : IntrospectionFailureLogger.DEBUG); |
||||||
|
this.mappingCursors[annotationResult]++; |
||||||
|
if (mergedAnnotation == null) { |
||||||
|
return tryAdvance(aggregate, action); |
||||||
|
} |
||||||
|
action.accept(mergedAnnotation); |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private AnnotationTypeMapping getNextSuitableMapping(Aggregate aggregate, |
||||||
|
int annotationIndex) { |
||||||
|
int[] cursors = this.mappingCursors; |
||||||
|
if (cursors != null) { |
||||||
|
AnnotationTypeMapping mapping; |
||||||
|
do { |
||||||
|
mapping = aggregate.getMapping(annotationIndex, cursors[annotationIndex]); |
||||||
|
if (isMappingForType(mapping, annotationFilter, this.requiredType)) { |
||||||
|
return mapping; |
||||||
|
} |
||||||
|
cursors[annotationIndex]++; |
||||||
|
} |
||||||
|
while (mapping != null); |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public Spliterator<MergedAnnotation<A>> trySplit() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public long estimateSize() { |
||||||
|
int size = 0; |
||||||
|
for (int aggregateIndex = this.aggregateCursor; |
||||||
|
aggregateIndex < this.aggregates.size(); aggregateIndex++) { |
||||||
|
Aggregate aggregate = this.aggregates.get(aggregateIndex); |
||||||
|
for (int annotationIndex = 0; annotationIndex < aggregate.size(); annotationIndex++) { |
||||||
|
AnnotationTypeMappings mappings = aggregate.getMappings(annotationIndex); |
||||||
|
int numberOfMappings = mappings.size(); |
||||||
|
if (aggregateIndex == this.aggregateCursor && this.mappingCursors != null) { |
||||||
|
numberOfMappings -= Math.min(this.mappingCursors[annotationIndex], mappings.size()); |
||||||
|
} |
||||||
|
size += numberOfMappings; |
||||||
|
} |
||||||
|
} |
||||||
|
return size; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int characteristics() { |
||||||
|
return NONNULL | IMMUTABLE; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,108 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Retention; |
||||||
|
import java.lang.annotation.RetentionPolicy; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests to ensure back-compatibility with Spring Framework 5.1. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
public class AnnotationBackCompatibiltyTests { |
||||||
|
|
||||||
|
@Test |
||||||
|
public void multiplRoutesToMetaAnnotation() { |
||||||
|
Class<WithMetaMetaTestAnnotation1AndMetaTestAnnotation2> source = WithMetaMetaTestAnnotation1AndMetaTestAnnotation2.class; |
||||||
|
// Merged annotation chooses lowest depth
|
||||||
|
MergedAnnotation<TestAnnotation> mergedAnnotation = MergedAnnotations.from(source).get(TestAnnotation.class); |
||||||
|
assertThat(mergedAnnotation.getString("value")).isEqualTo("testAndMetaTest"); |
||||||
|
// AnnotatedElementUtils finds first
|
||||||
|
TestAnnotation previousVersion = AnnotatedElementUtils.getMergedAnnotation(source, TestAnnotation.class); |
||||||
|
assertThat(previousVersion.value()).isEqualTo("metaTest"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void defaultValue() { |
||||||
|
DefaultValueAnnotation synthesized = MergedAnnotations.from(WithDefaultValue.class).get(DefaultValueAnnotation.class).synthesize(); |
||||||
|
assertThat(synthesized).isInstanceOf(SynthesizedAnnotation.class); |
||||||
|
Object defaultValue = AnnotationUtils.getDefaultValue(synthesized, "enumValue"); |
||||||
|
assertThat(defaultValue).isEqualTo(TestEnum.ONE); |
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@interface TestAnnotation { |
||||||
|
|
||||||
|
String value(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@TestAnnotation("metaTest") |
||||||
|
@interface MetaTestAnnotation { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@TestAnnotation("testAndMetaTest") |
||||||
|
@MetaTestAnnotation |
||||||
|
@interface TestAndMetaTestAnnotation { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@MetaTestAnnotation |
||||||
|
@interface MetaMetaTestAnnotation { |
||||||
|
} |
||||||
|
|
||||||
|
@MetaMetaTestAnnotation |
||||||
|
@TestAndMetaTestAnnotation |
||||||
|
static class WithMetaMetaTestAnnotation1AndMetaTestAnnotation2 { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@interface DefaultValueAnnotation { |
||||||
|
|
||||||
|
@AliasFor("enumAlais") |
||||||
|
TestEnum enumValue() default TestEnum.ONE; |
||||||
|
|
||||||
|
@AliasFor("enumValue") |
||||||
|
TestEnum enumAlais() default TestEnum.ONE; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@DefaultValueAnnotation |
||||||
|
static class WithDefaultValue { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static enum TestEnum { |
||||||
|
|
||||||
|
ONE, |
||||||
|
|
||||||
|
TWO |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,191 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.annotation.Retention; |
||||||
|
import java.lang.annotation.RetentionPolicy; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Collection; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.ObjectUtils; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for {@link AnnotationFilter}. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
*/ |
||||||
|
public class AnnotationFilterTests { |
||||||
|
|
||||||
|
private static final AnnotationFilter FILTER = annotationType -> ObjectUtils.nullSafeEquals( |
||||||
|
annotationType, TestAnnotation.class.getName()); |
||||||
|
|
||||||
|
@Test |
||||||
|
public void matchesAnnotationWhenAnnotationIsNullReturnsFalse() { |
||||||
|
TestAnnotation annotation = null; |
||||||
|
assertThat(FILTER.matches(annotation)).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void matchesAnnotationWhenMatchReturnsTrue() { |
||||||
|
TestAnnotation annotation = WithTestAnnotation.class.getDeclaredAnnotation( |
||||||
|
TestAnnotation.class); |
||||||
|
assertThat(FILTER.matches(annotation)).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void matchesAnnotationWhenNoMatchReturnsFalse() { |
||||||
|
OtherAnnotation annotation = WithOtherAnnotation.class.getDeclaredAnnotation( |
||||||
|
OtherAnnotation.class); |
||||||
|
assertThat(FILTER.matches(annotation)).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void matchesAnnotationClassWhenAnnotationClassIsNullReturnsFalse() { |
||||||
|
Class<Annotation> annotationType = null; |
||||||
|
assertThat(FILTER.matches(annotationType)).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void matchesAnnotationClassWhenMatchReturnsTrue() { |
||||||
|
Class<TestAnnotation> annotationType = TestAnnotation.class; |
||||||
|
assertThat(FILTER.matches(annotationType)).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void matchesAnnotationClassWhenNoMatchReturnsFalse() { |
||||||
|
Class<OtherAnnotation> annotationType = OtherAnnotation.class; |
||||||
|
assertThat(FILTER.matches(annotationType)).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void plainWhenJavaLangAnnotationReturnsTrue() { |
||||||
|
assertThat(AnnotationFilter.PLAIN.matches(Retention.class)).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void plainWhenSpringLangAnnotationReturnsTrue() { |
||||||
|
assertThat(AnnotationFilter.PLAIN.matches(Nullable.class)).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void plainWhenOtherAnnotationReturnsFalse() { |
||||||
|
assertThat(AnnotationFilter.PLAIN.matches(TestAnnotation.class)).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void javaWhenJavaLangAnnotationReturnsTrue() { |
||||||
|
assertThat(AnnotationFilter.JAVA.matches(Retention.class)).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void javaWhenSpringLangAnnotationReturnsFalse() { |
||||||
|
assertThat(AnnotationFilter.JAVA.matches(Nullable.class)).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void javaWhenOtherAnnotationReturnsFalse() { |
||||||
|
assertThat(AnnotationFilter.JAVA.matches(TestAnnotation.class)).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void noneWhenNonNullReturnsFalse() { |
||||||
|
assertThat(AnnotationFilter.NONE.matches(Retention.class)).isFalse(); |
||||||
|
assertThat(AnnotationFilter.NONE.matches(Nullable.class)).isFalse(); |
||||||
|
assertThat(AnnotationFilter.NONE.matches(TestAnnotation.class)).isFalse(); |
||||||
|
assertThat(AnnotationFilter.NONE.matches((Annotation) null)).isFalse(); |
||||||
|
assertThat(AnnotationFilter.NONE.matches((Class<Annotation>) null)).isFalse(); |
||||||
|
assertThat(AnnotationFilter.NONE.matches((String) null)).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void pacakgesReturnsPackagesAnnotationFilter() { |
||||||
|
assertThat(AnnotationFilter.packages("com.example")).isInstanceOf( |
||||||
|
PackagesAnnotationFilter.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void mostAppropriateForCollectionWhenAnnotationTypesIsNullThrowsException() { |
||||||
|
assertThatIllegalArgumentException().isThrownBy( |
||||||
|
() -> AnnotationFilter.mostAppropriateFor( |
||||||
|
(Collection<Class<? extends Annotation>>) null)).withMessage( |
||||||
|
"AnnotationTypes must not be null"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void mostAppropriateForCollectionReturnsPlainWhenPossible() { |
||||||
|
AnnotationFilter filter = AnnotationFilter.mostAppropriateFor( |
||||||
|
Arrays.asList(TestAnnotation.class, OtherAnnotation.class)); |
||||||
|
assertThat(filter).isSameAs(AnnotationFilter.PLAIN); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void mostAppropriateForCollectionWhenCantUsePlainReturnsNone() { |
||||||
|
AnnotationFilter filter = AnnotationFilter.mostAppropriateFor(Arrays.asList( |
||||||
|
TestAnnotation.class, OtherAnnotation.class, Nullable.class)); |
||||||
|
assertThat(filter).isSameAs(AnnotationFilter.NONE); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void mostAppropriateForArrayWhenAnnotationTypesIsNullThrowsException() { |
||||||
|
assertThatIllegalArgumentException().isThrownBy( |
||||||
|
() -> AnnotationFilter.mostAppropriateFor( |
||||||
|
(Class<? extends Annotation>[]) null)).withMessage( |
||||||
|
"AnnotationTypes must not be null"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void mostAppropriateForArrayReturnsPlainWhenPossible() { |
||||||
|
AnnotationFilter filter = AnnotationFilter.mostAppropriateFor( |
||||||
|
TestAnnotation.class, OtherAnnotation.class); |
||||||
|
assertThat(filter).isSameAs(AnnotationFilter.PLAIN); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void mostAppropriateForArrayWhenCantUsePlainReturnsNone() { |
||||||
|
AnnotationFilter filter = AnnotationFilter.mostAppropriateFor( |
||||||
|
TestAnnotation.class, OtherAnnotation.class, Nullable.class); |
||||||
|
assertThat(filter).isSameAs(AnnotationFilter.NONE); |
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface TestAnnotation { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestAnnotation |
||||||
|
static class WithTestAnnotation { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface OtherAnnotation { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@OtherAnnotation |
||||||
|
static class WithOtherAnnotation { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,825 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.io.Serializable; |
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.annotation.Inherited; |
||||||
|
import java.lang.annotation.Retention; |
||||||
|
import java.lang.annotation.RetentionPolicy; |
||||||
|
import java.lang.reflect.AnnotatedElement; |
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.List; |
||||||
|
import java.util.stream.Stream; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
|
||||||
|
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.ClassUtils; |
||||||
|
import org.springframework.util.ReflectionUtils; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.*; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for {@link AnnotationsScanner}. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
*/ |
||||||
|
public class AnnotationsScannerTests { |
||||||
|
|
||||||
|
@Test |
||||||
|
public void directStrategyOnClassWhenNotAnnoatedScansNone() { |
||||||
|
Class<?> source = WithNoAnnotations.class; |
||||||
|
assertThat(scan(source, SearchStrategy.DIRECT)).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void directStrategyOnClassScansAnnotations() { |
||||||
|
Class<?> source = WithSingleAnnotation.class; |
||||||
|
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void directStrategyOnClassWhenMultipleAnnotationsScansAnnotations() { |
||||||
|
Class<?> source = WithMultipleAnnotations.class; |
||||||
|
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( |
||||||
|
"0:TestAnnotation1", "0:TestAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void directStrategyOnClassWhenHasSuperclassScansOnlyDirect() { |
||||||
|
Class<?> source = WithSingleSuperclass.class; |
||||||
|
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void directStrategyOnClassWhenHasInterfaceScansOnlyDirect() { |
||||||
|
Class<?> source = WithSingleInterface.class; |
||||||
|
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void directStrategyOnClassHierarchyScansInCorrectOrder() { |
||||||
|
Class<?> source = WithHierarchy.class; |
||||||
|
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsStrategyOnClassWhenNotAnnoatedScansNone() { |
||||||
|
Class<?> source = WithNoAnnotations.class; |
||||||
|
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsStrategyOnClassScansAnnotations() { |
||||||
|
Class<?> source = WithSingleAnnotation.class; |
||||||
|
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsStrategyOnClassWhenMultipleAnnotationsScansAnnotations() { |
||||||
|
Class<?> source = WithMultipleAnnotations.class; |
||||||
|
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( |
||||||
|
"0:TestAnnotation1", "0:TestAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsStrategyOnClassWhenHasSuperclassScansOnlyInherited() { |
||||||
|
Class<?> source = WithSingleSuperclass.class; |
||||||
|
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( |
||||||
|
"0:TestAnnotation1", "1:TestInheritedAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsStrategyOnClassWhenHasInterfaceDoesNotIncludeInterfaces() { |
||||||
|
Class<?> source = WithSingleInterface.class; |
||||||
|
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsStrategyOnClassHierarchyScansInCorrectOrder() { |
||||||
|
Class<?> source = WithHierarchy.class; |
||||||
|
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( |
||||||
|
"0:TestAnnotation1", "1:TestInheritedAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsStrategyOnClassWhenHasAnnotationOnBothClassesIncudesOnlyOne() { |
||||||
|
Class<?> source = WithSingleSuperclassAndDoubleInherited.class; |
||||||
|
assertThat(Arrays.stream(source.getAnnotations()).map( |
||||||
|
Annotation::annotationType).map(Class::getName)).containsExactly( |
||||||
|
TestInheritedAnnotation2.class.getName()); |
||||||
|
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsOnly( |
||||||
|
"0:TestInheritedAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void superclassStrategyOnClassWhenNotAnnoatedScansNone() { |
||||||
|
Class<?> source = WithNoAnnotations.class; |
||||||
|
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void superclassStrategyOnClassScansAnnotations() { |
||||||
|
Class<?> source = WithSingleAnnotation.class; |
||||||
|
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void superclassStrategyOnClassWhenMultipleAnnotationsScansAnnotations() { |
||||||
|
Class<?> source = WithMultipleAnnotations.class; |
||||||
|
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( |
||||||
|
"0:TestAnnotation1", "0:TestAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void superclassStrategyOnClassWhenHasSuperclassScansSuperclass() { |
||||||
|
Class<?> source = WithSingleSuperclass.class; |
||||||
|
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( |
||||||
|
"0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void superclassStrategyOnClassWhenHasInterfaceDoesNotIncludeInterfaces() { |
||||||
|
Class<?> source = WithSingleInterface.class; |
||||||
|
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void superclassStrategyOnClassHierarchyScansInCorrectOrder() { |
||||||
|
Class<?> source = WithHierarchy.class; |
||||||
|
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( |
||||||
|
"0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2", |
||||||
|
"2:TestAnnotation3"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyOnClassWhenNotAnnoatedScansNone() { |
||||||
|
Class<?> source = WithNoAnnotations.class; |
||||||
|
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyOnClassScansAnnotations() { |
||||||
|
Class<?> source = WithSingleAnnotation.class; |
||||||
|
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyOnClassWhenMultipleAnnotationsScansAnnotations() { |
||||||
|
Class<?> source = WithMultipleAnnotations.class; |
||||||
|
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( |
||||||
|
"0:TestAnnotation1", "0:TestAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyOnClassWhenHasSuperclassScansSuperclass() { |
||||||
|
Class<?> source = WithSingleSuperclass.class; |
||||||
|
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( |
||||||
|
"0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyOnClassWhenHasInterfaceDoesNotIncludeInterfaces() { |
||||||
|
Class<?> source = WithSingleInterface.class; |
||||||
|
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( |
||||||
|
"0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyOnClassHierarchyScansInCorrectOrder() { |
||||||
|
Class<?> source = WithHierarchy.class; |
||||||
|
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( |
||||||
|
"0:TestAnnotation1", "1:TestAnnotation5", "1:TestInheritedAnnotation5", |
||||||
|
"2:TestAnnotation6", "3:TestAnnotation2", "3:TestInheritedAnnotation2", |
||||||
|
"4:TestAnnotation3", "5:TestAnnotation4"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void directStrategyOnMethodWhenNotAnnoatedScansNone() { |
||||||
|
Method source = methodFrom(WithNoAnnotations.class); |
||||||
|
assertThat(scan(source, SearchStrategy.DIRECT)).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void directStrategyOnMethodScansAnnotations() { |
||||||
|
Method source = methodFrom(WithSingleAnnotation.class); |
||||||
|
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void directStrategyOnMethodWhenMultipleAnnotationsScansAnnotations() { |
||||||
|
Method source = methodFrom(WithMultipleAnnotations.class); |
||||||
|
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( |
||||||
|
"0:TestAnnotation1", "0:TestAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void directStrategyOnMethodWhenHasSuperclassScansOnlyDirect() { |
||||||
|
Method source = methodFrom(WithSingleSuperclass.class); |
||||||
|
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void directStrategyOnMethodWhenHasInterfaceScansOnlyDirect() { |
||||||
|
Method source = methodFrom(WithSingleInterface.class); |
||||||
|
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void directStrategyOnMethodHierarchyScansInCorrectOrder() { |
||||||
|
Method source = methodFrom(WithHierarchy.class); |
||||||
|
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsStrategyOnMethodWhenNotAnnoatedScansNone() { |
||||||
|
Method source = methodFrom(WithNoAnnotations.class); |
||||||
|
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsStrategyOnMethodScansAnnotations() { |
||||||
|
Method source = methodFrom(WithSingleAnnotation.class); |
||||||
|
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsStrategyOnMethodWhenMultipleAnnotationsScansAnnotations() { |
||||||
|
Method source = methodFrom(WithMultipleAnnotations.class); |
||||||
|
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( |
||||||
|
"0:TestAnnotation1", "0:TestAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsMethodOnMethodWhenHasSuperclassIgnoresInherited() { |
||||||
|
Method source = methodFrom(WithSingleSuperclass.class); |
||||||
|
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsStrategyOnMethodWhenHasInterfaceDoesNotIncludeInterfaces() { |
||||||
|
Method source = methodFrom(WithSingleInterface.class); |
||||||
|
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsStrategyOnMethodHierarchyScansInCorrectOrder() { |
||||||
|
Method source = methodFrom(WithHierarchy.class); |
||||||
|
assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void superclassStrategyOnMethodWhenNotAnnoatedScansNone() { |
||||||
|
Method source = methodFrom(WithNoAnnotations.class); |
||||||
|
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void superclassStrategyOnMethodScansAnnotations() { |
||||||
|
Method source = methodFrom(WithSingleAnnotation.class); |
||||||
|
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void superclassStrategyOnMethodWhenMultipleAnnotationsScansAnnotations() { |
||||||
|
Method source = methodFrom(WithMultipleAnnotations.class); |
||||||
|
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( |
||||||
|
"0:TestAnnotation1", "0:TestAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void superclassStrategyOnMethodWhenHasSuperclassScansSuperclass() { |
||||||
|
Method source = methodFrom(WithSingleSuperclass.class); |
||||||
|
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( |
||||||
|
"0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void superclassStrategyOnMethodWhenHasInterfaceDoesNotIncludeInterfaces() { |
||||||
|
Method source = methodFrom(WithSingleInterface.class); |
||||||
|
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void superclassStrategyOnMethodHierarchyScansInCorrectOrder() { |
||||||
|
Method source = methodFrom(WithHierarchy.class); |
||||||
|
assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( |
||||||
|
"0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2", |
||||||
|
"2:TestAnnotation3"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyOnMethodWhenNotAnnoatedScansNone() { |
||||||
|
Method source = methodFrom(WithNoAnnotations.class); |
||||||
|
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyOnMethodScansAnnotations() { |
||||||
|
Method source = methodFrom(WithSingleAnnotation.class); |
||||||
|
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyOnMethodWhenMultipleAnnotationsScansAnnotations() { |
||||||
|
Method source = methodFrom(WithMultipleAnnotations.class); |
||||||
|
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( |
||||||
|
"0:TestAnnotation1", "0:TestAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyOnMethodWhenHasSuperclassScansSuperclass() { |
||||||
|
Method source = methodFrom(WithSingleSuperclass.class); |
||||||
|
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( |
||||||
|
"0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyOnMethodWhenHasInterfaceDoesNotIncludeInterfaces() { |
||||||
|
Method source = methodFrom(WithSingleInterface.class); |
||||||
|
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( |
||||||
|
"0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyOnMethodHierarchyScansInCorrectOrder() { |
||||||
|
Method source = methodFrom(WithHierarchy.class); |
||||||
|
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( |
||||||
|
"0:TestAnnotation1", "1:TestAnnotation5", "1:TestInheritedAnnotation5", |
||||||
|
"2:TestAnnotation6", "3:TestAnnotation2", "3:TestInheritedAnnotation2", |
||||||
|
"4:TestAnnotation3", "5:TestAnnotation4"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyOnBridgeMethodScansAnnotations() throws Exception { |
||||||
|
Method source = BridgedMethod.class.getDeclaredMethod("method", Object.class); |
||||||
|
assertThat(source.isBridge()).isTrue(); |
||||||
|
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( |
||||||
|
"0:TestAnnotation1", "1:TestAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyOnBridgedMethodScansAnnotations() throws Exception { |
||||||
|
Method source = BridgedMethod.class.getDeclaredMethod("method", String.class); |
||||||
|
assertThat(source.isBridge()).isFalse(); |
||||||
|
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( |
||||||
|
"0:TestAnnotation1", "1:TestAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void directStrategyOnBridgeMethodScansAnnotations() throws Exception { |
||||||
|
Method source = BridgedMethod.class.getDeclaredMethod("method", Object.class); |
||||||
|
assertThat(source.isBridge()).isTrue(); |
||||||
|
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void dirextStrategyOnBridgedMethodScansAnnotations() throws Exception { |
||||||
|
Method source = BridgedMethod.class.getDeclaredMethod("method", String.class); |
||||||
|
assertThat(source.isBridge()).isFalse(); |
||||||
|
assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyOnMethodWithIgnorablesScansAnnotations() |
||||||
|
throws Exception { |
||||||
|
Method source = methodFrom(Ignoreable.class); |
||||||
|
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyOnMethodWithMultipleCandidatesScansAnnotations() |
||||||
|
throws Exception { |
||||||
|
Method source = methodFrom(MultipleMethods.class); |
||||||
|
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyOnMethodWithGenericParameterOverrideScansAnnotations() |
||||||
|
throws Exception { |
||||||
|
Method source = ReflectionUtils.findMethod(GenericOverride.class, "method", |
||||||
|
String.class); |
||||||
|
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( |
||||||
|
"0:TestAnnotation1", "1:TestAnnotation2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyOnMethodWithGenericParameterNonOverrideScansAnnotations() |
||||||
|
throws Exception { |
||||||
|
Method source = ReflectionUtils.findMethod(GenericNonOverride.class, "method", |
||||||
|
StringBuilder.class); |
||||||
|
assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( |
||||||
|
"0:TestAnnotation1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void scanWhenProcessorReturnsFromDoWithAggregateExitsEarly() { |
||||||
|
String result = AnnotationsScanner.scan(this, WithSingleSuperclass.class, |
||||||
|
SearchStrategy.EXHAUSTIVE, new AnnotationsProcessor<Object, String>() { |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public String doWithAggregate(Object context, int aggregateIndex) { |
||||||
|
return ""; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public String doWithAnnotations(Object context, int aggregateIndex, |
||||||
|
Object source, Annotation[] annotations) { |
||||||
|
throw new IllegalStateException("Should not call"); |
||||||
|
} |
||||||
|
|
||||||
|
}); |
||||||
|
assertThat(result).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void scanWhenProcessorReturnsFromDoWithAnnotationsExitsEarly() { |
||||||
|
List<Integer> indexes = new ArrayList<>(); |
||||||
|
String result = AnnotationsScanner.scan(this, WithSingleSuperclass.class, |
||||||
|
SearchStrategy.EXHAUSTIVE, |
||||||
|
(context, aggregateIndex, source, annotations) -> { |
||||||
|
indexes.add(aggregateIndex); |
||||||
|
return ""; |
||||||
|
}); |
||||||
|
assertThat(result).isEmpty(); |
||||||
|
assertThat(indexes).containsOnly(0); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void scanWhenProcessorHasFinishMethodUsesFinishResult() { |
||||||
|
String result = AnnotationsScanner.scan(this, WithSingleSuperclass.class, |
||||||
|
SearchStrategy.EXHAUSTIVE, new AnnotationsProcessor<Object, String>() { |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public String doWithAnnotations(Object context, int aggregateIndex, |
||||||
|
Object source, Annotation[] annotations) { |
||||||
|
return "K"; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public String finish(String result) { |
||||||
|
return "O" + result; |
||||||
|
} |
||||||
|
|
||||||
|
}); |
||||||
|
assertThat(result).isEqualTo("OK"); |
||||||
|
} |
||||||
|
|
||||||
|
private Method methodFrom(Class<?> type) { |
||||||
|
return ReflectionUtils.findMethod(type, "method"); |
||||||
|
} |
||||||
|
|
||||||
|
private Stream<String> scan(AnnotatedElement element, SearchStrategy searchStrategy) { |
||||||
|
List<String> result = new ArrayList<>(); |
||||||
|
AnnotationsScanner.scan(this, element, searchStrategy, |
||||||
|
(criteria, aggregateIndex, source, annotations) -> { |
||||||
|
for (Annotation annotation : annotations) { |
||||||
|
if (annotation != null) { |
||||||
|
String name = ClassUtils.getShortName( |
||||||
|
annotation.annotationType()); |
||||||
|
name = name.substring(name.lastIndexOf(".") + 1); |
||||||
|
result.add(aggregateIndex + ":" + name); |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
}); |
||||||
|
return result.stream(); |
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface TestAnnotation1 { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface TestAnnotation2 { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface TestAnnotation3 { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface TestAnnotation4 { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface TestAnnotation5 { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface TestAnnotation6 { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Inherited |
||||||
|
static @interface TestInheritedAnnotation1 { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Inherited |
||||||
|
static @interface TestInheritedAnnotation2 { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Inherited |
||||||
|
static @interface TestInheritedAnnotation3 { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Inherited |
||||||
|
static @interface TestInheritedAnnotation4 { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Inherited |
||||||
|
static @interface TestInheritedAnnotation5 { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface OnSuperClass { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface OnInterface { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static class WithNoAnnotations { |
||||||
|
|
||||||
|
public void method() { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestAnnotation1 |
||||||
|
static class WithSingleAnnotation { |
||||||
|
|
||||||
|
@TestAnnotation1 |
||||||
|
public void method() { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestAnnotation1 |
||||||
|
@TestAnnotation2 |
||||||
|
static class WithMultipleAnnotations { |
||||||
|
|
||||||
|
@TestAnnotation1 |
||||||
|
@TestAnnotation2 |
||||||
|
public void method() { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestAnnotation2 |
||||||
|
@TestInheritedAnnotation2 |
||||||
|
static class SingleSuperclass { |
||||||
|
|
||||||
|
@TestAnnotation2 |
||||||
|
@TestInheritedAnnotation2 |
||||||
|
public void method() { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestAnnotation1 |
||||||
|
static class WithSingleSuperclass extends SingleSuperclass { |
||||||
|
|
||||||
|
@TestAnnotation1 |
||||||
|
public void method() { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestInheritedAnnotation2 |
||||||
|
static class WithSingleSuperclassAndDoubleInherited extends SingleSuperclass { |
||||||
|
|
||||||
|
@TestAnnotation1 |
||||||
|
public void method() { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestAnnotation1 |
||||||
|
static class WithSingleInterface implements SingleInterface { |
||||||
|
|
||||||
|
@TestAnnotation1 |
||||||
|
public void method() { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestAnnotation2 |
||||||
|
@TestInheritedAnnotation2 |
||||||
|
static interface SingleInterface { |
||||||
|
|
||||||
|
@TestAnnotation2 |
||||||
|
@TestInheritedAnnotation2 |
||||||
|
public void method(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestAnnotation1 |
||||||
|
static class WithHierarchy extends HierarchySuperclass implements HierarchyInterface { |
||||||
|
|
||||||
|
@TestAnnotation1 |
||||||
|
public void method() { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestAnnotation2 |
||||||
|
@TestInheritedAnnotation2 |
||||||
|
static class HierarchySuperclass extends HierarchySuperSuperclass { |
||||||
|
|
||||||
|
@TestAnnotation2 |
||||||
|
@TestInheritedAnnotation2 |
||||||
|
public void method() { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestAnnotation3 |
||||||
|
static class HierarchySuperSuperclass implements HierarchySuperSuperclassInterface { |
||||||
|
|
||||||
|
@TestAnnotation3 |
||||||
|
public void method() { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestAnnotation4 |
||||||
|
static interface HierarchySuperSuperclassInterface { |
||||||
|
|
||||||
|
@TestAnnotation4 |
||||||
|
public void method(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestAnnotation5 |
||||||
|
@TestInheritedAnnotation5 |
||||||
|
static interface HierarchyInterface extends HierarchyInterfaceInterface { |
||||||
|
|
||||||
|
@TestAnnotation5 |
||||||
|
@TestInheritedAnnotation5 |
||||||
|
public void method(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestAnnotation6 |
||||||
|
static interface HierarchyInterfaceInterface { |
||||||
|
|
||||||
|
@TestAnnotation6 |
||||||
|
public void method(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static class BridgedMethod implements BridgeMethod<String> { |
||||||
|
|
||||||
|
@Override |
||||||
|
@TestAnnotation1 |
||||||
|
public void method(String arg) { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static interface BridgeMethod<T> { |
||||||
|
|
||||||
|
@TestAnnotation2 |
||||||
|
void method(T arg); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static class Ignoreable implements IgnoreableOverrideInterface1, |
||||||
|
IgnoreableOverrideInterface2, Serializable { |
||||||
|
|
||||||
|
@TestAnnotation1 |
||||||
|
public void method() { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static interface IgnoreableOverrideInterface1 { |
||||||
|
|
||||||
|
@Nullable |
||||||
|
public void method(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static interface IgnoreableOverrideInterface2 { |
||||||
|
|
||||||
|
@Nullable |
||||||
|
public void method(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static abstract class MultipleMethods implements MultipleMethodsInterface { |
||||||
|
|
||||||
|
@TestAnnotation1 |
||||||
|
public void method() { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
interface MultipleMethodsInterface { |
||||||
|
|
||||||
|
@TestAnnotation2 |
||||||
|
void method(String arg); |
||||||
|
|
||||||
|
@TestAnnotation2 |
||||||
|
void method1(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static class GenericOverride implements GenericOverrideInterface<String> { |
||||||
|
|
||||||
|
@TestAnnotation1 |
||||||
|
public void method(String argument) { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static interface GenericOverrideInterface<T extends CharSequence> { |
||||||
|
|
||||||
|
@TestAnnotation2 |
||||||
|
void method(T argument); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static abstract class GenericNonOverride |
||||||
|
implements GenericNonOverrideInterface<String> { |
||||||
|
|
||||||
|
@TestAnnotation1 |
||||||
|
public void method(StringBuilder argument) { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static interface GenericNonOverrideInterface<T extends CharSequence> { |
||||||
|
|
||||||
|
@TestAnnotation2 |
||||||
|
void method(T argument); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,242 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.io.InputStream; |
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.annotation.Retention; |
||||||
|
import java.lang.annotation.RetentionPolicy; |
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.*; |
||||||
|
import static org.mockito.BDDMockito.*; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for {@link AttributeMethods}. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
*/ |
||||||
|
public class AttributeMethodsTests { |
||||||
|
|
||||||
|
@Test |
||||||
|
public void forAnnotationTypeWhenNullReturnsNone() { |
||||||
|
AttributeMethods methods = AttributeMethods.forAnnotationType(null); |
||||||
|
assertThat(methods).isSameAs(AttributeMethods.NONE); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void forAnnotationTypeWhenHasNoAttributesReturnsNone() { |
||||||
|
AttributeMethods methods = AttributeMethods.forAnnotationType(NoAttributes.class); |
||||||
|
assertThat(methods).isSameAs(AttributeMethods.NONE); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void forAnnotationTypeWhenHasMultipleAttributesReturnsAttributes() { |
||||||
|
AttributeMethods methods = AttributeMethods.forAnnotationType( |
||||||
|
MultipleAttributes.class); |
||||||
|
assertThat(methods.get("value").getName()).isEqualTo("value"); |
||||||
|
assertThat(methods.get("intValue").getName()).isEqualTo("intValue"); |
||||||
|
assertThat(getAll(methods)).flatExtracting(Method::getName).containsExactly( |
||||||
|
"intValue", "value"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void isOnlyValueAttributeWhenHasOnlyValueAttributeReturnsTrue() { |
||||||
|
AttributeMethods methods = AttributeMethods.forAnnotationType(ValueOnly.class); |
||||||
|
assertThat(methods.isOnlyValueAttribute()).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void isOnlyValueAttributeWhenHasOnlySingleNonValueAttributeReturnsFalse() { |
||||||
|
AttributeMethods methods = AttributeMethods.forAnnotationType(NonValueOnly.class); |
||||||
|
assertThat(methods.isOnlyValueAttribute()).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void isOnlyValueAttributeWhenHasOnlyMultipleAttributesIncludingValueReturnsFalse() { |
||||||
|
AttributeMethods methods = AttributeMethods.forAnnotationType( |
||||||
|
MultipleAttributes.class); |
||||||
|
assertThat(methods.isOnlyValueAttribute()).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void indexOfNameReturnsIndex() { |
||||||
|
AttributeMethods methods = AttributeMethods.forAnnotationType( |
||||||
|
MultipleAttributes.class); |
||||||
|
assertThat(methods.indexOf("value")).isEqualTo(1); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void indexOfMethodReturnsIndex() throws Exception { |
||||||
|
AttributeMethods methods = AttributeMethods.forAnnotationType( |
||||||
|
MultipleAttributes.class); |
||||||
|
Method method = MultipleAttributes.class.getDeclaredMethod("value"); |
||||||
|
assertThat(methods.indexOf(method)).isEqualTo(1); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void sizeReturnsSize() { |
||||||
|
AttributeMethods methods = AttributeMethods.forAnnotationType( |
||||||
|
MultipleAttributes.class); |
||||||
|
assertThat(methods.size()).isEqualTo(2); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void canThrowTypeNotPresentExceptionWhenHasClassAttributeReturnsTrue() { |
||||||
|
AttributeMethods methods = AttributeMethods.forAnnotationType(ClassValue.class); |
||||||
|
assertThat(methods.canThrowTypeNotPresentException(0)).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void canThrowTypeNotPresentExceptionWhenHasClassArrayAttributeReturnsTrue() { |
||||||
|
AttributeMethods methods = AttributeMethods.forAnnotationType( |
||||||
|
ClassArrayValue.class); |
||||||
|
assertThat(methods.canThrowTypeNotPresentException(0)).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void canThrowTypeNotPresentExceptionWhenNotClassOrClassArrayAttributeReturnsFalse() { |
||||||
|
AttributeMethods methods = AttributeMethods.forAnnotationType(ValueOnly.class); |
||||||
|
assertThat(methods.canThrowTypeNotPresentException(0)).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void hasDefaultValueMethodWhenHasDefaultValueMethodReturnsTrue() { |
||||||
|
AttributeMethods methods = AttributeMethods.forAnnotationType( |
||||||
|
DefaultValueAttribute.class); |
||||||
|
assertThat(methods.hasDefaultValueMethod()).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void hasDefaultValueMethodWhenHasNoDefaultValueMethodsReturnsFalse() { |
||||||
|
AttributeMethods methods = AttributeMethods.forAnnotationType( |
||||||
|
MultipleAttributes.class); |
||||||
|
assertThat(methods.hasDefaultValueMethod()).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void isValidWhenHasTypeNotPresentExceptionReturnsFalse() { |
||||||
|
ClassValue annotation = mockAnnotation(ClassValue.class); |
||||||
|
given(annotation.value()).willThrow(TypeNotPresentException.class); |
||||||
|
AttributeMethods attributes = AttributeMethods.forAnnotationType( |
||||||
|
annotation.annotationType()); |
||||||
|
assertThat(attributes.isValid(annotation)).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@SuppressWarnings({ "unchecked", "rawtypes" }) |
||||||
|
public void isValidWhenDoesNotHaveTypeNotPresentExceptionReturnsTrue() { |
||||||
|
ClassValue annotation = mock(ClassValue.class); |
||||||
|
given(annotation.value()).willReturn((Class) InputStream.class); |
||||||
|
AttributeMethods attributes = AttributeMethods.forAnnotationType( |
||||||
|
annotation.annotationType()); |
||||||
|
assertThat(attributes.isValid(annotation)).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void validateWhenHasTypeNotPresentExceptionThrowsException() { |
||||||
|
ClassValue annotation = mockAnnotation(ClassValue.class); |
||||||
|
given(annotation.value()).willThrow(TypeNotPresentException.class); |
||||||
|
AttributeMethods attributes = AttributeMethods.forAnnotationType( |
||||||
|
annotation.annotationType()); |
||||||
|
assertThatIllegalStateException().isThrownBy( |
||||||
|
() -> attributes.validate(annotation)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@SuppressWarnings({ "unchecked", "rawtypes" }) |
||||||
|
public void validateWhenDoesNotHaveTypeNotPresentExceptionThrowsNothing() { |
||||||
|
ClassValue annotation = mockAnnotation(ClassValue.class); |
||||||
|
given(annotation.value()).willReturn((Class) InputStream.class); |
||||||
|
AttributeMethods attributes = AttributeMethods.forAnnotationType( |
||||||
|
annotation.annotationType()); |
||||||
|
attributes.validate(annotation); |
||||||
|
} |
||||||
|
|
||||||
|
private List<Method> getAll(AttributeMethods attributes) { |
||||||
|
List<Method> result = new ArrayList<>(attributes.size()); |
||||||
|
for (int i = 0; i < attributes.size(); i++) { |
||||||
|
result.add(attributes.get(i)); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings({ "unchecked", "rawtypes" }) |
||||||
|
private <A extends Annotation> A mockAnnotation(Class<A> annotationType) { |
||||||
|
A annotation = mock(annotationType); |
||||||
|
given(annotation.annotationType()).willReturn((Class) annotationType); |
||||||
|
return annotation; |
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface NoAttributes { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface MultipleAttributes { |
||||||
|
|
||||||
|
int intValue(); |
||||||
|
|
||||||
|
String value(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface ValueOnly { |
||||||
|
|
||||||
|
String value(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface NonValueOnly { |
||||||
|
|
||||||
|
String test(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface ClassValue { |
||||||
|
|
||||||
|
Class<?> value(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface ClassArrayValue { |
||||||
|
|
||||||
|
Class<?>[] value(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface DefaultValueAttribute { |
||||||
|
|
||||||
|
String one(); |
||||||
|
|
||||||
|
String two(); |
||||||
|
|
||||||
|
String three() default "3"; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,172 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.annotation.Retention; |
||||||
|
import java.lang.annotation.RetentionPolicy; |
||||||
|
import java.lang.reflect.Method; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
|
||||||
|
import org.springframework.core.OverridingClassLoader; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for {@link MergedAnnotation} to ensure the correct class loader is |
||||||
|
* used. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
public class MergedAnnotationClassLoaderTests { |
||||||
|
|
||||||
|
private static final String TEST_ANNOTATION = TestAnnotation.class.getName(); |
||||||
|
|
||||||
|
private static final String TEST_META_ANNOTATION = TestMetaAnnotation.class.getName(); |
||||||
|
|
||||||
|
private static final String WITH_TEST_ANNOTATION = WithTestAnnotation.class.getName(); |
||||||
|
|
||||||
|
private static final String TEST_REFERENCE = TestReference.class.getName(); |
||||||
|
|
||||||
|
@Test |
||||||
|
public void synthesizedUsesCorrectClassLoader() throws Exception { |
||||||
|
ClassLoader parent = getClass().getClassLoader(); |
||||||
|
TestClassLoader child = new TestClassLoader(parent); |
||||||
|
Class<?> source = child.loadClass(WITH_TEST_ANNOTATION); |
||||||
|
Annotation annotation = getDeclaredAnnotation(source, TEST_ANNOTATION); |
||||||
|
Annotation metaAnnotation = getDeclaredAnnotation(annotation.annotationType(), |
||||||
|
TEST_META_ANNOTATION); |
||||||
|
// We should have loaded the source and initial annotation from child
|
||||||
|
assertThat(source.getClassLoader()).isEqualTo(child); |
||||||
|
assertThat(annotation.getClass().getClassLoader()).isEqualTo(child); |
||||||
|
assertThat(annotation.annotationType().getClassLoader()).isEqualTo(child); |
||||||
|
// The meta-annotation should have been loaded by the parent
|
||||||
|
assertThat(metaAnnotation.getClass().getClassLoader()).isEqualTo(parent); |
||||||
|
assertThat(metaAnnotation.getClass().getClassLoader()).isEqualTo(parent); |
||||||
|
assertThat( |
||||||
|
getEnumAttribute(metaAnnotation).getClass().getClassLoader()).isEqualTo( |
||||||
|
parent); |
||||||
|
assertThat(getClassAttribute(metaAnnotation).getClassLoader()).isEqualTo(child); |
||||||
|
// MergedAnnotation should follow the same class loader logic
|
||||||
|
MergedAnnotations mergedAnnotations = MergedAnnotations.from(source); |
||||||
|
Annotation synthesized = mergedAnnotations.get(TEST_ANNOTATION).synthesize(); |
||||||
|
Annotation synthesizedMeta = mergedAnnotations.get( |
||||||
|
TEST_META_ANNOTATION).synthesize(); |
||||||
|
assertThat(synthesized.getClass().getClassLoader()).isEqualTo(child); |
||||||
|
assertThat(synthesized.annotationType().getClassLoader()).isEqualTo(child); |
||||||
|
assertThat(synthesizedMeta.getClass().getClassLoader()).isEqualTo(parent); |
||||||
|
assertThat(synthesizedMeta.getClass().getClassLoader()).isEqualTo(parent); |
||||||
|
assertThat(getClassAttribute(synthesizedMeta).getClassLoader()).isEqualTo(child); |
||||||
|
assertThat( |
||||||
|
getEnumAttribute(synthesizedMeta).getClass().getClassLoader()).isEqualTo( |
||||||
|
parent); |
||||||
|
assertThat(synthesized).isEqualTo(annotation); |
||||||
|
assertThat(synthesizedMeta).isEqualTo(metaAnnotation); |
||||||
|
// Also check utils version
|
||||||
|
Annotation utilsMeta = AnnotatedElementUtils.getMergedAnnotation(source, |
||||||
|
TestMetaAnnotation.class); |
||||||
|
assertThat(utilsMeta.getClass().getClassLoader()).isEqualTo(parent); |
||||||
|
assertThat(utilsMeta.getClass().getClassLoader()).isEqualTo(parent); |
||||||
|
assertThat(getClassAttribute(utilsMeta).getClassLoader()).isEqualTo(child); |
||||||
|
assertThat(getEnumAttribute(utilsMeta).getClass().getClassLoader()).isEqualTo( |
||||||
|
parent); |
||||||
|
assertThat(utilsMeta).isEqualTo(metaAnnotation); |
||||||
|
} |
||||||
|
|
||||||
|
private Class<?> getClassAttribute(Annotation annotation) throws Exception { |
||||||
|
return (Class<?>) getAttributeValue(annotation, "classValue"); |
||||||
|
} |
||||||
|
|
||||||
|
private Enum<?> getEnumAttribute(Annotation annotation) throws Exception { |
||||||
|
return (Enum<?>) getAttributeValue(annotation, "enumValue"); |
||||||
|
} |
||||||
|
|
||||||
|
private Object getAttributeValue(Annotation annotation, String name) |
||||||
|
throws Exception { |
||||||
|
Method classValueMethod = annotation.annotationType().getDeclaredMethod(name); |
||||||
|
classValueMethod.setAccessible(true); |
||||||
|
return classValueMethod.invoke(annotation); |
||||||
|
} |
||||||
|
|
||||||
|
private Annotation getDeclaredAnnotation(Class<?> element, String annotationType) { |
||||||
|
for (Annotation annotation : element.getDeclaredAnnotations()) { |
||||||
|
if (annotation.annotationType().getName().equals(annotationType)) { |
||||||
|
return annotation; |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
private static class TestClassLoader extends OverridingClassLoader { |
||||||
|
|
||||||
|
public TestClassLoader(ClassLoader parent) { |
||||||
|
super(parent); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected boolean isEligibleForOverriding(String className) { |
||||||
|
return WITH_TEST_ANNOTATION.equals(className) |
||||||
|
|| TEST_ANNOTATION.equals(className) |
||||||
|
|| TEST_REFERENCE.equals(className); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface TestMetaAnnotation { |
||||||
|
|
||||||
|
@AliasFor("d") |
||||||
|
String c() default ""; |
||||||
|
|
||||||
|
@AliasFor("c") |
||||||
|
String d() default ""; |
||||||
|
|
||||||
|
Class<?> classValue(); |
||||||
|
|
||||||
|
TestEnum enumValue(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestMetaAnnotation(classValue = TestReference.class, enumValue = TestEnum.TWO) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface TestAnnotation { |
||||||
|
|
||||||
|
@AliasFor("b") |
||||||
|
String a() default ""; |
||||||
|
|
||||||
|
@AliasFor("a") |
||||||
|
String b() default ""; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestAnnotation |
||||||
|
static class WithTestAnnotation { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static class TestReference { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static enum TestEnum { |
||||||
|
|
||||||
|
ONE, TWO, THREE |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,137 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.annotation.Repeatable; |
||||||
|
import java.lang.annotation.Retention; |
||||||
|
import java.lang.annotation.RetentionPolicy; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.LinkedHashSet; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.function.Function; |
||||||
|
import java.util.stream.Stream; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
|
||||||
|
import org.springframework.core.annotation.MergedAnnotation.MapValues; |
||||||
|
import org.springframework.util.MultiValueMap; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for {@link MergedAnnotationCollectors}. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
*/ |
||||||
|
public class MergedAnnotationCollectorsTests { |
||||||
|
|
||||||
|
@Test |
||||||
|
public void toAnnotationSetCollectsLinkedHashSetWithSynthesizedAnnotations() { |
||||||
|
Set<TestAnnotation> set = stream().collect( |
||||||
|
MergedAnnotationCollectors.toAnnotationSet()); |
||||||
|
assertThat(set).isInstanceOf(LinkedHashSet.class).flatExtracting( |
||||||
|
TestAnnotation::value).containsExactly("a", "b", "c"); |
||||||
|
assertThat(set).allMatch(SynthesizedAnnotation.class::isInstance); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void toAnnotationArrayCollectsAnnotationArrayWithSynthesizedAnnotations() { |
||||||
|
Annotation[] array = stream().collect( |
||||||
|
MergedAnnotationCollectors.toAnnotationArray()); |
||||||
|
assertThat(Arrays.stream(array).map( |
||||||
|
annotation -> ((TestAnnotation) annotation).value())).containsExactly("a", |
||||||
|
"b", "c"); |
||||||
|
assertThat(array).allMatch(SynthesizedAnnotation.class::isInstance); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void toSuppliedAnnotationArrayCollectsAnnotationArrayWithSynthesizedAnnotations() { |
||||||
|
TestAnnotation[] array = stream().collect( |
||||||
|
MergedAnnotationCollectors.toAnnotationArray(TestAnnotation[]::new)); |
||||||
|
assertThat(Arrays.stream(array).map(TestAnnotation::value)).containsExactly("a", |
||||||
|
"b", "c"); |
||||||
|
assertThat(array).allMatch(SynthesizedAnnotation.class::isInstance); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void toMultiValueMapCollectsMultiValueMap() { |
||||||
|
MultiValueMap<String, Object> map = stream().map( |
||||||
|
MergedAnnotation::filterDefaultValues).collect( |
||||||
|
MergedAnnotationCollectors.toMultiValueMap( |
||||||
|
MapValues.CLASS_TO_STRING)); |
||||||
|
assertThat(map.get("value")).containsExactly("a", "b", "c"); |
||||||
|
assertThat(map.get("extra")).containsExactly("java.lang.String", |
||||||
|
"java.lang.Integer"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void toFinishedMultiValueMapCollectsMultiValueMap() { |
||||||
|
MultiValueMap<String, Object> map = stream().collect( |
||||||
|
MergedAnnotationCollectors.toMultiValueMap(result -> { |
||||||
|
result.add("finished", true); |
||||||
|
return result; |
||||||
|
})); |
||||||
|
assertThat(map.get("value")).containsExactly("a", "b", "c"); |
||||||
|
assertThat(map.get("extra")).containsExactly(void.class, String.class, |
||||||
|
Integer.class); |
||||||
|
assertThat(map.get("finished")).containsExactly(true); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@SuppressWarnings({ "unchecked", "rawtypes" }) |
||||||
|
public void toFinishedMultiValueMapWhenFinisherIsNullThrowsException() { |
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> stream().collect( |
||||||
|
MergedAnnotationCollectors.toMultiValueMap((Function) null))).withMessage( |
||||||
|
"Finisher must not be null"); |
||||||
|
} |
||||||
|
|
||||||
|
private Stream<MergedAnnotation<TestAnnotation>> stream() { |
||||||
|
return MergedAnnotations.from(WithTestAnnotations.class).stream( |
||||||
|
TestAnnotation.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Repeatable(TestAnnotations.class) |
||||||
|
@interface TestAnnotation { |
||||||
|
|
||||||
|
@AliasFor("name") |
||||||
|
String value() default ""; |
||||||
|
|
||||||
|
@AliasFor("value") |
||||||
|
String name() default ""; |
||||||
|
|
||||||
|
Class<?> extra() default void.class; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@interface TestAnnotations { |
||||||
|
|
||||||
|
TestAnnotation[] value(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestAnnotation("a") |
||||||
|
@TestAnnotation(name = "b", extra = String.class) |
||||||
|
@TestAnnotation(name = "c", extra = Integer.class) |
||||||
|
static class WithTestAnnotations { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,197 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.annotation.Repeatable; |
||||||
|
import java.lang.annotation.Retention; |
||||||
|
import java.lang.annotation.RetentionPolicy; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Collection; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.List; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for {@link MergedAnnotationPredicates}. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
*/ |
||||||
|
public class MergedAnnotationPredicatesTests { |
||||||
|
|
||||||
|
@Test |
||||||
|
public void typeInStringArrayWhenNameMatchesAccepts() { |
||||||
|
MergedAnnotation<TestAnnotation> annotation = MergedAnnotations.from( |
||||||
|
WithTestAnnotation.class).get(TestAnnotation.class); |
||||||
|
assertThat(MergedAnnotationPredicates.typeIn( |
||||||
|
TestAnnotation.class.getName())).accepts(annotation); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void typeInStringArrayWhenNameDoesNotMatchRejects() { |
||||||
|
MergedAnnotation<TestAnnotation> annotation = MergedAnnotations.from( |
||||||
|
WithTestAnnotation.class).get(TestAnnotation.class); |
||||||
|
assertThat(MergedAnnotationPredicates.typeIn( |
||||||
|
MissingAnnotation.class.getName())).rejects(annotation); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void typeInStringArrayWhenStringArraysIsNullThrowsException() { |
||||||
|
assertThatIllegalArgumentException().isThrownBy( |
||||||
|
() -> MergedAnnotationPredicates.typeIn((String[]) null)).withMessage( |
||||||
|
"TypeNames must not be null"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void typeInClassArrayWhenNameMatchesAccepts() { |
||||||
|
MergedAnnotation<TestAnnotation> annotation = MergedAnnotations.from( |
||||||
|
WithTestAnnotation.class).get(TestAnnotation.class); |
||||||
|
assertThat(MergedAnnotationPredicates.typeIn(TestAnnotation.class)).accepts( |
||||||
|
annotation); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void typeInClassArrayWhenNameDoesNotMatchRejects() { |
||||||
|
MergedAnnotation<TestAnnotation> annotation = MergedAnnotations.from( |
||||||
|
WithTestAnnotation.class).get(TestAnnotation.class); |
||||||
|
assertThat(MergedAnnotationPredicates.typeIn(MissingAnnotation.class)).rejects( |
||||||
|
annotation); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void typeInClassArrayWhenClassArraysIsNullThrowsException() { |
||||||
|
assertThatIllegalArgumentException().isThrownBy( |
||||||
|
() -> MergedAnnotationPredicates.typeIn( |
||||||
|
(Class<Annotation>[]) null)).withMessage( |
||||||
|
"Types must not be null"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void typeInCollectionWhenMatchesStringInCollectionAccepts() { |
||||||
|
MergedAnnotation<TestAnnotation> annotation = MergedAnnotations.from( |
||||||
|
WithTestAnnotation.class).get(TestAnnotation.class); |
||||||
|
assertThat(MergedAnnotationPredicates.typeIn( |
||||||
|
Collections.singleton(TestAnnotation.class.getName()))).accepts( |
||||||
|
annotation); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void typeInCollectionWhenMatchesClassInCollectionAccepts() { |
||||||
|
MergedAnnotation<TestAnnotation> annotation = MergedAnnotations.from( |
||||||
|
WithTestAnnotation.class).get(TestAnnotation.class); |
||||||
|
assertThat(MergedAnnotationPredicates.typeIn( |
||||||
|
Collections.singleton(TestAnnotation.class))).accepts(annotation); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void typeInCollectionWhenDoesNotMatchAnyRejects() { |
||||||
|
MergedAnnotation<TestAnnotation> annotation = MergedAnnotations.from( |
||||||
|
WithTestAnnotation.class).get(TestAnnotation.class); |
||||||
|
assertThat(MergedAnnotationPredicates.typeIn(Arrays.asList( |
||||||
|
MissingAnnotation.class.getName(), MissingAnnotation.class))).rejects( |
||||||
|
annotation); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void typeInCollectionWhenCollectionIsNullThrowsException() { |
||||||
|
assertThatIllegalArgumentException().isThrownBy( |
||||||
|
() -> MergedAnnotationPredicates.typeIn( |
||||||
|
(Collection<?>) null)).withMessage("Types must not be null"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void firstRunOfAcceptsOnlyFirstRun() { |
||||||
|
List<MergedAnnotation<TestAnnotation>> filtered = MergedAnnotations.from( |
||||||
|
WithMultipleTestAnnotation.class).stream(TestAnnotation.class).filter( |
||||||
|
MergedAnnotationPredicates.firstRunOf( |
||||||
|
this::firstCharOfValue)).collect(Collectors.toList()); |
||||||
|
assertThat(filtered.stream().map( |
||||||
|
annotation -> annotation.getString("value"))).containsExactly("a1", "a2", |
||||||
|
"a3"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void firstRunOfWhenValueExtractorIsNullThrowsException() { |
||||||
|
assertThatIllegalArgumentException().isThrownBy( |
||||||
|
() -> MergedAnnotationPredicates.firstRunOf(null)).withMessage( |
||||||
|
"ValueExtractor must not be null"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void uniqueAcceptsUniquely() { |
||||||
|
List<MergedAnnotation<TestAnnotation>> filtered = MergedAnnotations.from( |
||||||
|
WithMultipleTestAnnotation.class).stream(TestAnnotation.class).filter( |
||||||
|
MergedAnnotationPredicates.unique( |
||||||
|
this::firstCharOfValue)).collect(Collectors.toList()); |
||||||
|
assertThat(filtered.stream().map( |
||||||
|
annotation -> annotation.getString("value"))).containsExactly("a1", "b1", |
||||||
|
"c1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void uniqueWhenKeyExtractorIsNullThrowsException() { |
||||||
|
assertThatIllegalArgumentException().isThrownBy( |
||||||
|
() -> MergedAnnotationPredicates.unique(null)).withMessage( |
||||||
|
"KeyExtractor must not be null"); |
||||||
|
} |
||||||
|
|
||||||
|
private char firstCharOfValue(MergedAnnotation<TestAnnotation> annotation) { |
||||||
|
return annotation.getString("value").charAt(0); |
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Repeatable(TestAnnotations.class) |
||||||
|
static @interface TestAnnotation { |
||||||
|
|
||||||
|
String value() default ""; |
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface TestAnnotations { |
||||||
|
|
||||||
|
TestAnnotation[] value(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static @interface MissingAnnotation { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestAnnotation("test") |
||||||
|
static class WithTestAnnotation { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@TestAnnotation("a1") |
||||||
|
@TestAnnotation("a2") |
||||||
|
@TestAnnotation("a3") |
||||||
|
@TestAnnotation("b1") |
||||||
|
@TestAnnotation("b2") |
||||||
|
@TestAnnotation("b3") |
||||||
|
@TestAnnotation("c1") |
||||||
|
@TestAnnotation("c2") |
||||||
|
@TestAnnotation("c3") |
||||||
|
static class WithMultipleTestAnnotation { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,306 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.ElementType; |
||||||
|
import java.lang.annotation.Inherited; |
||||||
|
import java.lang.annotation.Retention; |
||||||
|
import java.lang.annotation.RetentionPolicy; |
||||||
|
import java.lang.annotation.Target; |
||||||
|
import java.lang.reflect.AnnotatedElement; |
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.stream.Stream; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
|
||||||
|
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; |
||||||
|
import org.springframework.util.ReflectionUtils; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.*; |
||||||
|
|
||||||
|
/** |
||||||
|
* Unit tests that verify support for finding multiple composed annotations on a single |
||||||
|
* annotated element. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @author Sam Brannen |
||||||
|
*/ |
||||||
|
public class MergedAnnotationsComposedOnSingleAnnotatedElementTests { |
||||||
|
|
||||||
|
// See SPR-13486
|
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedStrategyMultipleComposedAnnotationsOnClass() { |
||||||
|
assertInheritedStrategyBehavior(MultipleComposedCachesClass.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedStrategyMultipleInheritedComposedAnnotationsOnSuperclass() { |
||||||
|
assertInheritedStrategyBehavior(SubMultipleComposedCachesClass.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedStrategyMultipleNoninheritedComposedAnnotationsOnClass() { |
||||||
|
MergedAnnotations annotations = MergedAnnotations.from( |
||||||
|
MultipleNoninheritedComposedCachesClass.class, |
||||||
|
SearchStrategy.INHERITED_ANNOTATIONS); |
||||||
|
assertThat(stream(annotations, "value")).containsExactly("noninheritedCache1", |
||||||
|
"noninheritedCache2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedStrategyMultipleNoninheritedComposedAnnotationsOnSuperclass() { |
||||||
|
MergedAnnotations annotations = MergedAnnotations.from( |
||||||
|
SubMultipleNoninheritedComposedCachesClass.class, |
||||||
|
SearchStrategy.INHERITED_ANNOTATIONS); |
||||||
|
assertThat(annotations.stream(Cacheable.class)).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedStrategyComposedPlusLocalAnnotationsOnClass() { |
||||||
|
assertInheritedStrategyBehavior(ComposedPlusLocalCachesClass.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedStrategyMultipleComposedAnnotationsOnInterface() { |
||||||
|
MergedAnnotations annotations = MergedAnnotations.from( |
||||||
|
MultipleComposedCachesOnInterfaceClass.class, |
||||||
|
SearchStrategy.INHERITED_ANNOTATIONS); |
||||||
|
assertThat(annotations.stream(Cacheable.class)).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedStrategyMultipleComposedAnnotationsOnMethod() throws Exception { |
||||||
|
assertInheritedStrategyBehavior( |
||||||
|
getClass().getDeclaredMethod("multipleComposedCachesMethod")); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedStrategyComposedPlusLocalAnnotationsOnMethod() throws Exception { |
||||||
|
assertInheritedStrategyBehavior( |
||||||
|
getClass().getDeclaredMethod("composedPlusLocalCachesMethod")); |
||||||
|
} |
||||||
|
|
||||||
|
private void assertInheritedStrategyBehavior(AnnotatedElement element) { |
||||||
|
MergedAnnotations annotations = MergedAnnotations.from(element, |
||||||
|
SearchStrategy.INHERITED_ANNOTATIONS); |
||||||
|
assertThat(stream(annotations, "key")).containsExactly("fooKey", "barKey"); |
||||||
|
assertThat(stream(annotations, "value")).containsExactly("fooCache", "barCache"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyMultipleComposedAnnotationsOnClass() { |
||||||
|
assertExhaustiveStrategyBehavior(MultipleComposedCachesClass.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyMultipleInheritedComposedAnnotationsOnSuperclass() { |
||||||
|
assertExhaustiveStrategyBehavior(SubMultipleComposedCachesClass.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyMultipleNoninheritedComposedAnnotationsOnClass() { |
||||||
|
MergedAnnotations annotations = MergedAnnotations.from( |
||||||
|
MultipleNoninheritedComposedCachesClass.class, SearchStrategy.EXHAUSTIVE); |
||||||
|
assertThat(stream(annotations, "value")).containsExactly("noninheritedCache1", |
||||||
|
"noninheritedCache2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyMultipleNoninheritedComposedAnnotationsOnSuperclass() { |
||||||
|
MergedAnnotations annotations = MergedAnnotations.from( |
||||||
|
SubMultipleNoninheritedComposedCachesClass.class, |
||||||
|
SearchStrategy.EXHAUSTIVE); |
||||||
|
assertThat(stream(annotations, "value")).containsExactly("noninheritedCache1", |
||||||
|
"noninheritedCache2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyComposedPlusLocalAnnotationsOnClass() { |
||||||
|
assertExhaustiveStrategyBehavior(ComposedPlusLocalCachesClass.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyMultipleComposedAnnotationsOnInterface() { |
||||||
|
assertExhaustiveStrategyBehavior(MultipleComposedCachesOnInterfaceClass.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyComposedCacheOnInterfaceAndLocalCacheOnClass() { |
||||||
|
assertExhaustiveStrategyBehavior( |
||||||
|
ComposedCacheOnInterfaceAndLocalCacheClass.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyMultipleComposedAnnotationsOnMethod() throws Exception { |
||||||
|
assertExhaustiveStrategyBehavior( |
||||||
|
getClass().getDeclaredMethod("multipleComposedCachesMethod")); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyComposedPlusLocalAnnotationsOnMethod() |
||||||
|
throws Exception { |
||||||
|
assertExhaustiveStrategyBehavior( |
||||||
|
getClass().getDeclaredMethod("composedPlusLocalCachesMethod")); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveStrategyMultipleComposedAnnotationsOnBridgeMethod() |
||||||
|
throws Exception { |
||||||
|
assertExhaustiveStrategyBehavior(getBridgeMethod()); |
||||||
|
} |
||||||
|
|
||||||
|
private void assertExhaustiveStrategyBehavior(AnnotatedElement element) { |
||||||
|
MergedAnnotations annotations = MergedAnnotations.from(element, |
||||||
|
SearchStrategy.EXHAUSTIVE); |
||||||
|
assertThat(stream(annotations, "key")).containsExactly("fooKey", "barKey"); |
||||||
|
assertThat(stream(annotations, "value")).containsExactly("fooCache", "barCache"); |
||||||
|
} |
||||||
|
|
||||||
|
public Method getBridgeMethod() throws NoSuchMethodException { |
||||||
|
List<Method> methods = new ArrayList<>(); |
||||||
|
ReflectionUtils.doWithLocalMethods(StringGenericParameter.class, method -> { |
||||||
|
if ("getFor".equals(method.getName())) { |
||||||
|
methods.add(method); |
||||||
|
} |
||||||
|
}); |
||||||
|
Method bridgeMethod = methods.get(0).getReturnType().equals(Object.class) |
||||||
|
? methods.get(0) |
||||||
|
: methods.get(1); |
||||||
|
assertThat(bridgeMethod.isBridge()).isTrue(); |
||||||
|
return bridgeMethod; |
||||||
|
} |
||||||
|
|
||||||
|
private Stream<String> stream(MergedAnnotations annotations, String attributeName) { |
||||||
|
return annotations.stream(Cacheable.class).map( |
||||||
|
annotation -> annotation.getString(attributeName)); |
||||||
|
} |
||||||
|
|
||||||
|
// @formatter:off
|
||||||
|
|
||||||
|
@Target({ ElementType.METHOD, ElementType.TYPE }) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Inherited |
||||||
|
@interface Cacheable { |
||||||
|
@AliasFor("cacheName") |
||||||
|
String value() default ""; |
||||||
|
@AliasFor("value") |
||||||
|
String cacheName() default ""; |
||||||
|
String key() default ""; |
||||||
|
} |
||||||
|
|
||||||
|
@Cacheable("fooCache") |
||||||
|
@Target({ ElementType.METHOD, ElementType.TYPE }) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Inherited |
||||||
|
@interface FooCache { |
||||||
|
@AliasFor(annotation = Cacheable.class) |
||||||
|
String key() default ""; |
||||||
|
} |
||||||
|
|
||||||
|
@Cacheable("barCache") |
||||||
|
@Target({ ElementType.METHOD, ElementType.TYPE }) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Inherited |
||||||
|
@interface BarCache { |
||||||
|
@AliasFor(annotation = Cacheable.class) |
||||||
|
String key(); |
||||||
|
} |
||||||
|
|
||||||
|
@Cacheable("noninheritedCache1") |
||||||
|
@Target({ ElementType.METHOD, ElementType.TYPE }) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@interface NoninheritedCache1 { |
||||||
|
@AliasFor(annotation = Cacheable.class) |
||||||
|
String key() default ""; |
||||||
|
} |
||||||
|
|
||||||
|
@Cacheable("noninheritedCache2") |
||||||
|
@Target({ ElementType.METHOD, ElementType.TYPE }) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@interface NoninheritedCache2 { |
||||||
|
@AliasFor(annotation = Cacheable.class) |
||||||
|
String key() default ""; |
||||||
|
} |
||||||
|
|
||||||
|
@FooCache(key = "fooKey") |
||||||
|
@BarCache(key = "barKey") |
||||||
|
private static class MultipleComposedCachesClass { |
||||||
|
} |
||||||
|
|
||||||
|
private static class SubMultipleComposedCachesClass |
||||||
|
extends MultipleComposedCachesClass { |
||||||
|
} |
||||||
|
|
||||||
|
@NoninheritedCache1 |
||||||
|
@NoninheritedCache2 |
||||||
|
private static class MultipleNoninheritedComposedCachesClass { |
||||||
|
} |
||||||
|
|
||||||
|
private static class SubMultipleNoninheritedComposedCachesClass |
||||||
|
extends MultipleNoninheritedComposedCachesClass { |
||||||
|
} |
||||||
|
|
||||||
|
@Cacheable(cacheName = "fooCache", key = "fooKey") |
||||||
|
@BarCache(key = "barKey") |
||||||
|
private static class ComposedPlusLocalCachesClass { |
||||||
|
} |
||||||
|
|
||||||
|
@FooCache(key = "fooKey") |
||||||
|
@BarCache(key = "barKey") |
||||||
|
private interface MultipleComposedCachesInterface { |
||||||
|
} |
||||||
|
|
||||||
|
private static class MultipleComposedCachesOnInterfaceClass implements MultipleComposedCachesInterface { |
||||||
|
} |
||||||
|
|
||||||
|
@BarCache(key = "barKey") |
||||||
|
private interface ComposedCacheInterface { |
||||||
|
} |
||||||
|
|
||||||
|
@Cacheable(cacheName = "fooCache", key = "fooKey") |
||||||
|
private static class ComposedCacheOnInterfaceAndLocalCacheClass implements ComposedCacheInterface { |
||||||
|
} |
||||||
|
|
||||||
|
@FooCache(key = "fooKey") |
||||||
|
@BarCache(key = "barKey") |
||||||
|
private void multipleComposedCachesMethod() { |
||||||
|
} |
||||||
|
|
||||||
|
@Cacheable(cacheName = "fooCache", key = "fooKey") |
||||||
|
@BarCache(key = "barKey") |
||||||
|
private void composedPlusLocalCachesMethod() { |
||||||
|
} |
||||||
|
|
||||||
|
public interface GenericParameter<T> { |
||||||
|
T getFor(Class<T> cls); |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unused") |
||||||
|
private static class StringGenericParameter implements GenericParameter<String> { |
||||||
|
@FooCache(key = "fooKey") |
||||||
|
@BarCache(key = "barKey") |
||||||
|
@Override |
||||||
|
public String getFor(Class<String> cls) { return "foo"; } |
||||||
|
public String getFor(Integer integer) { return "foo"; } |
||||||
|
} |
||||||
|
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,424 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.annotation.ElementType; |
||||||
|
import java.lang.annotation.Inherited; |
||||||
|
import java.lang.annotation.Repeatable; |
||||||
|
import java.lang.annotation.Retention; |
||||||
|
import java.lang.annotation.RetentionPolicy; |
||||||
|
import java.lang.annotation.Target; |
||||||
|
import java.lang.reflect.AnnotatedElement; |
||||||
|
import java.util.Set; |
||||||
|
|
||||||
|
import org.assertj.core.api.ThrowableTypeAssert; |
||||||
|
import org.junit.Rule; |
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.rules.ExpectedException; |
||||||
|
|
||||||
|
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for {@link MergedAnnotations} and {@link RepeatableContainers} that |
||||||
|
* verify support for repeatable annotations. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @author Sam Brannen |
||||||
|
*/ |
||||||
|
public class MergedAnnotationsRepeatableAnnotationTests { |
||||||
|
|
||||||
|
// See SPR-13973
|
||||||
|
|
||||||
|
@Rule |
||||||
|
public ExpectedException exception = ExpectedException.none(); |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsWhenNonRepeatableThrowsException() { |
||||||
|
assertThatIllegalArgumentException().isThrownBy( |
||||||
|
() -> getAnnotations(null, NonRepeatable.class, |
||||||
|
SearchStrategy.INHERITED_ANNOTATIONS, getClass())).satisfies( |
||||||
|
this::nonRepeatableRequirements); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsWhenContainerMissingValueAttributeThrowsException() { |
||||||
|
assertThatAnnotationConfigurationException().isThrownBy( |
||||||
|
() -> getAnnotations(ContainerMissingValueAttribute.class, |
||||||
|
InvalidRepeatable.class, SearchStrategy.INHERITED_ANNOTATIONS, |
||||||
|
getClass())).satisfies(this::missingValueAttributeRequirements); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsWhenWhenNonArrayValueAttributeThrowsException() { |
||||||
|
assertThatAnnotationConfigurationException().isThrownBy( |
||||||
|
() -> getAnnotations(ContainerWithNonArrayValueAttribute.class, |
||||||
|
InvalidRepeatable.class, SearchStrategy.INHERITED_ANNOTATIONS, |
||||||
|
getClass())).satisfies(this::nonArrayValueAttributeRequirements); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsWhenWrongComponentTypeThrowsException() { |
||||||
|
assertThatAnnotationConfigurationException().isThrownBy(() -> getAnnotations( |
||||||
|
ContainerWithArrayValueAttributeButWrongComponentType.class, |
||||||
|
InvalidRepeatable.class, SearchStrategy.INHERITED_ANNOTATIONS, |
||||||
|
getClass())).satisfies(this::wrongComponentTypeRequirements); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsWhenOnClassReturnsAnnotations() { |
||||||
|
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class, |
||||||
|
SearchStrategy.INHERITED_ANNOTATIONS, RepeatableClass.class); |
||||||
|
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", |
||||||
|
"C"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsWhenWhenOnSuperclassReturnsAnnotations() { |
||||||
|
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class, |
||||||
|
SearchStrategy.INHERITED_ANNOTATIONS, SubRepeatableClass.class); |
||||||
|
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", |
||||||
|
"C"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsWhenComposedOnClassReturnsAnnotations() { |
||||||
|
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class, |
||||||
|
SearchStrategy.INHERITED_ANNOTATIONS, ComposedRepeatableClass.class); |
||||||
|
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", |
||||||
|
"C"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsWhenComposedMixedWithContainerOnClassReturnsAnnotations() { |
||||||
|
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class, |
||||||
|
SearchStrategy.INHERITED_ANNOTATIONS, |
||||||
|
ComposedRepeatableMixedWithContainerClass.class); |
||||||
|
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", |
||||||
|
"C"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsWhenComposedContainerForRepeatableOnClassReturnsAnnotations() { |
||||||
|
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class, |
||||||
|
SearchStrategy.INHERITED_ANNOTATIONS, ComposedContainerClass.class); |
||||||
|
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", |
||||||
|
"C"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsWhenNoninheritedComposedRepeatableOnClassReturnsAnnotations() { |
||||||
|
Set<Noninherited> annotations = getAnnotations(null, Noninherited.class, |
||||||
|
SearchStrategy.INHERITED_ANNOTATIONS, NoninheritedRepeatableClass.class); |
||||||
|
assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A", |
||||||
|
"B", "C"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void inheritedAnnotationsWhenNoninheritedComposedRepeatableOnSuperclassReturnsAnnotations() { |
||||||
|
Set<Noninherited> annotations = getAnnotations(null, Noninherited.class, |
||||||
|
SearchStrategy.INHERITED_ANNOTATIONS, |
||||||
|
SubNoninheritedRepeatableClass.class); |
||||||
|
assertThat(annotations).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveWhenNonRepeatableThrowsException() { |
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> getAnnotations(null, |
||||||
|
NonRepeatable.class, SearchStrategy.EXHAUSTIVE, getClass())).satisfies( |
||||||
|
this::nonRepeatableRequirements); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveWhenContainerMissingValueAttributeThrowsException() { |
||||||
|
assertThatAnnotationConfigurationException().isThrownBy( |
||||||
|
() -> getAnnotations(ContainerMissingValueAttribute.class, |
||||||
|
InvalidRepeatable.class, SearchStrategy.EXHAUSTIVE, |
||||||
|
getClass())).satisfies(this::missingValueAttributeRequirements); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveWhenWhenNonArrayValueAttributeThrowsException() { |
||||||
|
assertThatAnnotationConfigurationException().isThrownBy( |
||||||
|
() -> getAnnotations(ContainerWithNonArrayValueAttribute.class, |
||||||
|
InvalidRepeatable.class, SearchStrategy.EXHAUSTIVE, |
||||||
|
getClass())).satisfies(this::nonArrayValueAttributeRequirements); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveWhenWrongComponentTypeThrowsException() { |
||||||
|
assertThatAnnotationConfigurationException().isThrownBy(() -> getAnnotations( |
||||||
|
ContainerWithArrayValueAttributeButWrongComponentType.class, |
||||||
|
InvalidRepeatable.class, SearchStrategy.EXHAUSTIVE, |
||||||
|
getClass())).satisfies(this::wrongComponentTypeRequirements); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveWhenOnClassReturnsAnnotations() { |
||||||
|
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class, |
||||||
|
SearchStrategy.EXHAUSTIVE, RepeatableClass.class); |
||||||
|
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", |
||||||
|
"C"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveWhenWhenOnSuperclassReturnsAnnotations() { |
||||||
|
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class, |
||||||
|
SearchStrategy.EXHAUSTIVE, SubRepeatableClass.class); |
||||||
|
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", |
||||||
|
"C"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveWhenComposedOnClassReturnsAnnotations() { |
||||||
|
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class, |
||||||
|
SearchStrategy.EXHAUSTIVE, ComposedRepeatableClass.class); |
||||||
|
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", |
||||||
|
"C"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveWhenComposedMixedWithContainerOnClassReturnsAnnotations() { |
||||||
|
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class, |
||||||
|
SearchStrategy.EXHAUSTIVE, |
||||||
|
ComposedRepeatableMixedWithContainerClass.class); |
||||||
|
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", |
||||||
|
"C"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveWhenComposedContainerForRepeatableOnClassReturnsAnnotations() { |
||||||
|
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class, |
||||||
|
SearchStrategy.EXHAUSTIVE, ComposedContainerClass.class); |
||||||
|
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", |
||||||
|
"C"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveAnnotationsWhenNoninheritedComposedRepeatableOnClassReturnsAnnotations() { |
||||||
|
Set<Noninherited> annotations = getAnnotations(null, Noninherited.class, |
||||||
|
SearchStrategy.EXHAUSTIVE, NoninheritedRepeatableClass.class); |
||||||
|
assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A", |
||||||
|
"B", "C"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exhaustiveAnnotationsWhenNoninheritedComposedRepeatableOnSuperclassReturnsAnnotations() { |
||||||
|
Set<Noninherited> annotations = getAnnotations(null, Noninherited.class, |
||||||
|
SearchStrategy.EXHAUSTIVE, SubNoninheritedRepeatableClass.class); |
||||||
|
assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A", |
||||||
|
"B", "C"); |
||||||
|
} |
||||||
|
|
||||||
|
private <A extends Annotation> Set<A> getAnnotations( |
||||||
|
Class<? extends Annotation> container, Class<A> repeatable, |
||||||
|
SearchStrategy searchStrategy, AnnotatedElement element) { |
||||||
|
RepeatableContainers containers = RepeatableContainers.of(repeatable, container); |
||||||
|
MergedAnnotations annotations = MergedAnnotations.from(element, |
||||||
|
searchStrategy, containers, AnnotationFilter.PLAIN); |
||||||
|
return annotations.stream(repeatable).collect( |
||||||
|
MergedAnnotationCollectors.toAnnotationSet()); |
||||||
|
} |
||||||
|
|
||||||
|
private void nonRepeatableRequirements(Exception ex) { |
||||||
|
assertThat(ex.getMessage()).startsWith( |
||||||
|
"Annotation type must be a repeatable annotation").contains( |
||||||
|
"failed to resolve container type for", |
||||||
|
NonRepeatable.class.getName()); |
||||||
|
} |
||||||
|
|
||||||
|
private void missingValueAttributeRequirements(Exception ex) { |
||||||
|
ex.printStackTrace(); |
||||||
|
assertThat(ex.getMessage()).startsWith( |
||||||
|
"Invalid declaration of container type").contains( |
||||||
|
ContainerMissingValueAttribute.class.getName(), |
||||||
|
"for repeatable annotation", InvalidRepeatable.class.getName()); |
||||||
|
assertThat(ex).hasCauseInstanceOf(NoSuchMethodException.class); |
||||||
|
} |
||||||
|
|
||||||
|
private void nonArrayValueAttributeRequirements(Exception ex) { |
||||||
|
assertThat(ex.getMessage()).startsWith("Container type").contains( |
||||||
|
ContainerWithNonArrayValueAttribute.class.getName(), |
||||||
|
"must declare a 'value' attribute for an array of type", |
||||||
|
InvalidRepeatable.class.getName()); |
||||||
|
} |
||||||
|
|
||||||
|
private void wrongComponentTypeRequirements(Exception ex) { |
||||||
|
assertThat(ex.getMessage()).startsWith("Container type").contains( |
||||||
|
ContainerWithArrayValueAttributeButWrongComponentType.class.getName(), |
||||||
|
"must declare a 'value' attribute for an array of type", |
||||||
|
InvalidRepeatable.class.getName()); |
||||||
|
} |
||||||
|
|
||||||
|
private static ThrowableTypeAssert<AnnotationConfigurationException> assertThatAnnotationConfigurationException() { |
||||||
|
return assertThatExceptionOfType(AnnotationConfigurationException.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@interface NonRepeatable { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@interface ContainerMissingValueAttribute { |
||||||
|
|
||||||
|
// InvalidRepeatable[] value();
|
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@interface ContainerWithNonArrayValueAttribute { |
||||||
|
|
||||||
|
InvalidRepeatable value(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@interface ContainerWithArrayValueAttributeButWrongComponentType { |
||||||
|
|
||||||
|
String[] value(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@interface InvalidRepeatable { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Inherited |
||||||
|
@interface PeteRepeats { |
||||||
|
|
||||||
|
PeteRepeat[] value(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Inherited |
||||||
|
@Repeatable(PeteRepeats.class) |
||||||
|
@interface PeteRepeat { |
||||||
|
|
||||||
|
String value(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@PeteRepeat("shadowed") |
||||||
|
@Target({ ElementType.METHOD, ElementType.TYPE }) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Inherited |
||||||
|
@interface ForPetesSake { |
||||||
|
|
||||||
|
@AliasFor(annotation = PeteRepeat.class) |
||||||
|
String value(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@PeteRepeat("shadowed") |
||||||
|
@Target({ ElementType.METHOD, ElementType.TYPE }) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Inherited |
||||||
|
@interface ForTheLoveOfFoo { |
||||||
|
|
||||||
|
@AliasFor(annotation = PeteRepeat.class) |
||||||
|
String value(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") }) |
||||||
|
@Target({ ElementType.METHOD, ElementType.TYPE }) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Inherited |
||||||
|
@interface ComposedContainer { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@PeteRepeat("A") |
||||||
|
@PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") }) |
||||||
|
static class RepeatableClass { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static class SubRepeatableClass extends RepeatableClass { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@ForPetesSake("B") |
||||||
|
@ForTheLoveOfFoo("C") |
||||||
|
@PeteRepeat("A") |
||||||
|
static class ComposedRepeatableClass { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@ForPetesSake("C") |
||||||
|
@PeteRepeats(@PeteRepeat("A")) |
||||||
|
@PeteRepeat("B") |
||||||
|
static class ComposedRepeatableMixedWithContainerClass { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@PeteRepeat("A") |
||||||
|
@ComposedContainer |
||||||
|
static class ComposedContainerClass { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Target(ElementType.TYPE) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@interface Noninheriteds { |
||||||
|
|
||||||
|
Noninherited[] value(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Target(ElementType.TYPE) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Repeatable(Noninheriteds.class) |
||||||
|
@interface Noninherited { |
||||||
|
|
||||||
|
@AliasFor("name") |
||||||
|
String value() default ""; |
||||||
|
|
||||||
|
@AliasFor("value") |
||||||
|
String name() default ""; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Noninherited(name = "shadowed") |
||||||
|
@Target(ElementType.TYPE) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@interface ComposedNoninherited { |
||||||
|
|
||||||
|
@AliasFor(annotation = Noninherited.class) |
||||||
|
String name() default ""; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@ComposedNoninherited(name = "C") |
||||||
|
@Noninheriteds({ @Noninherited(value = "A"), @Noninherited(name = "B") }) |
||||||
|
static class NoninheritedRepeatableClass { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static class SubNoninheritedRepeatableClass extends NoninheritedRepeatableClass { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,81 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for {@link PackagesAnnotationFilter}. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
*/ |
||||||
|
public class PackagesAnnotationFilterTests { |
||||||
|
|
||||||
|
@Test |
||||||
|
public void createWhenPackagesIsNullThrowsException() { |
||||||
|
assertThatIllegalArgumentException().isThrownBy( |
||||||
|
() -> new PackagesAnnotationFilter((String[]) null)).withMessage( |
||||||
|
"Packages must not be null"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void createWhenPackagesContainsNullThrowsException() { |
||||||
|
assertThatIllegalArgumentException().isThrownBy( |
||||||
|
() -> new PackagesAnnotationFilter((String) null)).withMessage( |
||||||
|
"Package must not have empty elements"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void createWhenPackagesContainsEmptyTextThrowsException() { |
||||||
|
assertThatIllegalArgumentException().isThrownBy( |
||||||
|
() -> new PackagesAnnotationFilter("")).withMessage( |
||||||
|
"Package must not have empty elements"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void matchesWhenInPackageReturnsTrue() { |
||||||
|
PackagesAnnotationFilter filter = new PackagesAnnotationFilter("com.example"); |
||||||
|
assertThat(filter.matches("com.example.Component")).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void matchesWhenNotInPackageReturnsFalse() { |
||||||
|
PackagesAnnotationFilter filter = new PackagesAnnotationFilter("com.example"); |
||||||
|
assertThat(filter.matches("org.springframework.sterotype.Component")).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void matchesWhenInSimilarPackageReturnsFalse() { |
||||||
|
PackagesAnnotationFilter filter = new PackagesAnnotationFilter("com.example"); |
||||||
|
assertThat(filter.matches("com.examples.Component")).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void equalsAndHashCode() { |
||||||
|
PackagesAnnotationFilter filter1 = new PackagesAnnotationFilter("com.example", |
||||||
|
"org.springframework"); |
||||||
|
PackagesAnnotationFilter filter2 = new PackagesAnnotationFilter( |
||||||
|
"org.springframework", "com.example"); |
||||||
|
PackagesAnnotationFilter filter3 = new PackagesAnnotationFilter("com.examples"); |
||||||
|
assertThat(filter1.hashCode()).isEqualTo(filter2.hashCode()); |
||||||
|
assertThat(filter1).isEqualTo(filter1).isEqualTo(filter2).isNotEqualTo(filter3); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,283 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.annotation.Repeatable; |
||||||
|
import java.lang.annotation.Retention; |
||||||
|
import java.lang.annotation.RetentionPolicy; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.*; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for {@link RepeatableContainers}. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
*/ |
||||||
|
public class RepeatableContainersTests { |
||||||
|
|
||||||
|
@Test |
||||||
|
public void standardRepeatablesWhenNonRepeatableReturnsNull() { |
||||||
|
Object[] values = findRepeatedAnnotationValues( |
||||||
|
RepeatableContainers.standardRepeatables(), WithNonRepeatable.class, |
||||||
|
NonRepeatable.class); |
||||||
|
assertThat(values).isNull(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void standardRepeatablesWhenSingleReturnsNull() { |
||||||
|
Object[] values = findRepeatedAnnotationValues( |
||||||
|
RepeatableContainers.standardRepeatables(), |
||||||
|
WithSingleStandardRepeatable.class, StandardRepeatable.class); |
||||||
|
assertThat(values).isNull(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void standardRepeatablesWhenContainerReturnsRepeats() { |
||||||
|
Object[] values = findRepeatedAnnotationValues( |
||||||
|
RepeatableContainers.standardRepeatables(), WithStandardRepeatables.class, |
||||||
|
StandardContainer.class); |
||||||
|
assertThat(values).containsExactly("a", "b"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void standardRepeatablesWhenContainerButNotRepeatableReturnsNull() { |
||||||
|
Object[] values = findRepeatedAnnotationValues( |
||||||
|
RepeatableContainers.standardRepeatables(), WithExplicitRepeatables.class, |
||||||
|
ExplicitContainer.class); |
||||||
|
assertThat(values).isNull(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void ofExplicitWhenNonRepeatableReturnsNull() { |
||||||
|
Object[] values = findRepeatedAnnotationValues( |
||||||
|
RepeatableContainers.of(ExplicitRepeatable.class, |
||||||
|
ExplicitContainer.class), |
||||||
|
WithNonRepeatable.class, NonRepeatable.class); |
||||||
|
assertThat(values).isNull(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void ofExplicitWhenStandardRepeatableContainerReturnsNull() { |
||||||
|
Object[] values = findRepeatedAnnotationValues( |
||||||
|
RepeatableContainers.of(ExplicitRepeatable.class, |
||||||
|
ExplicitContainer.class), |
||||||
|
WithStandardRepeatables.class, StandardContainer.class); |
||||||
|
assertThat(values).isNull(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void ofExplicitWhenContainerReturnsRepeats() { |
||||||
|
Object[] values = findRepeatedAnnotationValues( |
||||||
|
RepeatableContainers.of(ExplicitRepeatable.class, |
||||||
|
ExplicitContainer.class), |
||||||
|
WithExplicitRepeatables.class, ExplicitContainer.class); |
||||||
|
assertThat(values).containsExactly("a", "b"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void ofExplicitWhenHasNoValueThrowsException() { |
||||||
|
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( |
||||||
|
() -> RepeatableContainers.of(ExplicitRepeatable.class, |
||||||
|
InvalidNoValue.class)).withMessageContaining( |
||||||
|
"Invalid declaration of container type [" |
||||||
|
+ InvalidNoValue.class.getName() |
||||||
|
+ "] for repeatable annotation [" |
||||||
|
+ ExplicitRepeatable.class.getName() + "]"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void ofExplicitWhenValueIsNotArrayThrowsException() { |
||||||
|
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( |
||||||
|
() -> RepeatableContainers.of(ExplicitRepeatable.class, |
||||||
|
InvalidNotArray.class)).withMessage("Container type [" |
||||||
|
+ InvalidNotArray.class.getName() |
||||||
|
+ "] must declare a 'value' attribute for an array of type [" |
||||||
|
+ ExplicitRepeatable.class.getName() + "]"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void ofExplicitWhenValueIsArrayOfWrongTypeThrowsException() { |
||||||
|
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( |
||||||
|
() -> RepeatableContainers.of(ExplicitRepeatable.class, |
||||||
|
InvalidWrongArrayType.class)).withMessage("Container type [" |
||||||
|
+ InvalidWrongArrayType.class.getName() |
||||||
|
+ "] must declare a 'value' attribute for an array of type [" |
||||||
|
+ ExplicitRepeatable.class.getName() + "]"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void ofExplicitWhenAnnotationIsNullThrowsException() { |
||||||
|
assertThatIllegalArgumentException().isThrownBy( |
||||||
|
() -> RepeatableContainers.of(null, null)).withMessage( |
||||||
|
"Repeatable must not be null"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void ofExplicitWhenContainerIsNullDeducesContainer() { |
||||||
|
Object[] values = findRepeatedAnnotationValues( |
||||||
|
RepeatableContainers.of(StandardRepeatable.class, null), |
||||||
|
WithStandardRepeatables.class, StandardContainer.class); |
||||||
|
assertThat(values).containsExactly("a", "b"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void ofExplicitWhenContainerIsNullAndNotRepeatableThrowsException() { |
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> RepeatableContainers.of( |
||||||
|
ExplicitRepeatable.class, null)).withMessage( |
||||||
|
"Annotation type must be a repeatable annotation: " |
||||||
|
+ "failed to resolve container type for " |
||||||
|
+ ExplicitRepeatable.class.getName()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void standardAndExplicitReturnsRepeats() { |
||||||
|
RepeatableContainers repeatableContainers = RepeatableContainers.standardRepeatables().and( |
||||||
|
ExplicitContainer.class, ExplicitRepeatable.class); |
||||||
|
assertThat(findRepeatedAnnotationValues(repeatableContainers, |
||||||
|
WithStandardRepeatables.class, StandardContainer.class)).containsExactly( |
||||||
|
"a", "b"); |
||||||
|
assertThat(findRepeatedAnnotationValues(repeatableContainers, |
||||||
|
WithExplicitRepeatables.class, ExplicitContainer.class)).containsExactly( |
||||||
|
"a", "b"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void noneAlwaysReturnsNull() { |
||||||
|
Object[] values = findRepeatedAnnotationValues( |
||||||
|
RepeatableContainers.none(), WithStandardRepeatables.class, |
||||||
|
StandardContainer.class); |
||||||
|
assertThat(values).isNull(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void equalsAndHashcode() { |
||||||
|
RepeatableContainers c1 = RepeatableContainers.of(ExplicitRepeatable.class, |
||||||
|
ExplicitContainer.class); |
||||||
|
RepeatableContainers c2 = RepeatableContainers.of(ExplicitRepeatable.class, |
||||||
|
ExplicitContainer.class); |
||||||
|
RepeatableContainers c3 = RepeatableContainers.standardRepeatables(); |
||||||
|
RepeatableContainers c4 = RepeatableContainers.standardRepeatables().and( |
||||||
|
ExplicitContainer.class, ExplicitRepeatable.class); |
||||||
|
assertThat(c1.hashCode()).isEqualTo(c2.hashCode()); |
||||||
|
assertThat(c1).isEqualTo(c1).isEqualTo(c2); |
||||||
|
assertThat(c1).isNotEqualTo(c3).isNotEqualTo(c4); |
||||||
|
} |
||||||
|
|
||||||
|
private Object[] findRepeatedAnnotationValues(RepeatableContainers containers, |
||||||
|
Class<?> element, Class<? extends Annotation> annotationType) { |
||||||
|
Annotation[] annotations = containers.findRepeatedAnnotations( |
||||||
|
element.getAnnotation(annotationType)); |
||||||
|
return extractValues(annotations); |
||||||
|
} |
||||||
|
|
||||||
|
private Object[] extractValues(Annotation[] annotations) { |
||||||
|
try { |
||||||
|
if (annotations == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
Object[] result = new String[annotations.length]; |
||||||
|
for (int i = 0; i < annotations.length; i++) { |
||||||
|
result[i] = annotations[i].annotationType().getMethod("value").invoke( |
||||||
|
annotations[i]); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
catch (Exception ex) { |
||||||
|
throw new RuntimeException(ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface NonRepeatable { |
||||||
|
|
||||||
|
String value() default ""; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Repeatable(StandardContainer.class) |
||||||
|
static @interface StandardRepeatable { |
||||||
|
|
||||||
|
String value() default ""; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface StandardContainer { |
||||||
|
|
||||||
|
StandardRepeatable[] value(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface ExplicitRepeatable { |
||||||
|
|
||||||
|
String value() default ""; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface ExplicitContainer { |
||||||
|
|
||||||
|
ExplicitRepeatable[] value(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface InvalidNoValue { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface InvalidNotArray { |
||||||
|
|
||||||
|
int value(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface InvalidWrongArrayType { |
||||||
|
|
||||||
|
StandardRepeatable[] value(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@NonRepeatable("a") |
||||||
|
static class WithNonRepeatable { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@StandardRepeatable("a") |
||||||
|
static class WithSingleStandardRepeatable { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@StandardRepeatable("a") |
||||||
|
@StandardRepeatable("b") |
||||||
|
static class WithStandardRepeatables { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@ExplicitContainer({ @ExplicitRepeatable("a"), @ExplicitRepeatable("b") }) |
||||||
|
static class WithExplicitRepeatables { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,163 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2019 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Annotation; |
||||||
|
import java.lang.annotation.Retention; |
||||||
|
import java.lang.annotation.RetentionPolicy; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.*; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for {@link TypeMappedAnnotation}. See also |
||||||
|
* {@link MergedAnnotationsTests} for a much more extensive collection of tests. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
*/ |
||||||
|
public class TypeMappedAnnotationTests { |
||||||
|
|
||||||
|
@Test |
||||||
|
public void mappingWhenMirroredReturnsMirroredValues() { |
||||||
|
testExplicitMirror(WithExplicitMirrorA.class); |
||||||
|
testExplicitMirror(WithExplicitMirrorB.class); |
||||||
|
} |
||||||
|
|
||||||
|
private void testExplicitMirror(Class<?> annotatedClass) { |
||||||
|
TypeMappedAnnotation<ExplicitMirror> annotation = getTypeMappedAnnotation( |
||||||
|
annotatedClass, ExplicitMirror.class); |
||||||
|
assertThat(annotation.getString("a")).isEqualTo("test"); |
||||||
|
assertThat(annotation.getString("b")).isEqualTo("test"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void mappingExplicitAliasToMetaAnnotationReturnsMappedValues() { |
||||||
|
TypeMappedAnnotation<?> annotation = getTypeMappedAnnotation( |
||||||
|
WithExplicitAliasToMetaAnnotation.class, |
||||||
|
ExplicitAliasToMetaAnnotation.class, |
||||||
|
ExplicitAliasMetaAnnotationTarget.class); |
||||||
|
assertThat(annotation.getString("aliased")).isEqualTo("aliased"); |
||||||
|
assertThat(annotation.getString("nonAliased")).isEqualTo("nonAliased"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void mappingConventionAliasToMetaAnnotationReturnsMappedValues() { |
||||||
|
TypeMappedAnnotation<?> annotation = getTypeMappedAnnotation( |
||||||
|
WithConventionAliasToMetaAnnotation.class, |
||||||
|
ConventionAliasToMetaAnnotation.class, |
||||||
|
ConventionAliasMetaAnnotationTarget.class); |
||||||
|
assertThat(annotation.getString("value")).isEqualTo(""); |
||||||
|
assertThat(annotation.getString("convention")).isEqualTo("convention"); |
||||||
|
} |
||||||
|
|
||||||
|
private <A extends Annotation> TypeMappedAnnotation<A> getTypeMappedAnnotation( |
||||||
|
Class<?> source, Class<A> annotationType) { |
||||||
|
return getTypeMappedAnnotation(source, annotationType, annotationType); |
||||||
|
} |
||||||
|
|
||||||
|
private <A extends Annotation> TypeMappedAnnotation<A> getTypeMappedAnnotation( |
||||||
|
Class<?> source, Class<? extends Annotation> rootAnnotationType, |
||||||
|
Class<A> annotationType) { |
||||||
|
Annotation rootAnnotation = source.getAnnotation(rootAnnotationType); |
||||||
|
AnnotationTypeMapping mapping = getMapping(rootAnnotation, annotationType); |
||||||
|
return TypeMappedAnnotation.createIfPossible(mapping, source, rootAnnotation, 0, IntrospectionFailureLogger.INFO); |
||||||
|
} |
||||||
|
|
||||||
|
private AnnotationTypeMapping getMapping(Annotation annotation, |
||||||
|
Class<? extends Annotation> mappedAnnotationType) { |
||||||
|
AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( |
||||||
|
annotation.annotationType()); |
||||||
|
for (int i = 0; i < mappings.size(); i++) { |
||||||
|
AnnotationTypeMapping candidate = mappings.get(i); |
||||||
|
if (candidate.getAnnotationType().equals(mappedAnnotationType)) { |
||||||
|
return candidate; |
||||||
|
} |
||||||
|
} |
||||||
|
throw new IllegalStateException( |
||||||
|
"No mapping from " + annotation + " to " + mappedAnnotationType); |
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface ExplicitMirror { |
||||||
|
|
||||||
|
@AliasFor("b") |
||||||
|
String a() default ""; |
||||||
|
|
||||||
|
@AliasFor("a") |
||||||
|
String b() default ""; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@ExplicitMirror(a = "test") |
||||||
|
static class WithExplicitMirrorA { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@ExplicitMirror(b = "test") |
||||||
|
static class WithExplicitMirrorB { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@ExplicitAliasMetaAnnotationTarget(nonAliased = "nonAliased") |
||||||
|
static @interface ExplicitAliasToMetaAnnotation { |
||||||
|
|
||||||
|
@AliasFor(annotation = ExplicitAliasMetaAnnotationTarget.class) |
||||||
|
String aliased() default ""; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface ExplicitAliasMetaAnnotationTarget { |
||||||
|
|
||||||
|
String aliased() default ""; |
||||||
|
|
||||||
|
String nonAliased() default ""; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@ExplicitAliasToMetaAnnotation(aliased = "aliased") |
||||||
|
private static class WithExplicitAliasToMetaAnnotation { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@ConventionAliasMetaAnnotationTarget |
||||||
|
static @interface ConventionAliasToMetaAnnotation { |
||||||
|
|
||||||
|
String value() default ""; |
||||||
|
|
||||||
|
String convention() default ""; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
static @interface ConventionAliasMetaAnnotationTarget { |
||||||
|
|
||||||
|
String value() default ""; |
||||||
|
|
||||||
|
String convention() default ""; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@ConventionAliasToMetaAnnotation(value = "value", convention = "convention") |
||||||
|
private static class WithConventionAliasToMetaAnnotation { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue