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) {