From 62918ca7797a9c7822fdf960c63a29b901c2343b 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 (cherry picked from commit bf9083d) --- ...tingConversionServiceFactoryBeanTests.java | 26 +++- .../core/convert/TypeDescriptor.java | 132 +++++++++++------- 2 files changed, 103 insertions(+), 55 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 067befcbc10..ed739b78cc4 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -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; @@ -84,7 +86,7 @@ public class FormattingConversionServiceFactoryBeanTests { @Test public void testCustomFormatter() throws Exception { FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean(); - Set formatters = new HashSet(); + Set formatters = new HashSet<>(); formatters.add(new TestBeanFormatter()); formatters.add(new SpecialIntAnnotationFormatterFactory()); factory.setFormatters(formatters); @@ -105,7 +107,7 @@ public class FormattingConversionServiceFactoryBeanTests { @Test public void testFormatterRegistrar() throws Exception { FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean(); - Set registrars = new HashSet(); + Set registrars = new HashSet<>(); registrars.add(new TestFormatterRegistrar()); factory.setFormatterRegistrars(registrars); factory.afterPropertiesSet(); @@ -119,7 +121,7 @@ public class FormattingConversionServiceFactoryBeanTests { @Test public void testInvalidFormatter() throws Exception { FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean(); - Set formatters = new HashSet(); + Set formatters = new HashSet<>(); formatters.add(new Object()); factory.setFormatters(formatters); try { @@ -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() { @@ -174,7 +182,7 @@ public class FormattingConversionServiceFactoryBeanTests { private static class SpecialIntAnnotationFormatterFactory implements AnnotationFormatterFactory { - private final Set> fieldTypes = new HashSet>(1); + private final Set> fieldTypes = new HashSet<>(1); public SpecialIntAnnotationFormatterFactory() { fieldTypes.add(Integer.class); @@ -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 ffbbd7ab0d0..c0a6e1768ca 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.lang.UsesJava8; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -70,7 +72,7 @@ public class TypeDescriptor implements Serializable { private final ResolvableType resolvableType; - private final Annotation[] annotations; + private final AnnotatedElement annotatedElement; /** @@ -83,9 +85,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()); } /** @@ -97,7 +98,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()); } /** @@ -110,7 +111,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()); } /** @@ -124,14 +125,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. @@ -193,8 +190,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()); } /** @@ -210,7 +207,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()); } /** @@ -232,7 +229,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(); } /** @@ -243,7 +240,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); } /** @@ -254,22 +251,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); } /** @@ -337,13 +319,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 (streamAvailable && StreamDelegate.isStream(this.type)) { + if (streamAvailable && StreamDelegate.isStream(getType())) { return StreamDelegate.getStreamElementType(this); } - return getRelatedIfResolvable(this, this.resolvableType.asCollection().getGeneric(0)); + return getRelatedIfResolvable(this, getResolvableType().asCollection().getGeneric(0)); } /** @@ -384,8 +366,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)); } /** @@ -419,8 +401,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)); } /** @@ -448,7 +430,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 @@ -494,7 +476,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(); } @@ -529,9 +511,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); @@ -552,8 +534,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); @@ -691,7 +674,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(); + } } @@ -706,7 +742,7 @@ public class TypeDescriptor implements Serializable { } public static TypeDescriptor getStreamElementType(TypeDescriptor source) { - return getRelatedIfResolvable(source, source.resolvableType.as(Stream.class).getGeneric(0)); + return getRelatedIfResolvable(source, source.getResolvableType().as(Stream.class).getGeneric(0)); } }