Polishing and minor refactoring in GraphQlArgumentInitializer

This commit is contained in:
Rossen Stoyanchev
2021-11-26 15:56:40 +00:00
parent 425dc9c461
commit 3321121c41
4 changed files with 103 additions and 90 deletions

View File

@@ -30,7 +30,6 @@ import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.validation.DataBinder;
/**
* Resolver for {@link Argument @Argument} annotated method parameters, obtained
@@ -43,15 +42,14 @@ import org.springframework.validation.DataBinder;
*/
public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentResolver {
private final GraphQlArgumentInstantiator instantiator;
private final GraphQlArgumentInitializer argumentInitializer;
private final ConversionService conversionService;
public ArgumentMethodArgumentResolver(@Nullable ConversionService conversionService) {
this.conversionService = conversionService;
this.instantiator = new GraphQlArgumentInstantiator(conversionService);
this.argumentInitializer = new GraphQlArgumentInitializer(conversionService);
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(Argument.class) != null;
@@ -73,49 +71,48 @@ public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentReso
}
Object rawValue = environment.getArgument(name);
TypeDescriptor parameterType = new TypeDescriptor(parameter);
TypeDescriptor typeDescriptor = new TypeDescriptor(parameter);
if (rawValue == null) {
return returnValue(rawValue, parameterType.getType());
return wrapAsOptionalIfNecessary(null, typeDescriptor.getType());
}
// From Collection
if (CollectionFactory.isApproximableCollectionType(rawValue.getClass())) {
Assert.isAssignable(Collection.class, parameterType.getType(),
"Argument '" + name + "' is a Collection while the @Argument method parameter is " + parameterType.getType());
Class<?> elementType = parameterType.getElementTypeDescriptor().getType();
return this.instantiator.instantiateCollection(elementType, (Collection<Object>) rawValue);
Assert.isAssignable(Collection.class, typeDescriptor.getType(),
"Argument '" + name + "' is a Collection " +
"while the @Argument method parameter is " + typeDescriptor.getType());
Class<?> elementType = typeDescriptor.getElementTypeDescriptor().getType();
return this.argumentInitializer.initializeFromCollection((Collection<Object>) rawValue, elementType);
}
MethodParameter nestedParameter = parameter.nestedIfOptional();
Object value = convert(rawValue, nestedParameter.getNestedParameterType());
return returnValue(value, parameterType.getType());
}
private Object returnValue(Object value, Class<?> parameterType) {
if (parameterType.equals(Optional.class)) {
return Optional.ofNullable(value);
}
return value;
}
@SuppressWarnings("unchecked")
private Object convert(Object rawValue, Class<?> targetType) {
Class<?> targetType = parameter.nestedIfOptional().getNestedParameterType();
Object target;
// From Map
if (rawValue instanceof Map) {
target = this.instantiator.instantiate((Map<String, Object>) rawValue, targetType);
target = this.argumentInitializer.initializeFromMap((Map<String, Object>) rawValue, targetType);
return wrapAsOptionalIfNecessary(target, typeDescriptor.getType());
}
else if (targetType.isAssignableFrom(rawValue.getClass())) {
return returnValue(rawValue, targetType);
// From Scalar
if (targetType.isAssignableFrom(rawValue.getClass())) {
return wrapAsOptionalIfNecessary(rawValue, targetType);
}
else {
DataBinder converter = new DataBinder(null);
converter.setConversionService(this.conversionService);
target = converter.convertIfNecessary(rawValue, targetType);
Assert.isTrue(target != null,
() -> "Value of type [" + rawValue.getClass() + "] cannot be converted to argument of type [" +
targetType.getName() + "].");
}
return target;
target = this.argumentInitializer.getTypeConverter().convertIfNecessary(rawValue, targetType);
Assert.state(target != null, () ->
"Cannot convert value type [" + rawValue.getClass() + "] " +
"to argument type [" + targetType.getName() + "].");
return wrapAsOptionalIfNecessary(target, typeDescriptor.getType());
}
@Nullable
private Object wrapAsOptionalIfNecessary(@Nullable Object value, Class<?> type) {
return (type.equals(Optional.class) ? Optional.ofNullable(value) : value);
}
}

View File

@@ -26,6 +26,8 @@ import java.util.Stack;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.SimpleTypeConverter;
import org.springframework.beans.TypeConverter;
import org.springframework.core.CollectionFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
@@ -39,21 +41,32 @@ import org.springframework.validation.DataBinder;
* {@link graphql.schema.DataFetchingEnvironment} arguments.
*
* @author Brian Clozel
* @author Greg Turnquist
* @author Rossen Stoyanchev
* @since 1.0.0
*/
public class GraphQlArgumentInstantiator {
public class GraphQlArgumentInitializer {
private final DataBinder converter;
private final SimpleTypeConverter typeConverter;
public GraphQlArgumentInstantiator(@Nullable ConversionService conversionService) {
this.converter = new DataBinder(null);
this.converter.setConversionService(conversionService);
public GraphQlArgumentInitializer(@Nullable ConversionService conversionService) {
this.typeConverter = new SimpleTypeConverter();
this.typeConverter.setConversionService(conversionService);
}
/**
* Instantiate the given target type and bind data from
* {@link graphql.schema.DataFetchingEnvironment} arguments.
* <p>This is considering the default constructor or a primary constructor
* Return the underlying {@link DataBinder}.
*/
public TypeConverter getTypeConverter() {
return this.typeConverter;
}
/**
* Instantiate an Object of the given target type and bind
* {@link graphql.schema.DataFetchingEnvironment} argument values to it.
* This considers using the default constructor or a primary constructor,
* if available.
*
* @param arguments the data fetching environment arguments
@@ -63,7 +76,7 @@ public class GraphQlArgumentInstantiator {
* @throws IllegalStateException if there is no suitable constructor.
*/
@SuppressWarnings("unchecked")
public <T> T instantiate(Map<String, Object> arguments, Class<T> targetType) {
public <T> T initializeFromMap(Map<String, Object> arguments, Class<T> targetType) {
Object target;
Constructor<?> ctor = BeanUtils.getResolvableConstructor(targetType);
@@ -72,33 +85,35 @@ public class GraphQlArgumentInstantiator {
target = BeanUtils.instantiateClass(ctor);
DataBinder dataBinder = new DataBinder(target);
dataBinder.bind(propertyValues);
return (T) target;
}
else {
// Data class constructor
String[] paramNames = BeanUtils.getParameterNames(ctor);
Class<?>[] paramTypes = ctor.getParameterTypes();
Object[] args = new Object[paramTypes.length];
for (int i = 0; i < paramNames.length; i++) {
String paramName = paramNames[i];
Object value = arguments.get(paramName);
MethodParameter methodParam = new MethodParameter(ctor, i);
if (value == null && methodParam.isOptional()) {
args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null);
}
else if (value != null && CollectionFactory.isApproximableCollectionType(value.getClass())) {
TypeDescriptor typeDescriptor = new TypeDescriptor(methodParam);
Class<?> elementType = typeDescriptor.getElementTypeDescriptor().getType();
args[i] = instantiateCollection(elementType, (Collection<Object>) value);
}
else if (value instanceof Map) {
args[i] = this.instantiate((Map<String, Object>) value, methodParam.getParameterType());
} else {
args[i] = this.converter.convertIfNecessary(value, paramTypes[i], methodParam);
}
// Data class constructor
String[] paramNames = BeanUtils.getParameterNames(ctor);
Class<?>[] paramTypes = ctor.getParameterTypes();
Object[] args = new Object[paramTypes.length];
for (int i = 0; i < paramNames.length; i++) {
String paramName = paramNames[i];
Object value = arguments.get(paramName);
MethodParameter methodParam = new MethodParameter(ctor, i);
if (value == null && methodParam.isOptional()) {
args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null);
}
else if (value != null && CollectionFactory.isApproximableCollectionType(value.getClass())) {
TypeDescriptor typeDescriptor = new TypeDescriptor(methodParam);
Class<?> elementType = typeDescriptor.getElementTypeDescriptor().getType();
args[i] = initializeFromCollection((Collection<Object>) value, elementType);
}
else if (value instanceof Map) {
args[i] = this.initializeFromMap((Map<String, Object>) value, methodParam.getParameterType());
}
else {
args[i] = this.typeConverter.convertIfNecessary(value, paramTypes[i], methodParam);
}
target = BeanUtils.instantiateClass(ctor, args);
}
return (T) target;
return (T) BeanUtils.instantiateClass(ctor, args);
}
/**
@@ -106,14 +121,14 @@ public class GraphQlArgumentInstantiator {
* <p>This will instantiate a new Collection of the closest type possible
* from the one provided as an argument.
*
* @param elementType the type of elements in the given Collection
* @param values the collection of values to bind and instantiate
* @param <T> the type of Collection elements
* @param values the collection of values to bind and instantiate
* @param elementType the type of elements in the given Collection
* @return the instantiated and populated Collection.
* @throws IllegalStateException if there is no suitable constructor.
*/
@SuppressWarnings("unchecked")
public <T> Collection<T> instantiateCollection(Class<T> elementType, Collection<Object> values) {
public <T> Collection<T> initializeFromCollection(Collection<Object> values, Class<T> elementType) {
Assert.state(CollectionFactory.isApproximableCollectionType(values.getClass()),
() -> "Cannot instantiate Collection for type " + values.getClass());
Collection<T> instances = CollectionFactory.createApproximateCollection(values, values.size());
@@ -123,10 +138,10 @@ public class GraphQlArgumentInstantiator {
value = (T) item;
}
else if (item instanceof Map) {
value = this.instantiate((Map<String, Object>)item, elementType);
value = this.initializeFromMap((Map<String, Object>)item, elementType);
}
else {
value = this.converter.convertIfNecessary(item, elementType);
value = this.typeConverter.convertIfNecessary(item, elementType);
}
instances.add(value);
});
@@ -179,4 +194,5 @@ public class GraphQlArgumentInstantiator {
}
return sb.toString();
}
}

View File

@@ -57,7 +57,7 @@ import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.graphql.data.GraphQlRepository;
import org.springframework.graphql.data.method.annotation.support.GraphQlArgumentInstantiator;
import org.springframework.graphql.data.method.annotation.support.GraphQlArgumentInitializer;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -110,12 +110,12 @@ public abstract class QueryByExampleDataFetcher<T> {
private final TypeInformation<T> domainType;
private final GraphQlArgumentInstantiator instantiator;
private final GraphQlArgumentInitializer argumentInitializer;
QueryByExampleDataFetcher(TypeInformation<T> domainType) {
this.domainType = domainType;
this.instantiator = new GraphQlArgumentInstantiator(null);
this.argumentInitializer = new GraphQlArgumentInitializer(null);
}
@@ -125,7 +125,7 @@ public abstract class QueryByExampleDataFetcher<T> {
* @return the resulting example
*/
protected Example<T> buildExample(DataFetchingEnvironment env) {
return Example.of(this.instantiator.instantiate(env.getArguments(), this.domainType.getType()));
return Example.of(this.argumentInitializer.initializeFromMap(env.getArguments(), this.domainType.getType()));
}
protected boolean requiresProjection(Class<?> resultType) {

View File

@@ -32,21 +32,21 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Tests for {@link GraphQlArgumentInstantiator}
* Tests for {@link GraphQlArgumentInitializer}
*
* @author Brian Clozel
*/
class GraphQlArgumentInstantiatorTests {
class GraphQlArgumentInitializerTests {
private ObjectMapper mapper = new ObjectMapper();
private GraphQlArgumentInstantiator instantiator = new GraphQlArgumentInstantiator(null);
private GraphQlArgumentInitializer instantiator = new GraphQlArgumentInitializer(null);
@Test
void shouldInstantiateDefaultConstructor() throws Exception {
String payload = "{\"simpleBean\": { \"name\": \"test\"} }";
DataFetchingEnvironment environment = initEnvironment(payload);
SimpleBean result = instantiator.instantiate(environment.getArgument("simpleBean"), SimpleBean.class);
SimpleBean result = instantiator.initializeFromMap(environment.getArgument("simpleBean"), SimpleBean.class);
assertThat(result).isNotNull().isInstanceOf(SimpleBean.class);
assertThat(result).hasFieldOrPropertyWithValue("name", "test");
@@ -56,7 +56,7 @@ class GraphQlArgumentInstantiatorTests {
void shouldInstantiatePrimaryConstructor() throws Exception {
String payload = "{\"constructorBean\": { \"name\": \"test\"} }";
DataFetchingEnvironment environment = initEnvironment(payload);
ContructorBean result = instantiator.instantiate(environment.getArgument("constructorBean"), ContructorBean.class);
ContructorBean result = instantiator.initializeFromMap(environment.getArgument("constructorBean"), ContructorBean.class);
assertThat(result).isNotNull().isInstanceOf(ContructorBean.class);
assertThat(result).hasFieldOrPropertyWithValue("name", "test");
@@ -66,7 +66,7 @@ class GraphQlArgumentInstantiatorTests {
void shouldFailIfNoPrimaryConstructor() throws Exception {
String payload = "{\"noPrimary\": { \"name\": \"test\"} }";
DataFetchingEnvironment environment = initEnvironment(payload);
assertThatThrownBy(() -> instantiator.instantiate(environment.getArgument("noPrimary"), NoPrimaryConstructor.class))
assertThatThrownBy(() -> instantiator.initializeFromMap(environment.getArgument("noPrimary"), NoPrimaryConstructor.class))
.isInstanceOf(IllegalStateException.class).hasMessageContaining("No primary or single unique constructor found");
}
@@ -74,7 +74,7 @@ class GraphQlArgumentInstantiatorTests {
void shouldInstantiateNestedBean() throws Exception {
String payload = "{\"book\": { \"name\": \"test name\", \"author\": { \"firstName\": \"Jane\", \"lastName\": \"Spring\"} } }";
DataFetchingEnvironment environment = initEnvironment(payload);
Book result = instantiator.instantiate(environment.getArgument("book"), Book.class);
Book result = instantiator.initializeFromMap(environment.getArgument("book"), Book.class);
assertThat(result).isNotNull().isInstanceOf(Book.class);
assertThat(result).hasFieldOrPropertyWithValue("name", "test name");
@@ -87,7 +87,7 @@ class GraphQlArgumentInstantiatorTests {
void shouldInstantiateNestedBeanLists() throws Exception {
String payload = "{\"nestedList\": { \"items\": [ {\"name\": \"first\"}, {\"name\": \"second\"}] } }";
DataFetchingEnvironment environment = initEnvironment(payload);
NestedList result = instantiator.instantiate(environment.getArgument("nestedList"), NestedList.class);
NestedList result = instantiator.initializeFromMap(environment.getArgument("nestedList"), NestedList.class);
assertThat(result).isNotNull().isInstanceOf(NestedList.class);
assertThat(result.getItems()).hasSize(2).extracting("name").containsExactly("first", "second");
@@ -97,7 +97,7 @@ class GraphQlArgumentInstantiatorTests {
void shouldInstantiatePrimaryConstructorNestedBeanLists() throws Exception {
String payload = "{\"nestedList\": { \"items\": [ {\"name\": \"first\"}, {\"name\": \"second\"}] } }";
DataFetchingEnvironment environment = initEnvironment(payload);
PrimaryConstructorNestedList result = instantiator.instantiate(environment.getArgument("nestedList"), PrimaryConstructorNestedList.class);
PrimaryConstructorNestedList result = instantiator.initializeFromMap(environment.getArgument("nestedList"), PrimaryConstructorNestedList.class);
assertThat(result).isNotNull().isInstanceOf(PrimaryConstructorNestedList.class);
assertThat(result.getItems()).hasSize(2).extracting("name").containsExactly("first", "second");
@@ -107,7 +107,7 @@ class GraphQlArgumentInstantiatorTests {
void shouldInstantiateComplexNestedBean() throws Exception {
String payload = "{\"complex\": { \"item\": {\"name\": \"Item name\"}, \"name\": \"Hello\" } }";
DataFetchingEnvironment environment = initEnvironment(payload);
PrimaryConstructorComplexInput result = instantiator.instantiate(environment.getArgument("complex"), PrimaryConstructorComplexInput.class);
PrimaryConstructorComplexInput result = instantiator.initializeFromMap(environment.getArgument("complex"), PrimaryConstructorComplexInput.class);
assertThat(result).isNotNull().isInstanceOf(PrimaryConstructorComplexInput.class);
assertThat(result.item.name).isEqualTo("Item name");