SPR-8364
This commit is contained in:
parent
4a6101a697
commit
4d6a5849f7
|
|
@ -343,24 +343,33 @@ public class TypeDescriptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a copy of this nested type descriptor and apply the specific type information from the indexed object.
|
* Create a copy of this nested type descriptor and apply the specific type information from the indexed value, if necessary.
|
||||||
* Used to support collection and map indexing scenarios, where the indexer has a reference to the indexed type descriptor but needs to ensure its type actually represents the indexed object type.
|
* Used to support collection and map indexing scenarios, where the indexer has a reference to the indexed type descriptor but needs to ensure its type actually represents the indexed object type.
|
||||||
* This is necessary to support type conversion during index object binding operations.
|
* This is necessary to support type conversion during collection and map binding operations where generic information may not be available.
|
||||||
*/
|
*/
|
||||||
public TypeDescriptor applyIndexedObject(Object object) {
|
public TypeDescriptor applyIndexedObject(Object object) {
|
||||||
if (object == null) {
|
if (object == null) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
// TODO preserve binding context with returned copy
|
if (isCollection() && Object.class.equals(getElementType())) {
|
||||||
// TODO fall back to generic info if collection is empty
|
Collection<?> collection = (Collection<?>) object;
|
||||||
if (object instanceof Collection<?>) {
|
if (collection.size() > 0) {
|
||||||
return new TypeDescriptor(object.getClass(), CollectionUtils.findCommonElementType((Collection<?>) object));
|
return new TypeDescriptor(object.getClass(), CollectionUtils.findCommonElementType((Collection<?>) object), methodParameter, field, fieldNestingLevel, annotations);
|
||||||
|
} else {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (object instanceof Map<?, ?>) {
|
else if (isMap() && Object.class.equals(getMapKeyType()) && Object.class.equals(getMapValueType())) {
|
||||||
return new TypeDescriptor(object.getClass(), CollectionUtils.findCommonElementType(((Map<?, ?>) object).keySet()), CollectionUtils.findCommonElementType(((Map<?, ?>) object).values()));
|
Map<?, ?> map = (Map<?, ?>) object;
|
||||||
|
if (map.size() > 0) {
|
||||||
|
return new TypeDescriptor(object.getClass(), CollectionUtils.findCommonElementType(((Map<?, ?>) object).keySet()),
|
||||||
|
CollectionUtils.findCommonElementType(((Map<?, ?>) object).values()), methodParameter, field, fieldNestingLevel, annotations);
|
||||||
|
} else {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return valueOf(object.getClass());
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -487,6 +496,7 @@ public class TypeDescriptor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
private Class<?> resolveCollectionElementType() {
|
private Class<?> resolveCollectionElementType() {
|
||||||
if (this.methodParameter != null) {
|
if (this.methodParameter != null) {
|
||||||
return GenericCollectionTypeResolver.getCollectionParameterType(this.methodParameter);
|
return GenericCollectionTypeResolver.getCollectionParameterType(this.methodParameter);
|
||||||
|
|
@ -495,10 +505,11 @@ public class TypeDescriptor {
|
||||||
return GenericCollectionTypeResolver.getCollectionFieldType(this.field, this.fieldNestingLevel);
|
return GenericCollectionTypeResolver.getCollectionFieldType(this.field, this.fieldNestingLevel);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return GenericCollectionTypeResolver.getCollectionType((Class<? extends Collection>) this.type);
|
return GenericCollectionTypeResolver.getCollectionType((Class<? extends Collection<?>>) this.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
private Class<?> resolveMapKeyType() {
|
private Class<?> resolveMapKeyType() {
|
||||||
if (this.methodParameter != null) {
|
if (this.methodParameter != null) {
|
||||||
return GenericCollectionTypeResolver.getMapKeyParameterType(this.methodParameter);
|
return GenericCollectionTypeResolver.getMapKeyParameterType(this.methodParameter);
|
||||||
|
|
@ -507,10 +518,11 @@ public class TypeDescriptor {
|
||||||
return GenericCollectionTypeResolver.getMapKeyFieldType(this.field, this.fieldNestingLevel);
|
return GenericCollectionTypeResolver.getMapKeyFieldType(this.field, this.fieldNestingLevel);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return GenericCollectionTypeResolver.getMapKeyType((Class<? extends Map>) this.type);
|
return GenericCollectionTypeResolver.getMapKeyType((Class<? extends Map<?, ?>>) this.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
private Class<?> resolveMapValueType() {
|
private Class<?> resolveMapValueType() {
|
||||||
if (this.methodParameter != null) {
|
if (this.methodParameter != null) {
|
||||||
return GenericCollectionTypeResolver.getMapValueParameterType(this.methodParameter);
|
return GenericCollectionTypeResolver.getMapValueParameterType(this.methodParameter);
|
||||||
|
|
@ -519,7 +531,7 @@ public class TypeDescriptor {
|
||||||
return GenericCollectionTypeResolver.getMapValueFieldType(this.field, this.fieldNestingLevel);
|
return GenericCollectionTypeResolver.getMapValueFieldType(this.field, this.fieldNestingLevel);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return GenericCollectionTypeResolver.getMapValueType((Class<? extends Map>) this.type);
|
return GenericCollectionTypeResolver.getMapValueType((Class<? extends Map<?, ?>>) this.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -550,7 +562,7 @@ public class TypeDescriptor {
|
||||||
}
|
}
|
||||||
this.elementType = TypeDescriptor.valueOf(elementType);
|
this.elementType = TypeDescriptor.valueOf(elementType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private TypeDescriptor(Class<?> mapType, Class<?> keyType, Class<?> valueType) {
|
private TypeDescriptor(Class<?> mapType, Class<?> keyType, Class<?> valueType) {
|
||||||
this.type = mapType;
|
this.type = mapType;
|
||||||
if (keyType == null) {
|
if (keyType == null) {
|
||||||
|
|
@ -563,6 +575,22 @@ public class TypeDescriptor {
|
||||||
this.mapValueType = TypeDescriptor.valueOf(valueType);
|
this.mapValueType = TypeDescriptor.valueOf(valueType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TypeDescriptor(Class<?> collectionType, Class<?> elementType, MethodParameter methodParameter, Field field, int fieldNestingLevel, Annotation[] annotations) {
|
||||||
|
this(collectionType, elementType);
|
||||||
|
this.methodParameter = methodParameter;
|
||||||
|
this.field = field;
|
||||||
|
this.fieldNestingLevel = fieldNestingLevel;
|
||||||
|
this.annotations = annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeDescriptor(Class<?> mapType, Class<?> keyType, Class<?> valueType, MethodParameter methodParameter, Field field, int fieldNestingLevel, Annotation[] annotations) {
|
||||||
|
this(mapType, keyType, valueType);
|
||||||
|
this.methodParameter = methodParameter;
|
||||||
|
this.field = field;
|
||||||
|
this.fieldNestingLevel = fieldNestingLevel;
|
||||||
|
this.annotations = annotations;
|
||||||
|
}
|
||||||
|
|
||||||
private TypeDescriptor(Class<?> nestedType, Field field, int nestingLevel) {
|
private TypeDescriptor(Class<?> nestedType, Field field, int nestingLevel) {
|
||||||
this.type = nestedType;
|
this.type = nestedType;
|
||||||
this.field = field;
|
this.field = field;
|
||||||
|
|
|
||||||
|
|
@ -66,17 +66,24 @@ final class MapToMapConverter implements ConditionalGenericConverter {
|
||||||
if (source == null) {
|
if (source == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Map<?, ?> sourceMap = (Map<?, ?>) source;
|
Map<Object, Object> sourceMap = (Map<Object, Object>) source;
|
||||||
Map targetMap = CollectionFactory.createMap(targetType.getType(), sourceMap.size());
|
Map<Object, Object> targetMap = CollectionFactory.createMap(targetType.getType(), sourceMap.size());
|
||||||
for (Object entry : sourceMap.entrySet()) {
|
TypeDescriptor targetKeyType = targetType.getMapKeyTypeDescriptor();
|
||||||
Map.Entry sourceMapEntry = (Map.Entry) entry;
|
TypeDescriptor targetValueType = targetType.getMapValueTypeDescriptor();
|
||||||
Object sourceKey = sourceMapEntry.getKey();
|
if (Object.class.equals(targetKeyType.getType()) && Object.class.equals(targetValueType.getType())) {
|
||||||
Object sourceValue = sourceMapEntry.getValue();
|
for (Map.Entry<Object, Object> entry : sourceMap.entrySet()) {
|
||||||
Object targetKey = this.conversionService.convert(sourceKey, sourceType.getMapKeyTypeDescriptor().applyIndexedObject(sourceKey), targetType.getMapKeyTypeDescriptor());
|
targetMap.put(entry.getKey(), entry.getValue());
|
||||||
Object targetValue = this.conversionService.convert(sourceValue, sourceType.getMapValueTypeDescriptor().applyIndexedObject(sourceValue), targetType.getMapValueTypeDescriptor());
|
}
|
||||||
targetMap.put(targetKey, targetValue);
|
} else {
|
||||||
|
for (Map.Entry<Object, Object> entry : sourceMap.entrySet()) {
|
||||||
|
Object sourceKey = entry.getKey();
|
||||||
|
Object sourceValue = entry.getValue();
|
||||||
|
Object targetKey = this.conversionService.convert(sourceKey, sourceType.getMapKeyTypeDescriptor().applyIndexedObject(sourceKey), targetType.getMapKeyTypeDescriptor());
|
||||||
|
Object targetValue = this.conversionService.convert(sourceValue, sourceType.getMapValueTypeDescriptor().applyIndexedObject(sourceValue), targetType.getMapValueTypeDescriptor());
|
||||||
|
targetMap.put(targetKey, targetValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return targetMap;
|
return targetMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -412,35 +412,6 @@ public class GenericConversionServiceTests {
|
||||||
assertNull(conversionService.convert(list, sourceType, targetType));
|
assertNull(conversionService.convert(list, sourceType, targetType));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void emptyMapToMap() throws Exception {
|
|
||||||
conversionService.addConverter(new MapToMapConverter(conversionService));
|
|
||||||
conversionService.addConverterFactory(new StringToNumberConverterFactory());
|
|
||||||
Map<String, String> map = new HashMap<String, String>();
|
|
||||||
TypeDescriptor sourceType = TypeDescriptor.forObject(map);
|
|
||||||
TypeDescriptor targetType = new TypeDescriptor(getClass().getField("emptyMapTarget"));
|
|
||||||
assertTrue(conversionService.canConvert(sourceType, targetType));
|
|
||||||
assertEquals(map, conversionService.convert(map, sourceType, targetType));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, String> emptyMapTarget;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void emptyMapToMapDifferentTargetType() throws Exception {
|
|
||||||
conversionService.addConverter(new MapToMapConverter(conversionService));
|
|
||||||
conversionService.addConverterFactory(new StringToNumberConverterFactory());
|
|
||||||
Map<String, String> map = new HashMap<String, String>();
|
|
||||||
TypeDescriptor sourceType = TypeDescriptor.forObject(map);
|
|
||||||
TypeDescriptor targetType = new TypeDescriptor(getClass().getField("emptyMapDifferentTarget"));
|
|
||||||
assertTrue(conversionService.canConvert(sourceType, targetType));
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
LinkedHashMap<String, String> result = (LinkedHashMap<String, String>) conversionService.convert(map, sourceType, targetType);
|
|
||||||
assertEquals(map, result);
|
|
||||||
assertEquals(LinkedHashMap.class, result.getClass());
|
|
||||||
}
|
|
||||||
|
|
||||||
public LinkedHashMap<String, String> emptyMapDifferentTarget;
|
|
||||||
|
|
||||||
private interface MyBaseInterface {
|
private interface MyBaseInterface {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
package org.springframework.core.convert.support;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.springframework.core.convert.TypeDescriptor;
|
||||||
|
|
||||||
|
public class MapToMapConverterTests {
|
||||||
|
|
||||||
|
private GenericConversionService conversionService = new GenericConversionService();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
conversionService.addConverter(new MapToMapConverter(conversionService));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void scalarMap() throws Exception {
|
||||||
|
Map<String, String> map = new HashMap<String, String>();
|
||||||
|
map.put("1", "9");
|
||||||
|
map.put("2", "37");
|
||||||
|
TypeDescriptor sourceType = TypeDescriptor.forObject(map);
|
||||||
|
TypeDescriptor targetType = new TypeDescriptor(getClass().getField("scalarMapTarget"));
|
||||||
|
assertFalse(conversionService.canConvert(sourceType, targetType));
|
||||||
|
conversionService.addConverterFactory(new StringToNumberConverterFactory());
|
||||||
|
assertTrue(conversionService.canConvert(sourceType, targetType));
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<Integer, Integer> result = (Map<Integer, Integer>) conversionService.convert(map, sourceType, targetType);
|
||||||
|
assertFalse(map.equals(result));
|
||||||
|
assertEquals((Integer) 9, result.get(1));
|
||||||
|
assertEquals((Integer) 37, result.get(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Integer, Integer> scalarMapTarget;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void scalarMapNotGenericTarget() throws Exception {
|
||||||
|
Map<String, String> map = new HashMap<String, String>();
|
||||||
|
map.put("1", "9");
|
||||||
|
map.put("2", "37");
|
||||||
|
assertTrue(conversionService.canConvert(Map.class, Map.class));
|
||||||
|
assertEquals(map, conversionService.convert(map, Map.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void collectionMap() throws Exception {
|
||||||
|
Map<String, List<String>> map = new HashMap<String, List<String>>();
|
||||||
|
map.put("1", Arrays.asList("9", "12"));
|
||||||
|
map.put("2", Arrays.asList("37", "23"));
|
||||||
|
TypeDescriptor sourceType = TypeDescriptor.forObject(map);
|
||||||
|
TypeDescriptor targetType = new TypeDescriptor(getClass().getField("collectionMapTarget"));
|
||||||
|
assertFalse(conversionService.canConvert(sourceType, targetType));
|
||||||
|
conversionService.addConverter(new CollectionToCollectionConverter(conversionService));
|
||||||
|
conversionService.addConverterFactory(new StringToNumberConverterFactory());
|
||||||
|
assertTrue(conversionService.canConvert(sourceType, targetType));
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<Integer, List<Integer>> result = (Map<Integer, List<Integer>>) conversionService.convert(map, sourceType, targetType);
|
||||||
|
assertFalse(map.equals(result));
|
||||||
|
assertEquals(Arrays.asList(9, 12), result.get(1));
|
||||||
|
assertEquals(Arrays.asList(37, 23), result.get(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Integer, List<Integer>> collectionMapTarget;
|
||||||
|
|
||||||
|
public Map<String, List<String>> sourceCollectionMapTarget;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void collectionMapSourceTarget() throws Exception {
|
||||||
|
Map<String, List<String>> map = new HashMap<String, List<String>>();
|
||||||
|
map.put("1", Arrays.asList("9", "12"));
|
||||||
|
map.put("2", Arrays.asList("37", "23"));
|
||||||
|
TypeDescriptor sourceType = new TypeDescriptor(getClass().getField("sourceCollectionMapTarget"));
|
||||||
|
TypeDescriptor targetType = new TypeDescriptor(getClass().getField("collectionMapTarget"));
|
||||||
|
assertFalse(conversionService.canConvert(sourceType, targetType));
|
||||||
|
conversionService.addConverter(new CollectionToCollectionConverter(conversionService));
|
||||||
|
conversionService.addConverterFactory(new StringToNumberConverterFactory());
|
||||||
|
assertTrue(conversionService.canConvert(sourceType, targetType));
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<Integer, List<Integer>> result = (Map<Integer, List<Integer>>) conversionService.convert(map, sourceType, targetType);
|
||||||
|
assertFalse(map.equals(result));
|
||||||
|
assertEquals(Arrays.asList(9, 12), result.get(1));
|
||||||
|
assertEquals(Arrays.asList(37, 23), result.get(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void collectionMapNotGenericTarget() throws Exception {
|
||||||
|
Map<String, List<String>> map = new HashMap<String, List<String>>();
|
||||||
|
map.put("1", Arrays.asList("9", "12"));
|
||||||
|
map.put("2", Arrays.asList("37", "23"));
|
||||||
|
assertTrue(conversionService.canConvert(Map.class, Map.class));
|
||||||
|
assertEquals(map, conversionService.convert(map, Map.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void collectionMapNotGenericTargetCollectionToObjectInteraction() throws Exception {
|
||||||
|
Map<String, List<String>> map = new HashMap<String, List<String>>();
|
||||||
|
map.put("1", Arrays.asList("9", "12"));
|
||||||
|
map.put("2", Arrays.asList("37", "23"));
|
||||||
|
conversionService.addConverter(new CollectionToObjectConverter(conversionService));
|
||||||
|
assertTrue(conversionService.canConvert(Map.class, Map.class));
|
||||||
|
assertEquals(map, conversionService.convert(map, Map.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void emptyMap() throws Exception {
|
||||||
|
Map<String, String> map = new HashMap<String, String>();
|
||||||
|
TypeDescriptor sourceType = TypeDescriptor.forObject(map);
|
||||||
|
TypeDescriptor targetType = new TypeDescriptor(getClass().getField("emptyMapTarget"));
|
||||||
|
assertTrue(conversionService.canConvert(sourceType, targetType));
|
||||||
|
assertEquals(map, conversionService.convert(map, sourceType, targetType));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> emptyMapTarget;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void emptyMapNoTargetGenericInfo() throws Exception {
|
||||||
|
Map<String, String> map = new HashMap<String, String>();
|
||||||
|
assertTrue(conversionService.canConvert(Map.class, Map.class));
|
||||||
|
assertEquals(map, conversionService.convert(map, Map.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void emptyMapDifferentTargetImplType() throws Exception {
|
||||||
|
Map<String, String> map = new HashMap<String, String>();
|
||||||
|
TypeDescriptor sourceType = TypeDescriptor.forObject(map);
|
||||||
|
TypeDescriptor targetType = new TypeDescriptor(getClass().getField("emptyMapDifferentTarget"));
|
||||||
|
assertTrue(conversionService.canConvert(sourceType, targetType));
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
LinkedHashMap<String, String> result = (LinkedHashMap<String, String>) conversionService.convert(map, sourceType, targetType);
|
||||||
|
assertEquals(map, result);
|
||||||
|
assertEquals(LinkedHashMap.class, result.getClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
public LinkedHashMap<String, String> emptyMapDifferentTarget;
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue