Browse Source

TypeDescriptor supports merged annotation lookups (for composable formatting annotations)

Issue: SPR-14844
pull/1226/head
Juergen Hoeller 10 years ago
parent
commit
bf9083d60f
  1. 16
      spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceFactoryBeanTests.java
  2. 132
      spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java

16
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 * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.format.support; package org.springframework.format.support;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
@ -27,6 +28,7 @@ import java.util.Set;
import org.junit.Test; import org.junit.Test;
import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.TypeDescriptor;
import org.springframework.format.AnnotationFormatterFactory; 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) @Retention(RetentionPolicy.RUNTIME)
private @interface SpecialInt { private @interface SpecialInt {
@AliasFor("alias")
String value() default "";
@AliasFor("value")
String alias() default "";
} }
@ -143,7 +151,7 @@ public class FormattingConversionServiceFactoryBeanTests {
@NumberFormat(pattern = "##,00") @NumberFormat(pattern = "##,00")
private double pattern; private double pattern;
@SpecialInt @SpecialInt("aliased")
private int specialInt; private int specialInt;
public int getSpecialInt() { public int getSpecialInt() {
@ -187,6 +195,8 @@ public class FormattingConversionServiceFactoryBeanTests {
@Override @Override
public Printer<?> getPrinter(SpecialInt annotation, Class<?> fieldType) { public Printer<?> getPrinter(SpecialInt annotation, Class<?> fieldType) {
assertEquals("aliased", annotation.value());
assertEquals("aliased", annotation.alias());
return new Printer<Integer>() { return new Printer<Integer>() {
@Override @Override
public String print(Integer object, Locale locale) { public String print(Integer object, Locale locale) {
@ -197,6 +207,8 @@ public class FormattingConversionServiceFactoryBeanTests {
@Override @Override
public Parser<?> getParser(SpecialInt annotation, Class<?> fieldType) { public Parser<?> getParser(SpecialInt annotation, Class<?> fieldType) {
assertEquals("aliased", annotation.value());
assertEquals("aliased", annotation.alias());
return new Parser<Integer>() { return new Parser<Integer>() {
@Override @Override
public Integer parse(String text, Locale locale) throws ParseException { public Integer parse(String text, Locale locale) throws ParseException {

132
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.io.Serializable;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -27,7 +29,7 @@ import java.util.stream.Stream;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType; 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.Assert;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
@ -66,7 +68,7 @@ public class TypeDescriptor implements Serializable {
private final ResolvableType resolvableType; 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"); Assert.notNull(methodParameter, "MethodParameter must not be null");
this.resolvableType = ResolvableType.forMethodParameter(methodParameter); this.resolvableType = ResolvableType.forMethodParameter(methodParameter);
this.type = this.resolvableType.resolve(methodParameter.getParameterType()); this.type = this.resolvableType.resolve(methodParameter.getParameterType());
this.annotations = (methodParameter.getParameterIndex() == -1 ? this.annotatedElement = new AnnotatedElementAdapter(methodParameter.getParameterIndex() == -1 ?
nullSafeAnnotations(methodParameter.getMethodAnnotations()) : methodParameter.getMethodAnnotations() : methodParameter.getParameterAnnotations());
nullSafeAnnotations(methodParameter.getParameterAnnotations()));
} }
/** /**
@ -93,7 +94,7 @@ public class TypeDescriptor implements Serializable {
Assert.notNull(field, "Field must not be null"); Assert.notNull(field, "Field must not be null");
this.resolvableType = ResolvableType.forField(field); this.resolvableType = ResolvableType.forField(field);
this.type = this.resolvableType.resolve(field.getType()); 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"); Assert.notNull(property, "Property must not be null");
this.resolvableType = ResolvableType.forMethodParameter(property.getMethodParameter()); this.resolvableType = ResolvableType.forMethodParameter(property.getMethodParameter());
this.type = this.resolvableType.resolve(property.getType()); 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) { protected TypeDescriptor(ResolvableType resolvableType, Class<?> type, Annotation[] annotations) {
this.resolvableType = resolvableType; this.resolvableType = resolvableType;
this.type = (type != null ? type : resolvableType.resolve(Object.class)); 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 * Variation of {@link #getType()} that accounts for a primitive type by
* returning its object wrapper type. * returning its object wrapper type.
@ -189,8 +186,8 @@ public class TypeDescriptor implements Serializable {
if (value == null) { if (value == null) {
return this; return this;
} }
ResolvableType narrowed = ResolvableType.forType(value.getClass(), this.resolvableType); ResolvableType narrowed = ResolvableType.forType(value.getClass(), getResolvableType());
return new TypeDescriptor(narrowed, null, this.annotations); return new TypeDescriptor(narrowed, null, getAnnotations());
} }
/** /**
@ -206,7 +203,7 @@ public class TypeDescriptor implements Serializable {
return null; return null;
} }
Assert.isAssignable(superType, getType()); 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 * @return the annotations, or an empty array if none
*/ */
public Annotation[] getAnnotations() { public Annotation[] getAnnotations() {
return this.annotations; return this.annotatedElement.getAnnotations();
} }
/** /**
@ -239,7 +236,7 @@ public class TypeDescriptor implements Serializable {
* @return <tt>true</tt> if the annotation is present * @return <tt>true</tt> if the annotation is present
*/ */
public boolean hasAnnotation(Class<? extends Annotation> annotationType) { public boolean hasAnnotation(Class<? extends Annotation> annotationType) {
return (getAnnotation(annotationType) != null); return AnnotatedElementUtils.isAnnotated(this.annotatedElement, annotationType);
} }
/** /**
@ -250,22 +247,7 @@ public class TypeDescriptor implements Serializable {
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <T extends Annotation> T getAnnotation(Class<T> annotationType) { public <T extends Annotation> T getAnnotation(Class<T> annotationType) {
// Search in annotations that are "present" (i.e., locally declared or inherited) return AnnotatedElementUtils.getMergedAnnotation(this.annotatedElement, annotationType);
// 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;
} }
/** /**
@ -333,13 +315,13 @@ public class TypeDescriptor implements Serializable {
* @throws IllegalStateException if this type is not a {@code java.util.Collection} or array type * @throws IllegalStateException if this type is not a {@code java.util.Collection} or array type
*/ */
public TypeDescriptor getElementTypeDescriptor() { public TypeDescriptor getElementTypeDescriptor() {
if (this.resolvableType.isArray()) { if (getResolvableType().isArray()) {
return new TypeDescriptor(this.resolvableType.getComponentType(), null, this.annotations); return new TypeDescriptor(getResolvableType().getComponentType(), null, getAnnotations());
} }
if (Stream.class.isAssignableFrom(this.type)) { if (Stream.class.isAssignableFrom(getType())) {
return getRelatedIfResolvable(this, this.resolvableType.as(Stream.class).getGeneric(0)); 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} * @throws IllegalStateException if this type is not a {@code java.util.Map}
*/ */
public TypeDescriptor getMapKeyTypeDescriptor() { public TypeDescriptor getMapKeyTypeDescriptor() {
Assert.state(isMap(), "Not a java.util.Map"); Assert.state(isMap(), "Not a [java.util.Map]");
return getRelatedIfResolvable(this, this.resolvableType.asMap().getGeneric(0)); 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} * @throws IllegalStateException if this type is not a {@code java.util.Map}
*/ */
public TypeDescriptor getMapValueTypeDescriptor() { public TypeDescriptor getMapValueTypeDescriptor() {
Assert.state(isMap(), "Not a java.util.Map"); Assert.state(isMap(), "Not a [java.util.Map]");
return getRelatedIfResolvable(this, this.resolvableType.asMap().getGeneric(1)); return getRelatedIfResolvable(this, getResolvableType().asMap().getGeneric(1));
} }
/** /**
@ -444,7 +426,7 @@ public class TypeDescriptor implements Serializable {
if (typeDescriptor != null) { if (typeDescriptor != null) {
return typeDescriptor.narrow(value); 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 @Override
@ -490,7 +472,7 @@ public class TypeDescriptor implements Serializable {
for (Annotation ann : getAnnotations()) { for (Annotation ann : getAnnotations()) {
builder.append("@").append(ann.annotationType().getName()).append(' '); builder.append("@").append(ann.annotationType().getName()).append(' ');
} }
builder.append(this.resolvableType.toString()); builder.append(getResolvableType().toString());
return builder.toString(); return builder.toString();
} }
@ -525,9 +507,9 @@ public class TypeDescriptor implements Serializable {
* @return the collection type descriptor * @return the collection type descriptor
*/ */
public static TypeDescriptor collection(Class<?> collectionType, TypeDescriptor elementTypeDescriptor) { 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)) { 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); ResolvableType element = (elementTypeDescriptor != null ? elementTypeDescriptor.resolvableType : null);
return new TypeDescriptor(ResolvableType.forClassWithGenerics(collectionType, element), null, null); return new TypeDescriptor(ResolvableType.forClassWithGenerics(collectionType, element), null, null);
@ -548,8 +530,9 @@ public class TypeDescriptor implements Serializable {
* @return the map type descriptor * @return the map type descriptor
*/ */
public static TypeDescriptor map(Class<?> mapType, TypeDescriptor keyTypeDescriptor, TypeDescriptor valueTypeDescriptor) { public static TypeDescriptor map(Class<?> mapType, TypeDescriptor keyTypeDescriptor, TypeDescriptor valueTypeDescriptor) {
Assert.notNull(mapType, "Map type must not be null");
if (!Map.class.isAssignableFrom(mapType)) { 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 key = (keyTypeDescriptor != null ? keyTypeDescriptor.resolvableType : null);
ResolvableType value = (valueTypeDescriptor != null ? valueTypeDescriptor.resolvableType : null); ResolvableType value = (valueTypeDescriptor != null ? valueTypeDescriptor.resolvableType : null);
@ -687,7 +670,60 @@ public class TypeDescriptor implements Serializable {
if (type.resolve() == null) { if (type.resolve() == null) {
return 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 extends Annotation> T getAnnotation(Class<T> 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();
}
} }
} }

Loading…
Cancel
Save