提交 37255afc 编写于 作者: P Phillip Webb 提交者: Juergen Hoeller

Complete migration of annotation utility methods

Delete `InternalAnnotationUtils` and `InternalAnnotatedElementUtils` and
migrate exclusively to the new `MergedAnnotations` API.

Closes gh-22562
上级 a14bfe9a
/*
* 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.List;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Abstract base class for {@link AnnotationAttributeExtractor} implementations
* that transparently enforce attribute alias semantics for annotation
* attributes that are annotated with {@link AliasFor @AliasFor}.
*
* @author Sam Brannen
* @since 4.2
* @param <S> the type of source supported by this extractor
* @see Annotation
* @see AliasFor
* @see AnnotationUtils#synthesizeAnnotation(Annotation, java.lang.reflect.AnnotatedElement)
*/
abstract class AbstractAliasAwareAnnotationAttributeExtractor<S> implements AnnotationAttributeExtractor<S> {
private final Class<? extends Annotation> annotationType;
@Nullable
private final Object annotatedElement;
private final S source;
private final Map<String, List<String>> attributeAliasMap;
/**
* Construct a new {@code AbstractAliasAwareAnnotationAttributeExtractor}.
* @param annotationType the annotation type to synthesize; never {@code null}
* @param annotatedElement the element that is annotated with the annotation
* of the supplied type; may be {@code null} if unknown
* @param source the underlying source of annotation attributes; never {@code null}
*/
AbstractAliasAwareAnnotationAttributeExtractor(
Class<? extends Annotation> annotationType, @Nullable Object annotatedElement, S source) {
Assert.notNull(annotationType, "annotationType must not be null");
Assert.notNull(source, "source must not be null");
this.annotationType = annotationType;
this.annotatedElement = annotatedElement;
this.source = source;
this.attributeAliasMap = InternalAnnotationUtils.getAttributeAliasMap(annotationType);
}
@Override
public final Class<? extends Annotation> getAnnotationType() {
return this.annotationType;
}
@Override
@Nullable
public final Object getAnnotatedElement() {
return this.annotatedElement;
}
@Override
public final S getSource() {
return this.source;
}
@Override
@Nullable
public final Object getAttributeValue(Method attributeMethod) {
String attributeName = attributeMethod.getName();
Object attributeValue = getRawAttributeValue(attributeMethod);
List<String> aliasNames = this.attributeAliasMap.get(attributeName);
if (aliasNames != null) {
Object defaultValue = AnnotationUtils.getDefaultValue(this.annotationType, attributeName);
for (String aliasName : aliasNames) {
Object aliasValue = getRawAttributeValue(aliasName);
if (!ObjectUtils.nullSafeEquals(attributeValue, aliasValue) &&
!ObjectUtils.nullSafeEquals(attributeValue, defaultValue) &&
!ObjectUtils.nullSafeEquals(aliasValue, defaultValue)) {
String elementName = (this.annotatedElement != null ? this.annotatedElement.toString() : "unknown element");
throw new AnnotationConfigurationException(String.format(
"In annotation [%s] declared on %s and synthesized from [%s], attribute '%s' and its " +
"alias '%s' are present with values of [%s] and [%s], but only one is permitted.",
this.annotationType.getName(), elementName, this.source, attributeName, aliasName,
ObjectUtils.nullSafeToString(attributeValue), ObjectUtils.nullSafeToString(aliasValue)));
}
// If the user didn't declare the annotation with an explicit value,
// use the value of the alias instead.
if (ObjectUtils.nullSafeEquals(attributeValue, defaultValue)) {
attributeValue = aliasValue;
}
}
}
return attributeValue;
}
/**
* Get the raw, unmodified attribute value from the underlying
* {@linkplain #getSource source} that corresponds to the supplied
* attribute method.
*/
@Nullable
protected abstract Object getRawAttributeValue(Method attributeMethod);
/**
* Get the raw, unmodified attribute value from the underlying
* {@linkplain #getSource source} that corresponds to the supplied
* attribute name.
*/
@Nullable
protected abstract Object getRawAttributeValue(String attributeName);
}
......@@ -51,15 +51,7 @@ import java.lang.annotation.Target;
* <h3>Usage Requirements</h3>
* <p>Like with any annotation in Java, the mere presence of {@code @AliasFor}
* on its own will not enforce alias semantics. For alias semantics to be
* enforced, annotations must be <em>loaded</em> via the utility methods in
* {@link AnnotationUtils}. Behind the scenes, Spring will <em>synthesize</em>
* an annotation by wrapping it in a dynamic proxy that transparently enforces
* <em>attribute alias</em> semantics for annotation attributes that are
* annotated with {@code @AliasFor}. Similarly, {@link AnnotatedElementUtils}
* supports explicit meta-annotation attribute overrides when {@code @AliasFor}
* is used within an annotation hierarchy. Typically you will not need to
* manually synthesize annotations on your own since Spring will do that for
* you transparently when looking up annotations on Spring-managed components.
* enforced, annotations must be <em>loaded</em> via {@link MergedAnnotations}.
*
* <h3>Implementation Requirements</h3>
* <ul>
......@@ -176,9 +168,7 @@ import java.lang.annotation.Target;
*
* @author Sam Brannen
* @since 4.2
* @see AnnotatedElementUtils
* @see AnnotationUtils
* @see AnnotationUtils#synthesizeAnnotation(Annotation, java.lang.reflect.AnnotatedElement)
* @see MergedAnnotations
* @see SynthesizedAnnotation
*/
@Retention(RetentionPolicy.RUNTIME)
......
......@@ -99,8 +99,8 @@ public abstract class AnnotatedElementUtils {
* @param annotations the annotations to expose through the {@code AnnotatedElement}
* @since 4.3
*/
public static AnnotatedElement forAnnotations(final Annotation... annotations) {
return InternalAnnotatedElementUtils.forAnnotations(annotations);
public static AnnotatedElement forAnnotations(Annotation... annotations) {
return new AnnotatedElementForAnnotations(annotations);
}
/**
......@@ -120,12 +120,7 @@ public abstract class AnnotatedElementUtils {
public static Set<String> getMetaAnnotationTypes(AnnotatedElement element,
Class<? extends Annotation> annotationType) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.getMetaAnnotationTypes(element, annotationType)
).withDescription(() -> element + " " + annotationType
).to(() ->
getMetaAnnotationTypes(element, element.getAnnotation(annotationType))
);
return getMetaAnnotationTypes(element, element.getAnnotation(annotationType));
}
/**
......@@ -145,17 +140,12 @@ public abstract class AnnotatedElementUtils {
public static Set<String> getMetaAnnotationTypes(AnnotatedElement element,
String annotationName) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.getMetaAnnotationTypes(element, annotationName)
).withDescription(() -> element + " " + annotationName
).to(() -> {
for (Annotation annotation : element.getAnnotations()) {
if (annotation.annotationType().getName().equals(annotationName)) {
return getMetaAnnotationTypes(element, annotation);
}
for (Annotation annotation : element.getAnnotations()) {
if (annotation.annotationType().getName().equals(annotationName)) {
return getMetaAnnotationTypes(element, annotation);
}
return Collections.emptySet();
});
}
return Collections.emptySet();
}
private static Set<String> getMetaAnnotationTypes(AnnotatedElement element,
......@@ -184,12 +174,8 @@ public abstract class AnnotatedElementUtils {
public static boolean hasMetaAnnotationTypes(AnnotatedElement element,
Class<? extends Annotation> annotationType) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.hasMetaAnnotationTypes(element, annotationType)
).to(() ->
getAnnotations(element).stream(annotationType)
.anyMatch(MergedAnnotation::isMetaPresent)
);
return getAnnotations(element).stream(annotationType)
.anyMatch(MergedAnnotation::isMetaPresent);
}
/**
......@@ -207,12 +193,8 @@ public abstract class AnnotatedElementUtils {
public static boolean hasMetaAnnotationTypes(AnnotatedElement element,
String annotationName) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.hasMetaAnnotationTypes(element, annotationName)
).to(() ->
getAnnotations(element).stream(annotationName)
.anyMatch(MergedAnnotation::isMetaPresent)
);
return getAnnotations(element).stream(annotationName)
.anyMatch(MergedAnnotation::isMetaPresent);
}
/**
......@@ -232,11 +214,7 @@ public abstract class AnnotatedElementUtils {
public static boolean isAnnotated(AnnotatedElement element,
Class<? extends Annotation> annotationType) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.isAnnotated(element, annotationType)
).to(() ->
getAnnotations(element).isPresent(annotationType)
);
return getAnnotations(element).isPresent(annotationType);
}
/**
......@@ -252,11 +230,7 @@ public abstract class AnnotatedElementUtils {
* @return {@code true} if a matching annotation is present
*/
public static boolean isAnnotated(AnnotatedElement element, String annotationName) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.isAnnotated(element, annotationName))
.to(() ->
getAnnotations(element).isPresent(annotationName)
);
return getAnnotations(element).isPresent(annotationName);
}
/**
......@@ -280,14 +254,9 @@ public abstract class AnnotatedElementUtils {
public static AnnotationAttributes getMergedAnnotationAttributes(
AnnotatedElement element, Class<? extends Annotation> annotationType) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.getMergedAnnotationAttributes(element,
annotationType))
.toNullable(() -> {
MergedAnnotation<?> mergedAnnotation = getAnnotations(element)
.get(annotationType, null, MergedAnnotationSelectors.firstDirectlyDeclared());
return getAnnotationAttributes(mergedAnnotation, false, false);
});
MergedAnnotation<?> mergedAnnotation = getAnnotations(element)
.get(annotationType, null, MergedAnnotationSelectors.firstDirectlyDeclared());
return getAnnotationAttributes(mergedAnnotation, false, false);
}
/**
......@@ -346,14 +315,9 @@ public abstract class AnnotatedElementUtils {
AnnotatedElement element, String annotationName, boolean classValuesAsString,
boolean nestedAnnotationsAsMap) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.getMergedAnnotationAttributes(element,
annotationName, classValuesAsString, nestedAnnotationsAsMap)
).toNullable(() -> {
MergedAnnotation<?> mergedAnnotation = getAnnotations(element)
.get(annotationName, null, MergedAnnotationSelectors.firstDirectlyDeclared());
return getAnnotationAttributes(mergedAnnotation, classValuesAsString, nestedAnnotationsAsMap);
});
MergedAnnotation<?> mergedAnnotation = getAnnotations(element)
.get(annotationName, null, MergedAnnotationSelectors.firstDirectlyDeclared());
return getAnnotationAttributes(mergedAnnotation, classValuesAsString, nestedAnnotationsAsMap);
}
/**
......@@ -377,13 +341,20 @@ public abstract class AnnotatedElementUtils {
@Nullable
public static <A extends Annotation> A getMergedAnnotation(AnnotatedElement element,
Class<A> annotationType) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.getMergedAnnotation(element, annotationType)
).toNullable(() ->
getAnnotations(element)
.get(annotationType, null, MergedAnnotationSelectors.firstDirectlyDeclared())
.synthesize(MergedAnnotation::isPresent).orElse(null)
);
if (AnnotationsScanner.hasPlainJavaAnnotationsOnly(element)) {
return null;
}
A annotation = AnnotationsScanner.getDeclaredAnnotation(element, annotationType);
if (annotation != null) {
return AnnotationUtils.synthesizeAnnotation(annotation, element);
}
if (AnnotationsScanner.hasPlainJavaAnnotationsOnly(element)) {
return null;
}
return getAnnotations(element)
.get(annotationType, null, MergedAnnotationSelectors.firstDirectlyDeclared())
.synthesize(MergedAnnotation::isPresent).orElse(null);
}
/**
......@@ -409,12 +380,8 @@ public abstract class AnnotatedElementUtils {
public static <A extends Annotation> Set<A> getAllMergedAnnotations(
AnnotatedElement element, Class<A> annotationType) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.getAllMergedAnnotations(element, annotationType)
).to(() ->
getAnnotations(element).stream(annotationType)
.collect(MergedAnnotationCollectors.toAnnotationSet())
);
return getAnnotations(element).stream(annotationType)
.collect(MergedAnnotationCollectors.toAnnotationSet());
}
/**
......@@ -438,14 +405,9 @@ public abstract class AnnotatedElementUtils {
public static Set<Annotation> getAllMergedAnnotations(AnnotatedElement element,
Set<Class<? extends Annotation>> annotationTypes) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.getAllMergedAnnotations(element,
annotationTypes)
).to(() ->
getAnnotations(element).stream()
.filter(MergedAnnotationPredicates.typeIn(annotationTypes))
.collect(MergedAnnotationCollectors.toAnnotationSet())
);
return getAnnotations(element).stream()
.filter(MergedAnnotationPredicates.typeIn(annotationTypes))
.collect(MergedAnnotationCollectors.toAnnotationSet());
}
/**
......@@ -508,14 +470,9 @@ public abstract class AnnotatedElementUtils {
AnnotatedElement element, Class<A> annotationType,
@Nullable Class<? extends Annotation> containerType) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.getMergedRepeatableAnnotations(element,
annotationType, containerType)
).to(() ->
getRepeatableAnnotations(element, containerType, annotationType)
.stream(annotationType)
.collect(MergedAnnotationCollectors.toAnnotationSet())
);
return getRepeatableAnnotations(element, containerType, annotationType)
.stream(annotationType)
.collect(MergedAnnotationCollectors.toAnnotationSet());
}
/**
......@@ -562,16 +519,11 @@ public abstract class AnnotatedElementUtils {
AnnotatedElement element, String annotationName,
final boolean classValuesAsString, final boolean nestedAnnotationsAsMap) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.getAllAnnotationAttributes(element,
annotationName, classValuesAsString, nestedAnnotationsAsMap)
).toNullable(() ->{
MapValues[] mapValues = MapValues.of(classValuesAsString, nestedAnnotationsAsMap);
return getAnnotations(element).stream(annotationName)
.filter(MergedAnnotationPredicates.unique(AnnotatedElementUtils::parentAndType))
.map(MergedAnnotation::withNonMergedAttributes)
.collect(MergedAnnotationCollectors.toMultiValueMap(AnnotatedElementUtils::nullIfEmpty, mapValues));
});
MapValues[] mapValues = MapValues.of(classValuesAsString, nestedAnnotationsAsMap);
return getAnnotations(element).stream(annotationName)
.filter(MergedAnnotationPredicates.unique(AnnotatedElementUtils::parentAndType))
.map(MergedAnnotation::withNonMergedAttributes)
.collect(MergedAnnotationCollectors.toMultiValueMap(AnnotatedElementUtils::nullIfEmpty, mapValues));
}
/**
......@@ -591,15 +543,14 @@ public abstract class AnnotatedElementUtils {
public static boolean hasAnnotation(AnnotatedElement element,
Class<? extends Annotation> annotationType) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.hasAnnotation(element, annotationType)
).withSkippedEquivalentCheck(() ->
InternalAnnotatedElementUtils.hasAnnotation(element, annotationType) &&
InternalAnnotatedElementUtils.findMergedAnnotationAttributes(element,
annotationType.getName(), false, false) == null
).to(() ->
findAnnotations(element).isPresent(annotationType)
);
// Shortcut: directly present on the element, with no processing needed?
if (AnnotationsScanner.hasPlainJavaAnnotationsOnly(element)) {
return false;
}
if (AnnotationsScanner.getDeclaredAnnotation(element, annotationType) != null) {
return true;
}
return findAnnotations(element).isPresent(annotationType);
}
/**
......@@ -634,14 +585,9 @@ public abstract class AnnotatedElementUtils {
AnnotatedElement element, Class<? extends Annotation> annotationType,
boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.findMergedAnnotationAttributes(element,
annotationType, classValuesAsString, nestedAnnotationsAsMap)
).toNullable(() -> {
MergedAnnotation<?> mergedAnnotation = findAnnotations(element)
.get(annotationType, null, MergedAnnotationSelectors.firstDirectlyDeclared());
return getAnnotationAttributes(mergedAnnotation, classValuesAsString, nestedAnnotationsAsMap);
});
MergedAnnotation<?> mergedAnnotation = findAnnotations(element)
.get(annotationType, null, MergedAnnotationSelectors.firstDirectlyDeclared());
return getAnnotationAttributes(mergedAnnotation, classValuesAsString, nestedAnnotationsAsMap);
}
/**
......@@ -676,14 +622,9 @@ public abstract class AnnotatedElementUtils {
AnnotatedElement element, String annotationName, boolean classValuesAsString,
boolean nestedAnnotationsAsMap) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.findMergedAnnotationAttributes(element,
annotationName, classValuesAsString, nestedAnnotationsAsMap)
).toNullable(() -> {
MergedAnnotation<?> mergedAnnotation = findAnnotations(element)
.get(annotationName, null, MergedAnnotationSelectors.firstDirectlyDeclared());
return getAnnotationAttributes(mergedAnnotation, classValuesAsString, nestedAnnotationsAsMap);
});
MergedAnnotation<?> mergedAnnotation = findAnnotations(element)
.get(annotationName, null, MergedAnnotationSelectors.firstDirectlyDeclared());
return getAnnotationAttributes(mergedAnnotation, classValuesAsString, nestedAnnotationsAsMap);
}
/**
......@@ -708,13 +649,16 @@ public abstract class AnnotatedElementUtils {
public static <A extends Annotation> A findMergedAnnotation(AnnotatedElement element,
Class<A> annotationType) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.findMergedAnnotation(element, annotationType)
).toNullable(() ->
findAnnotations(element)
.get(annotationType, null, MergedAnnotationSelectors.firstDirectlyDeclared())
.synthesize(MergedAnnotation::isPresent).orElse(null)
);
A annotation = AnnotationsScanner.getDeclaredAnnotation(element, annotationType);
if (annotation != null) {
return AnnotationUtils.synthesizeAnnotation(annotation, element);
}
if (AnnotationsScanner.hasPlainJavaAnnotationsOnly(element)) {
return null;
}
return findAnnotations(element)
.get(annotationType, null, MergedAnnotationSelectors.firstDirectlyDeclared())
.synthesize(MergedAnnotation::isPresent).orElse(null);
}
/**
......@@ -739,14 +683,9 @@ public abstract class AnnotatedElementUtils {
public static <A extends Annotation> Set<A> findAllMergedAnnotations(
AnnotatedElement element, Class<A> annotationType) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.findAllMergedAnnotations(element, annotationType)
).withSkippedOriginalExceptionCheck().to(() ->
findAnnotations(element)
.stream(annotationType)
.sorted(highAggregateIndexesFirst())
.collect(MergedAnnotationCollectors.toAnnotationSet())
);
return findAnnotations(element).stream(annotationType)
.sorted(highAggregateIndexesFirst())
.collect(MergedAnnotationCollectors.toAnnotationSet());
}
/**
......@@ -770,14 +709,10 @@ public abstract class AnnotatedElementUtils {
public static Set<Annotation> findAllMergedAnnotations(AnnotatedElement element,
Set<Class<? extends Annotation>> annotationTypes) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.findAllMergedAnnotations(element, annotationTypes)
).to(()->
findAnnotations(element).stream()
.filter(MergedAnnotationPredicates.typeIn(annotationTypes))
.sorted(highAggregateIndexesFirst())
.collect(MergedAnnotationCollectors.toAnnotationSet())
);
return findAnnotations(element).stream()
.filter(MergedAnnotationPredicates.typeIn(annotationTypes))
.sorted(highAggregateIndexesFirst())
.collect(MergedAnnotationCollectors.toAnnotationSet());
}
/**
......@@ -840,15 +775,10 @@ public abstract class AnnotatedElementUtils {
AnnotatedElement element, Class<A> annotationType,
@Nullable Class<? extends Annotation> containerType) {
return MigrateMethod.from(() ->
InternalAnnotatedElementUtils.findMergedRepeatableAnnotations(element,
annotationType, containerType)
).to(() ->
findRepeatableAnnotations(element, containerType, annotationType)
.stream(annotationType)
.sorted(highAggregateIndexesFirst())
.collect(MergedAnnotationCollectors.toAnnotationSet())
);
return findRepeatableAnnotations(element, containerType, annotationType)
.stream(annotationType)
.sorted(highAggregateIndexesFirst())
.collect(MergedAnnotationCollectors.toAnnotationSet());
}
private static MergedAnnotations getAnnotations(AnnotatedElement element) {
......@@ -893,7 +823,8 @@ public abstract class AnnotatedElementUtils {
}
private static <A extends Annotation> Comparator<MergedAnnotation<A>> highAggregateIndexesFirst() {
return Comparator.<MergedAnnotation<A>>comparingInt(MergedAnnotation::getAggregateIndex).reversed();
return Comparator.<MergedAnnotation<A>> comparingInt(
MergedAnnotation::getAggregateIndex).reversed();
}
@Nullable
......@@ -907,4 +838,40 @@ public abstract class AnnotatedElementUtils {
MapValues.of(classValuesAsString, nestedAnnotationsAsMap));
}
/**
* Adapted {@link AnnotatedElement} that hold specific annotations.
*/
private static class AnnotatedElementForAnnotations implements AnnotatedElement {
private final Annotation[] annotations;
AnnotatedElementForAnnotations(Annotation... annotations) {
this.annotations = annotations;
}
@Override
@SuppressWarnings("unchecked")
@Nullable
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
for (Annotation annotation : this.annotations) {
if (annotation.annotationType() == annotationClass) {
return (T) annotation;
}
}
return null;
}
@Override
public Annotation[] getAnnotations() {
return this.annotations;
}
@Override
public Annotation[] getDeclaredAnnotations() {
return this.annotations;
}
};
}
/*
* Copyright 2002-2016 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 org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
/**
* Default implementation of the {@link AnnotationAttributeExtractor} strategy
* that is backed by an {@link Annotation}.
*
* @author Sam Brannen
* @since 4.2
* @see Annotation
* @see AliasFor
* @see AbstractAliasAwareAnnotationAttributeExtractor
* @see MapAnnotationAttributeExtractor
* @see AnnotationUtils#synthesizeAnnotation
*/
class DefaultAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttributeExtractor<Annotation> {
/**
* Construct a new {@code DefaultAnnotationAttributeExtractor}.
* @param annotation the annotation to synthesize; never {@code null}
* @param annotatedElement the element that is annotated with the supplied
* annotation; may be {@code null} if unknown
*/
DefaultAnnotationAttributeExtractor(Annotation annotation, @Nullable Object annotatedElement) {
super(annotation.annotationType(), annotatedElement, annotation);
}
@Override
@Nullable
protected Object getRawAttributeValue(Method attributeMethod) {
ReflectionUtils.makeAccessible(attributeMethod);
return ReflectionUtils.invokeMethod(attributeMethod, getSource());
}
@Override
@Nullable
protected Object getRawAttributeValue(String attributeName) {
Method attributeMethod = ReflectionUtils.findMethod(getAnnotationType(), attributeName);
return (attributeMethod != null ? getRawAttributeValue(attributeMethod) : null);
}
}
/*
* 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.Array;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Implementation of the {@link AnnotationAttributeExtractor} strategy that
* is backed by a {@link Map}.
*
* @author Sam Brannen
* @since 4.2
* @see Annotation
* @see AliasFor
* @see AbstractAliasAwareAnnotationAttributeExtractor
* @see DefaultAnnotationAttributeExtractor
* @see AnnotationUtils#synthesizeAnnotation(Map, Class, AnnotatedElement)
*/
class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttributeExtractor<Map<String, Object>> {
/**
* Construct a new {@code MapAnnotationAttributeExtractor}.
* <p>The supplied map must contain a key-value pair for every attribute
* defined in the supplied {@code annotationType} that is not aliased or
* does not have a default value.
* @param attributes the map of annotation attributes; never {@code null}
* @param annotationType the type of annotation to synthesize; never {@code null}
* @param annotatedElement the element that is annotated with the annotation
* of the supplied type; may be {@code null} if unknown
*/
MapAnnotationAttributeExtractor(Map<String, Object> attributes, Class<? extends Annotation> annotationType,
@Nullable AnnotatedElement annotatedElement) {
super(annotationType, annotatedElement, enrichAndValidateAttributes(attributes, annotationType));
}
@Override
@Nullable
protected Object getRawAttributeValue(Method attributeMethod) {
return getRawAttributeValue(attributeMethod.getName());
}
@Override
@Nullable
protected Object getRawAttributeValue(String attributeName) {
return getSource().get(attributeName);
}
/**
* Enrich and validate the supplied <em>attributes</em> map by ensuring
* that it contains a non-null entry for each annotation attribute in
* the specified {@code annotationType} and that the type of the entry
* matches the return type for the corresponding annotation attribute.
* <p>If an entry is a map (presumably of annotation attributes), an
* attempt will be made to synthesize an annotation from it. Similarly,
* if an entry is an array of maps, an attempt will be made to synthesize
* an array of annotations from those maps.
* <p>If an attribute is missing in the supplied map, it will be set
* either to the value of its alias (if an alias exists) or to the
* value of the attribute's default value (if defined), and otherwise
* an {@link IllegalArgumentException} will be thrown.
*/
@SuppressWarnings("unchecked")
private static Map<String, Object> enrichAndValidateAttributes(
Map<String, Object> originalAttributes, Class<? extends Annotation> annotationType) {
Map<String, Object> attributes = new LinkedHashMap<>(originalAttributes);
Map<String, List<String>> attributeAliasMap = InternalAnnotationUtils.getAttributeAliasMap(annotationType);
for (Method attributeMethod : InternalAnnotationUtils.getAttributeMethods(annotationType)) {
String attributeName = attributeMethod.getName();
Object attributeValue = attributes.get(attributeName);
// if attribute not present, check aliases
if (attributeValue == null) {
List<String> aliasNames = attributeAliasMap.get(attributeName);
if (aliasNames != null) {
for (String aliasName : aliasNames) {
Object aliasValue = attributes.get(aliasName);
if (aliasValue != null) {
attributeValue = aliasValue;
attributes.put(attributeName, attributeValue);
break;
}
}
}
}
// if aliases not present, check default
if (attributeValue == null) {
Object defaultValue = AnnotationUtils.getDefaultValue(annotationType, attributeName);
if (defaultValue != null) {
attributeValue = defaultValue;
attributes.put(attributeName, attributeValue);
}
}
// if still null
Assert.notNull(attributeValue, () -> String.format(
"Attributes map %s returned null for required attribute '%s' defined by annotation type [%s].",
attributes, attributeName, annotationType.getName()));
// finally, ensure correct type
Class<?> requiredReturnType = attributeMethod.getReturnType();
Class<?> actualReturnType = attributeValue.getClass();
if (!ClassUtils.isAssignable(requiredReturnType, actualReturnType)) {
boolean converted = false;
// Single element overriding an array of the same type?
if (requiredReturnType.isArray() && requiredReturnType.getComponentType() == actualReturnType) {
Object array = Array.newInstance(requiredReturnType.getComponentType(), 1);
Array.set(array, 0, attributeValue);
attributes.put(attributeName, array);
converted = true;
}
// Nested map representing a single annotation?
else if (Annotation.class.isAssignableFrom(requiredReturnType) &&
Map.class.isAssignableFrom(actualReturnType)) {
Class<? extends Annotation> nestedAnnotationType =
(Class<? extends Annotation>) requiredReturnType;
Map<String, Object> map = (Map<String, Object>) attributeValue;
attributes.put(attributeName, AnnotationUtils.synthesizeAnnotation(map, nestedAnnotationType, null));
converted = true;
}
// Nested array of maps representing an array of annotations?
else if (requiredReturnType.isArray() && actualReturnType.isArray() &&
Annotation.class.isAssignableFrom(requiredReturnType.getComponentType()) &&
Map.class.isAssignableFrom(actualReturnType.getComponentType())) {
Class<? extends Annotation> nestedAnnotationType =
(Class<? extends Annotation>) requiredReturnType.getComponentType();
Map<String, Object>[] maps = (Map<String, Object>[]) attributeValue;
attributes.put(attributeName, InternalAnnotationUtils.synthesizeAnnotationArray(maps, nestedAnnotationType));
converted = true;
}
Assert.isTrue(converted, () -> String.format(
"Attributes map %s returned a value of type [%s] for attribute '%s', " +
"but a value of type [%s] is required as defined by annotation type [%s].",
attributes, actualReturnType.getName(), attributeName, requiredReturnType.getName(),
annotationType.getName()));
}
}
return attributes;
}
}
/*
* 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.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
import org.springframework.core.annotation.InternalAnnotationUtils.DefaultValueHolder;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Internal class used to help migrate annotation util methods to a new implementation.
*
* @author Phillip Webb
* @since 5.2
*/
final class MigrateMethod {
private MigrateMethod() {
}
/**
* Create a new {@link ReplacementMethod} builder for the deprecated method.
* @param originalMethod the original method being migrated
* @return a replacement builder.
*/
static <T> ReplacementMethod<T> from(Supplier<T> originalMethod) {
return new ReplacementMethod<>(originalMethod);
}
/**
* Create a new {@link ReplacementVoidMethod} for the deprecated method.
* @param originalMethod the original method being migrated
* @return a replacement builder.
*/
static ReplacementVoidMethod fromCall(Runnable originalMethod) {
return new ReplacementVoidMethod(originalMethod);
}
private static boolean isEquivalent(@Nullable Object result, @Nullable Object expectedResult) {
if (ObjectUtils.nullSafeEquals(result, expectedResult)) {
return true;
}
if (result == null && String.valueOf(expectedResult).startsWith(
"@org.springframework.lang.")) {
// Original methods don't filter spring annotation but we do
return true;
}
if (result == null || expectedResult == null) {
return false;
}
if (result instanceof DefaultValueHolder && expectedResult instanceof DefaultValueHolder) {
return isEquivalent(((DefaultValueHolder) result).defaultValue,
((DefaultValueHolder) expectedResult).defaultValue);
}
if (result instanceof Map && expectedResult instanceof Map) {
return isEquivalentMap((Map<?, ?>) result, (Map<?, ?>) expectedResult);
}
if (result instanceof List && expectedResult instanceof List) {
return isEquivalentList((List<?>) result, (List<?>) expectedResult);
}
if (result instanceof Object[] && expectedResult instanceof Object[]) {
return isEquivalentArray((Object[]) result, (Object[]) expectedResult);
}
if (result instanceof Object[]) {
if (isEquivalentArray((Object[]) result, new Object[] { expectedResult })) {
return true;
}
}
if (!(result instanceof Object[]) && expectedResult instanceof Object[]) {
if (isEquivalentArray(new Object[] { result }, (Object[]) expectedResult)) {
return true;
}
}
return false;
}
private static boolean isEquivalentMap(Map<?, ?> result, Map<?, ?> expectedResult) {
if (result.size() != expectedResult.size()) {
return false;
}
for (Map.Entry<?, ?> entry : result.entrySet()) {
if (!expectedResult.containsKey(entry.getKey())) {
return false;
}
if (!isEquivalent(entry.getValue(), expectedResult.get(entry.getKey()))) {
return false;
}
}
return true;
}
private static boolean isEquivalentList(List<?> result, List<?> expectedResult) {
if (result.size() != expectedResult.size()) {
return false;
}
for (int i = 0; i < result.size(); i++) {
if (!isEquivalent(result.get(i), expectedResult.get(i))) {
return false;
}
}
return true;
}
private static boolean isEquivalentArray(Object[] result, Object[] expectedResult) {
if (result.length != expectedResult.length) {
return false;
}
for (int i = 0; i < result.length; i++) {
if (!isEquivalent(result[i], expectedResult[i])) {
return false;
}
}
return true;
}
/**
* Builder to complete replacement details for a deprecated annotation method.
* @param <T> the return type
*/
static class ReplacementMethod<T> {
private final Supplier<T> originalMethod;
@Nullable
private Supplier<String> description;
private boolean skipOriginalExceptionCheck;
private BooleanSupplier skipEquivalentCheck = () -> false;
ReplacementMethod(Supplier<T> deprecatedMethod) {
this.originalMethod = deprecatedMethod;
}
/**
* Add a description for the method.
* @param description a description supplier
* @return this instance
*/
public ReplacementMethod<T> withDescription(Supplier<String> description) {
this.description = description;
return this;
}
public ReplacementMethod<T> withSkippedOriginalExceptionCheck() {
this.skipOriginalExceptionCheck = true;
return this;
}
public ReplacementMethod<T> withSkippedEquivalentCheck(BooleanSupplier supplier) {
this.skipEquivalentCheck = supplier;
return this;
}
/**
* Provide the replacement method that should be used instead of the deprecated
* one. The replacement method is called, and when appropriate the result is
* checked against the deprecated method.
* @param replacementMethod the replacement method
* @return the result of the replacement method
*/
public T to(Supplier<T> replacementMethod) {
T result = toNullable(replacementMethod);
if (result == null) {
throw new IllegalStateException("Unexpected null result");
}
return result;
}
/**
* Provide the replacement method that should be used instead of the deprecated
* one. The replacement method is called, and when appropriate the result is
* checked against the deprecated method.
* @param replacementMethod the replacement method
* @return the result of the replacement method
*/
@Nullable
public T toNullable(Supplier<T> replacementMethod) {
T result = tryInvoke(replacementMethod);
T expectedResult = this.originalMethod.get();
if (!isEquivalent(result, expectedResult)) {
if (this.skipEquivalentCheck.getAsBoolean()) {
return expectedResult;
}
String description = (this.description != null ? " [" +
this.description.get() + "]" : "");
throw new IllegalStateException("Expected " + expectedResult +
" got " + result + description);
}
return result;
}
private T tryInvoke(Supplier<T> replacementMethod) {
try {
return replacementMethod.get();
}
catch (RuntimeException expected) {
try {
T expectedResult = this.originalMethod.get();
if (this.skipOriginalExceptionCheck) {
return expectedResult;
}
throw new Error("Expected exception not thrown", expected);
}
catch (RuntimeException actual) {
if (!expected.getClass().isInstance(actual)) {
throw new Error(
"Exception is not " + expected.getClass().getName(),
actual);
}
throw actual;
}
}
}
}
/**
* Builder to complete replacement details for a deprecated annotation method that
* returns void.
*/
static class ReplacementVoidMethod {
private final Runnable originalMethod;
private final List<Object[]> argumentChecks = new ArrayList<>();
public ReplacementVoidMethod(Runnable originalMethod) {
this.originalMethod = originalMethod;
}
public ReplacementVoidMethod withArgumentCheck(Object originalArgument,
Object replacementArgument) {
this.argumentChecks.add(
new Object[] { originalArgument, replacementArgument });
return this;
}
public void to(Runnable replacementMethod) {
tryInvoke(this.originalMethod);
replacementMethod.run();
for (Object[] arguments : this.argumentChecks) {
Object expectedArgument = arguments[0];
Object actualArgument = arguments[1];
Assert.state(isEquivalent(actualArgument, expectedArgument),
() -> "Expected argument mutation of " + expectedArgument
+ " got " + actualArgument);
}
}
private void tryInvoke(Runnable replacementMethod) {
try {
replacementMethod.run();
}
catch (RuntimeException expected) {
try {
this.originalMethod.run();
throw new Error("Expected exception not thrown", expected);
}
catch (RuntimeException actual) {
if (!expected.getClass().isInstance(actual)) {
throw new Error(
"Exception is not " + expected.getClass().getName(),
actual);
}
throw actual;
}
}
}
}
}
/*
* 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.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* {@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
* @since 4.2
* @see Annotation
* @see AnnotationAttributeExtractor
* @see AnnotationUtils#synthesizeAnnotation(Annotation, AnnotatedElement)
*/
class SynthesizedAnnotationInvocationHandler implements InvocationHandler {
private final AnnotationAttributeExtractor<?> attributeExtractor;
private final Map<String, Object> valueCache = new ConcurrentHashMap<>(8);
/**
* Construct a new {@code SynthesizedAnnotationInvocationHandler} for
* the supplied {@link AnnotationAttributeExtractor}.
* @param attributeExtractor the extractor to delegate to
*/
SynthesizedAnnotationInvocationHandler(AnnotationAttributeExtractor<?> attributeExtractor) {
Assert.notNull(attributeExtractor, "AnnotationAttributeExtractor must not be null");
this.attributeExtractor = attributeExtractor;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (ReflectionUtils.isEqualsMethod(method)) {
return annotationEquals(args[0]);
}
if (ReflectionUtils.isHashCodeMethod(method)) {
return annotationHashCode();
}
if (ReflectionUtils.isToStringMethod(method)) {
return annotationToString();
}
if (InternalAnnotationUtils.isAnnotationTypeMethod(method)) {
return annotationType();
}
if (!InternalAnnotationUtils.isAttributeMethod(method)) {
throw new AnnotationConfigurationException(String.format(
"Method [%s] is unsupported for synthesized annotation type [%s]", method, annotationType()));
}
return getAttributeValue(method);
}
private Class<? extends Annotation> annotationType() {
return this.attributeExtractor.getAnnotationType();
}
private Object getAttributeValue(Method attributeMethod) {
String attributeName = attributeMethod.getName();
Object value = this.valueCache.get(attributeName);
if (value == null) {
value = this.attributeExtractor.getAttributeValue(attributeMethod);
if (value == null) {
String msg = String.format("%s returned null for attribute name [%s] from attribute source [%s]",
this.attributeExtractor.getClass().getName(), attributeName, this.attributeExtractor.getSource());
throw new IllegalStateException(msg);
}
// Synthesize nested annotations before returning them.
if (value instanceof Annotation) {
value = InternalAnnotationUtils.synthesizeAnnotation((Annotation) value, this.attributeExtractor.getAnnotatedElement());
}
else if (value instanceof Annotation[]) {
value = InternalAnnotationUtils.synthesizeAnnotationArray((Annotation[]) value, this.attributeExtractor.getAnnotatedElement());
}
this.valueCache.put(attributeName, value);
}
// Clone arrays so that users cannot alter the contents of values in our cache.
if (value.getClass().isArray()) {
value = cloneArray(value);
}
return value;
}
/**
* Clone the provided array, ensuring that original component type is
* retained.
* @param array the array to clone
*/
private Object cloneArray(Object array) {
if (array instanceof boolean[]) {
return ((boolean[]) array).clone();
}
if (array instanceof byte[]) {
return ((byte[]) array).clone();
}
if (array instanceof char[]) {
return ((char[]) array).clone();
}
if (array instanceof double[]) {
return ((double[]) array).clone();
}
if (array instanceof float[]) {
return ((float[]) array).clone();
}
if (array instanceof int[]) {
return ((int[]) array).clone();
}
if (array instanceof long[]) {
return ((long[]) array).clone();
}
if (array instanceof short[]) {
return ((short[]) array).clone();
}
// else
return ((Object[]) array).clone();
}
/**
* 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 (!annotationType().isInstance(other)) {
return false;
}
for (Method attributeMethod : InternalAnnotationUtils.getAttributeMethods(annotationType())) {
Object thisValue = getAttributeValue(attributeMethod);
Object otherValue = ReflectionUtils.invokeMethod(attributeMethod, other);
if (!ObjectUtils.nullSafeEquals(thisValue, otherValue)) {
return false;
}
}
return true;
}
/**
* See {@link Annotation#hashCode()} for a definition of the required algorithm.
*/
private int annotationHashCode() {
int result = 0;
for (Method attributeMethod : InternalAnnotationUtils.getAttributeMethods(annotationType())) {
Object value = getAttributeValue(attributeMethod);
int hashCode;
if (value.getClass().isArray()) {
hashCode = hashCodeForArray(value);
}
else {
hashCode = value.hashCode();
}
result += (127 * attributeMethod.getName().hashCode()) ^ hashCode;
}
return result;
}
/**
* WARNING: we can NOT use any of the {@code nullSafeHashCode()} methods
* in Spring's {@link ObjectUtils} because those hash code generation
* algorithms do not comply with the requirements specified in
* {@link Annotation#hashCode()}.
* @param array the array to compute the hash code for
*/
private int hashCodeForArray(Object array) {
if (array instanceof boolean[]) {
return Arrays.hashCode((boolean[]) array);
}
if (array instanceof byte[]) {
return Arrays.hashCode((byte[]) array);
}
if (array instanceof char[]) {
return Arrays.hashCode((char[]) array);
}
if (array instanceof double[]) {
return Arrays.hashCode((double[]) array);
}
if (array instanceof float[]) {
return Arrays.hashCode((float[]) array);
}
if (array instanceof int[]) {
return Arrays.hashCode((int[]) array);
}
if (array instanceof long[]) {
return Arrays.hashCode((long[]) array);
}
if (array instanceof short[]) {
return Arrays.hashCode((short[]) array);
}
// else
return Arrays.hashCode((Object[]) array);
}
/**
* See {@link Annotation#toString()} for guidelines on the recommended format.
*/
private String annotationToString() {
StringBuilder sb = new StringBuilder("@").append(annotationType().getName()).append("(");
Iterator<Method> iterator = InternalAnnotationUtils.getAttributeMethods(annotationType()).iterator();
while (iterator.hasNext()) {
Method attributeMethod = iterator.next();
sb.append(attributeMethod.getName());
sb.append('=');
sb.append(attributeValueToString(getAttributeValue(attributeMethod)));
sb.append(iterator.hasNext() ? ", " : "");
}
return sb.append(")").toString();
}
private String attributeValueToString(Object value) {
if (value instanceof Object[]) {
return "[" + StringUtils.arrayToDelimitedString((Object[]) value, ", ") + "]";
}
return String.valueOf(value);
}
}
......@@ -70,12 +70,10 @@ public class AnnotationIntrospectionFailureTests {
assertThat(AnnotatedElementUtils.getMergedAnnotationAttributes(
withExampleMetaAnnotation,
exampleMetaAnnotationClass)).isNull();
// Ideally hasAnnotation should return false, however, it currently
// detects annotations that might ultimately not load
assertThat(AnnotatedElementUtils.hasAnnotation(withExampleMetaAnnotation,
exampleAnnotationClass)).isTrue();
exampleAnnotationClass)).isFalse();
assertThat(AnnotatedElementUtils.hasAnnotation(withExampleMetaAnnotation,
exampleMetaAnnotationClass)).isTrue();
exampleMetaAnnotationClass)).isFalse();
}
@Test
......
/*
* Copyright 2002-2015 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;
/**
* Unit tests for {@link DefaultAnnotationAttributeExtractor}.
*
* @author Sam Brannen
* @since 4.2.1
*/
public class DefaultAnnotationAttributeExtractorTests extends AbstractAliasAwareAnnotationAttributeExtractorTestCase {
@Override
protected AnnotationAttributeExtractor<?> createExtractorFor(Class<?> clazz, String expected, Class<? extends Annotation> annotationType) {
return new DefaultAnnotationAttributeExtractor(clazz.getAnnotation(annotationType), clazz);
}
}
/*
* 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.Field;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.springframework.core.annotation.AnnotationUtilsTests.*;
/**
* Unit tests for {@link MapAnnotationAttributeExtractor}.
*
* @author Sam Brannen
* @since 4.2.1
*/
@SuppressWarnings("serial")
public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnnotationAttributeExtractorTestCase {
@Override
protected AnnotationAttributeExtractor<?> createExtractorFor(Class<?> clazz, String expected, Class<? extends Annotation> annotationType) {
Map<String, Object> attributes = Collections.singletonMap(expected, expected);
return new MapAnnotationAttributeExtractor(attributes, annotationType, clazz);
}
@Before
public void clearCacheBeforeTests() {
AnnotationUtils.clearCache();
}
@Test
public void enrichAndValidateAttributesWithImplicitAliasesAndMinimalAttributes() throws Exception {
Map<String, Object> attributes = new HashMap<>();
Map<String, Object> expectedAttributes = new HashMap<String, Object>() {{
put("groovyScript", "");
put("xmlFile", "");
put("value", "");
put("location1", "");
put("location2", "");
put("location3", "");
put("nonAliasedAttribute", "");
put("configClass", Object.class);
}};
assertEnrichAndValidateAttributes(attributes, expectedAttributes);
}
@Test
public void enrichAndValidateAttributesWithImplicitAliases() throws Exception {
Map<String, Object> attributes = new HashMap<String, Object>() {{
put("groovyScript", "groovy!");
}};
Map<String, Object> expectedAttributes = new HashMap<String, Object>() {{
put("groovyScript", "groovy!");
put("xmlFile", "groovy!");
put("value", "groovy!");
put("location1", "groovy!");
put("location2", "groovy!");
put("location3", "groovy!");
put("nonAliasedAttribute", "");
put("configClass", Object.class);
}};
assertEnrichAndValidateAttributes(attributes, expectedAttributes);
}
@Test
public void enrichAndValidateAttributesWithSingleElementThatOverridesAnArray() {
Map<String, Object> attributes = new HashMap<String, Object>() {{
// Intentionally storing 'value' as a single String instead of an array.
// put("value", asArray("/foo"));
put("value", "/foo");
put("name", "test");
}};
Map<String, Object> expected = new HashMap<String, Object>() {{
put("value", asArray("/foo"));
put("path", asArray("/foo"));
put("name", "test");
put("method", new RequestMethod[0]);
}};
MapAnnotationAttributeExtractor extractor = new MapAnnotationAttributeExtractor(attributes, WebMapping.class, null);
Map<String, Object> enriched = extractor.getSource();
assertEquals("attribute map size", expected.size(), enriched.size());
expected.forEach((attr, expectedValue) -> assertThat("for attribute '" + attr + "'", enriched.get(attr), is(expectedValue)));
}
@SuppressWarnings("unchecked")
private void assertEnrichAndValidateAttributes(Map<String, Object> sourceAttributes, Map<String, Object> expected) throws Exception {
Class<? extends Annotation> annotationType = ImplicitAliasesContextConfig.class;
// Since the ordering of attribute methods returned by the JVM is non-deterministic,
// we have to rig the attributeAliasesCache in AnnotationUtils so that the tests
// consistently fail in case enrichAndValidateAttributes() is buggy.
// Otherwise, these tests would intermittently pass even for an invalid implementation.
Field cacheField = InternalAnnotationUtils.class.getDeclaredField("attributeAliasesCache");
cacheField.setAccessible(true);
Map<Class<? extends Annotation>, MultiValueMap<String, String>> attributeAliasesCache =
(Map<Class<? extends Annotation>, MultiValueMap<String, String>>) cacheField.get(null);
// Declare aliases in an order that will cause enrichAndValidateAttributes() to
// fail unless it considers all aliases in the set of implicit aliases.
MultiValueMap<String, String> aliases = new LinkedMultiValueMap<>();
aliases.put("xmlFile", Arrays.asList("value", "groovyScript", "location1", "location2", "location3"));
aliases.put("groovyScript", Arrays.asList("value", "xmlFile", "location1", "location2", "location3"));
aliases.put("value", Arrays.asList("xmlFile", "groovyScript", "location1", "location2", "location3"));
aliases.put("location1", Arrays.asList("xmlFile", "groovyScript", "value", "location2", "location3"));
aliases.put("location2", Arrays.asList("xmlFile", "groovyScript", "value", "location1", "location3"));
aliases.put("location3", Arrays.asList("xmlFile", "groovyScript", "value", "location1", "location2"));
attributeAliasesCache.put(annotationType, aliases);
MapAnnotationAttributeExtractor extractor = new MapAnnotationAttributeExtractor(sourceAttributes, annotationType, null);
Map<String, Object> enriched = extractor.getSource();
assertEquals("attribute map size", expected.size(), enriched.size());
expected.forEach((attr, expectedValue) -> assertThat("for attribute '" + attr + "'", enriched.get(attr), is(expectedValue)));
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册