/* * Copyright 2002-2014 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 * * http://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.Array; import java.lang.reflect.Field; import java.util.Collection; import java.util.HashMap; import java.util.Map; import org.springframework.core.MethodParameter; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; /** * Context about a type to convert from or to. * * @author Keith Donald * @author Andy Clement * @author Juergen Hoeller * @author Phillip Webb * @author Sam Brannen * @since 2.0 */ @SuppressWarnings("serial") public class TypeDescriptor implements Serializable { static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; private static final Map<Class<?>, TypeDescriptor> typeDescriptorCache = new HashMap<Class<?>, TypeDescriptor>(); static { typeDescriptorCache.put(boolean.class, new TypeDescriptor(boolean.class)); typeDescriptorCache.put(Boolean.class, new TypeDescriptor(Boolean.class)); typeDescriptorCache.put(byte.class, new TypeDescriptor(byte.class)); typeDescriptorCache.put(Byte.class, new TypeDescriptor(Byte.class)); typeDescriptorCache.put(char.class, new TypeDescriptor(char.class)); typeDescriptorCache.put(Character.class, new TypeDescriptor(Character.class)); typeDescriptorCache.put(short.class, new TypeDescriptor(short.class)); typeDescriptorCache.put(Short.class, new TypeDescriptor(Short.class)); typeDescriptorCache.put(int.class, new TypeDescriptor(int.class)); typeDescriptorCache.put(Integer.class, new TypeDescriptor(Integer.class)); typeDescriptorCache.put(long.class, new TypeDescriptor(long.class)); typeDescriptorCache.put(Long.class, new TypeDescriptor(Long.class)); typeDescriptorCache.put(float.class, new TypeDescriptor(float.class)); typeDescriptorCache.put(Float.class, new TypeDescriptor(Float.class)); typeDescriptorCache.put(double.class, new TypeDescriptor(double.class)); typeDescriptorCache.put(Double.class, new TypeDescriptor(Double.class)); typeDescriptorCache.put(String.class, new TypeDescriptor(String.class)); } private final Class<?> type; private final TypeDescriptor elementTypeDescriptor; private final TypeDescriptor mapKeyTypeDescriptor; private final TypeDescriptor mapValueTypeDescriptor; private final Annotation[] annotations; /** * Create a new type descriptor from a {@link MethodParameter}. * <p>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(new ParameterDescriptor(methodParameter)); } /** * Create a new type descriptor from a {@link Field}. * <p>Use this constructor when a source or target conversion point is a field. * @param field the field */ public TypeDescriptor(Field field) { this(new FieldDescriptor(field)); } /** * Create a new type descriptor from a {@link Property}. * <p>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) { this(new BeanPropertyDescriptor(property)); } /** * Create a new type descriptor from the given type. * <p>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. * <p>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 * @return the type descriptor */ public static TypeDescriptor valueOf(Class<?> type) { TypeDescriptor desc = typeDescriptorCache.get(type); return (desc != null ? desc : new TypeDescriptor(type)); } /** * Create a new type descriptor from a {@link java.util.Collection} type. * <p>Useful for converting to typed Collections. * <p>For example, a {@code List<String>} could be converted to a * {@code List<EmailAddress>} 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, TypeDescriptor elementTypeDescriptor) { if (!Collection.class.isAssignableFrom(collectionType)) { throw new IllegalArgumentException("collectionType must be a java.util.Collection"); } return new TypeDescriptor(collectionType, elementTypeDescriptor); } /** * Create a new type descriptor from a {@link java.util.Map} type. * <p>Useful for converting to typed Maps. * <p>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, TypeDescriptor keyTypeDescriptor, TypeDescriptor valueTypeDescriptor) { if (!Map.class.isAssignableFrom(mapType)) { throw new IllegalArgumentException("mapType must be a java.util.Map"); } return new TypeDescriptor(mapType, keyTypeDescriptor, valueTypeDescriptor); } /** * Create a new type descriptor as an array of the specified type. * <p>For example to create a {@code Map<String,String>[]} use * {@code 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} */ public static TypeDescriptor array(TypeDescriptor elementTypeDescriptor) { if(elementTypeDescriptor == null) { return null; } Class<?> type = Array.newInstance(elementTypeDescriptor.getType(), 0).getClass(); return new TypeDescriptor(type, elementTypeDescriptor, null, null, elementTypeDescriptor.getAnnotations()); } /** * Creates a type descriptor for a nested type declared within the method parameter. * <p>For example, if the methodParameter is a {@code List<String>} and the * nesting level is 1, the nested type descriptor will be String.class. * <p>If the methodParameter is a {@code List<List<String>>} and the nesting * level is 2, the nested type descriptor will also be a String.class. * <p>If the methodParameter is a {@code Map<Integer, String>} and the nesting * level is 1, the nested type descriptor will be String, derived from the map value. * <p>If the methodParameter is a {@code List<Map<Integer, String>>} and the * nesting level is 2, the nested type descriptor will be String, derived from the map value. * <p>Returns {@code null} if a nested type cannot be obtained because it was not declared. * For example, if the method parameter is a {@code List<?>}, the nested type * descriptor returned will be {@code null}. * @param methodParameter the method parameter with a nestingLevel of 1 * @param nestingLevel the nesting level of the collection/array element or * map key/value declaration within the method parameter * @return the nested type descriptor at the specified nesting level, or null * if it could not be obtained * @throws IllegalArgumentException if the nesting level of the input * {@link MethodParameter} argument is not 1 * @throws IllegalArgumentException if the types up to the specified nesting * level are not of collection, array, or map types */ public static TypeDescriptor nested(MethodParameter methodParameter, int nestingLevel) { if (methodParameter.getNestingLevel() != 1) { throw new IllegalArgumentException("methodParameter nesting level must be 1: use the nestingLevel parameter to specify the desired nestingLevel for nested type traversal"); } return nested(new ParameterDescriptor(methodParameter), nestingLevel); } /** * Creates a type descriptor for a nested type declared within the field. * <p>For example, if the field is a {@code List<String>} and the nesting * level is 1, the nested type descriptor will be {@code String.class}. * <p>If the field is a {@code List<List<String>>} and the nesting level is * 2, the nested type descriptor will also be a {@code String.class}. * <p>If the field is a {@code Map<Integer, String>} and the nesting level * is 1, the nested type descriptor will be String, derived from the map value. * <p>If the field is a {@code List<Map<Integer, String>>} and the nesting * level is 2, the nested type descriptor will be String, derived from the map value. * <p>Returns {@code null} if a nested type cannot be obtained because it was not declared. * For example, if the field is a {@code List<?>}, the nested type descriptor returned will be {@code null}. * @param field the field * @param nestingLevel the nesting level of the collection/array element or * map key/value declaration within the field * @return the nested type descriptor at the specified nesting level, or null * if it could not be obtained * @throws IllegalArgumentException if the types up to the specified nesting * level are not of collection, array, or map types */ public static TypeDescriptor nested(Field field, int nestingLevel) { return nested(new FieldDescriptor(field), nestingLevel); } /** * Creates a type descriptor for a nested type declared within the property. * <p>For example, if the property is a {@code List<String>} and the nesting * level is 1, the nested type descriptor will be {@code String.class}. * <p>If the property is a {@code List<List<String>>} and the nesting level * is 2, the nested type descriptor will also be a {@code String.class}. * <p>If the property is a {@code Map<Integer, String>} and the nesting level * is 1, the nested type descriptor will be String, derived from the map value. * <p>If the property is a {@code List<Map<Integer, String>>} and the nesting * level is 2, the nested type descriptor will be String, derived from the map value. * <p>Returns {@code null} if a nested type cannot be obtained because it was not declared. * For example, if the property is a {@code List<?>}, the nested type descriptor * returned will be {@code null}. * @param property the property * @param nestingLevel the nesting level of the collection/array element or * map key/value declaration within the property * @return the nested type descriptor at the specified nesting level, or * {@code null} if it could not be obtained * @throws IllegalArgumentException if the types up to the specified nesting * level are not of collection, array, or map types */ public static TypeDescriptor nested(Property property, int nestingLevel) { return nested(new BeanPropertyDescriptor(property), nestingLevel); } /** * Create a new type descriptor for an object. * <p>Use this factory method to introspect a source object before asking the conversion system to convert it to some another type. * <p>If the provided object is null, returns null, else calls {@link #valueOf(Class)} to build a TypeDescriptor from the object's class. * @param source the source object * @return the type descriptor */ public static TypeDescriptor forObject(Object source) { return (source != null ? valueOf(source.getClass()) : null); } /** * The type of the backing class, method parameter, field, or property described by this TypeDescriptor. * <p>Returns primitive types as-is. * <p>See {@link #getObjectType()} for a variation of this operation that resolves primitive types * to their corresponding Object types if necessary. * @return the type, or {@code null} * @see #getObjectType() */ public Class<?> getType() { return this.type; } /** * Variation of {@link #getType()} that accounts for a primitive type by returning its object wrapper type. * <p>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()); } /** * Narrows this {@link TypeDescriptor} by setting its type to the class of the provided value. * <p>If the value is {@code null}, no narrowing is performed and this TypeDescriptor is returned unchanged. * <p>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(Object value) { if (value == null) { return this; } return new TypeDescriptor(value.getClass(), this.elementTypeDescriptor, this.mapKeyTypeDescriptor, this.mapValueTypeDescriptor, this.annotations); } /** * 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 */ public TypeDescriptor upcast(Class<?> superType) { if (superType == null) { return null; } Assert.isAssignable(superType, getType()); return new TypeDescriptor(superType, this.elementTypeDescriptor, this.mapKeyTypeDescriptor, this.mapValueTypeDescriptor, this.annotations); } /** * Returns 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(); } /** * The annotations associated with this type descriptor, if any. * @return the annotations, or an empty array if none */ public Annotation[] getAnnotations() { return this.annotations; } /** * Determine if this type descriptor has the specified annotation. * @param annotationType the annotation type * @return <tt>true</tt> if the annotation is present */ public boolean hasAnnotation(Class<? extends Annotation> annotationType) { return getAnnotation(annotationType) != null; } /** * Obtain the annotation associated with this type descriptor of the specified type. * @param annotationType the annotation type * @return the annotation, or {@code null} if no such annotation exists on this type descriptor */ @SuppressWarnings("unchecked") public <T extends Annotation> T getAnnotation(Class<T> annotationType) { for (Annotation annotation : this.annotations) { if (annotation.annotationType().equals(annotationType)) { return (T) annotation; } } for (Annotation metaAnn : this.annotations) { T ann = metaAnn.annotationType().getAnnotation(annotationType); if (ann != null) { return ann; } } return null; } /** * Returns true if an object of this type descriptor can be assigned to the location described by the given type descriptor. * <p>For example, valueOf(String.class).isAssignableTo(valueOf(CharSequence.class)) returns true because a String value can be assigned to a CharSequence variable. * On the other hand, valueOf(Number.class).isAssignableTo(valueOf(Integer.class)) returns false because, while all Integers are Numbers, not all Numbers are Integers. * <p> * 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 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 getElementTypeDescriptor().isAssignableTo(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; } } // indexable type descriptor operations /** * 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 {@link Collection} and it is parameterized, returns the Collection's element type. * If the Collection is not parameterized, returns null indicating the element type is not declared. * @return the array component type or Collection element type, or {@code null} if this type is a Collection but its element type is not parameterized * @throws IllegalStateException if this type is not a java.util.Collection or Array type */ public TypeDescriptor getElementTypeDescriptor() { assertCollectionOrArray(); return this.elementTypeDescriptor; } /** * If this type is a {@link Collection} or an Array, creates a element TypeDescriptor from the provided collection or array element. * <p>Narrows the {@link #getElementTypeDescriptor() elementType} property to the class of the provided collection or array element. * For example, if this describes a java.util.List<java.lang.Number< and the element argument is a java.lang.Integer, the returned TypeDescriptor will be java.lang.Integer. * If this describes a java.util.List<?> and the element argument is a java.lang.Integer, the returned TypeDescriptor will be java.lang.Integer as well. * <p>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 * @throws IllegalStateException if this type is not a java.util.Collection or Array type * @see #narrow(Object) */ public TypeDescriptor elementTypeDescriptor(Object element) { return narrow(element, getElementTypeDescriptor()); } // map type descriptor operations /** * 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 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 java.util.Map */ public TypeDescriptor getMapKeyTypeDescriptor() { assertMap(); return this.mapKeyTypeDescriptor; } /** * If this type is a {@link Map}, creates a mapKey {@link TypeDescriptor} from the provided map key. * <p>Narrows the {@link #getMapKeyTypeDescriptor() mapKeyType} property to the class of the provided map key. * For example, if this describes a java.util.Map<java.lang.Number, java.lang.String< and the key argument is a java.lang.Integer, the returned TypeDescriptor will be java.lang.Integer. * <p>If this describes a java.util.Map<?, ?> and the key argument is a java.lang.Integer, the returned TypeDescriptor will be java.lang.Integer as well. * <p>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 java.util.Map * @see #narrow(Object) */ 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. * <p>If the Map's value type is not parameterized, returns 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 java.util.Map */ public TypeDescriptor getMapValueTypeDescriptor() { assertMap(); return this.mapValueTypeDescriptor; } /** * If this type is a {@link Map}, creates a mapValue {@link TypeDescriptor} from the provided map value. * <p>Narrows the {@link #getMapValueTypeDescriptor() mapValueType} property to the class of the provided map value. * For example, if this describes a java.util.Map<java.lang.String, java.lang.Number< and the value argument is a java.lang.Integer, the returned TypeDescriptor will be java.lang.Integer. * If this describes a java.util.Map<?, ?> and the value argument is a java.lang.Integer, the returned TypeDescriptor will be java.lang.Integer as well. * <p>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 java.util.Map */ public TypeDescriptor getMapValueTypeDescriptor(Object mapValue) { return narrow(mapValue, getMapValueTypeDescriptor()); } /** * Returns the value of {@link TypeDescriptor#getType() getType()} for the {@link #getElementTypeDescriptor() elementTypeDescriptor}. * @deprecated in favor of {@link #getElementTypeDescriptor()} * @throws IllegalStateException if this type is not a java.util.Collection or Array type */ @Deprecated public Class<?> getElementType() { return getElementTypeDescriptor().getType(); } /** * Returns the value of {@link TypeDescriptor#getType() getType()} for the {@link #getMapKeyTypeDescriptor() getMapKeyTypeDescriptor}. * @deprecated in favor of {@link #getMapKeyTypeDescriptor()} * @throws IllegalStateException if this type is not a java.util.Map */ @Deprecated public Class<?> getMapKeyType() { return getMapKeyTypeDescriptor().getType(); } /** * Returns the value of {@link TypeDescriptor#getType() getType()} for the {@link #getMapValueTypeDescriptor() getMapValueTypeDescriptor}. * @deprecated in favor of {@link #getMapValueTypeDescriptor()} * @throws IllegalStateException if this type is not a java.util.Map */ @Deprecated public Class<?> getMapValueType() { return getMapValueTypeDescriptor().getType(); } // package private helpers TypeDescriptor(AbstractDescriptor descriptor) { this.type = descriptor.getType(); this.elementTypeDescriptor = descriptor.getElementTypeDescriptor(); this.mapKeyTypeDescriptor = descriptor.getMapKeyTypeDescriptor(); this.mapValueTypeDescriptor = descriptor.getMapValueTypeDescriptor(); this.annotations = descriptor.getAnnotations(); } static Annotation[] nullSafeAnnotations(Annotation[] annotations) { return annotations != null ? annotations : EMPTY_ANNOTATION_ARRAY; } // internal constructors private TypeDescriptor(Class<?> type) { this(new ClassDescriptor(type)); } private TypeDescriptor(Class<?> collectionType, TypeDescriptor elementTypeDescriptor) { this(collectionType, elementTypeDescriptor, null, null, EMPTY_ANNOTATION_ARRAY); } private TypeDescriptor(Class<?> mapType, TypeDescriptor keyTypeDescriptor, TypeDescriptor valueTypeDescriptor) { this(mapType, null, keyTypeDescriptor, valueTypeDescriptor, EMPTY_ANNOTATION_ARRAY); } private TypeDescriptor(Class<?> type, TypeDescriptor elementTypeDescriptor, TypeDescriptor mapKeyTypeDescriptor, TypeDescriptor mapValueTypeDescriptor, Annotation[] annotations) { this.type = type; this.elementTypeDescriptor = elementTypeDescriptor; this.mapKeyTypeDescriptor = mapKeyTypeDescriptor; this.mapValueTypeDescriptor = mapValueTypeDescriptor; this.annotations = annotations; } private static TypeDescriptor nested(AbstractDescriptor descriptor, int nestingLevel) { for (int i = 0; i < nestingLevel; i++) { descriptor = descriptor.nested(); if (descriptor == null) { return null; } } return new TypeDescriptor(descriptor); } // internal helpers private void assertCollectionOrArray() { if (!isCollection() && !isArray()) { throw new IllegalStateException("Not a java.util.Collection or Array"); } } private void assertMap() { if (!isMap()) { throw new IllegalStateException("Not a java.util.Map"); } } private TypeDescriptor narrow(Object value, TypeDescriptor typeDescriptor) { if (typeDescriptor != null) { return typeDescriptor.narrow(value); } else { return (value != null ? new TypeDescriptor(value.getClass(), null, null, null, this.annotations) : null); } } private boolean isNestedAssignable(TypeDescriptor nestedTypeDescriptor, TypeDescriptor otherNestedTypeDescriptor) { if (nestedTypeDescriptor == null || otherNestedTypeDescriptor == null) { return true; } return nestedTypeDescriptor.isAssignableTo(otherNestedTypeDescriptor); } private String wildcard(TypeDescriptor typeDescriptor) { return (typeDescriptor != null ? typeDescriptor.toString() : "?"); } public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof TypeDescriptor)) { return false; } TypeDescriptor other = (TypeDescriptor) obj; if (!ObjectUtils.nullSafeEquals(this.type, other.type)) { return false; } if (this.annotations.length != other.annotations.length) { return false; } for (Annotation ann : this.annotations) { if (other.getAnnotation(ann.annotationType()) == null) { return false; } } if (isCollection() || isArray()) { return ObjectUtils.nullSafeEquals(this.elementTypeDescriptor, other.elementTypeDescriptor); } else if (isMap()) { return ObjectUtils.nullSafeEquals(this.mapKeyTypeDescriptor, other.mapKeyTypeDescriptor) && ObjectUtils.nullSafeEquals(this.mapValueTypeDescriptor, other.mapValueTypeDescriptor); } else { return true; } } public int hashCode() { return getType().hashCode(); } public String toString() { StringBuilder builder = new StringBuilder(); for (Annotation ann : this.annotations) { builder.append("@").append(ann.annotationType().getName()).append(' '); } builder.append(ClassUtils.getQualifiedName(getType())); if (isMap()) { builder.append("<").append(wildcard(this.mapKeyTypeDescriptor)); builder.append(", ").append(wildcard(this.mapValueTypeDescriptor)).append(">"); } else if (isCollection()) { builder.append("<").append(wildcard(this.elementTypeDescriptor)).append(">"); } return builder.toString(); } }