Support binding to Map as target value

Before this commit, we supported binding to a higher level object,
or a scalar, or a List of either, but not to a Map of either, which
requires similar support as what we have for a List, i.e. creating
the target Map and populating it in a recursive manner.

This commit adds that support.

Closes gh-449
This commit is contained in:
rstoyanchev
2022-10-18 18:02:38 +01:00
parent 13683eea17
commit 9be5a95948
2 changed files with 82 additions and 7 deletions

View File

@@ -71,7 +71,7 @@ public class GraphQlArgumentBinder {
private final BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor();
private List<Consumer<DataBinder>> dataBinderInitializers = new ArrayList<>();
private final List<Consumer<DataBinder>> dataBinderInitializers = new ArrayList<>();
public GraphQlArgumentBinder() {
@@ -160,7 +160,7 @@ public class GraphQlArgumentBinder {
// From Map
if (rawValue instanceof Map) {
Object target = createValue((Map<String, Object>) rawValue, targetClass, bindingResult, segments);
Object target = createValue((Map<String, Object>) rawValue, targetType, bindingResult, segments);
return wrapAsOptionalIfNecessary(target, targetType);
}
@@ -204,6 +204,7 @@ public class GraphQlArgumentBinder {
return Collections.emptyList();
}
ResolvableType elementType = collectionType.asCollection().getGeneric(0);
Class<?> elementClass = collectionType.asCollection().getGeneric(0).resolve();
if (elementClass == null) {
bindingResult.rejectValue(toArgumentPath(segments), "unknownElementType", "Unknown element type");
@@ -218,7 +219,7 @@ public class GraphQlArgumentBinder {
collection.add((T) rawValue);
}
else if (rawValue instanceof Map) {
collection.add((T) createValueOrNull((Map<String, Object>) rawValue, elementClass, bindingResult, segments));
collection.add((T) createValueOrNull((Map<String, Object>) rawValue, elementType, bindingResult, segments));
}
else {
collection.add((T) convertValue(rawValue, elementClass, bindingResult, segments));
@@ -230,7 +231,7 @@ public class GraphQlArgumentBinder {
@Nullable
private Object createValueOrNull(
Map<String, Object> rawMap, Class<?> targetType, BindingResult result, Stack<String> segments) {
Map<String, Object> rawMap, ResolvableType targetType, BindingResult result, Stack<String> segments) {
try {
return createValue(rawMap, targetType, result, segments);
@@ -242,11 +243,40 @@ public class GraphQlArgumentBinder {
@SuppressWarnings("unchecked")
private Object createValue(
Map<String, Object> rawMap, Class<?> targetType, BindingResult bindingResult,
Map<String, Object> rawMap, ResolvableType targetType, BindingResult bindingResult,
Stack<String> segments) throws BindException {
Class<?> targetClass = targetType.resolve();
Assert.notNull(targetClass, "Unknown target class");
if (Map.class.isAssignableFrom(targetClass)) {
ResolvableType valueType = targetType.asMap().getGeneric(1);
Class<?> valueClass = valueType.resolve();
if (valueClass == null) {
bindingResult.rejectValue(toArgumentPath(segments), "unknownMapValueType", "Unknown Map value type");
return Collections.emptyMap();
}
Map<String, Object> map = CollectionFactory.createMap(targetClass, rawMap.size());
for (Map.Entry<String, Object> entry : rawMap.entrySet()) {
Object rawValue = entry.getValue();
segments.push("[" + entry.getKey() + "]");
if (rawValue == null || valueType.isAssignableFrom(rawValue.getClass())) {
map.put(entry.getKey(), entry.getValue());
}
else if (rawValue instanceof Map) {
map.put(entry.getKey(), createValueOrNull(
(Map<String, Object>) rawValue, valueType, bindingResult, segments));
}
else {
map.put(entry.getKey(), convertValue(rawValue, valueClass, bindingResult, segments));
}
segments.pop();
}
return map;
}
Object target;
Constructor<?> ctor = BeanUtils.getResolvableConstructor(targetType);
Constructor<?> ctor = BeanUtils.getResolvableConstructor(targetClass);
// Default constructor + data binding via properties
@@ -293,7 +323,7 @@ public class GraphQlArgumentBinder {
}
else if (rawValue instanceof Map) {
boolean isOptional = (paramTypes[i] == Optional.class);
Class<?> type = (isOptional ? methodParam.nestedIfOptional().getNestedParameterType() : paramTypes[i]);
ResolvableType type = ResolvableType.forMethodParameter(methodParam.nestedIfOptional());
Object value = createValueOrNull((Map<String, Object>) rawValue, type, bindingResult, segments);
args[i] = (isOptional ? Optional.ofNullable(value) : value);
}

View File

@@ -280,6 +280,37 @@ class GraphQlArgumentBinderTests {
});
}
@Test
void primaryConstructorWithMapArgument() throws Exception {
Object result = this.binder.bind(
environment(
"{\"key\":{" +
"\"map\":{" +
"\"item1\":{" +
"\"name\":\"Jason\"," +
"\"age\":\"21\"" +
"}," +
"\"item2\":{" +
"\"name\":\"James\"," +
"\"age\":\"22\"" +
"}" +
"}}}"),
"key",
ResolvableType.forClass(PrimaryConstructorItemMapBean.class));
assertThat(result).isNotNull().isInstanceOf(PrimaryConstructorItemMapBean.class);
Map<String, Item> map = ((PrimaryConstructorItemMapBean) result).getMap();
Item item1 = map.get("item1");
assertThat(item1.getName()).isEqualTo("Jason");
assertThat(item1.getAge()).isEqualTo(21);
Item item2 = map.get("item2");
assertThat(item2.getName()).isEqualTo("James");
assertThat(item2.getAge()).isEqualTo(22);
}
@Test // gh-447
@SuppressWarnings("unchecked")
void primaryConstructorWithGenericObject() throws Exception {
@@ -428,6 +459,20 @@ class GraphQlArgumentBinderTests {
}
static class PrimaryConstructorItemMapBean {
private final Map<String, Item> map;
public PrimaryConstructorItemMapBean(Map<String, Item> map) {
this.map = map;
}
public Map<String, Item> getMap() {
return this.map;
}
}
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
static class PrimaryConstructorOptionalItemBean {