Skip to content

Commit 02b16bd

Browse files
committed
Polish "Support contructor binding for input arguments"
Prior to this commit, only default constructors were supported for instantiating input argument types. This commit adds a new `GraphQlInstantiator` class that manages the instantiation and binding of data fetching environment arguments. Default constructors and primary constructors are now handled. Also, List-like properties were not bound from the data fetching environment arguments to the `MutablePropertyValues` used for binding. This commit ensures that List elements are bound to the property values with array-like property paths. Fixes gh-139 Fixes gh-141
1 parent 2f5aa58 commit 02b16bd

File tree

7 files changed

+333
-126
lines changed

7 files changed

+333
-126
lines changed

build.gradle

+20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
plugins {
22
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
3+
id 'org.jetbrains.kotlin.jvm' version '1.5.31' apply false
34
}
45

56
ext {
@@ -35,13 +36,32 @@ configure(moduleProjects) {
3536
targetCompatibility = JavaVersion.VERSION_1_8
3637
}
3738

39+
pluginManager.withPlugin("kotlin") {
40+
compileKotlin {
41+
kotlinOptions {
42+
jvmTarget = "1.8"
43+
languageVersion = "1.3"
44+
apiVersion = "1.3"
45+
freeCompilerArgs = ["-Xjsr305=strict", "-Xsuppress-version-warnings", "-Xopt-in=kotlin.RequiresOptIn"]
46+
allWarningsAsErrors = true
47+
}
48+
}
49+
compileTestKotlin {
50+
kotlinOptions {
51+
jvmTarget = "1.8"
52+
freeCompilerArgs = ["-Xjsr305=strict"]
53+
}
54+
}
55+
}
56+
3857
dependencyManagement {
3958
imports {
4059
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.5"
4160
mavenBom "io.projectreactor:reactor-bom:2020.0.10"
4261
mavenBom "org.springframework:spring-framework-bom:5.3.9"
4362
mavenBom "org.springframework.data:spring-data-bom:2021.0.4"
4463
mavenBom "org.springframework.security:spring-security-bom:5.5.2"
64+
mavenBom "org.jetbrains.kotlin:kotlin-bom:1.5.31"
4565
mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2"
4666
mavenBom "org.junit:junit-bom:5.7.2"
4767
}

spring-graphql/build.gradle

+3-16
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
plugins {
2-
id 'org.jetbrains.kotlin.jvm' version '1.5.31'
3-
}
41
description = "GraphQL Support for Spring Applications"
52

3+
apply plugin: "kotlin"
4+
65
dependencies {
76
api 'com.graphql-java:graphql-java'
87
api 'io.projectreactor:reactor-core'
@@ -21,6 +20,7 @@ dependencies {
2120

2221
compileOnly 'com.google.code.findbugs:jsr305'
2322
compileOnly 'org.jetbrains.kotlinx:kotlinx-coroutines-core'
23+
compileOnly 'org.jetbrains.kotlin:kotlin-stdlib'
2424

2525
testImplementation 'org.junit.jupiter:junit-jupiter'
2626
testImplementation 'org.assertj:assertj-core'
@@ -46,16 +46,3 @@ test {
4646
events "passed", "skipped", "failed"
4747
}
4848
}
49-
repositories {
50-
mavenCentral()
51-
}
52-
compileKotlin {
53-
kotlinOptions {
54-
jvmTarget = "1.8"
55-
}
56-
}
57-
compileTestKotlin {
58-
kotlinOptions {
59-
jvmTarget = "1.8"
60-
}
61-
}

spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolver.java

+8-73
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,12 @@
1515
*/
1616
package org.springframework.graphql.data.method.annotation.support;
1717

18-
import java.lang.reflect.Constructor;
1918
import java.util.Collection;
20-
import java.util.Iterator;
21-
import java.util.List;
2219
import java.util.Map;
2320
import java.util.Optional;
24-
import java.util.Stack;
2521

2622
import graphql.schema.DataFetchingEnvironment;
2723

28-
import org.springframework.beans.BeanUtils;
29-
import org.springframework.beans.MutablePropertyValues;
3024
import org.springframework.core.CollectionFactory;
3125
import org.springframework.core.MethodParameter;
3226
import org.springframework.core.convert.TypeDescriptor;
@@ -48,6 +42,8 @@
4842
*/
4943
public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentResolver {
5044

45+
private final GraphQlArgumentInstantiator instantiator = new GraphQlArgumentInstantiator();
46+
5147
@Override
5248
public boolean supportsParameter(MethodParameter parameter) {
5349
return parameter.getParameterAnnotation(Argument.class) != null;
@@ -78,10 +74,7 @@ public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment
7874
if (annotation.required()) {
7975
throw new MissingArgumentException(name, parameter);
8076
}
81-
if (parameterType.getType().equals(Optional.class)) {
82-
return Optional.empty();
83-
}
84-
return null;
77+
returnValue(rawValue, parameterType.getType());
8578
}
8679

8780
if (CollectionFactory.isApproximableCollectionType(rawValue.getClass())) {
@@ -100,40 +93,17 @@ public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment
10093
}
10194

10295
private Object returnValue(Object value, Class<?> parameterType) {
103-
return (parameterType.equals(Optional.class) ? Optional.of(value) : value);
96+
if (parameterType.equals(Optional.class)) {
97+
return Optional.ofNullable(value);
98+
}
99+
return value;
104100
}
105101

106102
@SuppressWarnings("unchecked")
107103
private Object convert(Object rawValue, Class<?> targetType) {
108104
Object target;
109105
if (rawValue instanceof Map) {
110-
Constructor<?> ctor = BeanUtils.getResolvableConstructor(targetType);
111-
MutablePropertyValues propertyValues = extractPropertyValues((Map) rawValue);
112-
113-
if (ctor.getParameterCount() == 0) {
114-
target = BeanUtils.instantiateClass(ctor);
115-
DataBinder dataBinder = new DataBinder(target);
116-
dataBinder.bind(propertyValues);
117-
} else {
118-
// Data class constructor
119-
DataBinder binder = new DataBinder(null);
120-
String[] paramNames = BeanUtils.getParameterNames(ctor);
121-
Class<?>[] paramTypes = ctor.getParameterTypes();
122-
Object[] args = new Object[paramTypes.length];
123-
for (int i = 0; i < paramNames.length; i++) {
124-
String paramName = paramNames[i];
125-
Object value = propertyValues.get(paramName);
126-
value = (value instanceof List ? ((List<?>) value).toArray() : value);
127-
MethodParameter methodParam = new MethodParameter(ctor, i);
128-
if (value == null && methodParam.isOptional()) {
129-
args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null);
130-
}
131-
else {
132-
args[i] = binder.convertIfNecessary(value, paramTypes[i], methodParam);
133-
}
134-
}
135-
target = BeanUtils.instantiateClass(ctor, args);
136-
}
106+
target = this.instantiator.instantiate(targetType, (Map<String, Object>) rawValue);
137107
}
138108
else if (targetType.isAssignableFrom(rawValue.getClass())) {
139109
return returnValue(rawValue, targetType);
@@ -148,39 +118,4 @@ else if (targetType.isAssignableFrom(rawValue.getClass())) {
148118
return target;
149119
}
150120

151-
private MutablePropertyValues extractPropertyValues(Map<String, Object> arguments) {
152-
MutablePropertyValues mpvs = new MutablePropertyValues();
153-
Stack<String> path = new Stack<>();
154-
visitArgumentMap(arguments, mpvs, path);
155-
return mpvs;
156-
}
157-
158-
@SuppressWarnings("unchecked")
159-
private void visitArgumentMap(Map<String, Object> arguments, MutablePropertyValues mpvs, Stack<String> path) {
160-
for (String key : arguments.keySet()) {
161-
path.push(key);
162-
Object value = arguments.get(key);
163-
if (value instanceof Map) {
164-
visitArgumentMap((Map<String, Object>) value, mpvs, path);
165-
}
166-
else {
167-
String propertyName = pathToPropertyName(path);
168-
mpvs.add(propertyName, value);
169-
}
170-
path.pop();
171-
}
172-
}
173-
174-
private String pathToPropertyName(Stack<String> path) {
175-
StringBuilder sb = new StringBuilder();
176-
Iterator<String> it = path.iterator();
177-
while (it.hasNext()) {
178-
sb.append(it.next());
179-
if (it.hasNext()) {
180-
sb.append(".");
181-
}
182-
}
183-
return sb.toString();
184-
}
185-
186121
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright 2020-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.data.method.annotation.support;
18+
19+
import java.lang.reflect.Constructor;
20+
import java.util.HashMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.Optional;
24+
import java.util.Stack;
25+
26+
import org.springframework.beans.BeanUtils;
27+
import org.springframework.beans.MutablePropertyValues;
28+
import org.springframework.core.MethodParameter;
29+
import org.springframework.validation.DataBinder;
30+
31+
/**
32+
* Instantiate a target type and bind data from
33+
* {@link graphql.schema.DataFetchingEnvironment} arguments.
34+
*
35+
* @author Brian Clozel
36+
*/
37+
class GraphQlArgumentInstantiator {
38+
39+
/**
40+
* Instantiate the given target type and bind data from
41+
* {@link graphql.schema.DataFetchingEnvironment} arguments.
42+
* <p>This is considering the default constructor or a primary constructor
43+
* if available.
44+
*
45+
* @param targetType the type of the argument to instantiate
46+
* @param arguments the data fetching environment arguments
47+
* @param <T> the type of the input argument
48+
* @return the instantiated and populated input argument.
49+
* @throws IllegalStateException if there is no suitable constructor.
50+
*/
51+
@SuppressWarnings("unchecked")
52+
public <T> T instantiate(Class<T> targetType, Map<String, Object> arguments) {
53+
Object target;
54+
Constructor<?> ctor = BeanUtils.getResolvableConstructor(targetType);
55+
MutablePropertyValues propertyValues = extractPropertyValues(arguments);
56+
57+
if (ctor.getParameterCount() == 0) {
58+
target = BeanUtils.instantiateClass(ctor);
59+
DataBinder dataBinder = new DataBinder(target);
60+
dataBinder.bind(propertyValues);
61+
}
62+
else {
63+
// Data class constructor
64+
DataBinder binder = new DataBinder(null);
65+
String[] paramNames = BeanUtils.getParameterNames(ctor);
66+
Class<?>[] paramTypes = ctor.getParameterTypes();
67+
Object[] args = new Object[paramTypes.length];
68+
for (int i = 0; i < paramNames.length; i++) {
69+
String paramName = paramNames[i];
70+
Object value = propertyValues.get(paramName);
71+
value = (value instanceof List ? ((List<?>) value).toArray() : value);
72+
MethodParameter methodParam = new MethodParameter(ctor, i);
73+
if (value == null && methodParam.isOptional()) {
74+
args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null);
75+
}
76+
else {
77+
args[i] = binder.convertIfNecessary(value, paramTypes[i], methodParam);
78+
}
79+
}
80+
target = BeanUtils.instantiateClass(ctor, args);
81+
}
82+
return (T) target;
83+
}
84+
85+
/**
86+
* Perform a Depth First Search in the given JSON map to collect attribute values
87+
* as {@link MutablePropertyValues} using the full property path as key.
88+
*/
89+
private MutablePropertyValues extractPropertyValues(Map<String, Object> arguments) {
90+
MutablePropertyValues mpvs = new MutablePropertyValues();
91+
Stack<String> path = new Stack<>();
92+
visitArgumentMap(arguments, mpvs, path);
93+
return mpvs;
94+
}
95+
96+
@SuppressWarnings("unchecked")
97+
private void visitArgumentMap(Map<String, Object> arguments, MutablePropertyValues mpvs, Stack<String> path) {
98+
for (String key : arguments.keySet()) {
99+
Object value = arguments.get(key);
100+
if (value instanceof List) {
101+
List<Object> items = (List<Object>) value;
102+
Map<String, Object> subValues = new HashMap<>(items.size());
103+
for (int i = 0; i < items.size(); i++) {
104+
subValues.put(key + "[" + i + "]", items.get(i));
105+
}
106+
visitArgumentMap(subValues, mpvs, path);
107+
}
108+
else if (value instanceof Map) {
109+
path.push(key);
110+
path.push(".");
111+
visitArgumentMap((Map<String, Object>) value, mpvs, path);
112+
path.pop();
113+
path.pop();
114+
}
115+
else {
116+
path.push(key);
117+
String propertyName = pathToPropertyName(path);
118+
mpvs.add(propertyName, value);
119+
path.pop();
120+
}
121+
}
122+
}
123+
124+
private String pathToPropertyName(Stack<String> path) {
125+
StringBuilder sb = new StringBuilder();
126+
for (String s : path) {
127+
sb.append(s);
128+
}
129+
return sb.toString();
130+
}
131+
}

0 commit comments

Comments
 (0)