Skip to content

Commit 62918ca

Browse files
committed
TypeDescriptor supports merged annotation lookups (for composable formatting annotations)
Issue: SPR-14844 (cherry picked from commit bf9083d)
1 parent 3cca57a commit 62918ca

File tree

2 files changed

+103
-55
lines changed

2 files changed

+103
-55
lines changed

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

+19-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -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;
@@ -84,7 +86,7 @@ public void testDefaultFormattersOff() throws Exception {
8486
@Test
8587
public void testCustomFormatter() throws Exception {
8688
FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean();
87-
Set<Object> formatters = new HashSet<Object>();
89+
Set<Object> formatters = new HashSet<>();
8890
formatters.add(new TestBeanFormatter());
8991
formatters.add(new SpecialIntAnnotationFormatterFactory());
9092
factory.setFormatters(formatters);
@@ -105,7 +107,7 @@ public void testCustomFormatter() throws Exception {
105107
@Test
106108
public void testFormatterRegistrar() throws Exception {
107109
FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean();
108-
Set<FormatterRegistrar> registrars = new HashSet<FormatterRegistrar>();
110+
Set<FormatterRegistrar> registrars = new HashSet<>();
109111
registrars.add(new TestFormatterRegistrar());
110112
factory.setFormatterRegistrars(registrars);
111113
factory.afterPropertiesSet();
@@ -119,7 +121,7 @@ public void testFormatterRegistrar() throws Exception {
119121
@Test
120122
public void testInvalidFormatter() throws Exception {
121123
FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean();
122-
Set<Object> formatters = new HashSet<Object>();
124+
Set<Object> formatters = new HashSet<>();
123125
formatters.add(new Object());
124126
factory.setFormatters(formatters);
125127
try {
@@ -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() {
@@ -174,7 +182,7 @@ public TestBean parse(String text, Locale locale) throws ParseException {
174182

175183
private static class SpecialIntAnnotationFormatterFactory implements AnnotationFormatterFactory<SpecialInt> {
176184

177-
private final Set<Class<?>> fieldTypes = new HashSet<Class<?>>(1);
185+
private final Set<Class<?>> fieldTypes = new HashSet<>(1);
178186

179187
public SpecialIntAnnotationFormatterFactory() {
180188
fieldTypes.add(Integer.class);
@@ -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.lang.UsesJava8;
3234
import org.springframework.util.Assert;
3335
import org.springframework.util.ClassUtils;
@@ -70,7 +72,7 @@ public class TypeDescriptor implements Serializable {
7072

7173
private final ResolvableType resolvableType;
7274

73-
private final Annotation[] annotations;
75+
private final AnnotatedElement annotatedElement;
7476

7577

7678
/**
@@ -83,9 +85,8 @@ public TypeDescriptor(MethodParameter methodParameter) {
8385
Assert.notNull(methodParameter, "MethodParameter must not be null");
8486
this.resolvableType = ResolvableType.forMethodParameter(methodParameter);
8587
this.type = this.resolvableType.resolve(methodParameter.getParameterType());
86-
this.annotations = (methodParameter.getParameterIndex() == -1 ?
87-
nullSafeAnnotations(methodParameter.getMethodAnnotations()) :
88-
nullSafeAnnotations(methodParameter.getParameterAnnotations()));
88+
this.annotatedElement = new AnnotatedElementAdapter(methodParameter.getParameterIndex() == -1 ?
89+
methodParameter.getMethodAnnotations() : methodParameter.getParameterAnnotations());
8990
}
9091

9192
/**
@@ -97,7 +98,7 @@ public TypeDescriptor(Field field) {
9798
Assert.notNull(field, "Field must not be null");
9899
this.resolvableType = ResolvableType.forField(field);
99100
this.type = this.resolvableType.resolve(field.getType());
100-
this.annotations = nullSafeAnnotations(field.getAnnotations());
101+
this.annotatedElement = new AnnotatedElementAdapter(field.getAnnotations());
101102
}
102103

103104
/**
@@ -110,7 +111,7 @@ public TypeDescriptor(Property property) {
110111
Assert.notNull(property, "Property must not be null");
111112
this.resolvableType = ResolvableType.forMethodParameter(property.getMethodParameter());
112113
this.type = this.resolvableType.resolve(property.getType());
113-
this.annotations = nullSafeAnnotations(property.getAnnotations());
114+
this.annotatedElement = new AnnotatedElementAdapter(property.getAnnotations());
114115
}
115116

116117
/**
@@ -124,14 +125,10 @@ public TypeDescriptor(Property property) {
124125
protected TypeDescriptor(ResolvableType resolvableType, Class<?> type, Annotation[] annotations) {
125126
this.resolvableType = resolvableType;
126127
this.type = (type != null ? type : resolvableType.resolve(Object.class));
127-
this.annotations = nullSafeAnnotations(annotations);
128+
this.annotatedElement = new AnnotatedElementAdapter(annotations);
128129
}
129130

130131

131-
private Annotation[] nullSafeAnnotations(Annotation[] annotations) {
132-
return (annotations != null ? annotations : EMPTY_ANNOTATION_ARRAY);
133-
}
134-
135132
/**
136133
* Variation of {@link #getType()} that accounts for a primitive type by
137134
* returning its object wrapper type.
@@ -193,8 +190,8 @@ public TypeDescriptor narrow(Object value) {
193190
if (value == null) {
194191
return this;
195192
}
196-
ResolvableType narrowed = ResolvableType.forType(value.getClass(), this.resolvableType);
197-
return new TypeDescriptor(narrowed, null, this.annotations);
193+
ResolvableType narrowed = ResolvableType.forType(value.getClass(), getResolvableType());
194+
return new TypeDescriptor(narrowed, null, getAnnotations());
198195
}
199196

200197
/**
@@ -210,7 +207,7 @@ public TypeDescriptor upcast(Class<?> superType) {
210207
return null;
211208
}
212209
Assert.isAssignable(superType, getType());
213-
return new TypeDescriptor(this.resolvableType.as(superType), superType, this.annotations);
210+
return new TypeDescriptor(getResolvableType().as(superType), superType, getAnnotations());
214211
}
215212

216213
/**
@@ -232,7 +229,7 @@ public boolean isPrimitive() {
232229
* @return the annotations, or an empty array if none
233230
*/
234231
public Annotation[] getAnnotations() {
235-
return this.annotations;
232+
return this.annotatedElement.getAnnotations();
236233
}
237234

238235
/**
@@ -243,7 +240,7 @@ public Annotation[] getAnnotations() {
243240
* @return <tt>true</tt> if the annotation is present
244241
*/
245242
public boolean hasAnnotation(Class<? extends Annotation> annotationType) {
246-
return (getAnnotation(annotationType) != null);
243+
return AnnotatedElementUtils.isAnnotated(this.annotatedElement, annotationType);
247244
}
248245

249246
/**
@@ -254,22 +251,7 @@ public boolean hasAnnotation(Class<? extends Annotation> annotationType) {
254251
*/
255252
@SuppressWarnings("unchecked")
256253
public <T extends Annotation> T getAnnotation(Class<T> annotationType) {
257-
// Search in annotations that are "present" (i.e., locally declared or inherited)
258-
// NOTE: this unfortunately favors inherited annotations over locally declared composed annotations.
259-
for (Annotation annotation : getAnnotations()) {
260-
if (annotation.annotationType() == annotationType) {
261-
return (T) annotation;
262-
}
263-
}
264-
265-
// Search in annotation hierarchy
266-
for (Annotation composedAnnotation : getAnnotations()) {
267-
T ann = AnnotationUtils.findAnnotation(composedAnnotation.annotationType(), annotationType);
268-
if (ann != null) {
269-
return ann;
270-
}
271-
}
272-
return null;
254+
return AnnotatedElementUtils.getMergedAnnotation(this.annotatedElement, annotationType);
273255
}
274256

275257
/**
@@ -337,13 +319,13 @@ public boolean isArray() {
337319
* @throws IllegalStateException if this type is not a {@code java.util.Collection} or array type
338320
*/
339321
public TypeDescriptor getElementTypeDescriptor() {
340-
if (this.resolvableType.isArray()) {
341-
return new TypeDescriptor(this.resolvableType.getComponentType(), null, this.annotations);
322+
if (getResolvableType().isArray()) {
323+
return new TypeDescriptor(getResolvableType().getComponentType(), null, getAnnotations());
342324
}
343-
if (streamAvailable && StreamDelegate.isStream(this.type)) {
325+
if (streamAvailable && StreamDelegate.isStream(getType())) {
344326
return StreamDelegate.getStreamElementType(this);
345327
}
346-
return getRelatedIfResolvable(this, this.resolvableType.asCollection().getGeneric(0));
328+
return getRelatedIfResolvable(this, getResolvableType().asCollection().getGeneric(0));
347329
}
348330

349331
/**
@@ -384,8 +366,8 @@ public boolean isMap() {
384366
* @throws IllegalStateException if this type is not a {@code java.util.Map}
385367
*/
386368
public TypeDescriptor getMapKeyTypeDescriptor() {
387-
Assert.state(isMap(), "Not a java.util.Map");
388-
return getRelatedIfResolvable(this, this.resolvableType.asMap().getGeneric(0));
369+
Assert.state(isMap(), "Not a [java.util.Map]");
370+
return getRelatedIfResolvable(this, getResolvableType().asMap().getGeneric(0));
389371
}
390372

391373
/**
@@ -419,8 +401,8 @@ public TypeDescriptor getMapKeyTypeDescriptor(Object mapKey) {
419401
* @throws IllegalStateException if this type is not a {@code java.util.Map}
420402
*/
421403
public TypeDescriptor getMapValueTypeDescriptor() {
422-
Assert.state(isMap(), "Not a java.util.Map");
423-
return getRelatedIfResolvable(this, this.resolvableType.asMap().getGeneric(1));
404+
Assert.state(isMap(), "Not a [java.util.Map]");
405+
return getRelatedIfResolvable(this, getResolvableType().asMap().getGeneric(1));
424406
}
425407

426408
/**
@@ -448,7 +430,7 @@ private TypeDescriptor narrow(Object value, TypeDescriptor typeDescriptor) {
448430
if (typeDescriptor != null) {
449431
return typeDescriptor.narrow(value);
450432
}
451-
return (value != null ? new TypeDescriptor(this.resolvableType, value.getClass(), this.annotations) : null);
433+
return (value != null ? new TypeDescriptor(getResolvableType(), value.getClass(), getAnnotations()) : null);
452434
}
453435

454436
@Override
@@ -494,7 +476,7 @@ public String toString() {
494476
for (Annotation ann : getAnnotations()) {
495477
builder.append("@").append(ann.annotationType().getName()).append(' ');
496478
}
497-
builder.append(this.resolvableType.toString());
479+
builder.append(getResolvableType().toString());
498480
return builder.toString();
499481
}
500482

@@ -529,9 +511,9 @@ public static TypeDescriptor valueOf(Class<?> type) {
529511
* @return the collection type descriptor
530512
*/
531513
public static TypeDescriptor collection(Class<?> collectionType, TypeDescriptor elementTypeDescriptor) {
532-
Assert.notNull(collectionType, "collectionType must not be null");
514+
Assert.notNull(collectionType, "Collection type must not be null");
533515
if (!Collection.class.isAssignableFrom(collectionType)) {
534-
throw new IllegalArgumentException("collectionType must be a java.util.Collection");
516+
throw new IllegalArgumentException("Collection type must be a [java.util.Collection]");
535517
}
536518
ResolvableType element = (elementTypeDescriptor != null ? elementTypeDescriptor.resolvableType : null);
537519
return new TypeDescriptor(ResolvableType.forClassWithGenerics(collectionType, element), null, null);
@@ -552,8 +534,9 @@ public static TypeDescriptor collection(Class<?> collectionType, TypeDescriptor
552534
* @return the map type descriptor
553535
*/
554536
public static TypeDescriptor map(Class<?> mapType, TypeDescriptor keyTypeDescriptor, TypeDescriptor valueTypeDescriptor) {
537+
Assert.notNull(mapType, "Map type must not be null");
555538
if (!Map.class.isAssignableFrom(mapType)) {
556-
throw new IllegalArgumentException("mapType must be a java.util.Map");
539+
throw new IllegalArgumentException("Map type must be a [java.util.Map]");
557540
}
558541
ResolvableType key = (keyTypeDescriptor != null ? keyTypeDescriptor.resolvableType : null);
559542
ResolvableType value = (valueTypeDescriptor != null ? valueTypeDescriptor.resolvableType : null);
@@ -691,7 +674,60 @@ private static TypeDescriptor getRelatedIfResolvable(TypeDescriptor source, Reso
691674
if (type.resolve() == null) {
692675
return null;
693676
}
694-
return new TypeDescriptor(type, null, source.annotations);
677+
return new TypeDescriptor(type, null, source.getAnnotations());
678+
}
679+
680+
681+
/**
682+
* Adapter class for exposing a {@code TypeDescriptor}'s annotations as an
683+
* {@link AnnotatedElement}, in particular to {@link AnnotatedElementUtils}.
684+
* @see AnnotatedElementUtils#isAnnotated(AnnotatedElement, Class)
685+
* @see AnnotatedElementUtils#getMergedAnnotation(AnnotatedElement, Class)
686+
*/
687+
private class AnnotatedElementAdapter implements AnnotatedElement, Serializable {
688+
689+
private final Annotation[] annotations;
690+
691+
public AnnotatedElementAdapter(Annotation[] annotations) {
692+
this.annotations = annotations;
693+
}
694+
695+
@Override
696+
@SuppressWarnings("unchecked")
697+
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
698+
for (Annotation annotation : getAnnotations()) {
699+
if (annotation.annotationType() == annotationClass) {
700+
return (T) annotation;
701+
}
702+
}
703+
return null;
704+
}
705+
706+
@Override
707+
public Annotation[] getAnnotations() {
708+
return (this.annotations != null ? this.annotations : EMPTY_ANNOTATION_ARRAY);
709+
}
710+
711+
@Override
712+
public Annotation[] getDeclaredAnnotations() {
713+
return getAnnotations();
714+
}
715+
716+
@Override
717+
public boolean equals(Object other) {
718+
return (this == other || (other instanceof AnnotatedElementAdapter &&
719+
Arrays.equals(this.annotations, ((AnnotatedElementAdapter) other).annotations)));
720+
}
721+
722+
@Override
723+
public int hashCode() {
724+
return Arrays.hashCode(this.annotations);
725+
}
726+
727+
@Override
728+
public String toString() {
729+
return TypeDescriptor.this.toString();
730+
}
695731
}
696732

697733

@@ -706,7 +742,7 @@ public static boolean isStream(Class<?> type) {
706742
}
707743

708744
public static TypeDescriptor getStreamElementType(TypeDescriptor source) {
709-
return getRelatedIfResolvable(source, source.resolvableType.as(Stream.class).getGeneric(0));
745+
return getRelatedIfResolvable(source, source.getResolvableType().as(Stream.class).getGeneric(0));
710746
}
711747
}
712748

0 commit comments

Comments
 (0)