diff --git a/spring-context/src/main/java/org/springframework/validation/DataBinder.java b/spring-context/src/main/java/org/springframework/validation/DataBinder.java index 8702cc52f8..ee94dc55d3 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -18,11 +18,11 @@ package org.springframework.validation; import java.beans.PropertyEditor; import java.lang.annotation.Annotation; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -30,6 +30,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.function.Predicate; import org.apache.commons.logging.Log; @@ -49,6 +51,7 @@ import org.springframework.beans.PropertyValues; import org.springframework.beans.SimpleTypeConverter; import org.springframework.beans.TypeConverter; import org.springframework.beans.TypeMismatchException; +import org.springframework.core.CollectionFactory; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; @@ -948,11 +951,24 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { String paramPath = nestedPath + lookupName; Class paramType = paramTypes[i]; + ResolvableType resolvableType = ResolvableType.forMethodParameter(param); + Object value = valueResolver.resolveValue(paramPath, paramType); + if (value == null) { + if (List.class.isAssignableFrom(paramType)) { + value = createList(paramPath, paramType, resolvableType, valueResolver); + } + else if (Map.class.isAssignableFrom(paramType)) { + value = createMap(paramPath, paramType, resolvableType, valueResolver); + } + else if (paramType.isArray()) { + value = createArray(paramPath, resolvableType, valueResolver); + } + } + if (value == null && shouldConstructArgument(param) && hasValuesFor(paramPath, valueResolver)) { - ResolvableType type = ResolvableType.forMethodParameter(param); - args[i] = createObject(type, paramPath + ".", valueResolver); + args[i] = createObject(resolvableType, paramPath + ".", valueResolver); } else { try { @@ -1019,9 +1035,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { */ protected boolean shouldConstructArgument(MethodParameter param) { Class type = param.nestedIfOptional().getNestedParameterType(); - return !(BeanUtils.isSimpleValueType(type) || - Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type) || type.isArray() || - type.getPackageName().startsWith("java.")); + return !BeanUtils.isSimpleValueType(type) && !type.getPackageName().startsWith("java."); } private boolean hasValuesFor(String paramPath, ValueResolver resolver) { @@ -1033,6 +1047,82 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { return false; } + @SuppressWarnings("unchecked") + @Nullable + private List createList( + String paramPath, Class paramType, ResolvableType type, ValueResolver valueResolver) { + + ResolvableType elementType = type.getNested(2); + SortedSet indexes = getIndexes(paramPath, valueResolver); + if (indexes == null) { + return null; + } + int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1 : 0); + List list = (List) CollectionFactory.createCollection(paramType, size); + indexes.forEach(i -> list.add(null)); + for (int index : indexes) { + list.set(index, (V) createObject(elementType, paramPath + "[" + index + "].", valueResolver)); + } + return list; + } + + @SuppressWarnings("unchecked") + @Nullable + private Map createMap( + String paramPath, Class paramType, ResolvableType type, ValueResolver valueResolver) { + + ResolvableType elementType = type.getNested(2); + Map map = null; + for (String name : valueResolver.getNames()) { + if (!name.startsWith(paramPath + "[")) { + continue; + } + int startIdx = paramPath.length() + 1; + int endIdx = name.indexOf(']', startIdx); + String nestedPath = name.substring(0, endIdx + 2); + boolean quoted = (endIdx - startIdx > 2 && name.charAt(startIdx) == '\'' && name.charAt(endIdx - 1) == '\''); + String key = (quoted ? name.substring(startIdx + 1, endIdx - 1) : name.substring(startIdx, endIdx)); + if (map == null) { + map = CollectionFactory.createMap(paramType, 16); + } + if (!map.containsKey(key)) { + map.put(key, (V) createObject(elementType, nestedPath, valueResolver)); + } + } + return map; + } + + @SuppressWarnings("unchecked") + @Nullable + private V[] createArray(String paramPath, ResolvableType type, ValueResolver valueResolver) { + ResolvableType elementType = type.getNested(2); + SortedSet indexes = getIndexes(paramPath, valueResolver); + if (indexes == null) { + return null; + } + int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1: 0); + V[] array = (V[]) Array.newInstance(elementType.resolve(), size); + for (int index : indexes) { + array[index] = (V) createObject(elementType, paramPath + "[" + index + "].", valueResolver); + } + return array; + } + + @Nullable + private static SortedSet getIndexes(String paramPath, ValueResolver valueResolver) { + SortedSet indexes = null; + for (String name : valueResolver.getNames()) { + if (name.startsWith(paramPath + "[")) { + int endIndex = name.indexOf(']', paramPath.length() + 2); + String rawIndex = name.substring(paramPath.length() + 1, endIndex); + int index = Integer.parseInt(rawIndex); + indexes = (indexes != null ? indexes : new TreeSet<>()); + indexes.add(index); + } + } + return indexes; + } + private void validateConstructorArgument( Class constructorClass, String nestedPath, String name, @Nullable Object value) { diff --git a/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java b/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java index 9eb00934de..4816643270 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java @@ -17,6 +17,7 @@ package org.springframework.validation; import java.beans.ConstructorProperties; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -101,6 +102,63 @@ class DataBinderConstructTests { assertThat(bindingResult.getFieldValue("param3")).isNull(); } + @Test + void listBinding() { + MapValueResolver valueResolver = new MapValueResolver(Map.of( + "dataClassList[0].param1", "value1", "dataClassList[0].param2", "true", + "dataClassList[1].param1", "value2", "dataClassList[1].param2", "true", + "dataClassList[2].param1", "value3", "dataClassList[2].param2", "true")); + + DataBinder binder = initDataBinder(ListDataClass.class); + binder.construct(valueResolver); + + ListDataClass dataClass = getTarget(binder); + List list = dataClass.dataClassList(); + + assertThat(list).hasSize(3); + assertThat(list.get(0).param1()).isEqualTo("value1"); + assertThat(list.get(1).param1()).isEqualTo("value2"); + assertThat(list.get(2).param1()).isEqualTo("value3"); + } + + @Test + void mapBinding() { + MapValueResolver valueResolver = new MapValueResolver(Map.of( + "dataClassMap[a].param1", "value1", "dataClassMap[a].param2", "true", + "dataClassMap[b].param1", "value2", "dataClassMap[b].param2", "true", + "dataClassMap['c'].param1", "value3", "dataClassMap['c'].param2", "true")); + + DataBinder binder = initDataBinder(MapDataClass.class); + binder.construct(valueResolver); + + MapDataClass dataClass = getTarget(binder); + Map map = dataClass.dataClassMap(); + + assertThat(map).hasSize(3); + assertThat(map.get("a").param1()).isEqualTo("value1"); + assertThat(map.get("b").param1()).isEqualTo("value2"); + assertThat(map.get("c").param1()).isEqualTo("value3"); + } + + @Test + void arrayBinding() { + MapValueResolver valueResolver = new MapValueResolver(Map.of( + "dataClassArray[0].param1", "value1", "dataClassArray[0].param2", "true", + "dataClassArray[1].param1", "value2", "dataClassArray[1].param2", "true", + "dataClassArray[2].param1", "value3", "dataClassArray[2].param2", "true")); + + DataBinder binder = initDataBinder(ArrayDataClass.class); + binder.construct(valueResolver); + + ArrayDataClass dataClass = getTarget(binder); + DataClass[] array = dataClass.dataClassArray(); + + assertThat(array).hasSize(3); + assertThat(array[0].param1()).isEqualTo("value1"); + assertThat(array[1].param1()).isEqualTo("value2"); + assertThat(array[2].param1()).isEqualTo("value3"); + } + @SuppressWarnings("SameParameterValue") private static DataBinder initDataBinder(Class targetType) { DataBinder binder = new DataBinder(null); @@ -172,13 +230,19 @@ class DataBinderConstructTests { } - private static class MapValueResolver implements DataBinder.ValueResolver { + private record ListDataClass(List dataClassList) { + } - private final Map map; - private MapValueResolver(Map map) { - this.map = map; - } + private record MapDataClass(Map dataClassMap) { + } + + + private record ArrayDataClass(DataClass[] dataClassArray) { + } + + + private record MapValueResolver(Map map) implements DataBinder.ValueResolver { @Override public Object resolveValue(String name, Class type) {