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 11508ec3..a5b9686c 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 @@ -17,12 +17,14 @@ package org.springframework.graphql.data; import java.lang.reflect.Constructor; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Stack; +import java.util.function.Consumer; import graphql.schema.DataFetchingEnvironment; @@ -59,11 +61,18 @@ import org.springframework.validation.FieldError; */ public class GraphQlArgumentBinder { + /** + * Use a larger {@link DataBinder#DEFAULT_AUTO_GROW_COLLECTION_LIMIT} for GraphQL use cases + */ + private static final int DEFAULT_AUTO_GROW_COLLECTION_LIMIT = 1024; + @Nullable private final SimpleTypeConverter typeConverter; private final BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor(); + private List> dataBinderInitializers = new ArrayList<>(); + public GraphQlArgumentBinder() { this(null); @@ -90,6 +99,15 @@ public class GraphQlArgumentBinder { return (this.typeConverter != null ? this.typeConverter.getConversionService() : null); } + /** + * Add a {@link DataBinder} consumer that initializes the binder instance before the binding process. + * @param dataBinderInitializer the data binder initializer + * @since 1.0.1 + */ + public void addDataBinderInitializer(Consumer dataBinderInitializer) { + this.dataBinderInitializers.add(dataBinderInitializer); + } + /** * Bind a single argument, or the full arguments map, onto an object of the @@ -122,6 +140,7 @@ public class GraphQlArgumentBinder { Assert.notNull(targetClass, "Could not determine target type from " + targetType); DataBinder binder = new DataBinder(null, argumentName != null ? argumentName : "arguments"); + initDataBinder(binder); BindingResult bindingResult = binder.getBindingResult(); Stack segments = new Stack<>(); @@ -159,6 +178,11 @@ public class GraphQlArgumentBinder { } } + private void initDataBinder(DataBinder binder) { + binder.setAutoGrowCollectionLimit(DEFAULT_AUTO_GROW_COLLECTION_LIMIT); + this.dataBinderInitializers.forEach(initializer -> initializer.accept(binder)); + } + @Nullable private Object wrapAsOptionalIfNecessary(@Nullable Object value, ResolvableType type) { return (type.resolve(Object.class).equals(Optional.class) ? Optional.ofNullable(value) : value); @@ -228,6 +252,7 @@ public class GraphQlArgumentBinder { if (ctor.getParameterCount() == 0) { target = BeanUtils.instantiateClass(ctor); DataBinder dataBinder = new DataBinder(target); + initDataBinder(dataBinder); dataBinder.getBindingResult().setNestedPath(toArgumentPath(segments)); dataBinder.setConversionService(getConversionService()); dataBinder.bind(initBindValues(rawMap)); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java index 1dc67894..2ae093f1 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java @@ -27,6 +27,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.Executor; +import java.util.function.Consumer; import java.util.stream.Collectors; import javax.validation.Validator; @@ -69,6 +70,7 @@ import org.springframework.stereotype.Controller; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; +import org.springframework.validation.DataBinder; /** * {@link RuntimeWiringConfigurer} that detects {@link SchemaMapping @SchemaMapping} @@ -127,6 +129,9 @@ public class AnnotatedControllerConfigurer @Nullable private HandlerMethodInputValidator validator; + @Nullable + private Consumer dataBinderInitializer; + /** * Add a {@code FormatterRegistrar} to customize the {@link ConversionService} @@ -149,6 +154,15 @@ public class AnnotatedControllerConfigurer this.executor = executor; } + /** + * Configure an initializer that configures the {@link DataBinder} before the binding process. + * @param dataBinderInitializer the data binder initializer + * @since 1.0.1 + */ + public void setDataBinderInitializer(@Nullable Consumer dataBinderInitializer) { + this.dataBinderInitializer = dataBinderInitializer; + } + @Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; @@ -176,6 +190,9 @@ public class AnnotatedControllerConfigurer } resolvers.addResolver(new ArgumentMapMethodArgumentResolver()); GraphQlArgumentBinder argumentBinder = new GraphQlArgumentBinder(this.conversionService); + if (this.dataBinderInitializer != null) { + argumentBinder.addDataBinderInitializer(this.dataBinderInitializer); + } resolvers.addResolver(new ArgumentMethodArgumentResolver(argumentBinder)); resolvers.addResolver(new ArgumentsMethodArgumentResolver(argumentBinder)); resolvers.addResolver(new ContextValueMethodArgumentResolver()); 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 89949929..394920ff 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 @@ -16,10 +16,14 @@ package org.springframework.graphql.data; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -30,6 +34,8 @@ import org.junit.jupiter.api.Test; import org.springframework.core.ResolvableType; import org.springframework.graphql.Book; import org.springframework.graphql.data.GraphQlArgumentBinder; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; @@ -227,6 +233,7 @@ class GraphQlArgumentBinderTests { } @Test // gh-410 + @SuppressWarnings("unchecked") void coercionWithSingletonList() throws Exception { Map itemMap = new HashMap<>(); @@ -250,6 +257,17 @@ class GraphQlArgumentBinderTests { assertThat(items.get(0).getAge()).isEqualTo(37); } + @Test // gh-392 + @SuppressWarnings("unchecked") + void shouldHaveHigherDefaultAutoGrowLimit() throws Exception { + String items = IntStream.range(0, 260).mapToObj(value -> "{\"name\":\"test\"}").collect(Collectors.joining(",")); + Object result = this.binder.bind( + environment("{\"key\":{\"items\":[" + items + "]}}"), "key", + ResolvableType.forClass(ItemListHolder.class)); + assertThat(result).isNotNull().isInstanceOf(ItemListHolder.class); + assertThat(((ItemListHolder) result).getItems()).hasSize(260); + } + @SuppressWarnings("unchecked") private DataFetchingEnvironment environment(String jsonPayload) throws JsonProcessingException {