48
48
49
49
50
50
/**
51
- * Bind a GraphQL argument, or the full arguments map, onto a target object.
51
+ * Binder that instantiates and populates a target Object to reflect the
52
+ * complete structure of the {@link DataFetchingEnvironment#getArguments()
53
+ * GraphQL arguments} input map.
52
54
*
53
- * <p>Complex objects (non-scalar) are initialized either through the primary
54
- * data constructor where arguments are matched to constructor parameters, or
55
- * through the default constructor where arguments are matched to setter
56
- * property methods. In case objects are related to other objects, binding is
57
- * applied recursively to create nested objects.
55
+ * <p>The input map is navigated recursively to create the full structure of
56
+ * the target type. Objects in the target type are created either through a
57
+ * primary, data constructor, in which case arguments are matched to constructor
58
+ * parameters by name, or through the default constructor, in which case
59
+ * arguments are matched to properties. Scalar values are converted, if
60
+ * necessary, through a {@link ConversionService}.
58
61
*
59
- * <p>Scalar values are converted to the expected target type through a
60
- * {@link ConversionService}, if provided.
62
+ * <p>The binder does not stop at the first error, but rather accumulates as
63
+ * many errors as it can in a {@link org.springframework.validation.BindingResult}.
64
+ * At the end it raises a {@link BindException} that contains all recorded
65
+ * errors along with the path at which each error occurred.
61
66
*
62
- * <p>In case of any errors, when creating objects or converting scalar values,
63
- * a {@link BindException} is raised that contains all errors recorded along
64
- * with the path at which the errors occurred.
67
+ * <p>The binder supports {@link Optional} as a wrapper around any Object or
68
+ * scalar value in the target Object structure. In addition, it also supports
69
+ * {@link ArgumentValue} as a wrapper that indicates whether a given input
70
+ * argument was omitted rather than set to the {@literal "null"} literal.
65
71
*
66
72
* @author Brian Clozel
67
73
* @author Rossen Stoyanchev
@@ -103,18 +109,15 @@ public void addDataBinderInitializer(Consumer<DataBinder> consumer) {
103
109
104
110
105
111
/**
106
- * Bind a single argument, or the full arguments map, onto an object of the
107
- * given target type .
112
+ * Create and populate an Object of the given target type, from a single
113
+ * GraphQL argument, or from the full GraphQL arguments map .
108
114
* @param environment for access to the arguments
109
- * @param name the name of the argument to bind, or {@code null} to
110
- * use the full arguments map
115
+ * @param name the name of an argument, or {@code null} to use the full map
111
116
* @param targetType the type of Object to create
112
- * @return the created Object, possibly {@code null}
113
- * @throws BindException in case of binding issues such as conversion errors,
114
- * mismatches between the source and the target object structure, and so on.
115
- * Binding issues are accumulated as {@link BindException#getFieldErrors()
116
- * field errors} where the {@link FieldError#getField() field} of each error
117
- * is the argument path where the issue occurred.
117
+ * @return the created Object, possibly wrapped in {@link Optional} or in
118
+ * {@link ArgumentValue}, or {@code null} if there is no value
119
+ * @throws BindException containing one or more accumulated errors from
120
+ * matching and/or converting arguments to the target Object
118
121
*/
119
122
@ Nullable
120
123
public Object bind (
@@ -137,21 +140,21 @@ public Object bind(
137
140
}
138
141
139
142
/**
140
- * Bind the raw GraphQL argument value to an Object of the specified type .
141
- * @param name the name of a constructor parameter or a bean property of the
142
- * target Object that is to be initialized from the given raw value;
143
- * {@code "$"} if binding the top level Object; possibly indexed if binding
144
- * to a Collection element or to a Map value.
143
+ * Create an Object from the given raw GraphQL argument value.
144
+ * @param name the name of the constructor parameter or the property that
145
+ * will be set from the returned value, possibly {@code "$"} for the top
146
+ * Object, or an indexed property for a Collection element or Map value;
147
+ * mainly for error recording, to keep track of the nested path
145
148
* @param rawValue the raw argument value (Collection, Map, or scalar)
146
- * @param isOmitted whether the value with the given name was not provided
147
- * at all, as opposed to provided but set to the {@literal " null"} literal
149
+ * @param isOmitted {@code true} if the argument was omitted from the input
150
+ * and {@code false} if it was provided, but possibly {@code null}
148
151
* @param targetType the type of Object to create
149
- * @param targetClass the resolved class from the targetType
150
- * @param bindingResult for keeping track of the nested path and errors
152
+ * @param targetClass the target class, resolved from the targetType
153
+ * @param bindingResult to accumulate errors
151
154
* @return the target Object instance, possibly {@code null} if the source
152
- * value is {@code null} or if binding failed in which case the result will
153
- * contain errors; nevertheless we keep going to record as many errors as
154
- * we can accumulate
155
+ * value is {@code null}, or if binding failed in which case the result will
156
+ * contain errors; generally we keep going as far as we can and only raise
157
+ * a {@link BindException} at the end to record as many errors as possible
155
158
*/
156
159
@ SuppressWarnings ({"ConstantConditions" , "unchecked" })
157
160
@ Nullable
@@ -199,8 +202,8 @@ private Collection<?> bindCollection(
199
202
ResolvableType elementType = collectionType .asCollection ().getGeneric (0 );
200
203
Class <?> elementClass = collectionType .asCollection ().getGeneric (0 ).resolve ();
201
204
if (elementClass == null ) {
202
- bindingResult .rejectValue ( null , "unknownType" , "Unknown Collection element type" );
203
- return Collections .emptyList (); // Keep going, report as many errors as we can
205
+ bindingResult .rejectArgumentValue ( name , null , "unknownType" , "Unknown Collection element type" );
206
+ return Collections .emptyList (); // Keep going, to record more errors
204
207
}
205
208
206
209
Collection <Object > collection =
@@ -228,9 +231,9 @@ private Object bindMap(
228
231
229
232
Constructor <?> constructor = BeanUtils .getResolvableConstructor (targetClass );
230
233
231
- Object value = constructor .getParameterCount () > 0 ?
234
+ Object value = ( constructor .getParameterCount () > 0 ?
232
235
bindMapToObjectViaConstructor (rawMap , constructor , targetType , bindingResult ) :
233
- bindMapToObjectViaSetters (rawMap , constructor , targetType , bindingResult );
236
+ bindMapToObjectViaSetters (rawMap , constructor , targetType , bindingResult )) ;
234
237
235
238
bindingResult .popNestedPath ();
236
239
@@ -244,8 +247,8 @@ private Map<?, Object> bindMapToMap(
244
247
ResolvableType valueType = targetType .asMap ().getGeneric (1 );
245
248
Class <?> valueClass = valueType .resolve ();
246
249
if (valueClass == null ) {
247
- bindingResult .rejectValue ( null , "unknownType" , "Unknown Map value type" );
248
- return Collections .emptyMap (); // Keep going, report as many errors as we can
250
+ bindingResult .rejectArgumentValue ( name , null , "unknownType" , "Unknown Map value type" );
251
+ return Collections .emptyMap (); // Keep going, to record more errors
249
252
}
250
253
251
254
Map <String , Object > map = CollectionFactory .createMap (targetClass , rawMap .size ());
@@ -261,28 +264,28 @@ private Map<?, Object> bindMapToMap(
261
264
262
265
@ Nullable
263
266
private Object bindMapToObjectViaConstructor (
264
- Map <String , Object > rawMap , Constructor <?> constructor , ResolvableType parentType ,
267
+ Map <String , Object > rawMap , Constructor <?> constructor , ResolvableType ownerType ,
265
268
ArgumentsBindingResult bindingResult ) {
266
269
267
270
String [] paramNames = BeanUtils .getParameterNames (constructor );
268
271
Class <?>[] paramTypes = constructor .getParameterTypes ();
269
- Object [] args = new Object [paramTypes .length ];
272
+ Object [] constructorArguments = new Object [paramTypes .length ];
270
273
271
274
for (int i = 0 ; i < paramNames .length ; i ++) {
272
275
String name = paramNames [i ];
273
- boolean isOmitted = !rawMap .containsKey (name );
274
276
275
277
ResolvableType targetType = ResolvableType .forType (
276
- ResolvableType .forConstructorParameter (constructor , i ).getType (), parentType );
278
+ ResolvableType .forConstructorParameter (constructor , i ).getType (), ownerType );
277
279
278
- args [i ] = bindRawValue (name , rawMap .get (name ), isOmitted , targetType , paramTypes [i ], bindingResult );
280
+ constructorArguments [i ] = bindRawValue (
281
+ name , rawMap .get (name ), !rawMap .containsKey (name ), targetType , paramTypes [i ], bindingResult );
279
282
}
280
283
281
284
try {
282
- return BeanUtils .instantiateClass (constructor , args );
285
+ return BeanUtils .instantiateClass (constructor , constructorArguments );
283
286
}
284
287
catch (BeanInstantiationException ex ) {
285
- // Ignore: we had binding errors to begin with
288
+ // Ignore, if we had binding errors to begin with
286
289
if (bindingResult .hasErrors ()) {
287
290
return null ;
288
291
}
@@ -291,7 +294,7 @@ private Object bindMapToObjectViaConstructor(
291
294
}
292
295
293
296
private Object bindMapToObjectViaSetters (
294
- Map <String , Object > rawMap , Constructor <?> constructor , ResolvableType parentType ,
297
+ Map <String , Object > rawMap , Constructor <?> constructor , ResolvableType ownerType ,
295
298
ArgumentsBindingResult bindingResult ) {
296
299
297
300
Object target = BeanUtils .instantiateClass (constructor );
@@ -306,7 +309,7 @@ private Object bindMapToObjectViaSetters(
306
309
}
307
310
308
311
ResolvableType targetType =
309
- ResolvableType .forType (typeDescriptor .getResolvableType ().getType (), parentType );
312
+ ResolvableType .forType (typeDescriptor .getResolvableType ().getType (), ownerType );
310
313
311
314
Object value = bindRawValue (
312
315
key , entry .getValue (), false , targetType , typeDescriptor .getType (), bindingResult );
@@ -320,7 +323,7 @@ private Object bindMapToObjectViaSetters(
320
323
// Ignore unknown property
321
324
}
322
325
catch (Exception ex ) {
323
- bindingResult .rejectValue ( value , "invalidPropertyValue" , "Failed to set property value" );
326
+ bindingResult .rejectArgumentValue ( key , value , "invalidPropertyValue" , "Failed to set property value" );
324
327
}
325
328
}
326
329
@@ -339,22 +342,19 @@ private <T> T convertValue(
339
342
(this .typeConverter != null ? this .typeConverter : new SimpleTypeConverter ());
340
343
341
344
value = converter .convertIfNecessary (
342
- rawValue , (Class <?>) clazz ,
343
- (type .getSource () instanceof MethodParameter param ? new TypeDescriptor (param ) : null ));
345
+ rawValue , (Class <?>) clazz , new TypeDescriptor (type , null , null ));
344
346
}
345
347
catch (TypeMismatchException ex ) {
346
- bindingResult .pushNestedPath (name );
347
- bindingResult .rejectValue (rawValue , ex .getErrorCode (), "Failed to convert argument value" );
348
- bindingResult .popNestedPath ();
348
+ bindingResult .rejectArgumentValue (name , rawValue , ex .getErrorCode (), "Failed to convert argument value" );
349
349
}
350
350
351
351
return (T ) value ;
352
352
}
353
353
354
354
355
355
/**
356
- * BindingResult without a target Object, only for keeping track of errors
357
- * and their associated, nested paths .
356
+ * Subclass of {@link AbstractBindingResult} that doesn't have a target Object,
357
+ * and takes the raw value as input when recording errors .
358
358
*/
359
359
@ SuppressWarnings ("serial" )
360
360
private static class ArgumentsBindingResult extends AbstractBindingResult {
@@ -379,9 +379,11 @@ protected Object getActualFieldValue(String field) {
379
379
return null ;
380
380
}
381
381
382
- public void rejectValue (@ Nullable Object rawValue , String code , String defaultMessage ) {
382
+ public void rejectArgumentValue (
383
+ String field , @ Nullable Object rawValue , String code , String defaultMessage ) {
384
+
383
385
addError (new FieldError (
384
- getObjectName (), fixedField (null ), rawValue , true , resolveMessageCodes (code ),
386
+ getObjectName (), fixedField (field ), rawValue , true , resolveMessageCodes (code ),
385
387
null , defaultMessage ));
386
388
}
387
389
}
0 commit comments