Skip to content

Commit 5db1687

Browse files
author
Keith Donald
committed
added TypeDescriptor resolveCollectionElement and Map key/value types
1 parent 9c3c1c6 commit 5db1687

File tree

14 files changed

+269
-146
lines changed

14 files changed

+269
-146
lines changed

org.springframework.core/src/main/java/org/springframework/core/convert/TypeDescriptor.java

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,13 @@
3636
*/
3737
public class TypeDescriptor {
3838

39+
static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0];
40+
3941
/** Constant defining a TypeDescriptor for a <code>null</code> value */
4042
public static final TypeDescriptor NULL = new TypeDescriptor();
4143

4244
private static final Map<Class<?>, TypeDescriptor> typeDescriptorCache = new HashMap<Class<?>, TypeDescriptor>();
4345

44-
static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0];
45-
4646
static {
4747
typeDescriptorCache.put(boolean.class, new TypeDescriptor(boolean.class));
4848
typeDescriptorCache.put(Boolean.class, new TypeDescriptor(Boolean.class));
@@ -237,21 +237,21 @@ public Class<?> getType() {
237237
* Returns the Object wrapper type if the underlying type is a primitive.
238238
*/
239239
public Class<?> getObjectType() {
240-
return ClassUtils.resolvePrimitiveIfNecessary(getType());
240+
return getType() != null ? ClassUtils.resolvePrimitiveIfNecessary(getType()) : null;
241241
}
242242

243243
/**
244244
* Returns the name of this type: the fully qualified class name.
245245
*/
246246
public String getName() {
247-
return ClassUtils.getQualifiedName(getType());
247+
return getType() != null ? ClassUtils.getQualifiedName(getType()) : null;
248248
}
249249

250250
/**
251251
* Is this type a primitive type?
252252
*/
253253
public boolean isPrimitive() {
254-
return getType().isPrimitive();
254+
return getType() != null && getType().isPrimitive();
255255
}
256256

257257
/**
@@ -304,60 +304,83 @@ else if (isMap() && targetType.isMap()) {
304304
* Is this type a {@link Collection} type?
305305
*/
306306
public boolean isCollection() {
307-
return Collection.class.isAssignableFrom(getType());
307+
return getType() != null && Collection.class.isAssignableFrom(getType());
308308
}
309309

310310
/**
311311
* Is this type an array type?
312312
*/
313313
public boolean isArray() {
314-
return getType().isArray();
314+
return getType() != null && getType().isArray();
315315
}
316316

317317
/**
318318
* If this type is a {@link Collection} or array, returns the underlying element type.
319-
* Returns <code>null</code> if this type is neither an array or collection.
320319
* Returns Object.class if this type is a collection and the element type was not explicitly declared.
321320
* @return the map element type, or <code>null</code> if not a collection or array.
321+
* @throws IllegalStateException if this descriptor is not for a java.util.Collection or Array
322322
*/
323323
public Class<?> getElementType() {
324324
return getElementTypeDescriptor().getType();
325325
}
326326

327327
/**
328328
* The collection or array element type as a type descriptor.
329-
* Returns {@link TypeDescriptor#NULL} if this type is not a collection or an array.
330329
* Returns TypeDescriptor.valueOf(Object.class) if this type is a collection and the element type is not explicitly declared.
330+
* @throws IllegalStateException if this descriptor is not for a java.util.Collection or Array
331331
*/
332332
public TypeDescriptor getElementTypeDescriptor() {
333+
if (!isCollection() && !isArray()) {
334+
throw new IllegalStateException("Not a java.util.Collection or Array");
335+
}
333336
return this.elementType;
334337
}
335338

339+
/**
340+
* Returns a copy of this type descriptor that has its elementType populated from the specified Collection.
341+
* This property will be set by calculating the "common element type" of the specified Collection.
342+
* For example, if the collection contains String elements, the returned TypeDescriptor will have its elementType set to String.
343+
* This method is designed to be used when converting values read from Collection fields or method return values that are not parameterized e.g. Collection vs. Collection<String>
344+
* In this scenario the elementType will be Object.class before invoking this method.
345+
* @param colection the collection to derive the elementType from
346+
* @return a new TypeDescriptor with the resolved elementType property
347+
* @throws IllegalArgumentException if this is not a type descriptor for a java.util.Collection.
348+
*/
349+
public TypeDescriptor resolveCollectionElementType(Collection<?> collection) {
350+
if (!isCollection()) {
351+
throw new IllegalStateException("Not a java.util.Collection");
352+
}
353+
return new TypeDescriptor(type, CommonElement.typeDescriptor(collection), mapKeyType, mapValueType, annotations);
354+
}
355+
336356
// map type descriptor operations
337357

338358
/**
339359
* Is this type a {@link Map} type?
340360
*/
341361
public boolean isMap() {
342-
return Map.class.isAssignableFrom(getType());
362+
return getType() != null && Map.class.isAssignableFrom(getType());
343363
}
344364

345365
/**
346366
* If this type is a {@link Map}, returns the underlying key type.
347-
* Returns <code>null</code> if this type is not map.
348367
* Returns Object.class if this type is a map and its key type was not explicitly declared.
349368
* @return the map key type, or <code>null</code> if not a map.
369+
* @throws IllegalStateException if this descriptor is not for a java.util.Map
350370
*/
351371
public Class<?> getMapKeyType() {
352372
return getMapKeyTypeDescriptor().getType();
353373
}
354374

355375
/**
356376
* The map key type as a type descriptor.
357-
* Returns {@link TypeDescriptor#NULL} if this type is not a map.
358377
* Returns TypeDescriptor.valueOf(Object.class) if this type is a map and the key type is not explicitly declared.
378+
* @throws IllegalStateException if this descriptor is not for a java.util.Map
359379
*/
360380
public TypeDescriptor getMapKeyTypeDescriptor() {
381+
if (!isMap()) {
382+
throw new IllegalStateException("Not a map");
383+
}
361384
return this.mapKeyType;
362385
}
363386

@@ -366,40 +389,63 @@ public TypeDescriptor getMapKeyTypeDescriptor() {
366389
* Returns <code>null</code> if this type is not map.
367390
* Returns Object.class if this type is a map and its value type was not explicitly declared.
368391
* @return the map value type, or <code>null</code> if not a map.
392+
* @throws IllegalStateException if this descriptor is not for a java.util.Map
369393
*/
370394
public Class<?> getMapValueType() {
371395
return getMapValueTypeDescriptor().getType();
372396
}
373397

374398
/**
375399
* The map value type as a type descriptor.
376-
* Returns {@link TypeDescriptor#NULL} if this type is not a map.
377400
* Returns TypeDescriptor.valueOf(Object.class) if this type is a map and the value type is not explicitly declared.
401+
* @throws IllegalStateException if this descriptor is not for a java.util.Map
378402
*/
379403
public TypeDescriptor getMapValueTypeDescriptor() {
404+
if (!isMap()) {
405+
throw new IllegalStateException("Not a map");
406+
}
380407
return this.mapValueType;
381408
}
409+
410+
/**
411+
* Returns a copy of this type descriptor that has its mapKeyType and mapValueType properties populated from the specified Map.
412+
* These properties will be set by calculating the "common element type" of the specified Map's keySet and values collection.
413+
* For example, if the Map contains String keys and Integer values, the returned TypeDescriptor will have its mapKeyType set to String and its mapValueType to Integer.
414+
* This method is designed to be used when converting values read from Map fields or method return values that are not parameterized e.g. Map vs. Map<String, Integer>.
415+
* In this scenario the key and value types will be Object.class before invoking this method.
416+
* @param map the map to derive key and value types from
417+
* @return a new TypeDescriptor with the resolved mapKeyType and mapValueType properties
418+
* @throws IllegalArgumentException if this is not a type descriptor for a java.util.Map.
419+
*/
420+
public TypeDescriptor resolveMapKeyValueTypes(Map<?, ?> map) {
421+
if (!isMap()) {
422+
throw new IllegalStateException("Not a java.util.Map");
423+
}
424+
return new TypeDescriptor(type, elementType, CommonElement.typeDescriptor(map.keySet()), CommonElement.typeDescriptor(map.values()), annotations);
425+
}
382426

383427
// extending Object
384428

385429
public boolean equals(Object obj) {
386430
if (this == obj) {
387431
return true;
388432
}
389-
if (!(obj instanceof TypeDescriptor) || obj == TypeDescriptor.NULL) {
433+
if (!(obj instanceof TypeDescriptor)) {
390434
return false;
391435
}
392436
TypeDescriptor other = (TypeDescriptor) obj;
393-
boolean annotatedTypeEquals = getType().equals(other.getType()) && ObjectUtils.nullSafeEquals(getAnnotations(), other.getAnnotations());
394-
if (isCollection()) {
395-
return annotatedTypeEquals && ObjectUtils.nullSafeEquals(getElementType(), other.getElementType());
437+
boolean annotatedTypeEquals = ObjectUtils.nullSafeEquals(getType(), other.getType()) && ObjectUtils.nullSafeEquals(getAnnotations(), other.getAnnotations());
438+
if (!annotatedTypeEquals) {
439+
return false;
440+
}
441+
if (isCollection() || isArray()) {
442+
return ObjectUtils.nullSafeEquals(getElementType(), other.getElementType());
396443
}
397444
else if (isMap()) {
398-
return annotatedTypeEquals && ObjectUtils.nullSafeEquals(getMapKeyType(), other.getMapKeyType()) &&
399-
ObjectUtils.nullSafeEquals(getMapValueType(), other.getMapValueType());
445+
return ObjectUtils.nullSafeEquals(getMapKeyType(), other.getMapKeyType()) && ObjectUtils.nullSafeEquals(getMapValueType(), other.getMapValueType());
400446
}
401447
else {
402-
return annotatedTypeEquals;
448+
return true;
403449
}
404450
}
405451

@@ -440,11 +486,11 @@ else if (isCollection()) {
440486
}
441487

442488
TypeDescriptor(Class<?> collectionType, TypeDescriptor elementType) {
443-
this(collectionType, elementType, TypeDescriptor.NULL, TypeDescriptor.NULL);
489+
this(collectionType, elementType, TypeDescriptor.NULL, TypeDescriptor.NULL, EMPTY_ANNOTATION_ARRAY);
444490
}
445491

446492
TypeDescriptor(Class<?> mapType, TypeDescriptor keyType, TypeDescriptor valueType) {
447-
this(mapType, TypeDescriptor.NULL, keyType, valueType);
493+
this(mapType, TypeDescriptor.NULL, keyType, valueType, EMPTY_ANNOTATION_ARRAY);
448494
}
449495

450496
static Annotation[] nullSafeAnnotations(Annotation[] annotations) {
@@ -458,15 +504,15 @@ private TypeDescriptor(Class<?> type) {
458504
}
459505

460506
private TypeDescriptor() {
461-
this(null, TypeDescriptor.NULL, TypeDescriptor.NULL, TypeDescriptor.NULL);
507+
this(null, TypeDescriptor.NULL, TypeDescriptor.NULL, TypeDescriptor.NULL, EMPTY_ANNOTATION_ARRAY);
462508
}
463509

464-
private TypeDescriptor(Class<?> type, TypeDescriptor elementType, TypeDescriptor mapKeyType, TypeDescriptor mapValueType) {
510+
private TypeDescriptor(Class<?> type, TypeDescriptor elementType, TypeDescriptor mapKeyType, TypeDescriptor mapValueType, Annotation[] annotations) {
465511
this.type = type;
466512
this.elementType = elementType;
467513
this.mapKeyType = mapKeyType;
468514
this.mapValueType = mapValueType;
469-
this.annotations = EMPTY_ANNOTATION_ARRAY;
515+
this.annotations = annotations;
470516
}
471517

472518
// internal helpers
@@ -477,5 +523,5 @@ private static TypeDescriptor nested(AbstractDescriptor descriptor, int nestingL
477523
}
478524
return new TypeDescriptor(descriptor);
479525
}
480-
526+
481527
}

org.springframework.core/src/main/java/org/springframework/core/convert/support/CollectionToArrayConverter.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,11 @@ public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor t
5757
return null;
5858
}
5959
Collection<?> sourceCollection = (Collection<?>) source;
60-
Object array = Array.newInstance(targetType.getElementType(), sourceCollection.size());
60+
TypeDescriptor targetElementType = targetType.getElementTypeDescriptor();
61+
Object array = Array.newInstance(targetElementType.getType(), sourceCollection.size());
6162
int i = 0;
6263
for (Object sourceElement : sourceCollection) {
63-
Object targetElement = this.conversionService.convert(sourceElement, sourceType.getElementTypeDescriptor(), targetType.getElementTypeDescriptor());
64+
Object targetElement = this.conversionService.convert(sourceElement, sourceType.getElementTypeDescriptor(), targetElementType);
6465
Array.set(array, i++, targetElement);
6566
}
6667
return array;

org.springframework.core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -167,36 +167,24 @@ public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor t
167167
logger.debug("Converting value " + StylerUtils.style(source) + " of " + sourceType + " to " + targetType);
168168
}
169169
if (sourceType == TypeDescriptor.NULL) {
170-
Assert.isTrue(source == null, "The value must be null if sourceType == TypeDescriptor.NULL");
171-
Object result = convertNullSource(sourceType, targetType);
172-
if (result == null) {
173-
assertNotPrimitiveTargetType(sourceType, targetType);
174-
}
175-
if (logger.isDebugEnabled()) {
176-
logger.debug("Converted to " + StylerUtils.style(result));
177-
}
178-
return result;
170+
Assert.isTrue(source == null, "The source must be [null] if sourceType == [null]");
171+
return handleResult(sourceType, targetType, convertNullSource(sourceType, targetType));
179172
}
180173
if (targetType == TypeDescriptor.NULL) {
181174
logger.debug("Converted to null");
182175
return null;
183176
}
184-
Assert.isTrue(source == null || sourceType.getObjectType().isInstance(source));
185-
GenericConverter converter = getConverter(sourceType, targetType);
186-
if (converter == null) {
187-
return handleConverterNotFound(source, sourceType, targetType);
177+
if (source != null && !sourceType.getObjectType().isInstance(source)) {
178+
throw new IllegalArgumentException("The source to convert from must be an instance of " + sourceType + "; instead it was a " + source.getClass().getName());
188179
}
189-
Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
190-
if (result == null) {
191-
assertNotPrimitiveTargetType(sourceType, targetType);
192-
}
193-
if (logger.isDebugEnabled()) {
194-
logger.debug("Converted to " + StylerUtils.style(result));
180+
GenericConverter converter = getConverter(sourceType, targetType);
181+
if (converter != null) {
182+
return handleResult(sourceType, targetType, ConversionUtils.invokeConverter(converter, source, sourceType, targetType));
183+
} else {
184+
return handleConverterNotFound(source, sourceType, targetType);
195185
}
196-
return result;
197186
}
198187

199-
200188
public String toString() {
201189
List<String> converterStrings = new ArrayList<String>();
202190
for (Map<Class<?>, MatchableConverters> targetConverters : this.converters.values()) {
@@ -325,7 +313,7 @@ private Map<Class<?>, MatchableConverters> getSourceConverterMap(Class<?> source
325313
}
326314

327315
private void assertNotNull(TypeDescriptor sourceType, TypeDescriptor targetType) {
328-
Assert.notNull(sourceType, "The sourceType to convert to is required");
316+
Assert.notNull(sourceType, "The sourceType to convert from is required");
329317
Assert.notNull(targetType, "The targetType to convert to is required");
330318
}
331319

@@ -537,6 +525,15 @@ private Object handleConverterNotFound(Object source, TypeDescriptor sourceType,
537525
}
538526
}
539527

528+
private Object handleResult(TypeDescriptor sourceType, TypeDescriptor targetType, Object result) {
529+
if (result == null) {
530+
assertNotPrimitiveTargetType(sourceType, targetType);
531+
}
532+
if (logger.isDebugEnabled()) {
533+
logger.debug("Converted to " + StylerUtils.style(result));
534+
}
535+
return result;
536+
}
540537
private void assertNotPrimitiveTargetType(TypeDescriptor sourceType, TypeDescriptor targetType) {
541538
if (targetType.isPrimitive()) {
542539
throw new ConversionFailedException(sourceType, targetType, null,

org.springframework.core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ public Set<ConvertiblePair> getConvertibleTypes() {
4848
}
4949

5050
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
51-
Class<?> source = sourceType.getObjectType();
52-
Class<?> target = targetType.getObjectType();
53-
return (!source.equals(target) && hasValueOfMethodOrConstructor(target, source));
51+
Class<?> source = sourceType.getType();
52+
Class<?> target = targetType.getType();
53+
return !source.equals(target) && hasValueOfMethodOrConstructor(target, source);
5454
}
5555

5656
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
57-
Class<?> sourceClass = sourceType.getObjectType();
58-
Class<?> targetClass = targetType.getObjectType();
57+
Class<?> sourceClass = sourceType.getType();
58+
Class<?> targetClass = targetType.getType();
5959
Method method = getValueOfMethodOn(targetClass, sourceClass);
6060
try {
6161
if (method != null) {
@@ -79,9 +79,8 @@ public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor t
7979
") method or Constructor(" + sourceClass.getName() + ") exists on " + targetClass.getName());
8080
}
8181

82-
8382
public static boolean hasValueOfMethodOrConstructor(Class<?> targetClass, Class<?> sourceClass) {
84-
return (getValueOfMethodOn(targetClass, sourceClass) != null || getConstructor(targetClass, sourceClass) != null);
83+
return getValueOfMethodOn(targetClass, sourceClass) != null || getConstructor(targetClass, sourceClass) != null;
8584
}
8685

8786
private static Method getValueOfMethodOn(Class<?> targetClass, Class<?> sourceClass) {

org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToArrayConverter.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,7 @@ public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor t
5656
Object target = Array.newInstance(targetType.getElementType(), fields.length);
5757
for (int i = 0; i < fields.length; i++) {
5858
String sourceElement = fields[i];
59-
Object targetElement = this.conversionService.convert(sourceElement.trim(),
60-
sourceType, targetType.getElementTypeDescriptor());
59+
Object targetElement = this.conversionService.convert(sourceElement.trim(), sourceType, targetType.getElementTypeDescriptor());
6160
Array.set(target, i, targetElement);
6261
}
6362
return target;

org.springframework.core/src/main/java/org/springframework/core/convert/support/StringToCharacterConverter.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ public Character convert(String source) {
3232
}
3333
if (source.length() > 1) {
3434
throw new IllegalArgumentException(
35-
"Can only convert a [String] with length of 1 to a [Character]; string value '" + source
36-
+ "' has length of " + source.length());
35+
"Can only convert a [String] with length of 1 to a [Character]; string value '" + source + "' has length of " + source.length());
3736
}
3837
return source.charAt(0);
3938
}

0 commit comments

Comments
 (0)