From 9be5a95948bfc6d974f5f39dd945a2d335bfe9b4 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 18 Oct 2022 18:02:38 +0100 Subject: [PATCH] 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 --- .../graphql/data/GraphQlArgumentBinder.java | 44 +++++++++++++++--- .../data/GraphQlArgumentBinderTests.java | 45 +++++++++++++++++++ 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java b/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java index b99aef13..ca306179 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java @@ -71,7 +71,7 @@ public class GraphQlArgumentBinder { private final BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor(); - private List> dataBinderInitializers = new ArrayList<>(); + private final List> dataBinderInitializers = new ArrayList<>(); public GraphQlArgumentBinder() { @@ -160,7 +160,7 @@ public class GraphQlArgumentBinder { // From Map if (rawValue instanceof Map) { - Object target = createValue((Map) rawValue, targetClass, bindingResult, segments); + Object target = createValue((Map) 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) rawValue, elementClass, bindingResult, segments)); + collection.add((T) createValueOrNull((Map) 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 rawMap, Class targetType, BindingResult result, Stack segments) { + Map rawMap, ResolvableType targetType, BindingResult result, Stack segments) { try { return createValue(rawMap, targetType, result, segments); @@ -242,11 +243,40 @@ public class GraphQlArgumentBinder { @SuppressWarnings("unchecked") private Object createValue( - Map rawMap, Class targetType, BindingResult bindingResult, + Map rawMap, ResolvableType targetType, BindingResult bindingResult, Stack 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 map = CollectionFactory.createMap(targetClass, rawMap.size()); + for (Map.Entry 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) 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) rawValue, type, bindingResult, segments); args[i] = (isOptional ? Optional.ofNullable(value) : value); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java index 60bd142c..dead81ad 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java @@ -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 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 map; + + public PrimaryConstructorItemMapBean(Map map) { + this.map = map; + } + + public Map getMap() { + return this.map; + } + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") static class PrimaryConstructorOptionalItemBean {