org.springframework.core.annotation.TypeMappedAnnotation Maven / Gradle / Ivy
/*
* 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
*
* https://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.Member;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
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}.
*
* 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 {@linkplain 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)}.
*
*
Extracted root attribute values must be compatible with the attribute
* return type, namely:
*
*
* Return Type Extracted Type
* Class Class or String
* Class[] Class[] or String[]
* Annotation Annotation, Map, or Object compatible with the value
* extractor
* Annotation[] Annotation[], Map[], or Object[] where elements are
* compatible with the value extractor
* Other types An exact match or the appropriate primitive wrapper
*
*
* @author Phillip Webb
* @author Juergen Hoeller
* @since 5.2
* @param the annotation type
* @see TypeMappedAnnotations
*/
final class TypeMappedAnnotation extends AbstractMergedAnnotation {
private static final Map, Object> EMPTY_ARRAYS;
static {
Map, Object> emptyArrays = new HashMap<>();
emptyArrays.put(String.class, new String[] {});
emptyArrays.put(boolean.class, new boolean[] {});
emptyArrays.put(byte.class, new byte[] {});
emptyArrays.put(char.class, new char[] {});
emptyArrays.put(double.class, new double[] {});
emptyArrays.put(float.class, new float[] {});
emptyArrays.put(int.class, new int[] {});
emptyArrays.put(long.class, new long[] {});
emptyArrays.put(short.class, new short[] {});
EMPTY_ARRAYS = Collections.unmodifiableMap(emptyArrays);
}
private final AnnotationTypeMapping mapping;
@Nullable
private final ClassLoader classLoader;
@Nullable
private final Object source;
@Nullable
private final Object rootAttributes;
private final BiFunction valueExtractor;
private final int aggregateIndex;
private final boolean useMergedValues;
@Nullable
private final Predicate attributeFilter;
private final int[] resolvedRootMirrors;
private final int[] resolvedMirrors;
@Nullable
private String string;
private TypeMappedAnnotation(AnnotationTypeMapping mapping, @Nullable ClassLoader classLoader,
@Nullable Object source, @Nullable Object rootAttributes,
BiFunction valueExtractor, int aggregateIndex) {
this(mapping, classLoader, source, rootAttributes, valueExtractor, aggregateIndex, null);
}
private TypeMappedAnnotation(AnnotationTypeMapping mapping, @Nullable ClassLoader classLoader,
@Nullable Object source, @Nullable Object rootAttributes,
BiFunction valueExtractor, int aggregateIndex,
@Nullable int[] resolvedRootMirrors) {
this.mapping = mapping;
this.classLoader = classLoader;
this.source = source;
this.rootAttributes = rootAttributes;
this.valueExtractor = valueExtractor;
this.aggregateIndex = aggregateIndex;
this.useMergedValues = true;
this.attributeFilter = null;
this.resolvedRootMirrors = (resolvedRootMirrors != null ? resolvedRootMirrors :
mapping.getRoot().getMirrorSets().resolve(source, rootAttributes, this.valueExtractor));
this.resolvedMirrors = (getDistance() == 0 ? this.resolvedRootMirrors :
mapping.getMirrorSets().resolve(source, this, this::getValueForMirrorResolution));
}
private TypeMappedAnnotation(AnnotationTypeMapping mapping, @Nullable ClassLoader classLoader,
@Nullable Object source, @Nullable Object rootAnnotation,
BiFunction valueExtractor, int aggregateIndex,
boolean useMergedValues, @Nullable Predicate attributeFilter,
int[] resolvedRootMirrors, int[] resolvedMirrors) {
this.classLoader = classLoader;
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;
}
@Override
@SuppressWarnings("unchecked")
public Class getType() {
return (Class) this.mapping.getAnnotationType();
}
@Override
public List> getMetaTypes() {
return this.mapping.getMetaTypes();
}
@Override
public boolean isPresent() {
return true;
}
@Override
public int getDistance() {
return this.mapping.getDistance();
}
@Override
public int getAggregateIndex() {
return this.aggregateIndex;
}
@Override
@Nullable
public Object getSource() {
return this.source;
}
@Override
@Nullable
public MergedAnnotation getMetaSource() {
AnnotationTypeMapping metaSourceMapping = this.mapping.getSource();
if (metaSourceMapping == null) {
return null;
}
return new TypeMappedAnnotation<>(metaSourceMapping, this.classLoader, this.source,
this.rootAttributes, this.valueExtractor, this.aggregateIndex, this.resolvedRootMirrors);
}
@Override
public MergedAnnotation getRoot() {
if (getDistance() == 0) {
return this;
}
AnnotationTypeMapping rootMapping = this.mapping.getRoot();
return new TypeMappedAnnotation<>(rootMapping, this.classLoader, 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, false);
return (value == null || this.mapping.isEquivalentToDefaultValue(attributeIndex, value, this.valueExtractor));
}
@Override
@SuppressWarnings("unchecked")
public MergedAnnotation getAnnotation(String attributeName, Class type)
throws NoSuchElementException {
int attributeIndex = getAttributeIndex(attributeName, true);
Method attribute = this.mapping.getAttributes().get(attributeIndex);
Assert.notNull(type, "Type must not be null");
Assert.isAssignable(type, attribute.getReturnType(),
() -> "Attribute " + attributeName + " type mismatch:");
return (MergedAnnotation) getRequiredValue(attributeIndex, attributeName);
}
@Override
@SuppressWarnings("unchecked")
public MergedAnnotation[] getAnnotationArray(
String attributeName, Class type) throws NoSuchElementException {
int attributeIndex = getAttributeIndex(attributeName, true);
Method attribute = this.mapping.getAttributes().get(attributeIndex);
Class componentType = attribute.getReturnType().getComponentType();
Assert.notNull(type, "Type must not be null");
Assert.notNull(componentType, () -> "Attribute " + attributeName + " is not an array");
Assert.isAssignable(type, componentType, () -> "Attribute " + attributeName + " component type mismatch:");
return (MergedAnnotation[]) getRequiredValue(attributeIndex, attributeName);
}
@Override
public Optional getDefaultValue(String attributeName, Class 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 filterAttributes(Predicate predicate) {
if (this.attributeFilter != null) {
predicate = this.attributeFilter.and(predicate);
}
return new TypeMappedAnnotation<>(this.mapping, this.classLoader, this.source, this.rootAttributes,
this.valueExtractor, this.aggregateIndex, this.useMergedValues, predicate,
this.resolvedRootMirrors, this.resolvedMirrors);
}
@Override
public MergedAnnotation withNonMergedAttributes() {
return new TypeMappedAnnotation<>(this.mapping, this.classLoader, this.source, this.rootAttributes,
this.valueExtractor, this.aggregateIndex, false, this.attributeFilter,
this.resolvedRootMirrors, this.resolvedMirrors);
}
@Override
public Map asMap(Adapt... adaptations) {
return Collections.unmodifiableMap(asMap(mergedAnnotation -> new LinkedHashMap<>(), adaptations));
}
@Override
public > T asMap(Function, T> factory, Adapt... adaptations) {
T map = factory.apply(this);
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, adaptations)));
if (value != null) {
map.put(attribute.getName(),
adaptValueForMapOptions(attribute, value, map.getClass(), factory, adaptations));
}
}
return map;
}
private Class getTypeForMapOptions(Method attribute, Adapt[] adaptations) {
Class attributeType = attribute.getReturnType();
Class componentType = (attributeType.isArray() ? attributeType.getComponentType() : attributeType);
if (Adapt.CLASS_TO_STRING.isIn(adaptations) && componentType == Class.class) {
return (attributeType.isArray() ? String[].class : String.class);
}
return Object.class;
}
private > Object adaptValueForMapOptions(Method attribute, Object value,
Class mapType, Function, T> factory, Adapt[] adaptations) {
if (value instanceof MergedAnnotation) {
MergedAnnotation annotation = (MergedAnnotation) value;
return (Adapt.ANNOTATION_TO_MAP.isIn(adaptations) ?
annotation.asMap(factory, adaptations) : annotation.synthesize());
}
if (value instanceof MergedAnnotation[]) {
MergedAnnotation[] annotations = (MergedAnnotation[]) value;
if (Adapt.ANNOTATION_TO_MAP.isIn(adaptations)) {
Object result = Array.newInstance(mapType, annotations.length);
for (int i = 0; i < annotations.length; i++) {
Array.set(result, i, annotations[i].asMap(factory, adaptations));
}
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, getType());
}
@Override
public String toString() {
String string = this.string;
if (string == null) {
StringBuilder builder = new StringBuilder();
builder.append("@");
builder.append(getType().getName());
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);
}
@Override
@Nullable
protected T getAttributeValue(String attributeName, Class type) {
int attributeIndex = getAttributeIndex(attributeName, false);
return (attributeIndex != -1 ? getValue(attributeIndex, type) : null);
}
private Object getRequiredValue(int attributeIndex, String attributeName) {
Object value = getValue(attributeIndex, Object.class);
if (value == null) {
throw new NoSuchElementException("No element at attribute index "
+ attributeIndex + " for name " + attributeName);
}
return value;
}
@Nullable
private T getValue(int attributeIndex, Class type) {
Method attribute = this.mapping.getAttributes().get(attributeIndex);
Object value = getValue(attributeIndex, true, false);
if (value == null) {
value = attribute.getDefaultValue();
}
return adapt(attribute, value, type);
}
@Nullable
private Object getValue(int attributeIndex, boolean useConventionMapping, boolean forMirrorResolution) {
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 (!forMirrorResolution) {
attributeIndex = (mapping.getDistance() != 0 ?
this.resolvedMirrors :
this.resolvedRootMirrors)[attributeIndex];
}
if (attributeIndex == -1) {
return null;
}
if (mapping.getDistance() == 0) {
Method attribute = mapping.getAttributes().get(attributeIndex);
Object result = this.valueExtractor.apply(attribute, this.rootAttributes);
return (result != null) ? result : attribute.getDefaultValue();
}
return getValueFromMetaAnnotation(attributeIndex, forMirrorResolution);
}
@Nullable
private Object getValueFromMetaAnnotation(int attributeIndex,
boolean forMirrorResolution) {
Object value = null;
if (this.useMergedValues || forMirrorResolution) {
value = this.mapping.getMappedAnnotationValue(attributeIndex, forMirrorResolution);
}
if (value == null) {
Method attribute = this.mapping.getAttributes().get(attributeIndex);
value = ReflectionUtils.invokeMethod(attribute, this.mapping.getAnnotation());
}
return value;
}
@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, true);
}
@SuppressWarnings("unchecked")
@Nullable
private T adapt(Method attribute, @Nullable Object value, Class type) {
if (value == null) {
return null;
}
value = adaptForAttribute(attribute, value);
type = getAdaptType(attribute, type);
if (value instanceof Class && type == String.class) {
value = ((Class) value).getName();
}
else if (value instanceof String && type == Class.class) {
value = ClassUtils.resolveClassName((String) value, getClassLoader());
}
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 String[] && type == Class[].class) {
String[] names = (String[]) value;
Class[] classes = new Class[names.length];
for (int i = 0; i < names.length; i++) {
classes[i] = ClassUtils.resolveClassName(names[i], getClassLoader());
}
value = classes;
}
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) 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) attributeType.getComponentType());
}
return result;
}
if ((attributeType == Class.class && value instanceof String) ||
(attributeType == Class[].class && value instanceof String[]) ||
(attributeType == String.class && value instanceof Class) ||
(attributeType == String[].class && value instanceof Class[])) {
return value;
}
if (attributeType.isArray() && isEmptyObjectArray(value)) {
return emptyArray(attributeType.getComponentType());
}
if (!attributeType.isInstance(value)) {
throw new IllegalStateException("Attribute '" + attribute.getName() +
"' in annotation " + getType().getName() + " should be compatible with " +
attributeType.getName() + " but a " + value.getClass().getName() +
" value was returned");
}
return value;
}
private boolean isEmptyObjectArray(Object value) {
return (value instanceof Object[] && ((Object[]) value).length == 0);
}
private Object emptyArray(Class componentType) {
Object result = EMPTY_ARRAYS.get(componentType);
if (result == null) {
result = Array.newInstance(componentType, 0);
}
return result;
}
private MergedAnnotation adaptToMergedAnnotation(Object value, Class annotationType) {
if (value instanceof MergedAnnotation) {
return (MergedAnnotation) value;
}
AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(annotationType).get(0);
return new TypeMappedAnnotation<>(
mapping, null, this.source, value, getValueExtractor(value), this.aggregateIndex);
}
private BiFunction getValueExtractor(Object value) {
if (value instanceof Annotation) {
return ReflectionUtils::invokeMethod;
}
if (value instanceof Map) {
return TypeMappedAnnotation::extractFromMap;
}
return this.valueExtractor;
}
@SuppressWarnings("unchecked")
private Class getAdaptType(Method attribute, Class type) {
if (type != Object.class) {
return type;
}
Class attributeType = attribute.getReturnType();
if (attributeType.isAnnotation()) {
return (Class) MergedAnnotation.class;
}
if (attributeType.isArray() && attributeType.getComponentType().isAnnotation()) {
return (Class) MergedAnnotation[].class;
}
return (Class) ClassUtils.resolvePrimitiveIfNecessary(attributeType);
}
private int getAttributeIndex(String attributeName, boolean required) {
Assert.hasText(attributeName, "Attribute name 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().getName());
}
return attributeIndex;
}
private boolean isFiltered(String attributeName) {
if (this.attributeFilter != null) {
return !this.attributeFilter.test(attributeName);
}
return false;
}
@Nullable
private ClassLoader getClassLoader() {
if (this.classLoader != null) {
return this.classLoader;
}
if (this.source != null) {
if (this.source instanceof Class) {
return ((Class) source).getClassLoader();
}
if (this.source instanceof Member) {
((Member) this.source).getDeclaringClass().getClassLoader();
}
}
return null;
}
static MergedAnnotation 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), null, source, annotation, ReflectionUtils::invokeMethod, 0);
}
static MergedAnnotation of(
@Nullable ClassLoader classLoader, @Nullable Object source,
Class annotationType, @Nullable Map attributes) {
Assert.notNull(annotationType, "Annotation type must not be null");
AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(annotationType);
return new TypeMappedAnnotation<>(
mappings.get(0), classLoader, source, attributes, TypeMappedAnnotation::extractFromMap, 0);
}
@Nullable
static TypeMappedAnnotation createIfPossible(
AnnotationTypeMapping mapping, MergedAnnotation annotation, IntrospectionFailureLogger logger) {
if (annotation instanceof TypeMappedAnnotation) {
TypeMappedAnnotation typeMappedAnnotation = (TypeMappedAnnotation) annotation;
return createIfPossible(mapping, typeMappedAnnotation.source,
typeMappedAnnotation.rootAttributes,
typeMappedAnnotation.valueExtractor,
typeMappedAnnotation.aggregateIndex, logger);
}
return createIfPossible(mapping, annotation.getSource(), annotation.synthesize(),
annotation.getAggregateIndex(), logger);
}
@Nullable
static TypeMappedAnnotation createIfPossible(
AnnotationTypeMapping mapping, @Nullable Object source, Annotation annotation,
int aggregateIndex, IntrospectionFailureLogger logger) {
return createIfPossible(mapping, source, annotation,
ReflectionUtils::invokeMethod, aggregateIndex, logger);
}
@Nullable
private static TypeMappedAnnotation createIfPossible(
AnnotationTypeMapping mapping, @Nullable Object source, @Nullable Object rootAttribute,
BiFunction valueExtractor,
int aggregateIndex, IntrospectionFailureLogger logger) {
try {
return new TypeMappedAnnotation<>(mapping, null, source, rootAttribute,
valueExtractor, aggregateIndex);
}
catch (Exception ex) {
if (ex instanceof AnnotationConfigurationException) {
throw (AnnotationConfigurationException) ex;
}
if (logger.isEnabled()) {
String type = mapping.getAnnotationType().getName();
String item = (mapping.getDistance() == 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) map).get(attribute.getName()) : null);
}
}