From bf9083d60f5265e055eb756f9a27d8facd49618e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 28 Oct 2016 23:33:26 +0200 Subject: [PATCH] TypeDescriptor supports merged annotation lookups (for composable formatting annotations) Issue: SPR-14844 --- ...tingConversionServiceFactoryBeanTests.java | 16 ++- .../core/convert/TypeDescriptor.java | 132 +++++++++++------- 2 files changed, 98 insertions(+), 50 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceFactoryBeanTests.java index 72bfdd5b45..ed739b78cc 100644 --- a/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceFactoryBeanTests.java +++ b/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceFactoryBeanTests.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.format.support; import java.lang.annotation.ElementType; @@ -27,6 +28,7 @@ import java.util.Set; import org.junit.Test; import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.annotation.AliasFor; import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.TypeDescriptor; import org.springframework.format.AnnotationFormatterFactory; @@ -132,9 +134,15 @@ public class FormattingConversionServiceFactoryBeanTests { } - @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) + @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) private @interface SpecialInt { + + @AliasFor("alias") + String value() default ""; + + @AliasFor("value") + String alias() default ""; } @@ -143,7 +151,7 @@ public class FormattingConversionServiceFactoryBeanTests { @NumberFormat(pattern = "##,00") private double pattern; - @SpecialInt + @SpecialInt("aliased") private int specialInt; public int getSpecialInt() { @@ -187,6 +195,8 @@ public class FormattingConversionServiceFactoryBeanTests { @Override public Printer getPrinter(SpecialInt annotation, Class fieldType) { + assertEquals("aliased", annotation.value()); + assertEquals("aliased", annotation.alias()); return new Printer() { @Override public String print(Integer object, Locale locale) { @@ -197,6 +207,8 @@ public class FormattingConversionServiceFactoryBeanTests { @Override public Parser getParser(SpecialInt annotation, Class fieldType) { + assertEquals("aliased", annotation.value()); + assertEquals("aliased", annotation.alias()); return new Parser() { @Override public Integer parse(String text, Locale locale) throws ParseException { diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index 53856f91aa..54e2dea432 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -18,8 +18,10 @@ package org.springframework.core.convert; import java.io.Serializable; import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Type; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -27,7 +29,7 @@ import java.util.stream.Stream; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -66,7 +68,7 @@ public class TypeDescriptor implements Serializable { private final ResolvableType resolvableType; - private final Annotation[] annotations; + private final AnnotatedElement annotatedElement; /** @@ -79,9 +81,8 @@ public class TypeDescriptor implements Serializable { Assert.notNull(methodParameter, "MethodParameter must not be null"); this.resolvableType = ResolvableType.forMethodParameter(methodParameter); this.type = this.resolvableType.resolve(methodParameter.getParameterType()); - this.annotations = (methodParameter.getParameterIndex() == -1 ? - nullSafeAnnotations(methodParameter.getMethodAnnotations()) : - nullSafeAnnotations(methodParameter.getParameterAnnotations())); + this.annotatedElement = new AnnotatedElementAdapter(methodParameter.getParameterIndex() == -1 ? + methodParameter.getMethodAnnotations() : methodParameter.getParameterAnnotations()); } /** @@ -93,7 +94,7 @@ public class TypeDescriptor implements Serializable { Assert.notNull(field, "Field must not be null"); this.resolvableType = ResolvableType.forField(field); this.type = this.resolvableType.resolve(field.getType()); - this.annotations = nullSafeAnnotations(field.getAnnotations()); + this.annotatedElement = new AnnotatedElementAdapter(field.getAnnotations()); } /** @@ -106,7 +107,7 @@ public class TypeDescriptor implements Serializable { Assert.notNull(property, "Property must not be null"); this.resolvableType = ResolvableType.forMethodParameter(property.getMethodParameter()); this.type = this.resolvableType.resolve(property.getType()); - this.annotations = nullSafeAnnotations(property.getAnnotations()); + this.annotatedElement = new AnnotatedElementAdapter(property.getAnnotations()); } /** @@ -120,14 +121,10 @@ public class TypeDescriptor implements Serializable { protected TypeDescriptor(ResolvableType resolvableType, Class type, Annotation[] annotations) { this.resolvableType = resolvableType; this.type = (type != null ? type : resolvableType.resolve(Object.class)); - this.annotations = nullSafeAnnotations(annotations); + this.annotatedElement = new AnnotatedElementAdapter(annotations); } - private Annotation[] nullSafeAnnotations(Annotation[] annotations) { - return (annotations != null ? annotations : EMPTY_ANNOTATION_ARRAY); - } - /** * Variation of {@link #getType()} that accounts for a primitive type by * returning its object wrapper type. @@ -189,8 +186,8 @@ public class TypeDescriptor implements Serializable { if (value == null) { return this; } - ResolvableType narrowed = ResolvableType.forType(value.getClass(), this.resolvableType); - return new TypeDescriptor(narrowed, null, this.annotations); + ResolvableType narrowed = ResolvableType.forType(value.getClass(), getResolvableType()); + return new TypeDescriptor(narrowed, null, getAnnotations()); } /** @@ -206,7 +203,7 @@ public class TypeDescriptor implements Serializable { return null; } Assert.isAssignable(superType, getType()); - return new TypeDescriptor(this.resolvableType.as(superType), superType, this.annotations); + return new TypeDescriptor(getResolvableType().as(superType), superType, getAnnotations()); } /** @@ -228,7 +225,7 @@ public class TypeDescriptor implements Serializable { * @return the annotations, or an empty array if none */ public Annotation[] getAnnotations() { - return this.annotations; + return this.annotatedElement.getAnnotations(); } /** @@ -239,7 +236,7 @@ public class TypeDescriptor implements Serializable { * @return true if the annotation is present */ public boolean hasAnnotation(Class annotationType) { - return (getAnnotation(annotationType) != null); + return AnnotatedElementUtils.isAnnotated(this.annotatedElement, annotationType); } /** @@ -250,22 +247,7 @@ public class TypeDescriptor implements Serializable { */ @SuppressWarnings("unchecked") public T getAnnotation(Class annotationType) { - // Search in annotations that are "present" (i.e., locally declared or inherited) - // NOTE: this unfortunately favors inherited annotations over locally declared composed annotations. - for (Annotation annotation : getAnnotations()) { - if (annotation.annotationType() == annotationType) { - return (T) annotation; - } - } - - // Search in annotation hierarchy - for (Annotation composedAnnotation : getAnnotations()) { - T ann = AnnotationUtils.findAnnotation(composedAnnotation.annotationType(), annotationType); - if (ann != null) { - return ann; - } - } - return null; + return AnnotatedElementUtils.getMergedAnnotation(this.annotatedElement, annotationType); } /** @@ -333,13 +315,13 @@ public class TypeDescriptor implements Serializable { * @throws IllegalStateException if this type is not a {@code java.util.Collection} or array type */ public TypeDescriptor getElementTypeDescriptor() { - if (this.resolvableType.isArray()) { - return new TypeDescriptor(this.resolvableType.getComponentType(), null, this.annotations); + if (getResolvableType().isArray()) { + return new TypeDescriptor(getResolvableType().getComponentType(), null, getAnnotations()); } - if (Stream.class.isAssignableFrom(this.type)) { - return getRelatedIfResolvable(this, this.resolvableType.as(Stream.class).getGeneric(0)); + if (Stream.class.isAssignableFrom(getType())) { + return getRelatedIfResolvable(this, getResolvableType().as(Stream.class).getGeneric(0)); } - return getRelatedIfResolvable(this, this.resolvableType.asCollection().getGeneric(0)); + return getRelatedIfResolvable(this, getResolvableType().asCollection().getGeneric(0)); } /** @@ -380,8 +362,8 @@ public class TypeDescriptor implements Serializable { * @throws IllegalStateException if this type is not a {@code java.util.Map} */ public TypeDescriptor getMapKeyTypeDescriptor() { - Assert.state(isMap(), "Not a java.util.Map"); - return getRelatedIfResolvable(this, this.resolvableType.asMap().getGeneric(0)); + Assert.state(isMap(), "Not a [java.util.Map]"); + return getRelatedIfResolvable(this, getResolvableType().asMap().getGeneric(0)); } /** @@ -415,8 +397,8 @@ public class TypeDescriptor implements Serializable { * @throws IllegalStateException if this type is not a {@code java.util.Map} */ public TypeDescriptor getMapValueTypeDescriptor() { - Assert.state(isMap(), "Not a java.util.Map"); - return getRelatedIfResolvable(this, this.resolvableType.asMap().getGeneric(1)); + Assert.state(isMap(), "Not a [java.util.Map]"); + return getRelatedIfResolvable(this, getResolvableType().asMap().getGeneric(1)); } /** @@ -444,7 +426,7 @@ public class TypeDescriptor implements Serializable { if (typeDescriptor != null) { return typeDescriptor.narrow(value); } - return (value != null ? new TypeDescriptor(this.resolvableType, value.getClass(), this.annotations) : null); + return (value != null ? new TypeDescriptor(getResolvableType(), value.getClass(), getAnnotations()) : null); } @Override @@ -490,7 +472,7 @@ public class TypeDescriptor implements Serializable { for (Annotation ann : getAnnotations()) { builder.append("@").append(ann.annotationType().getName()).append(' '); } - builder.append(this.resolvableType.toString()); + builder.append(getResolvableType().toString()); return builder.toString(); } @@ -525,9 +507,9 @@ public class TypeDescriptor implements Serializable { * @return the collection type descriptor */ public static TypeDescriptor collection(Class collectionType, TypeDescriptor elementTypeDescriptor) { - Assert.notNull(collectionType, "collectionType must not be null"); + Assert.notNull(collectionType, "Collection type must not be null"); if (!Collection.class.isAssignableFrom(collectionType)) { - throw new IllegalArgumentException("collectionType must be a java.util.Collection"); + throw new IllegalArgumentException("Collection type must be a [java.util.Collection]"); } ResolvableType element = (elementTypeDescriptor != null ? elementTypeDescriptor.resolvableType : null); return new TypeDescriptor(ResolvableType.forClassWithGenerics(collectionType, element), null, null); @@ -548,8 +530,9 @@ public class TypeDescriptor implements Serializable { * @return the map type descriptor */ public static TypeDescriptor map(Class mapType, TypeDescriptor keyTypeDescriptor, TypeDescriptor valueTypeDescriptor) { + Assert.notNull(mapType, "Map type must not be null"); if (!Map.class.isAssignableFrom(mapType)) { - throw new IllegalArgumentException("mapType must be a java.util.Map"); + throw new IllegalArgumentException("Map type must be a [java.util.Map]"); } ResolvableType key = (keyTypeDescriptor != null ? keyTypeDescriptor.resolvableType : null); ResolvableType value = (valueTypeDescriptor != null ? valueTypeDescriptor.resolvableType : null); @@ -687,7 +670,60 @@ public class TypeDescriptor implements Serializable { if (type.resolve() == null) { return null; } - return new TypeDescriptor(type, null, source.annotations); + return new TypeDescriptor(type, null, source.getAnnotations()); + } + + + /** + * Adapter class for exposing a {@code TypeDescriptor}'s annotations as an + * {@link AnnotatedElement}, in particular to {@link AnnotatedElementUtils}. + * @see AnnotatedElementUtils#isAnnotated(AnnotatedElement, Class) + * @see AnnotatedElementUtils#getMergedAnnotation(AnnotatedElement, Class) + */ + private class AnnotatedElementAdapter implements AnnotatedElement, Serializable { + + private final Annotation[] annotations; + + public AnnotatedElementAdapter(Annotation[] annotations) { + this.annotations = annotations; + } + + @Override + @SuppressWarnings("unchecked") + public T getAnnotation(Class annotationClass) { + for (Annotation annotation : getAnnotations()) { + if (annotation.annotationType() == annotationClass) { + return (T) annotation; + } + } + return null; + } + + @Override + public Annotation[] getAnnotations() { + return (this.annotations != null ? this.annotations : EMPTY_ANNOTATION_ARRAY); + } + + @Override + public Annotation[] getDeclaredAnnotations() { + return getAnnotations(); + } + + @Override + public boolean equals(Object other) { + return (this == other || (other instanceof AnnotatedElementAdapter && + Arrays.equals(this.annotations, ((AnnotatedElementAdapter) other).annotations))); + } + + @Override + public int hashCode() { + return Arrays.hashCode(this.annotations); + } + + @Override + public String toString() { + return TypeDescriptor.this.toString(); + } } }