Support list/map/array constructor data binding

See gh-32426
This commit is contained in:
rstoyanchev
2024-06-19 16:42:56 +01:00
parent f9af5d400d
commit e48cbc5ba8
2 changed files with 165 additions and 11 deletions

View File

@@ -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 <V> List<V> createList(
String paramPath, Class<?> paramType, ResolvableType type, ValueResolver valueResolver) {
ResolvableType elementType = type.getNested(2);
SortedSet<Integer> indexes = getIndexes(paramPath, valueResolver);
if (indexes == null) {
return null;
}
int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1 : 0);
List<V> list = (List<V>) 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 <V> Map<String, V> createMap(
String paramPath, Class<?> paramType, ResolvableType type, ValueResolver valueResolver) {
ResolvableType elementType = type.getNested(2);
Map<String, V> 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> V[] createArray(String paramPath, ResolvableType type, ValueResolver valueResolver) {
ResolvableType elementType = type.getNested(2);
SortedSet<Integer> 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<Integer> getIndexes(String paramPath, ValueResolver valueResolver) {
SortedSet<Integer> 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) {

View File

@@ -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<DataClass> 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<String, DataClass> 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<DataClass> dataClassList) {
}
private final Map<String, Object> map;
private MapValueResolver(Map<String, Object> map) {
this.map = map;
}
private record MapDataClass(Map<String, DataClass> dataClassMap) {
}
private record ArrayDataClass(DataClass[] dataClassArray) {
}
private record MapValueResolver(Map<String, Object> map) implements DataBinder.ValueResolver {
@Override
public Object resolveValue(String name, Class<?> type) {