org.springframework.core.convert.TypeDescriptor Maven / Gradle / Ivy
/*
* Copyright 2002-2019 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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;
import java.util.stream.Stream;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* Contextual descriptor about a type to convert from or to.
* Capable of representing arrays and generic collection types.
*
* @author Keith Donald
* @author Andy Clement
* @author Juergen Hoeller
* @author Phillip Webb
* @author Sam Brannen
* @author Stephane Nicoll
* @since 3.0
* @see ConversionService#canConvert(TypeDescriptor, TypeDescriptor)
* @see ConversionService#convert(Object, TypeDescriptor, TypeDescriptor)
*/
@SuppressWarnings("serial")
public class TypeDescriptor implements Serializable {
private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0];
private static final Map, TypeDescriptor> commonTypesCache = new HashMap<>(32);
private static final Class[] CACHED_COMMON_TYPES = {
boolean.class, Boolean.class, byte.class, Byte.class, char.class, Character.class,
double.class, Double.class, float.class, Float.class, int.class, Integer.class,
long.class, Long.class, short.class, Short.class, String.class, Object.class};
static {
for (Class preCachedClass : CACHED_COMMON_TYPES) {
commonTypesCache.put(preCachedClass, valueOf(preCachedClass));
}
}
private final Class type;
private final ResolvableType resolvableType;
private final AnnotatedElementAdapter annotatedElement;
/**
* Create a new type descriptor from a {@link MethodParameter}.
* Use this constructor when a source or target conversion point is a
* constructor parameter, method parameter, or method return value.
* @param methodParameter the method parameter
*/
public TypeDescriptor(MethodParameter methodParameter) {
this.resolvableType = ResolvableType.forMethodParameter(methodParameter);
this.type = this.resolvableType.resolve(methodParameter.getNestedParameterType());
this.annotatedElement = new AnnotatedElementAdapter(methodParameter.getParameterIndex() == -1 ?
methodParameter.getMethodAnnotations() : methodParameter.getParameterAnnotations());
}
/**
* Create a new type descriptor from a {@link Field}.
*
Use this constructor when a source or target conversion point is a field.
* @param field the field
*/
public TypeDescriptor(Field field) {
this.resolvableType = ResolvableType.forField(field);
this.type = this.resolvableType.resolve(field.getType());
this.annotatedElement = new AnnotatedElementAdapter(field.getAnnotations());
}
/**
* Create a new type descriptor from a {@link Property}.
*
Use this constructor when a source or target conversion point is a
* property on a Java class.
* @param property the property
*/
public TypeDescriptor(Property property) {
Assert.notNull(property, "Property must not be null");
this.resolvableType = ResolvableType.forMethodParameter(property.getMethodParameter());
this.type = this.resolvableType.resolve(property.getType());
this.annotatedElement = new AnnotatedElementAdapter(property.getAnnotations());
}
/**
* Create a new type descriptor from a {@link ResolvableType}.
*
This constructor is used internally and may also be used by subclasses
* that support non-Java languages with extended type systems. It is public
* as of 5.1.4 whereas it was protected before.
* @param resolvableType the resolvable type
* @param type the backing type (or {@code null} if it should get resolved)
* @param annotations the type annotations
* @since 4.0
*/
public TypeDescriptor(ResolvableType resolvableType, @Nullable Class type, @Nullable Annotation[] annotations) {
this.resolvableType = resolvableType;
this.type = (type != null ? type : resolvableType.toClass());
this.annotatedElement = new AnnotatedElementAdapter(annotations);
}
/**
* Variation of {@link #getType()} that accounts for a primitive type by
* returning its object wrapper type.
*
This is useful for conversion service implementations that wish to
* normalize to object-based types and not work with primitive types directly.
*/
public Class getObjectType() {
return ClassUtils.resolvePrimitiveIfNecessary(getType());
}
/**
* The type of the backing class, method parameter, field, or property
* described by this TypeDescriptor.
*
Returns primitive types as-is. See {@link #getObjectType()} for a
* variation of this operation that resolves primitive types to their
* corresponding Object types if necessary.
* @see #getObjectType()
*/
public Class getType() {
return this.type;
}
/**
* Return the underlying {@link ResolvableType}.
* @since 4.0
*/
public ResolvableType getResolvableType() {
return this.resolvableType;
}
/**
* Return the underlying source of the descriptor. Will return a {@link Field},
* {@link MethodParameter} or {@link Type} depending on how the {@link TypeDescriptor}
* was constructed. This method is primarily to provide access to additional
* type information or meta-data that alternative JVM languages may provide.
* @since 4.0
*/
public Object getSource() {
return this.resolvableType.getSource();
}
/**
* Narrows this {@link TypeDescriptor} by setting its type to the class of the
* provided value.
*
If the value is {@code null}, no narrowing is performed and this TypeDescriptor
* is returned unchanged.
*
Designed to be called by binding frameworks when they read property, field,
* or method return values. Allows such frameworks to narrow a TypeDescriptor built
* from a declared property, field, or method return value type. For example, a field
* declared as {@code java.lang.Object} would be narrowed to {@code java.util.HashMap}
* if it was set to a {@code java.util.HashMap} value. The narrowed TypeDescriptor
* can then be used to convert the HashMap to some other type. Annotation and nested
* type context is preserved by the narrowed copy.
* @param value the value to use for narrowing this type descriptor
* @return this TypeDescriptor narrowed (returns a copy with its type updated to the
* class of the provided value)
*/
public TypeDescriptor narrow(@Nullable Object value) {
if (value == null) {
return this;
}
ResolvableType narrowed = ResolvableType.forType(value.getClass(), getResolvableType());
return new TypeDescriptor(narrowed, value.getClass(), getAnnotations());
}
/**
* Cast this {@link TypeDescriptor} to a superclass or implemented interface
* preserving annotations and nested type context.
* @param superType the super type to cast to (can be {@code null})
* @return a new TypeDescriptor for the up-cast type
* @throws IllegalArgumentException if this type is not assignable to the super-type
* @since 3.2
*/
@Nullable
public TypeDescriptor upcast(@Nullable Class superType) {
if (superType == null) {
return null;
}
Assert.isAssignable(superType, getType());
return new TypeDescriptor(getResolvableType().as(superType), superType, getAnnotations());
}
/**
* Return the name of this type: the fully qualified class name.
*/
public String getName() {
return ClassUtils.getQualifiedName(getType());
}
/**
* Is this type a primitive type?
*/
public boolean isPrimitive() {
return getType().isPrimitive();
}
/**
* Return the annotations associated with this type descriptor, if any.
* @return the annotations, or an empty array if none
*/
public Annotation[] getAnnotations() {
return this.annotatedElement.getAnnotations();
}
/**
* Determine if this type descriptor has the specified annotation.
*
As of Spring Framework 4.2, this method supports arbitrary levels
* of meta-annotations.
* @param annotationType the annotation type
* @return true if the annotation is present
*/
public boolean hasAnnotation(Class annotationType) {
if (this.annotatedElement.isEmpty()) {
// Shortcut: AnnotatedElementUtils would have to expect AnnotatedElement.getAnnotations()
// to return a copy of the array, whereas we can do it more efficiently here.
return false;
}
return AnnotatedElementUtils.isAnnotated(this.annotatedElement, annotationType);
}
/**
* Obtain the annotation of the specified {@code annotationType} that is on this type descriptor.
*
As of Spring Framework 4.2, this method supports arbitrary levels of meta-annotations.
* @param annotationType the annotation type
* @return the annotation, or {@code null} if no such annotation exists on this type descriptor
*/
@Nullable
public T getAnnotation(Class annotationType) {
if (this.annotatedElement.isEmpty()) {
// Shortcut: AnnotatedElementUtils would have to expect AnnotatedElement.getAnnotations()
// to return a copy of the array, whereas we can do it more efficiently here.
return null;
}
return AnnotatedElementUtils.getMergedAnnotation(this.annotatedElement, annotationType);
}
/**
* Returns true if an object of this type descriptor can be assigned to the location
* described by the given type descriptor.
* For example, {@code valueOf(String.class).isAssignableTo(valueOf(CharSequence.class))}
* returns {@code true} because a String value can be assigned to a CharSequence variable.
* On the other hand, {@code valueOf(Number.class).isAssignableTo(valueOf(Integer.class))}
* returns {@code false} because, while all Integers are Numbers, not all Numbers are Integers.
*
For arrays, collections, and maps, element and key/value types are checked if declared.
* For example, a List<String> field value is assignable to a Collection<CharSequence>
* field, but List<Number> is not assignable to List<Integer>.
* @return {@code true} if this type is assignable to the type represented by the provided
* type descriptor
* @see #getObjectType()
*/
public boolean isAssignableTo(TypeDescriptor typeDescriptor) {
boolean typesAssignable = typeDescriptor.getObjectType().isAssignableFrom(getObjectType());
if (!typesAssignable) {
return false;
}
if (isArray() && typeDescriptor.isArray()) {
return isNestedAssignable(getElementTypeDescriptor(), typeDescriptor.getElementTypeDescriptor());
}
else if (isCollection() && typeDescriptor.isCollection()) {
return isNestedAssignable(getElementTypeDescriptor(), typeDescriptor.getElementTypeDescriptor());
}
else if (isMap() && typeDescriptor.isMap()) {
return isNestedAssignable(getMapKeyTypeDescriptor(), typeDescriptor.getMapKeyTypeDescriptor()) &&
isNestedAssignable(getMapValueTypeDescriptor(), typeDescriptor.getMapValueTypeDescriptor());
}
else {
return true;
}
}
private boolean isNestedAssignable(@Nullable TypeDescriptor nestedTypeDescriptor,
@Nullable TypeDescriptor otherNestedTypeDescriptor) {
return (nestedTypeDescriptor == null || otherNestedTypeDescriptor == null ||
nestedTypeDescriptor.isAssignableTo(otherNestedTypeDescriptor));
}
/**
* Is this type a {@link Collection} type?
*/
public boolean isCollection() {
return Collection.class.isAssignableFrom(getType());
}
/**
* Is this type an array type?
*/
public boolean isArray() {
return getType().isArray();
}
/**
* If this type is an array, returns the array's component type.
* If this type is a {@code Stream}, returns the stream's component type.
* If this type is a {@link Collection} and it is parameterized, returns the Collection's element type.
* If the Collection is not parameterized, returns {@code null} indicating the element type is not declared.
* @return the array component type or Collection element type, or {@code null} if this type is not
* an array type or a {@code java.util.Collection} or if its element type is not parameterized
* @see #elementTypeDescriptor(Object)
*/
@Nullable
public TypeDescriptor getElementTypeDescriptor() {
if (getResolvableType().isArray()) {
return new TypeDescriptor(getResolvableType().getComponentType(), null, getAnnotations());
}
if (Stream.class.isAssignableFrom(getType())) {
return getRelatedIfResolvable(this, getResolvableType().as(Stream.class).getGeneric(0));
}
return getRelatedIfResolvable(this, getResolvableType().asCollection().getGeneric(0));
}
/**
* If this type is a {@link Collection} or an array, creates a element TypeDescriptor
* from the provided collection or array element.
*
Narrows the {@link #getElementTypeDescriptor() elementType} property to the class
* of the provided collection or array element. For example, if this describes a
* {@code java.util.List<java.lang.Number<} and the element argument is an
* {@code java.lang.Integer}, the returned TypeDescriptor will be {@code java.lang.Integer}.
* If this describes a {@code java.util.List<?>} and the element argument is an
* {@code java.lang.Integer}, the returned TypeDescriptor will be {@code java.lang.Integer}
* as well.
*
Annotation and nested type context will be preserved in the narrowed
* TypeDescriptor that is returned.
* @param element the collection or array element
* @return a element type descriptor, narrowed to the type of the provided element
* @see #getElementTypeDescriptor()
* @see #narrow(Object)
*/
@Nullable
public TypeDescriptor elementTypeDescriptor(Object element) {
return narrow(element, getElementTypeDescriptor());
}
/**
* Is this type a {@link Map} type?
*/
public boolean isMap() {
return Map.class.isAssignableFrom(getType());
}
/**
* If this type is a {@link Map} and its key type is parameterized,
* returns the map's key type. If the Map's key type is not parameterized,
* returns {@code null} indicating the key type is not declared.
* @return the Map key type, or {@code null} if this type is a Map
* but its key type is not parameterized
* @throws IllegalStateException if this type is not a {@code java.util.Map}
*/
@Nullable
public TypeDescriptor getMapKeyTypeDescriptor() {
Assert.state(isMap(), "Not a [java.util.Map]");
return getRelatedIfResolvable(this, getResolvableType().asMap().getGeneric(0));
}
/**
* If this type is a {@link Map}, creates a mapKey {@link TypeDescriptor}
* from the provided map key.
*
Narrows the {@link #getMapKeyTypeDescriptor() mapKeyType} property
* to the class of the provided map key. For example, if this describes a
* {@code java.util.Map<java.lang.Number, java.lang.String<} and the key
* argument is a {@code java.lang.Integer}, the returned TypeDescriptor will be
* {@code java.lang.Integer}. If this describes a {@code java.util.Map<?, ?>}
* and the key argument is a {@code java.lang.Integer}, the returned
* TypeDescriptor will be {@code java.lang.Integer} as well.
*
Annotation and nested type context will be preserved in the narrowed
* TypeDescriptor that is returned.
* @param mapKey the map key
* @return the map key type descriptor
* @throws IllegalStateException if this type is not a {@code java.util.Map}
* @see #narrow(Object)
*/
@Nullable
public TypeDescriptor getMapKeyTypeDescriptor(Object mapKey) {
return narrow(mapKey, getMapKeyTypeDescriptor());
}
/**
* If this type is a {@link Map} and its value type is parameterized,
* returns the map's value type.
*
If the Map's value type is not parameterized, returns {@code null}
* indicating the value type is not declared.
* @return the Map value type, or {@code null} if this type is a Map
* but its value type is not parameterized
* @throws IllegalStateException if this type is not a {@code java.util.Map}
*/
@Nullable
public TypeDescriptor getMapValueTypeDescriptor() {
Assert.state(isMap(), "Not a [java.util.Map]");
return getRelatedIfResolvable(this, getResolvableType().asMap().getGeneric(1));
}
/**
* If this type is a {@link Map}, creates a mapValue {@link TypeDescriptor}
* from the provided map value.
*
Narrows the {@link #getMapValueTypeDescriptor() mapValueType} property
* to the class of the provided map value. For example, if this describes a
* {@code java.util.Map<java.lang.String, java.lang.Number<} and the value
* argument is a {@code java.lang.Integer}, the returned TypeDescriptor will be
* {@code java.lang.Integer}. If this describes a {@code java.util.Map<?, ?>}
* and the value argument is a {@code java.lang.Integer}, the returned
* TypeDescriptor will be {@code java.lang.Integer} as well.
*
Annotation and nested type context will be preserved in the narrowed
* TypeDescriptor that is returned.
* @param mapValue the map value
* @return the map value type descriptor
* @throws IllegalStateException if this type is not a {@code java.util.Map}
* @see #narrow(Object)
*/
@Nullable
public TypeDescriptor getMapValueTypeDescriptor(Object mapValue) {
return narrow(mapValue, getMapValueTypeDescriptor());
}
@Nullable
private TypeDescriptor narrow(@Nullable Object value, @Nullable TypeDescriptor typeDescriptor) {
if (typeDescriptor != null) {
return typeDescriptor.narrow(value);
}
if (value != null) {
return narrow(value);
}
return null;
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (!(other instanceof TypeDescriptor)) {
return false;
}
TypeDescriptor otherDesc = (TypeDescriptor) other;
if (getType() != otherDesc.getType()) {
return false;
}
if (!annotationsMatch(otherDesc)) {
return false;
}
if (isCollection() || isArray()) {
return ObjectUtils.nullSafeEquals(getElementTypeDescriptor(), otherDesc.getElementTypeDescriptor());
}
else if (isMap()) {
return (ObjectUtils.nullSafeEquals(getMapKeyTypeDescriptor(), otherDesc.getMapKeyTypeDescriptor()) &&
ObjectUtils.nullSafeEquals(getMapValueTypeDescriptor(), otherDesc.getMapValueTypeDescriptor()));
}
else {
return true;
}
}
private boolean annotationsMatch(TypeDescriptor otherDesc) {
Annotation[] anns = getAnnotations();
Annotation[] otherAnns = otherDesc.getAnnotations();
if (anns == otherAnns) {
return true;
}
if (anns.length != otherAnns.length) {
return false;
}
if (anns.length > 0) {
for (int i = 0; i < anns.length; i++) {
if (!annotationEquals(anns[i], otherAnns[i])) {
return false;
}
}
}
return true;
}
private boolean annotationEquals(Annotation ann, Annotation otherAnn) {
// Annotation.equals is reflective and pretty slow, so let's check identity and proxy type first.
return (ann == otherAnn || (ann.getClass() == otherAnn.getClass() && ann.equals(otherAnn)));
}
@Override
public int hashCode() {
return getType().hashCode();
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
for (Annotation ann : getAnnotations()) {
builder.append("@").append(ann.annotationType().getName()).append(' ');
}
builder.append(getResolvableType().toString());
return builder.toString();
}
/**
* Create a new type descriptor for an object.
*
Use this factory method to introspect a source object before asking the
* conversion system to convert it to some another type.
*
If the provided object is {@code null}, returns {@code null}, else calls
* {@link #valueOf(Class)} to build a TypeDescriptor from the object's class.
* @param source the source object
* @return the type descriptor
*/
@Nullable
public static TypeDescriptor forObject(@Nullable Object source) {
return (source != null ? valueOf(source.getClass()) : null);
}
/**
* Create a new type descriptor from the given type.
*
Use this to instruct the conversion system to convert an object to a
* specific target type, when no type location such as a method parameter or
* field is available to provide additional conversion context.
*
Generally prefer use of {@link #forObject(Object)} for constructing type
* descriptors from source objects, as it handles the {@code null} object case.
* @param type the class (may be {@code null} to indicate {@code Object.class})
* @return the corresponding type descriptor
*/
public static TypeDescriptor valueOf(@Nullable Class type) {
if (type == null) {
type = Object.class;
}
TypeDescriptor desc = commonTypesCache.get(type);
return (desc != null ? desc : new TypeDescriptor(ResolvableType.forClass(type), null, null));
}
/**
* Create a new type descriptor from a {@link java.util.Collection} type.
*
Useful for converting to typed Collections.
*
For example, a {@code List} could be converted to a
* {@code List} by converting to a targetType built with this method.
* The method call to construct such a {@code TypeDescriptor} would look something
* like: {@code collection(List.class, TypeDescriptor.valueOf(EmailAddress.class));}
* @param collectionType the collection type, which must implement {@link Collection}.
* @param elementTypeDescriptor a descriptor for the collection's element type,
* used to convert collection elements
* @return the collection type descriptor
*/
public static TypeDescriptor collection(Class collectionType, @Nullable TypeDescriptor elementTypeDescriptor) {
Assert.notNull(collectionType, "Collection type must not be null");
if (!Collection.class.isAssignableFrom(collectionType)) {
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);
}
/**
* Create a new type descriptor from a {@link java.util.Map} type.
* Useful for converting to typed Maps.
*
For example, a Map<String, String> could be converted to a Map<Id, EmailAddress>
* by converting to a targetType built with this method:
* The method call to construct such a TypeDescriptor would look something like:
*
* map(Map.class, TypeDescriptor.valueOf(Id.class), TypeDescriptor.valueOf(EmailAddress.class));
*
* @param mapType the map type, which must implement {@link Map}
* @param keyTypeDescriptor a descriptor for the map's key type, used to convert map keys
* @param valueTypeDescriptor the map's value type, used to convert map values
* @return the map type descriptor
*/
public static TypeDescriptor map(Class mapType, @Nullable TypeDescriptor keyTypeDescriptor,
@Nullable TypeDescriptor valueTypeDescriptor) {
Assert.notNull(mapType, "Map type must not be null");
if (!Map.class.isAssignableFrom(mapType)) {
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);
return new TypeDescriptor(ResolvableType.forClassWithGenerics(mapType, key, value), null, null);
}
/**
* Create a new type descriptor as an array of the specified type.
* For example to create a {@code Map[]} use:
*
* TypeDescriptor.array(TypeDescriptor.map(Map.class, TypeDescriptor.value(String.class), TypeDescriptor.value(String.class)));
*
* @param elementTypeDescriptor the {@link TypeDescriptor} of the array element or {@code null}
* @return an array {@link TypeDescriptor} or {@code null} if {@code elementTypeDescriptor} is {@code null}
* @since 3.2.1
*/
@Nullable
public static TypeDescriptor array(@Nullable TypeDescriptor elementTypeDescriptor) {
if (elementTypeDescriptor == null) {
return null;
}
return new TypeDescriptor(ResolvableType.forArrayComponent(elementTypeDescriptor.resolvableType),
null, elementTypeDescriptor.getAnnotations());
}
/**
* Create a type descriptor for a nested type declared within the method parameter.
* For example, if the methodParameter is a {@code List} and the
* nesting level is 1, the nested type descriptor will be String.class.
* If the methodParameter is a {@code List>} and the nesting
* level is 2, the nested type descriptor will also be a String.class.
* If the methodParameter is a {@code Map} and the nesting
* level is 1, the nested type descriptor will be String, derived from the map value.
* If the methodParameter is a {@code List