Skip to content

Commit 27b0403

Browse files
committed
ConversionService is able to deal with empty collections and nested collections (fixed regression; SPR-7289, SPR-7293); ConversionService properly handles nested Resource arrays in Map values (fixed regression; SPR-7295); ConversionService does not accidentally use copy constructor for same type (SPR-7304)
1 parent 7f91153 commit 27b0403

File tree

8 files changed

+110
-41
lines changed

8 files changed

+110
-41
lines changed

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

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,12 @@
3939
*/
4040
public class TypeDescriptor {
4141

42-
/** Constant defining an 'unknown type' TypeDescriptor */
42+
/** Constant defining a TypeDescriptor for a <code>null</code> value */
4343
public static final TypeDescriptor NULL = new TypeDescriptor();
4444

45+
/** Constant defining a TypeDescriptor for 'unknown type' */
46+
public static final TypeDescriptor UNKNOWN = new TypeDescriptor(Object.class);
47+
4548
private static final Map<Class<?>, TypeDescriptor> typeDescriptorCache = new HashMap<Class<?>, TypeDescriptor>();
4649

4750
private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0];
@@ -140,7 +143,7 @@ private TypeDescriptor() {
140143
* Create a new descriptor for the type of the given value.
141144
* <p>Use this constructor when a conversion point comes from a source such as a Map or
142145
* Collection, where no additional context is available but elements can be introspected.
143-
* @param type the actual type to wrap
146+
* @param value the value to determine the actual type from
144147
*/
145148
private TypeDescriptor(Object value) {
146149
Assert.notNull(value, "Value must not be null");
@@ -263,7 +266,7 @@ public synchronized TypeDescriptor getElementTypeDescriptor() {
263266
*/
264267
public TypeDescriptor getElementTypeDescriptor(Object element) {
265268
TypeDescriptor elementType = getElementTypeDescriptor();
266-
return (elementType != TypeDescriptor.NULL ? elementType : forObject(element));
269+
return (elementType != TypeDescriptor.UNKNOWN ? elementType : forObject(element));
267270
}
268271

269272
/**
@@ -306,7 +309,7 @@ public synchronized TypeDescriptor getMapKeyTypeDescriptor() {
306309
*/
307310
public TypeDescriptor getMapKeyTypeDescriptor(Object key) {
308311
TypeDescriptor keyType = getMapKeyTypeDescriptor();
309-
return keyType != TypeDescriptor.NULL ? keyType : TypeDescriptor.forObject(key);
312+
return (keyType != TypeDescriptor.UNKNOWN ? keyType : TypeDescriptor.forObject(key));
310313
}
311314

312315
/**
@@ -335,7 +338,7 @@ public synchronized TypeDescriptor getMapValueTypeDescriptor() {
335338
*/
336339
public TypeDescriptor getMapValueTypeDescriptor(Object value) {
337340
TypeDescriptor valueType = getMapValueTypeDescriptor();
338-
return (valueType != TypeDescriptor.NULL ? valueType : TypeDescriptor.forObject(value));
341+
return (valueType != TypeDescriptor.UNKNOWN ? valueType : TypeDescriptor.forObject(value));
339342
}
340343

341344
/**
@@ -390,11 +393,8 @@ else if (isMap() && targetType.isMap()) {
390393
* @return the type descriptor
391394
*/
392395
public TypeDescriptor forElementType(Class<?> elementType) {
393-
if (getType().equals(elementType)) {
394-
return this;
395-
}
396-
else if (elementType == null) {
397-
return TypeDescriptor.NULL;
396+
if (elementType == null) {
397+
return TypeDescriptor.UNKNOWN;
398398
}
399399
else if (this.methodParameter != null) {
400400
return new TypeDescriptor(this.methodParameter, elementType);
@@ -408,29 +408,29 @@ else if (this.field != null) {
408408
}
409409

410410
public boolean equals(Object obj) {
411-
if (!(obj instanceof TypeDescriptor)) {
412-
return false;
413-
}
414-
TypeDescriptor td = (TypeDescriptor) obj;
415-
if (this == td) {
411+
if (this == obj) {
416412
return true;
417413
}
414+
if (!(obj instanceof TypeDescriptor) || obj == TypeDescriptor.NULL) {
415+
return false;
416+
}
417+
TypeDescriptor other = (TypeDescriptor) obj;
418418
boolean annotatedTypeEquals =
419-
getType().equals(td.getType()) && ObjectUtils.nullSafeEquals(getAnnotations(), td.getAnnotations());
419+
getType().equals(other.getType()) && ObjectUtils.nullSafeEquals(getAnnotations(), other.getAnnotations());
420420
if (isCollection()) {
421-
return annotatedTypeEquals && ObjectUtils.nullSafeEquals(getElementType(), td.getElementType());
421+
return annotatedTypeEquals && ObjectUtils.nullSafeEquals(getElementType(), other.getElementType());
422422
}
423423
else if (isMap()) {
424-
return annotatedTypeEquals && ObjectUtils.nullSafeEquals(getMapKeyType(), td.getMapKeyType()) &&
425-
ObjectUtils.nullSafeEquals(getMapValueType(), td.getMapValueType());
424+
return annotatedTypeEquals && ObjectUtils.nullSafeEquals(getMapKeyType(), other.getMapKeyType()) &&
425+
ObjectUtils.nullSafeEquals(getMapValueType(), other.getMapValueType());
426426
}
427427
else {
428428
return annotatedTypeEquals;
429429
}
430430
}
431431

432432
public int hashCode() {
433-
return getType().hashCode();
433+
return (this == TypeDescriptor.NULL ? 0 : getType().hashCode());
434434
}
435435

436436
/**

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2009 the original author or authors.
2+
* Copyright 2002-2010 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -58,9 +58,14 @@ public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor t
5858
return null;
5959
}
6060
Collection<?> sourceCollection = (Collection<?>) source;
61+
if (sourceCollection.isEmpty()) {
62+
return sourceCollection;
63+
}
6164
Collection target = CollectionFactory.createCollection(targetType.getType(), sourceCollection.size());
6265
for (Object sourceElement : sourceCollection) {
63-
Object targetElement = this.conversionService.convert(sourceElement, sourceType.getElementTypeDescriptor(sourceElement), targetType.getElementTypeDescriptor(sourceElement));
66+
Object targetElement = this.conversionService.convert(sourceElement,
67+
sourceType.getElementTypeDescriptor(sourceElement),
68+
targetType.getElementTypeDescriptor(sourceElement));
6469
target.add(targetElement);
6570
}
6671
return target;

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2009 the original author or authors.
2+
* Copyright 2002-2010 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -62,7 +62,8 @@ public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor t
6262
if (i > 0) {
6363
string.append(DELIMITER);
6464
}
65-
Object targetElement = this.conversionService.convert(sourceElement, sourceType.getElementTypeDescriptor(sourceElement), targetType);
65+
Object targetElement = this.conversionService.convert(
66+
sourceElement, sourceType.getElementTypeDescriptor(sourceElement), targetType);
6667
string.append(targetElement);
6768
i++;
6869
}

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

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2009 the original author or authors.
2+
* Copyright 2002-2010 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.core.convert.support;
1818

19-
import java.util.Collection;
2019
import java.util.Map;
2120

2221
import org.springframework.core.convert.ConversionFailedException;
@@ -41,15 +40,6 @@ public static Object invokeConverter(GenericConverter converter, Object source,
4140
}
4241
}
4342

44-
public static TypeDescriptor getElementType(Collection<?> collection) {
45-
for (Object element : collection) {
46-
if (element != null) {
47-
return TypeDescriptor.valueOf(element.getClass());
48-
}
49-
}
50-
return TypeDescriptor.NULL;
51-
}
52-
5343
public static TypeDescriptor[] getMapEntryTypes(Map<?, ?> sourceMap) {
5444
Class<?> keyType = null;
5545
Class<?> valueType = null;

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescripto
244244
if (logger.isTraceEnabled()) {
245245
logger.trace("Matched cached converter " + converter);
246246
}
247-
return converter != NO_MATCH ? converter : null;
247+
return (converter != NO_MATCH ? converter : null);
248248
}
249249
else {
250250
converter = findConverterForClassPair(sourceType, targetType);
@@ -394,6 +394,7 @@ private Map<Class<?>, MatchableConverters> getTargetConvertersForSource(Class<?>
394394

395395
private GenericConverter getMatchingConverterForTarget(TypeDescriptor sourceType, TypeDescriptor targetType,
396396
Map<Class<?>, MatchableConverters> converters) {
397+
397398
Class<?> targetObjectType = targetType.getObjectType();
398399
if (targetObjectType.isInterface()) {
399400
LinkedList<Class<?>> classQueue = new LinkedList<Class<?>>();

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2009 the original author or authors.
2+
* Copyright 2002-2010 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -59,13 +59,20 @@ public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor t
5959
return null;
6060
}
6161
Map<?, ?> sourceMap = (Map<?, ?>) source;
62+
if (sourceMap.isEmpty()) {
63+
return sourceMap;
64+
}
6265
Map targetMap = CollectionFactory.createMap(targetType.getType(), sourceMap.size());
6366
for (Object entry : sourceMap.entrySet()) {
6467
Map.Entry sourceMapEntry = (Map.Entry) entry;
6568
Object sourceKey = sourceMapEntry.getKey();
6669
Object sourceValue = sourceMapEntry.getValue();
67-
Object targetKey = this.conversionService.convert(sourceKey, sourceType.getMapKeyTypeDescriptor(sourceKey), targetType.getMapKeyTypeDescriptor(sourceKey));
68-
Object targetValue = this.conversionService.convert(sourceValue, sourceType.getMapValueTypeDescriptor(sourceValue), targetType.getMapValueTypeDescriptor(sourceValue));
70+
Object targetKey = this.conversionService.convert(sourceKey,
71+
sourceType.getMapKeyTypeDescriptor(sourceKey),
72+
targetType.getMapKeyTypeDescriptor(sourceKey));
73+
Object targetValue = this.conversionService.convert(sourceValue,
74+
sourceType.getMapValueTypeDescriptor(sourceValue),
75+
targetType.getMapValueTypeDescriptor(sourceValue));
6976
targetMap.put(targetKey, targetValue);
7077
}
7178
return targetMap;

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,11 @@
4242
* @since 3.0
4343
*/
4444
final class ObjectToObjectConverter implements ConditionalGenericConverter {
45-
45+
4646
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
47-
return hasValueOfMethodOrConstructor(targetType.getObjectType(), sourceType.getObjectType());
47+
Class source = sourceType.getObjectType();
48+
Class target = targetType.getObjectType();
49+
return (!source.equals(target) && hasValueOfMethodOrConstructor(target, source));
4850
}
4951

5052
public Set<ConvertiblePair> getConvertibleTypes() {

org.springframework.core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.core.convert.support;
1818

1919
import java.util.ArrayList;
20+
import java.util.Collections;
2021
import java.util.HashMap;
2122
import java.util.LinkedHashMap;
2223
import java.util.LinkedList;
@@ -225,6 +226,57 @@ public void testWildcardMap() throws Exception {
225226
assertEquals(input, converted);
226227
}
227228

229+
@Test
230+
public void testListOfList() {
231+
GenericConversionService service = ConversionServiceFactory.createDefaultConversionService();
232+
List<List<String>> list = Collections.singletonList(Collections.singletonList("Foo"));
233+
assertNotNull(service.convert(list, String.class));
234+
}
235+
236+
@Test
237+
public void testEmptyList() {
238+
GenericConversionService service = ConversionServiceFactory.createDefaultConversionService();
239+
List list = Collections.emptyList();
240+
List result = service.convert(list, List.class);
241+
assertSame(list, result);
242+
result = service.convert(list, list.getClass());
243+
assertSame(list, result);
244+
}
245+
246+
@Test
247+
public void testEmptyMap() {
248+
GenericConversionService service = ConversionServiceFactory.createDefaultConversionService();
249+
Map map = Collections.emptyMap();
250+
Map result = service.convert(map, Map.class);
251+
assertSame(map, result);
252+
result = service.convert(map, map.getClass());
253+
assertSame(map, result);
254+
}
255+
256+
@Test
257+
public void testStringToString() {
258+
GenericConversionService service = ConversionServiceFactory.createDefaultConversionService();
259+
String value = "myValue";
260+
String result = service.convert(value, String.class);
261+
assertSame(value, result);
262+
}
263+
264+
@Test
265+
public void testStringToObject() {
266+
GenericConversionService service = ConversionServiceFactory.createDefaultConversionService();
267+
String value = "myValue";
268+
Object result = service.convert(value, Object.class);
269+
assertSame(value, result);
270+
}
271+
272+
@Test
273+
public void testIgnoreCopyConstructor() {
274+
GenericConversionService service = ConversionServiceFactory.createDefaultConversionService();
275+
WithCopyConstructor value = new WithCopyConstructor();
276+
Object result = service.convert(value, WithCopyConstructor.class);
277+
assertSame(value, result);
278+
}
279+
228280
@Test
229281
public void testPerformance1() {
230282
GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService();
@@ -296,6 +348,7 @@ public void testPerformance3() throws Exception {
296348

297349
public static Map<String, Integer> map;
298350

351+
299352
private interface MyBaseInterface {
300353

301354
}
@@ -319,6 +372,16 @@ public String convert(MyBaseInterface source) {
319372
}
320373

321374

375+
public static class WithCopyConstructor {
376+
377+
public WithCopyConstructor() {
378+
}
379+
380+
public WithCopyConstructor(WithCopyConstructor value) {
381+
}
382+
}
383+
384+
322385
public static Map<String, ?> wildcardMap;
323386

324387
}

0 commit comments

Comments
 (0)