Support list/map/array constructor data binding
See gh-32426
This commit is contained in:
@@ -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) {
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user