Skip to content

Commit bf9083d

Browse files
committed
TypeDescriptor supports merged annotation lookups (for composable formatting annotations)
Issue: SPR-14844
1 parent 08972ef commit bf9083d

File tree

2 files changed

+98
-50
lines changed

2 files changed

+98
-50
lines changed

spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceFactoryBeanTests.java

+14-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
1617
package org.springframework.format.support;
1718

1819
import java.lang.annotation.ElementType;
@@ -27,6 +28,7 @@
2728
import org.junit.Test;
2829

2930
import org.springframework.context.i18n.LocaleContextHolder;
31+
import org.springframework.core.annotation.AliasFor;
3032
import org.springframework.core.convert.ConversionFailedException;
3133
import org.springframework.core.convert.TypeDescriptor;
3234
import org.springframework.format.AnnotationFormatterFactory;
@@ -132,9 +134,15 @@ public void testInvalidFormatter() throws Exception {
132134
}
133135

134136

135-
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER })
137+
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
136138
@Retention(RetentionPolicy.RUNTIME)
137139
private @interface SpecialInt {
140+
141+
@AliasFor("alias")
142+
String value() default "";
143+
144+
@AliasFor("value")
145+
String alias() default "";
138146
}
139147

140148

@@ -143,7 +151,7 @@ private static class TestBean {
143151
@NumberFormat(pattern = "##,00")
144152
private double pattern;
145153

146-
@SpecialInt
154+
@SpecialInt("aliased")
147155
private int specialInt;
148156

149157
public int getSpecialInt() {
@@ -187,6 +195,8 @@ public Set<Class<?>> getFieldTypes() {
187195

188196
@Override
189197
public Printer<?> getPrinter(SpecialInt annotation, Class<?> fieldType) {
198+
assertEquals("aliased", annotation.value());
199+
assertEquals("aliased", annotation.alias());
190200
return new Printer<Integer>() {
191201
@Override
192202
public String print(Integer object, Locale locale) {
@@ -197,6 +207,8 @@ public String print(Integer object, Locale locale) {
197207

198208
@Override
199209
public Parser<?> getParser(SpecialInt annotation, Class<?> fieldType) {
210+
assertEquals("aliased", annotation.value());
211+
assertEquals("aliased", annotation.alias());
200212
return new Parser<Integer>() {
201213
@Override
202214
public Integer parse(String text, Locale locale) throws ParseException {

spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java

+84-48
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,18 @@
1818

1919
import java.io.Serializable;
2020
import java.lang.annotation.Annotation;
21+
import java.lang.reflect.AnnotatedElement;
2122
import java.lang.reflect.Field;
2223
import java.lang.reflect.Type;
24+
import java.util.Arrays;
2325
import java.util.Collection;
2426
import java.util.HashMap;
2527
import java.util.Map;
2628
import java.util.stream.Stream;
2729

2830
import org.springframework.core.MethodParameter;
2931
import org.springframework.core.ResolvableType;
30-
import org.springframework.core.annotation.AnnotationUtils;
32+
import org.springframework.core.annotation.AnnotatedElementUtils;
3133
import org.springframework.util.Assert;
3234
import org.springframework.util.ClassUtils;
3335
import org.springframework.util.ObjectUtils;
@@ -66,7 +68,7 @@ public class TypeDescriptor implements Serializable {
6668

6769
private final ResolvableType resolvableType;
6870

69-
private final Annotation[] annotations;
71+
private final AnnotatedElement annotatedElement;
7072

7173

7274
/**
@@ -79,9 +81,8 @@ public TypeDescriptor(MethodParameter methodParameter) {
7981
Assert.notNull(methodParameter, "MethodParameter must not be null");
8082
this.resolvableType = ResolvableType.forMethodParameter(methodParameter);
8183
this.type = this.resolvableType.resolve(methodParameter.getParameterType());
82-
this.annotations = (methodParameter.getParameterIndex() == -1 ?
83-
nullSafeAnnotations(methodParameter.getMethodAnnotations()) :
84-
nullSafeAnnotations(methodParameter.getParameterAnnotations()));
84+
this.annotatedElement = new AnnotatedElementAdapter(methodParameter.getParameterIndex() == -1 ?
85+
methodParameter.getMethodAnnotations() : methodParameter.getParameterAnnotations());
8586
}
8687

8788
/**
@@ -93,7 +94,7 @@ public TypeDescriptor(Field field) {
9394
Assert.notNull(field, "Field must not be null");
9495
this.resolvableType = ResolvableType.forField(field);
9596
this.type = this.resolvableType.resolve(field.getType());
96-
this.annotations = nullSafeAnnotations(field.getAnnotations());
97+
this.annotatedElement = new AnnotatedElementAdapter(field.getAnnotations());
9798
}
9899

99100
/**
@@ -106,7 +107,7 @@ public TypeDescriptor(Property property) {
106107
Assert.notNull(property, "Property must not be null");
107108
this.resolvableType = ResolvableType.forMethodParameter(property.getMethodParameter());
108109
this.type = this.resolvableType.resolve(property.getType());
109-
this.annotations = nullSafeAnnotations(property.getAnnotations());
110+
this.annotatedElement = new AnnotatedElementAdapter(property.getAnnotations());
110111
}
111112

112113
/**
@@ -120,14 +121,10 @@ public TypeDescriptor(Property property) {
120121
protected TypeDescriptor(ResolvableType resolvableType, Class<?> type, Annotation[] annotations) {
121122
this.resolvableType = resolvableType;
122123
this.type = (type != null ? type : resolvableType.resolve(Object.class));
123-
this.annotations = nullSafeAnnotations(annotations);
124+
this.annotatedElement = new AnnotatedElementAdapter(annotations);
124125
}
125126

126127

127-
private Annotation[] nullSafeAnnotations(Annotation[] annotations) {
128-
return (annotations != null ? annotations : EMPTY_ANNOTATION_ARRAY);
129-
}
130-
131128
/**
132129
* Variation of {@link #getType()} that accounts for a primitive type by
133130
* returning its object wrapper type.
@@ -189,8 +186,8 @@ public TypeDescriptor narrow(Object value) {
189186
if (value == null) {
190187
return this;
191188
}
192-
ResolvableType narrowed = ResolvableType.forType(value.getClass(), this.resolvableType);
193-
return new TypeDescriptor(narrowed, null, this.annotations);
189+
ResolvableType narrowed = ResolvableType.forType(value.getClass(), getResolvableType());
190+
return new TypeDescriptor(narrowed, null, getAnnotations());
194191
}
195192

196193
/**
@@ -206,7 +203,7 @@ public TypeDescriptor upcast(Class<?> superType) {
206203
return null;
207204
}
208205
Assert.isAssignable(superType, getType());
209-
return new TypeDescriptor(this.resolvableType.as(superType), superType, this.annotations);
206+
return new TypeDescriptor(getResolvableType().as(superType), superType, getAnnotations());
210207
}
211208

212209
/**
@@ -228,7 +225,7 @@ public boolean isPrimitive() {
228225
* @return the annotations, or an empty array if none
229226
*/
230227
public Annotation[] getAnnotations() {
231-
return this.annotations;
228+
return this.annotatedElement.getAnnotations();
232229
}
233230

234231
/**
@@ -239,7 +236,7 @@ public Annotation[] getAnnotations() {
239236
* @return <tt>true</tt> if the annotation is present
240237
*/
241238
public boolean hasAnnotation(Class<? extends Annotation> annotationType) {
242-
return (getAnnotation(annotationType) != null);
239+
return AnnotatedElementUtils.isAnnotated(this.annotatedElement, annotationType);
243240
}
244241

245242
/**
@@ -250,22 +247,7 @@ public boolean hasAnnotation(Class<? extends Annotation> annotationType) {
250247
*/
251248
@SuppressWarnings("unchecked")
252249
public <T extends Annotation> T getAnnotation(Class<T> annotationType) {
253-
// Search in annotations that are "present" (i.e., locally declared or inherited)
254-
// NOTE: this unfortunately favors inherited annotations over locally declared composed annotations.
255-
for (Annotation annotation : getAnnotations()) {
256-
if (annotation.annotationType() == annotationType) {
257-
return (T) annotation;
258-
}
259-
}
260-
261-
// Search in annotation hierarchy
262-
for (Annotation composedAnnotation : getAnnotations()) {
263-
T ann = AnnotationUtils.findAnnotation(composedAnnotation.annotationType(), annotationType);
264-
if (ann != null) {
265-
return ann;
266-
}
267-
}
268-
return null;
250+
return AnnotatedElementUtils.getMergedAnnotation(this.annotatedElement, annotationType);
269251
}
270252

271253
/**
@@ -333,13 +315,13 @@ public boolean isArray() {
333315
* @throws IllegalStateException if this type is not a {@code java.util.Collection} or array type
334316
*/
335317
public TypeDescriptor getElementTypeDescriptor() {
336-
if (this.resolvableType.isArray()) {
337-
return new TypeDescriptor(this.resolvableType.getComponentType(), null, this.annotations);
318+
if (getResolvableType().isArray()) {
319+
return new TypeDescriptor(getResolvableType().getComponentType(), null, getAnnotations());
338320
}
339-
if (Stream.class.isAssignableFrom(this.type)) {
340-
return getRelatedIfResolvable(this, this.resolvableType.as(Stream.class).getGeneric(0));
321+
if (Stream.class.isAssignableFrom(getType())) {
322+
return getRelatedIfResolvable(this, getResolvableType().as(Stream.class).getGeneric(0));
341323
}
342-
return getRelatedIfResolvable(this, this.resolvableType.asCollection().getGeneric(0));
324+
return getRelatedIfResolvable(this, getResolvableType().asCollection().getGeneric(0));
343325
}
344326

345327
/**
@@ -380,8 +362,8 @@ public boolean isMap() {
380362
* @throws IllegalStateException if this type is not a {@code java.util.Map}
381363
*/
382364
public TypeDescriptor getMapKeyTypeDescriptor() {
383-
Assert.state(isMap(), "Not a java.util.Map");
384-
return getRelatedIfResolvable(this, this.resolvableType.asMap().getGeneric(0));
365+
Assert.state(isMap(), "Not a [java.util.Map]");
366+
return getRelatedIfResolvable(this, getResolvableType().asMap().getGeneric(0));
385367
}
386368

387369
/**
@@ -415,8 +397,8 @@ public TypeDescriptor getMapKeyTypeDescriptor(Object mapKey) {
415397
* @throws IllegalStateException if this type is not a {@code java.util.Map}
416398
*/
417399
public TypeDescriptor getMapValueTypeDescriptor() {
418-
Assert.state(isMap(), "Not a java.util.Map");
419-
return getRelatedIfResolvable(this, this.resolvableType.asMap().getGeneric(1));
400+
Assert.state(isMap(), "Not a [java.util.Map]");
401+
return getRelatedIfResolvable(this, getResolvableType().asMap().getGeneric(1));
420402
}
421403

422404
/**
@@ -444,7 +426,7 @@ private TypeDescriptor narrow(Object value, TypeDescriptor typeDescriptor) {
444426
if (typeDescriptor != null) {
445427
return typeDescriptor.narrow(value);
446428
}
447-
return (value != null ? new TypeDescriptor(this.resolvableType, value.getClass(), this.annotations) : null);
429+
return (value != null ? new TypeDescriptor(getResolvableType(), value.getClass(), getAnnotations()) : null);
448430
}
449431

450432
@Override
@@ -490,7 +472,7 @@ public String toString() {
490472
for (Annotation ann : getAnnotations()) {
491473
builder.append("@").append(ann.annotationType().getName()).append(' ');
492474
}
493-
builder.append(this.resolvableType.toString());
475+
builder.append(getResolvableType().toString());
494476
return builder.toString();
495477
}
496478

@@ -525,9 +507,9 @@ public static TypeDescriptor valueOf(Class<?> type) {
525507
* @return the collection type descriptor
526508
*/
527509
public static TypeDescriptor collection(Class<?> collectionType, TypeDescriptor elementTypeDescriptor) {
528-
Assert.notNull(collectionType, "collectionType must not be null");
510+
Assert.notNull(collectionType, "Collection type must not be null");
529511
if (!Collection.class.isAssignableFrom(collectionType)) {
530-
throw new IllegalArgumentException("collectionType must be a java.util.Collection");
512+
throw new IllegalArgumentException("Collection type must be a [java.util.Collection]");
531513
}
532514
ResolvableType element = (elementTypeDescriptor != null ? elementTypeDescriptor.resolvableType : null);
533515
return new TypeDescriptor(ResolvableType.forClassWithGenerics(collectionType, element), null, null);
@@ -548,8 +530,9 @@ public static TypeDescriptor collection(Class<?> collectionType, TypeDescriptor
548530
* @return the map type descriptor
549531
*/
550532
public static TypeDescriptor map(Class<?> mapType, TypeDescriptor keyTypeDescriptor, TypeDescriptor valueTypeDescriptor) {
533+
Assert.notNull(mapType, "Map type must not be null");
551534
if (!Map.class.isAssignableFrom(mapType)) {
552-
throw new IllegalArgumentException("mapType must be a java.util.Map");
535+
throw new IllegalArgumentException("Map type must be a [java.util.Map]");
553536
}
554537
ResolvableType key = (keyTypeDescriptor != null ? keyTypeDescriptor.resolvableType : null);
555538
ResolvableType value = (valueTypeDescriptor != null ? valueTypeDescriptor.resolvableType : null);
@@ -687,7 +670,60 @@ private static TypeDescriptor getRelatedIfResolvable(TypeDescriptor source, Reso
687670
if (type.resolve() == null) {
688671
return null;
689672
}
690-
return new TypeDescriptor(type, null, source.annotations);
673+
return new TypeDescriptor(type, null, source.getAnnotations());
674+
}
675+
676+
677+
/**
678+
* Adapter class for exposing a {@code TypeDescriptor}'s annotations as an
679+
* {@link AnnotatedElement}, in particular to {@link AnnotatedElementUtils}.
680+
* @see AnnotatedElementUtils#isAnnotated(AnnotatedElement, Class)
681+
* @see AnnotatedElementUtils#getMergedAnnotation(AnnotatedElement, Class)
682+
*/
683+
private class AnnotatedElementAdapter implements AnnotatedElement, Serializable {
684+
685+
private final Annotation[] annotations;
686+
687+
public AnnotatedElementAdapter(Annotation[] annotations) {
688+
this.annotations = annotations;
689+
}
690+
691+
@Override
692+
@SuppressWarnings("unchecked")
693+
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
694+
for (Annotation annotation : getAnnotations()) {
695+
if (annotation.annotationType() == annotationClass) {
696+
return (T) annotation;
697+
}
698+
}
699+
return null;
700+
}
701+
702+
@Override
703+
public Annotation[] getAnnotations() {
704+
return (this.annotations != null ? this.annotations : EMPTY_ANNOTATION_ARRAY);
705+
}
706+
707+
@Override
708+
public Annotation[] getDeclaredAnnotations() {
709+
return getAnnotations();
710+
}
711+
712+
@Override
713+
public boolean equals(Object other) {
714+
return (this == other || (other instanceof AnnotatedElementAdapter &&
715+
Arrays.equals(this.annotations, ((AnnotatedElementAdapter) other).annotations)));
716+
}
717+
718+
@Override
719+
public int hashCode() {
720+
return Arrays.hashCode(this.annotations);
721+
}
722+
723+
@Override
724+
public String toString() {
725+
return TypeDescriptor.this.toString();
726+
}
691727
}
692728

693729
}

0 commit comments

Comments
 (0)