Skip to content

Commit d38ffea

Browse files
committed
Consistent raw value handling in GraphQlArgumentBinder
This commit ensures a single method is used to bind raw values, whether those a top level argument value, or a value nested within a collection or map. See gh-449
1 parent bc8ee16 commit d38ffea

File tree

2 files changed

+123
-172
lines changed

2 files changed

+123
-172
lines changed

spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java

Lines changed: 115 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import org.springframework.beans.SimpleTypeConverter;
3535
import org.springframework.beans.TypeMismatchException;
3636
import org.springframework.core.CollectionFactory;
37-
import org.springframework.core.MethodParameter;
3837
import org.springframework.core.ResolvableType;
3938
import org.springframework.core.convert.ConversionService;
4039
import org.springframework.core.convert.TypeDescriptor;
@@ -113,7 +112,7 @@ public void addDataBinderInitializer(Consumer<DataBinder> dataBinderInitializer)
113112
* Bind a single argument, or the full arguments map, onto an object of the
114113
* given target type.
115114
* @param environment for access to the arguments
116-
* @param argumentName the name of the argument to bind, or {@code null} to
115+
* @param name the name of the argument to bind, or {@code null} to
117116
* use the full arguments map
118117
* @param targetType the type of Object to create
119118
* @return the created Object, possibly {@code null}
@@ -124,82 +123,78 @@ public void addDataBinderInitializer(Consumer<DataBinder> dataBinderInitializer)
124123
* is the argument path where the issue occurred.
125124
*/
126125
@Nullable
127-
@SuppressWarnings("unchecked")
128126
public Object bind(
129-
DataFetchingEnvironment environment, @Nullable String argumentName, ResolvableType targetType)
127+
DataFetchingEnvironment environment, @Nullable String name, ResolvableType targetType)
130128
throws BindException {
131129

132-
Object rawValue = (argumentName != null ?
133-
environment.getArgument(argumentName) : environment.getArguments());
134-
135-
if (rawValue == null) {
136-
return wrapAsOptionalIfNecessary(null, targetType);
137-
}
138-
139-
Class<?> targetClass = targetType.resolve();
140-
Assert.notNull(targetClass, "Could not determine target type from " + targetType);
130+
Object rawValue = (name != null ?
131+
environment.getArgument(name) : environment.getArguments());
141132

142-
DataBinder binder = new DataBinder(null, argumentName != null ? argumentName : "arguments");
133+
DataBinder binder = new DataBinder(null, name != null ? ("Arguments[" + name + "]") : "Arguments");
143134
initDataBinder(binder);
144135
BindingResult bindingResult = binder.getBindingResult();
145-
Stack<String> segments = new Stack<>();
146-
147-
try {
148-
// From Collection
149-
150-
if (isApproximableCollectionType(rawValue)) {
151-
segments.push(argumentName);
152-
return bindCollection((Collection<Object>) rawValue, targetType, bindingResult, segments);
153-
}
154136

155-
if (targetClass == Optional.class) {
156-
targetClass = targetType.getNested(2).resolve();
157-
Assert.notNull(targetClass, "Could not determine Optional<T> type from " + targetType);
158-
}
159-
160-
// From Map
161-
162-
if (rawValue instanceof Map) {
163-
Object target = bindMap((Map<String, Object>) rawValue, targetType, bindingResult, segments);
164-
return wrapAsOptionalIfNecessary(target, targetType);
165-
}
137+
Stack<String> segments = new Stack<>();
138+
if (name != null) {
139+
segments.push(name);
140+
}
166141

167-
// From Scalar
142+
Class<?> targetClass = targetType.resolve();
143+
Assert.notNull(targetClass, "Could not determine target type from " + targetType);
168144

169-
if (targetClass.isInstance(rawValue)) {
170-
return wrapAsOptionalIfNecessary(rawValue, targetType);
171-
}
145+
Object targetValue = bindRawValue(rawValue, targetType, targetClass, bindingResult, segments);
172146

173-
Object target = convertValue(rawValue, targetClass, bindingResult, segments);
174-
return wrapAsOptionalIfNecessary(target, targetType);
175-
}
176-
finally {
177-
checkBindingResult(bindingResult);
147+
if (bindingResult.hasErrors()) {
148+
throw new BindException(bindingResult);
178149
}
150+
151+
return targetValue;
179152
}
180153

181154
private void initDataBinder(DataBinder binder) {
182155
binder.setAutoGrowCollectionLimit(DEFAULT_AUTO_GROW_COLLECTION_LIMIT);
183156
this.dataBinderInitializers.forEach(initializer -> initializer.accept(binder));
184157
}
185158

159+
@SuppressWarnings({"ConstantConditions", "unchecked"})
186160
@Nullable
187-
private Object wrapAsOptionalIfNecessary(@Nullable Object value, ResolvableType type) {
188-
return (type.resolve(Object.class).equals(Optional.class) ? Optional.ofNullable(value) : value);
189-
}
161+
private Object bindRawValue(
162+
Object rawValue, ResolvableType targetValueType, Class<?> targetValueClass,
163+
BindingResult bindingResult, Stack<String> segments) {
164+
165+
boolean isOptional = targetValueClass == Optional.class;
166+
167+
if (isOptional) {
168+
targetValueType = targetValueType.getNested(2);
169+
targetValueClass = targetValueType.resolve();
170+
}
190171

191-
private boolean isApproximableCollectionType(@Nullable Object rawValue) {
192-
return (rawValue != null &&
193-
(CollectionFactory.isApproximableCollectionType(rawValue.getClass()) ||
194-
rawValue instanceof List)); // it may be SingletonList
172+
Object targetValue;
173+
if (rawValue == null || targetValueClass == Object.class) {
174+
targetValue = rawValue;
175+
}
176+
else if (rawValue instanceof Collection) {
177+
targetValue = bindCollection((Collection<Object>) rawValue, targetValueType, bindingResult, segments);
178+
}
179+
else if (rawValue instanceof Map) {
180+
targetValue = bindMap((Map<String, Object>) rawValue, targetValueType, bindingResult, segments);
181+
}
182+
else {
183+
targetValue = (targetValueClass.isAssignableFrom(rawValue.getClass()) ?
184+
rawValue : convertValue(rawValue, targetValueClass, bindingResult, segments));
185+
}
186+
187+
return (isOptional ? Optional.ofNullable(targetValue) : targetValue);
195188
}
196189

197-
@SuppressWarnings({"ConstantConditions", "unchecked"})
198-
private <T> Collection<T> bindCollection(
190+
private Collection<?> bindCollection(
199191
Collection<Object> rawCollection, ResolvableType collectionType,
200192
BindingResult bindingResult, Stack<String> segments) {
201193

202-
if (!Collection.class.isAssignableFrom(collectionType.resolve())) {
194+
Class<?> collectionClass = collectionType.resolve();
195+
Assert.notNull(collectionClass, "Unknown Collection class");
196+
197+
if (!Collection.class.isAssignableFrom(collectionClass)) {
203198
bindingResult.rejectValue(toArgumentPath(segments), "typeMismatch", "Expected collection: " + collectionType);
204199
return Collections.emptyList();
205200
}
@@ -211,133 +206,95 @@ private <T> Collection<T> bindCollection(
211206
return Collections.emptyList();
212207
}
213208

214-
Collection<T> collection = CollectionFactory.createCollection(collectionType.getRawClass(), elementClass, rawCollection.size());
215-
int i = 0;
209+
Collection<Object> collection =
210+
CollectionFactory.createCollection(collectionClass, elementClass, rawCollection.size());
211+
212+
int index = 0;
216213
for (Object rawValue : rawCollection) {
217-
segments.push("[" + i++ + "]");
218-
if (rawValue == null || elementClass.isAssignableFrom(rawValue.getClass())) {
219-
collection.add((T) rawValue);
220-
}
221-
else if (rawValue instanceof Map) {
222-
collection.add((T) bindMap((Map<String, Object>) rawValue, elementType, bindingResult, segments));
223-
}
224-
else {
225-
collection.add((T) convertValue(rawValue, elementClass, bindingResult, segments));
226-
}
214+
segments.push("[" + index++ + "]");
215+
collection.add(bindRawValue(rawValue, elementType, elementClass, bindingResult, segments));
227216
segments.pop();
228217
}
229218
return collection;
230219
}
231220

232221
@Nullable
233-
@SuppressWarnings("unchecked")
234222
private Object bindMap(
235223
Map<String, Object> rawMap, ResolvableType targetType, BindingResult bindingResult,
236224
Stack<String> segments) {
237225

238-
try {
239-
Class<?> targetClass = targetType.resolve();
240-
Assert.notNull(targetClass, "Unknown target class");
241-
242-
if (Map.class.isAssignableFrom(targetClass)) {
243-
ResolvableType valueType = targetType.asMap().getGeneric(1);
244-
Class<?> valueClass = valueType.resolve();
245-
if (valueClass == null) {
246-
bindingResult.rejectValue(toArgumentPath(segments), "unknownMapValueType", "Unknown Map value type");
247-
return Collections.emptyMap();
248-
}
249-
Map<String, Object> map = CollectionFactory.createMap(targetClass, rawMap.size());
250-
for (Map.Entry<String, Object> entry : rawMap.entrySet()) {
251-
Object rawValue = entry.getValue();
252-
segments.push("[" + entry.getKey() + "]");
253-
if (rawValue == null || valueType.isAssignableFrom(rawValue.getClass())) {
254-
map.put(entry.getKey(), entry.getValue());
255-
}
256-
else if (rawValue instanceof Map) {
257-
map.put(entry.getKey(), bindMap(
258-
(Map<String, Object>) rawValue, valueType, bindingResult, segments));
259-
}
260-
else {
261-
map.put(entry.getKey(), convertValue(rawValue, valueClass, bindingResult, segments));
262-
}
263-
segments.pop();
264-
}
265-
return map;
226+
Class<?> targetClass = targetType.resolve();
227+
Assert.notNull(targetClass, "Unknown target class");
228+
229+
if (Map.class.isAssignableFrom(targetClass)) {
230+
ResolvableType valueType = targetType.asMap().getGeneric(1);
231+
Class<?> valueClass = valueType.resolve();
232+
if (valueClass == null) {
233+
bindingResult.rejectValue(toArgumentPath(segments), "unknownMapValueType", "Unknown Map value type");
234+
return Collections.emptyMap();
266235
}
236+
Map<String, Object> map = CollectionFactory.createMap(targetClass, rawMap.size());
237+
for (Map.Entry<String, Object> entry : rawMap.entrySet()) {
238+
String key = entry.getKey();
239+
segments.push("[" + key + "]");
240+
map.put(key, bindRawValue(entry.getValue(), valueType, valueClass, bindingResult, segments));
241+
segments.pop();
242+
}
243+
return map;
244+
}
267245

268-
Object target;
269-
Constructor<?> ctor = BeanUtils.getResolvableConstructor(targetClass);
270-
271-
// Default constructor + data binding via properties
246+
Object target;
247+
Constructor<?> constructor = BeanUtils.getResolvableConstructor(targetClass);
272248

273-
if (ctor.getParameterCount() == 0) {
274-
target = BeanUtils.instantiateClass(ctor);
275-
DataBinder dataBinder = new DataBinder(target);
276-
initDataBinder(dataBinder);
277-
dataBinder.getBindingResult().setNestedPath(toArgumentPath(segments));
278-
dataBinder.setConversionService(getConversionService());
279-
dataBinder.bind(toPropertyValues(rawMap));
249+
// Default constructor + data binding via properties
280250

281-
if (dataBinder.getBindingResult().hasErrors()) {
282-
addDataBinderErrors(dataBinder, bindingResult, segments);
283-
throw new BindException(bindingResult);
284-
}
251+
if (constructor.getParameterCount() == 0) {
252+
target = BeanUtils.instantiateClass(constructor);
253+
DataBinder dataBinder = new DataBinder(target);
254+
initDataBinder(dataBinder);
255+
dataBinder.getBindingResult().setNestedPath(toArgumentPath(segments));
256+
dataBinder.setConversionService(getConversionService());
257+
dataBinder.bind(toPropertyValues(rawMap));
285258

286-
return target;
259+
if (dataBinder.getBindingResult().hasErrors()) {
260+
copyBindingErrors(dataBinder, bindingResult, segments);
261+
return null;
287262
}
288263

289-
// Data class constructor
264+
return target;
265+
}
290266

291-
if (!segments.isEmpty()) {
292-
segments.push(".");
293-
}
267+
// Data class constructor
294268

295-
String[] paramNames = BeanUtils.getParameterNames(ctor);
296-
Class<?>[] paramTypes = ctor.getParameterTypes();
297-
Object[] args = new Object[paramTypes.length];
298-
299-
for (int i = 0; i < paramNames.length; i++) {
300-
String paramName = paramNames[i];
301-
Object rawValue = rawMap.get(paramName);
302-
segments.push(paramName);
303-
MethodParameter methodParam = new MethodParameter(ctor, i);
304-
if (rawValue == null && methodParam.isOptional()) {
305-
args[i] = (paramTypes[i] == Optional.class ? Optional.empty() : null);
306-
}
307-
else if (paramTypes[i] == Object.class) {
308-
args[i] = rawValue;
309-
}
310-
else if (isApproximableCollectionType(rawValue)) {
311-
ResolvableType elementType = ResolvableType.forMethodParameter(methodParam);
312-
args[i] = bindCollection((Collection<Object>) rawValue, elementType, bindingResult, segments);
313-
}
314-
else if (rawValue instanceof Map) {
315-
boolean isOptional = (paramTypes[i] == Optional.class);
316-
ResolvableType type = ResolvableType.forMethodParameter(methodParam.nestedIfOptional());
317-
Object value = bindMap((Map<String, Object>) rawValue, type, bindingResult, segments);
318-
args[i] = (isOptional ? Optional.ofNullable(value) : value);
319-
}
320-
else {
321-
args[i] = convertValue(rawValue, paramTypes[i], new TypeDescriptor(methodParam), bindingResult, segments);
322-
}
323-
segments.pop();
324-
}
269+
if (!segments.isEmpty()) {
270+
segments.push(".");
271+
}
325272

326-
if (segments.size() > 1) {
327-
segments.pop();
328-
}
273+
String[] paramNames = BeanUtils.getParameterNames(constructor);
274+
Class<?>[] paramTypes = constructor.getParameterTypes();
275+
Object[] args = new Object[paramTypes.length];
329276

330-
try {
331-
return BeanUtils.instantiateClass(ctor, args);
332-
}
333-
catch (BeanInstantiationException ex) {
334-
// Swallow if we had binding errors, it's as far as we could go
335-
checkBindingResult(bindingResult);
336-
throw ex;
337-
}
277+
for (int i = 0; i < paramNames.length; i++) {
278+
String name = paramNames[i];
279+
segments.push(name);
280+
ResolvableType paramType = ResolvableType.forConstructorParameter(constructor, i);
281+
args[i] = bindRawValue(rawMap.get(name), paramType, paramTypes[i], bindingResult, segments);
282+
segments.pop();
283+
}
284+
285+
if (segments.size() > 1) {
286+
segments.pop();
338287
}
339-
catch (BindException ex) {
340-
return null;
288+
289+
try {
290+
return BeanUtils.instantiateClass(constructor, args);
291+
}
292+
catch (BeanInstantiationException ex) {
293+
// Ignore if we had binding errors already
294+
if (bindingResult.hasErrors()) {
295+
return null;
296+
}
297+
throw ex;
341298
}
342299
}
343300

@@ -409,18 +366,12 @@ private Object convertValue(
409366
return null;
410367
}
411368

412-
private static void addDataBinderErrors(DataBinder binder, BindingResult bindingResult, Stack<String> segments) {
369+
private static void copyBindingErrors(DataBinder binder, BindingResult bindingResult, Stack<String> segments) {
413370
String path = (!segments.isEmpty() ? toArgumentPath(segments) + "." : "");
414371
binder.getBindingResult().getFieldErrors().forEach(error -> bindingResult.addError(
415372
new FieldError(bindingResult.getObjectName(), path + error.getField(),
416373
error.getRejectedValue(), error.isBindingFailure(), error.getCodes(),
417374
error.getArguments(), error.getDefaultMessage())));
418375
}
419376

420-
private void checkBindingResult(BindingResult bindingResult) throws BindException {
421-
if (bindingResult.hasErrors()) {
422-
throw new BindException(bindingResult);
423-
}
424-
}
425-
426377
}

0 commit comments

Comments
 (0)