TypeDescriptor supports merged annotation lookups (for composable formatting annotations)
Issue: SPR-14844
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user