diff --git a/spring-graphql-docs/src/docs/asciidoc/index.adoc b/spring-graphql-docs/src/docs/asciidoc/index.adoc index dd1d6dc5c..ae419509d 100644 --- a/spring-graphql-docs/src/docs/asciidoc/index.adoc +++ b/spring-graphql-docs/src/docs/asciidoc/index.adoc @@ -1460,6 +1460,14 @@ Batch mapping methods support the following arguments: | `List` | The source/parent objects. +| `@Argument` +| For access to a named field argument converted to a higher-level, typed Object. +See <>. + +| `@Arguments` +| For access to all field arguments converted to a higher-level, typed Object. +See <>. + | `java.security.Principal` | Obtained from Spring Security context, if available. diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/BatchHandlerMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/BatchHandlerMethodArgumentResolver.java new file mode 100644 index 000000000..018247ce9 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/BatchHandlerMethodArgumentResolver.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.graphql.data.method; + +import graphql.schema.DataFetchingEnvironment; +import org.dataloader.BatchLoaderEnvironment; +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; + +import java.util.Collection; +import java.util.Map; + +/** + * Strategy interface for resolving method parameters into argument values in + * the context of a given {@link BatchLoaderEnvironment} and parent/source list. + * + *

Most implementations will be synchronous, simply resolving values from the + * {@code BatchLoaderEnvironment} and parent/source list. However, a resolver may + * also return a {@link reactor.core.publisher.Mono} if it needs to be asynchronous. + * + * @author Genkui Du + * @since 1.0.0 + */ +public interface BatchHandlerMethodArgumentResolver { + + /** + * Whether this resolver supports the given {@link MethodParameter}. + * + * @param parameter the method parameter + */ + boolean supportsParameter(MethodParameter parameter); + + /** + * Resolve a method parameter into an argument value. + * + * @param parameter the method parameter to resolve. This parameter must + * have previously checked via {@link #supportsParameter}. + * @param keys the list of keys to load + * @param keyContexts keys and their context objects map + * @param environment the environment to use to resolve the value + * @param the type of parent/source + * @return the resolved value, which may be {@code null} if not resolved; + * the value may also be a {@link reactor.core.publisher.Mono} if it + * requires asynchronous resolution. + * @throws Exception in case of errors with the preparation of argument values + */ + @Nullable + Object resolveArgument(MethodParameter parameter, + Collection keys, + Map keyContexts, + BatchLoaderEnvironment environment) throws Exception; + +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/BatchHandlerMethodArgumentResolverComposite.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/BatchHandlerMethodArgumentResolverComposite.java new file mode 100644 index 000000000..78e662351 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/BatchHandlerMethodArgumentResolverComposite.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.graphql.data.method; + +import graphql.schema.DataFetchingEnvironment; +import org.dataloader.BatchLoaderEnvironment; +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Resolves method parameters by delegating to a list of registered + * {@link BatchHandlerMethodArgumentResolver BatchHandlerMethodArgumentResolvers}. + * Previously resolved method parameters are cached for faster lookups. + * + * @author Genkui Du + * @since 1.0.0 + */ +public class BatchHandlerMethodArgumentResolverComposite implements BatchHandlerMethodArgumentResolver { + + private final List argumentResolvers = new ArrayList<>(); + + private final Map argumentResolverCache = + new ConcurrentHashMap<>(256); + + + /** + * Return a read-only list with the contained resolvers, or an empty list. + */ + public List getArgumentResolvers() { + return Collections.unmodifiableList(argumentResolvers); + } + + /** + * Add the given {@link BatchHandlerMethodArgumentResolver BatchHandlerMethodArgumentResolvers}. + */ + public BatchHandlerMethodArgumentResolverComposite addResolvers( + @Nullable List resolvers) { + + if (resolvers != null) { + this.argumentResolvers.addAll(resolvers); + } + return this; + } + + /** + * Clear the list of configured resolvers and the resolver cache. + */ + public void clear() { + this.argumentResolvers.clear(); + this.argumentResolverCache.clear(); + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return this.getArgumentResolver(parameter) != null; + } + + @Override + public Object resolveArgument(MethodParameter parameter, + Collection keys, + Map keyContexts, + BatchLoaderEnvironment environment) throws Exception { + + BatchHandlerMethodArgumentResolver argumentResolver = getArgumentResolver(parameter); + if (argumentResolver == null) { + throw new IllegalArgumentException("Unsupported parameter type [" + + parameter.getParameterType().getName() + "]. supportsParameter should be called first."); + } + return argumentResolver.resolveArgument(parameter, keys, keyContexts, environment); + + } + + /** + * Find a registered {@link BatchHandlerMethodArgumentResolver} that supports + * the given method parameter. + */ + @Nullable + private BatchHandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { + BatchHandlerMethodArgumentResolver result = argumentResolverCache.get(parameter); + if (result == null) { + for (BatchHandlerMethodArgumentResolver argumentResolver : this.argumentResolvers) { + if (argumentResolver.supportsParameter(parameter)) { + result = argumentResolver; + this.argumentResolverCache.put(parameter, argumentResolver); + break; + } + } + } + + return result; + } + +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/HandlerMethodArgumentResolverComposite.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/HandlerMethodArgumentResolverComposite.java index b6b7c7f5c..115fa1014 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/HandlerMethodArgumentResolverComposite.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/HandlerMethodArgumentResolverComposite.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,6 +49,18 @@ public void addResolver(HandlerMethodArgumentResolver resolver) { this.argumentResolvers.add(resolver); } + /** + * Add the given {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}. + */ + public HandlerMethodArgumentResolverComposite addResolvers( + @Nullable List resolvers) { + + if (resolvers != null) { + this.argumentResolvers.addAll(resolvers); + } + return this; + } + /** * Return a read-only list with the contained resolvers, or an empty list. */ 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 b0d526cb9..fc6366477 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 @@ -18,11 +18,13 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -36,6 +38,19 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.dataloader.DataLoader; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.expression.BeanResolver; +import org.springframework.graphql.data.method.BatchHandlerMethodArgumentResolver; +import org.springframework.graphql.data.method.BatchHandlerMethodArgumentResolverComposite; +import org.springframework.graphql.data.method.annotation.support.batch.ArgumentBatchMethodArgumentResolver; +import org.springframework.graphql.data.method.annotation.support.batch.ArgumentMapBatchMethodArgumentResolver; +import org.springframework.graphql.data.method.annotation.support.batch.ArgumentsBatchMethodArgumentResolver; +import org.springframework.graphql.data.method.annotation.support.batch.BatchLoaderEnvironmentBatchMethodArgumentResolver; +import org.springframework.graphql.data.method.annotation.support.batch.ContextValueBatchMethodArgumentResolver; +import org.springframework.graphql.data.method.annotation.support.batch.ContinuationBatchMethodArgumentResolver; +import org.springframework.graphql.data.method.annotation.support.batch.GraphQLContextBatchMethodArgumentResolver; +import org.springframework.graphql.data.method.annotation.support.batch.PrincipalBatchMethodArgumentResolver; +import org.springframework.graphql.data.method.annotation.support.batch.SourceBatchMethodArgumentResolver; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -43,13 +58,11 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodIntrospector; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.convert.ConversionService; -import org.springframework.expression.BeanResolver; import org.springframework.format.FormatterRegistrar; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.FormattingConversionService; @@ -74,6 +87,7 @@ * * @author Rossen Stoyanchev * @author Brian Clozel + * @author Genkui Du * @since 1.0.0 */ public class AnnotatedControllerConfigurer @@ -111,6 +125,12 @@ public class AnnotatedControllerConfigurer @Nullable private HandlerMethodArgumentResolverComposite argumentResolvers; + @Nullable + private BatchHandlerMethodArgumentResolverComposite batchMethodArgumentResolvers; + + @Nullable + private List customBatchMethodArgumentResolvers; + @Nullable private HandlerMethodInputValidator validator; @@ -138,6 +158,22 @@ public void setConversionService(ConversionService conversionService) { this.conversionService = (FormattingConversionService) conversionService; } + /** + * Provide resolvers for custom batch method argument types. Custom resolvers + * are ordered after built-in ones. + */ + public void setCustomBatchMethodArgumentResolvers(@Nullable List customBatchMethodArgumentResolvers) { + this.customBatchMethodArgumentResolvers = customBatchMethodArgumentResolvers; + } + + /** + * Return the custom custom batch method argument resolvers, or {@code null}. + */ + @Nullable + public List getCustomBatchMethodArgumentResolvers() { + return this.customBatchMethodArgumentResolvers; + } + @Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; @@ -151,37 +187,73 @@ protected final ApplicationContext obtainApplicationContext() { @Override public void afterPropertiesSet() { - this.argumentResolvers = new HandlerMethodArgumentResolverComposite(); + + List resolvers = getDefaultArgumentResolvers(); + this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); + + List batchMethodResolvers = getBatchMethodDefaultArgumentResolvers(); + batchMethodArgumentResolvers = new BatchHandlerMethodArgumentResolverComposite().addResolvers(batchMethodResolvers); + + if (beanValidationPresent) { + this.validator = HandlerMethodInputValidatorFactory.create(obtainApplicationContext()); + } + } + + private List getDefaultArgumentResolvers() { + List resolvers = new ArrayList<>(); // Annotation based if (springDataPresent) { // Must be ahead of ArgumentMethodArgumentResolver - this.argumentResolvers.addResolver(new ProjectedPayloadMethodArgumentResolver(obtainApplicationContext())); + resolvers.add(new ProjectedPayloadMethodArgumentResolver(obtainApplicationContext())); } - this.argumentResolvers.addResolver(new ArgumentMapMethodArgumentResolver()); + resolvers.add(new ArgumentMapMethodArgumentResolver()); GraphQlArgumentBinder argumentBinder = new GraphQlArgumentBinder(this.conversionService); - this.argumentResolvers.addResolver(new ArgumentMethodArgumentResolver(argumentBinder)); - this.argumentResolvers.addResolver(new ArgumentsMethodArgumentResolver(argumentBinder)); - this.argumentResolvers.addResolver(new ContextValueMethodArgumentResolver()); + resolvers.add(new ArgumentMethodArgumentResolver(argumentBinder)); + resolvers.add(new ArgumentsMethodArgumentResolver(argumentBinder)); + resolvers.add(new ContextValueMethodArgumentResolver()); // Type based - this.argumentResolvers.addResolver(new DataFetchingEnvironmentMethodArgumentResolver()); - this.argumentResolvers.addResolver(new DataLoaderMethodArgumentResolver()); + resolvers.add(new DataFetchingEnvironmentMethodArgumentResolver()); + resolvers.add(new DataLoaderMethodArgumentResolver()); if (springSecurityPresent) { - this.argumentResolvers.addResolver(new PrincipalMethodArgumentResolver()); + resolvers.add(new PrincipalMethodArgumentResolver()); BeanResolver beanResolver = new BeanFactoryResolver(obtainApplicationContext()); - this.argumentResolvers.addResolver(new AuthenticationPrincipalArgumentResolver(beanResolver)); + resolvers.add(new AuthenticationPrincipalArgumentResolver(beanResolver)); } if (KotlinDetector.isKotlinPresent()) { - this.argumentResolvers.addResolver(new ContinuationHandlerMethodArgumentResolver()); + resolvers.add(new ContinuationHandlerMethodArgumentResolver()); } // This works as a fallback, after other resolvers - this.argumentResolvers.addResolver(new SourceMethodArgumentResolver()); + resolvers.add(new SourceMethodArgumentResolver()); - if (beanValidationPresent) { - this.validator = HandlerMethodInputValidatorFactory.create(obtainApplicationContext()); + return resolvers; + } + + private List getBatchMethodDefaultArgumentResolvers() { + List resolvers = new ArrayList<>(); + + resolvers.add(new ArgumentMapBatchMethodArgumentResolver()); + GraphQlArgumentBinder argumentBinder = new GraphQlArgumentBinder(this.conversionService); + resolvers.add(new ArgumentBatchMethodArgumentResolver(argumentBinder)); + resolvers.add(new ArgumentsBatchMethodArgumentResolver(argumentBinder)); + resolvers.add(new SourceBatchMethodArgumentResolver()); + resolvers.add(new ContextValueBatchMethodArgumentResolver()); + resolvers.add(new GraphQLContextBatchMethodArgumentResolver()); + resolvers.add(new BatchLoaderEnvironmentBatchMethodArgumentResolver()); + if (KotlinDetector.isKotlinPresent()) { + resolvers.add(new ContinuationBatchMethodArgumentResolver()); + } + if (springSecurityPresent) { + resolvers.add(new PrincipalBatchMethodArgumentResolver()); + } + + // Custom batch method argument resolvers + if (getCustomBatchMethodArgumentResolvers() != null) { + resolvers.addAll(getCustomBatchMethodArgumentResolvers()); } + return resolvers; } @Override @@ -355,7 +427,7 @@ private String registerBatchLoader(MappingInfo info) { BatchLoaderRegistry registry = obtainApplicationContext().getBean(BatchLoaderRegistry.class); HandlerMethod handlerMethod = info.getHandlerMethod(); - BatchLoaderHandlerMethod invocable = new BatchLoaderHandlerMethod(handlerMethod); + BatchLoaderHandlerMethod invocable = new BatchLoaderHandlerMethod(handlerMethod, this.batchMethodArgumentResolvers, this.validator); Class clazz = handlerMethod.getReturnType().getParameterType(); if (clazz.equals(Flux.class) || Collection.class.isAssignableFrom(clazz)) { @@ -421,7 +493,7 @@ static class SchemaMappingDataFetcher implements DataFetcher { private final boolean subscription; public SchemaMappingDataFetcher(MappingInfo info, HandlerMethodArgumentResolverComposite resolvers, - @Nullable HandlerMethodInputValidator validator) { + @Nullable HandlerMethodInputValidator validator) { this.info = info; this.argumentResolvers = resolvers; this.validator = validator; @@ -465,7 +537,7 @@ public Object get(DataFetchingEnvironment env) { if (dataLoader == null) { throw new IllegalStateException("No DataLoader for key '" + this.dataLoaderKey + "'"); } - return dataLoader.load(env.getSource()); + return dataLoader.load(env.getSource(), env); } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/BatchLoaderHandlerMethod.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/BatchLoaderHandlerMethod.java index cd5824321..73b8042f8 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/BatchLoaderHandlerMethod.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/BatchLoaderHandlerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,23 +15,23 @@ */ package org.springframework.graphql.data.method.annotation.support; -import java.security.Principal; import java.util.Arrays; import java.util.Collection; import java.util.Map; -import graphql.GraphQLContext; import org.dataloader.BatchLoaderEnvironment; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.graphql.data.method.BatchHandlerMethodArgumentResolverComposite; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.core.CollectionFactory; import org.springframework.core.MethodParameter; import org.springframework.graphql.data.method.HandlerMethod; import org.springframework.graphql.data.method.InvocableHandlerMethodSupport; -import org.springframework.graphql.data.method.annotation.ContextValue; import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; /** * An extension of {@link HandlerMethod} for annotated handler methods adapted to @@ -40,20 +40,51 @@ * {@link BatchLoaderEnvironment} as their input. * * @author Rossen Stoyanchev + * @author Genkui Du * @since 1.0.0 */ public class BatchLoaderHandlerMethod extends InvocableHandlerMethodSupport { - private final static boolean springSecurityPresent = ClassUtils.isPresent( - "org.springframework.security.core.context.SecurityContext", - AnnotatedControllerConfigurer.class.getClassLoader()); + private static final Object[] EMPTY_ARGS = new Object[0]; + private final BatchHandlerMethodArgumentResolverComposite resolvers; - public BatchLoaderHandlerMethod(HandlerMethod handlerMethod) { + @Nullable + private final HandlerMethodInputValidator validator; + + private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + /** + * Constructor with a parent handler method. + * @param handlerMethod the handler method + * @param resolvers the argument resolvers + * @param validator the input validator + */ + public BatchLoaderHandlerMethod(HandlerMethod handlerMethod, + BatchHandlerMethodArgumentResolverComposite resolvers, + @Nullable HandlerMethodInputValidator validator) { super(handlerMethod); + Assert.isTrue(!resolvers.getArgumentResolvers().isEmpty(), "No argument resolvers"); + this.resolvers = resolvers; + this.validator = validator; } + /** + * Return the configured argument resolvers. + */ + public BatchHandlerMethodArgumentResolverComposite getResolvers() { + return resolvers; + } + + /** + * Return the configured input validator. + */ + @Nullable + public HandlerMethodInputValidator getValidator() { + return this.validator; + } + /** * Invoke the underlying batch loader method with a collection of keys to * return a Map of key-value pairs. @@ -66,7 +97,16 @@ public BatchLoaderHandlerMethod(HandlerMethod handlerMethod) { */ @Nullable public Mono> invokeForMap(Collection keys, BatchLoaderEnvironment environment) { - Object[] args = getMethodArgumentValues(keys, environment); + Object[] args; + try { + args = getMethodArgumentValues(keys, environment); + if (this.validator != null) { + this.validator.validate(this, args); + } + } + catch (Throwable ex) { + return Mono.error(ex); + } if (doesNotHaveAsyncArgs(args)) { Object result = doInvoke(args); return toMonoMap(result); @@ -87,7 +127,16 @@ public Mono> invokeForMap(Collection keys, BatchLoaderEnviro * @return a {@code Flux} of values. */ public Flux invokeForIterable(Collection keys, BatchLoaderEnvironment environment) { - Object[] args = getMethodArgumentValues(keys, environment); + Object[] args; + try { + args = getMethodArgumentValues(keys, environment); + if (this.validator != null) { + this.validator.validate(this, args); + } + } + catch (Throwable ex) { + return Flux.error(ex); + } if (doesNotHaveAsyncArgs(args)) { Object result = doInvoke(args); return toFlux(result); @@ -98,47 +147,37 @@ public Flux invokeForIterable(Collection keys, BatchLoaderEnvironment }); } - private Object[] getMethodArgumentValues(Collection keys, BatchLoaderEnvironment environment) { - Object[] args = new Object[getMethodParameters().length]; - for (int i = 0; i < getMethodParameters().length; i++) { - args[i] = resolveArgument(getMethodParameters()[i], keys, environment); - } - return args; - } - - @Nullable - private Object resolveArgument( - MethodParameter parameter, Collection keys, BatchLoaderEnvironment environment) { + @SuppressWarnings("unchecked") + private Object[] getMethodArgumentValues(Collection keys, BatchLoaderEnvironment environment) throws Exception { - Class parameterType = parameter.getParameterType(); + MethodParameter[] parameters = getMethodParameters(); + if (ObjectUtils.isEmpty(parameters)) { + return EMPTY_ARGS; + } - if (Collection.class.isAssignableFrom(parameterType)) { - if (parameterType.isInstance(keys)) { - return keys; + Object[] args = new Object[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + MethodParameter parameter = parameters[i]; + parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); + if(!this.resolvers.supportsParameter(parameter)){ + throw new IllegalStateException(formatArgumentError(parameter, "Unexpected argument type.")); } - Class elementType = parameter.nested().getNestedParameterType(); - Collection collection = CollectionFactory.createCollection(parameterType, elementType, keys.size()); - collection.addAll(keys); - return collection; - } - else if (parameter.hasParameterAnnotation(ContextValue.class)) { - return ContextValueMethodArgumentResolver.resolveContextValue(parameter, null, environment.getContext()); - } - else if (parameterType.equals(GraphQLContext.class)) { - return environment.getContext(); - } - else if (parameterType.isInstance(environment)) { - return environment; - } - else if ("kotlin.coroutines.Continuation".equals(parameterType.getName())) { - return null; - } - else if (springSecurityPresent && Principal.class.isAssignableFrom(parameter.getParameterType())) { - return PrincipalMethodArgumentResolver.doResolve(); - } - else { - throw new IllegalStateException(formatArgumentError(parameter, "Unexpected argument type.")); + try { + args[i] = this.resolvers.resolveArgument(parameter, keys, (Map) environment.getKeyContexts(), environment); + } + catch (Exception ex) { + // Leave stack trace for later, exception may actually be resolved and handled... + if (logger.isDebugEnabled()) { + String exMsg = ex.getMessage(); + if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) { + logger.debug(formatArgumentError(parameter, exMsg)); + } + } + throw ex; + } + } + return args; } private boolean doesNotHaveAsyncArgs(Object[] args) { diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ContextValueMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ContextValueMethodArgumentResolver.java index 6368d54a5..b50eb1d5c 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ContextValueMethodArgumentResolver.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ContextValueMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/ArgumentBatchMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/ArgumentBatchMethodArgumentResolver.java new file mode 100644 index 000000000..0cb9f13f5 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/ArgumentBatchMethodArgumentResolver.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.graphql.data.method.annotation.support.batch; + +import graphql.schema.DataFetchingEnvironment; +import org.dataloader.BatchLoaderEnvironment; +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.graphql.data.GraphQlArgumentBinder; +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.util.Collection; +import java.util.Map; + +/** + * Resolver for {@link Argument @Argument} annotated method parameters, obtained + * via {@link DataFetchingEnvironment#getArgument(String)} and converted to the + * declared type of the method parameter. + * + * @author Genkui Du + * @since 1.0.0 + */ +public class ArgumentBatchMethodArgumentResolver extends BatchHandlerMethodArgumentResolverSupport { + + private final GraphQlArgumentBinder argumentBinder; + + public ArgumentBatchMethodArgumentResolver(GraphQlArgumentBinder argumentBinder) { + Assert.notNull(argumentBinder, "GraphQlArgumentBinder is required"); + this.argumentBinder = argumentBinder; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterAnnotation(Argument.class) != null; + } + + @Override + public Object resolveArgument(MethodParameter parameter, + Collection keys, + Map keyContexts, + BatchLoaderEnvironment environment) throws Exception { + String name = getArgumentName(parameter); + ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter); + + return this.argumentBinder.bind(findFirstDataFetchingEnvironmentFromContexts(keyContexts), name, resolvableType); + } + + static String getArgumentName(MethodParameter parameter) { + Argument annotation = parameter.getParameterAnnotation(Argument.class); + Assert.state(annotation != null, "Expected @Argument annotation"); + if (StringUtils.hasText(annotation.name())) { + return annotation.name(); + } + String parameterName = parameter.getParameterName(); + if (parameterName != null) { + return parameterName; + } + throw new IllegalArgumentException( + "Name for argument of type [" + parameter.getNestedParameterType().getName() + + "] not specified, and parameter name information not found in class file either."); + } +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/ArgumentMapBatchMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/ArgumentMapBatchMethodArgumentResolver.java new file mode 100644 index 000000000..70f66cee7 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/ArgumentMapBatchMethodArgumentResolver.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.graphql.data.method.annotation.support.batch; + +import graphql.schema.DataFetchingEnvironment; +import org.dataloader.BatchLoaderEnvironment; +import org.springframework.core.MethodParameter; +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.util.StringUtils; + +import java.util.Collection; +import java.util.Map; + +/** + * Resolves {@link Map} method arguments annotated with an @{@link Argument} + * where the annotation does not specify an argument name. + * + * @author Genkui Du + * @since 1.0.0 + */ +public class ArgumentMapBatchMethodArgumentResolver extends BatchHandlerMethodArgumentResolverSupport { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + Argument argument = parameter.getParameterAnnotation(Argument.class); + return (argument != null && + Map.class.isAssignableFrom(parameter.getParameterType()) && + !StringUtils.hasText(argument.name())); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + Collection keys, + Map keyContexts, + BatchLoaderEnvironment environment) throws Exception { + return findFirstDataFetchingEnvironmentFromContexts(keyContexts).getArguments(); + } +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/ArgumentsBatchMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/ArgumentsBatchMethodArgumentResolver.java new file mode 100644 index 000000000..378aea2c4 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/ArgumentsBatchMethodArgumentResolver.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.graphql.data.method.annotation.support.batch; + +import graphql.schema.DataFetchingEnvironment; +import org.dataloader.BatchLoaderEnvironment; +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.graphql.data.GraphQlArgumentBinder; +import org.springframework.graphql.data.method.annotation.Arguments; +import org.springframework.util.Assert; + +import java.util.Collection; +import java.util.Map; + +/** + * Resolver for {@link Arguments @Arguments} annotated method parameters, + * obtained via {@link DataFetchingEnvironment#getArgument(String)} and + * converted to the declared type of the method parameter. + * + * @author Genkui Du + * @since 1.0.0 + */ +public class ArgumentsBatchMethodArgumentResolver extends BatchHandlerMethodArgumentResolverSupport { + + private final GraphQlArgumentBinder argumentBinder; + + public ArgumentsBatchMethodArgumentResolver(GraphQlArgumentBinder argumentBinder) { + Assert.notNull(argumentBinder, "GraphQlArgumentBinder is required"); + this.argumentBinder = argumentBinder; + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterAnnotation(Arguments.class) != null; + } + + @Override + public Object resolveArgument(MethodParameter parameter, + Collection keys, + Map keyContexts, + BatchLoaderEnvironment environment) throws Exception { + ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter); + + return this.argumentBinder.bind(findFirstDataFetchingEnvironmentFromContexts(keyContexts), null, resolvableType); + } +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/BatchHandlerMethodArgumentResolverSupport.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/BatchHandlerMethodArgumentResolverSupport.java new file mode 100644 index 000000000..d5696d0df --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/BatchHandlerMethodArgumentResolverSupport.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.graphql.data.method.annotation.support.batch; + +import graphql.schema.DataFetchingEnvironment; +import org.springframework.graphql.data.method.BatchHandlerMethodArgumentResolver; + +import java.util.Map; + +/** + * Base implementation of the {@link BatchHandlerMethodArgumentResolver} with some methods + * which may be useful in subclasses. + * + * @author Genkui Du + * @since 1.0.0 + */ +public abstract class BatchHandlerMethodArgumentResolverSupport implements BatchHandlerMethodArgumentResolver { + + protected DataFetchingEnvironment findFirstDataFetchingEnvironmentFromContexts(Map keyContexts) { + return keyContexts.values().iterator().next(); + } + +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/BatchLoaderEnvironmentBatchMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/BatchLoaderEnvironmentBatchMethodArgumentResolver.java new file mode 100644 index 000000000..83bfd95d1 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/BatchLoaderEnvironmentBatchMethodArgumentResolver.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.graphql.data.method.annotation.support.batch; + +import graphql.schema.DataFetchingEnvironment; +import org.dataloader.BatchLoaderEnvironment; +import org.springframework.core.MethodParameter; + +import java.util.Collection; +import java.util.Map; + +/** + * Resolves {@link BatchLoaderEnvironment} method arguments. + * + *

For direct access to the underlying {@link BatchLoaderEnvironment}. + * + * @author Genkui Du + * @since 1.0.0 + */ +public class BatchLoaderEnvironmentBatchMethodArgumentResolver extends BatchHandlerMethodArgumentResolverSupport { + + static BatchLoaderEnvironment DUMMY_ENVIRONMENT = BatchLoaderEnvironment.newBatchLoaderEnvironment().build(); + + @Override + public boolean supportsParameter(MethodParameter parameter) { + Class parameterType = parameter.getParameterType(); + return parameterType.isInstance(DUMMY_ENVIRONMENT); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + Collection keys, + Map keyContexts, + BatchLoaderEnvironment environment) throws Exception { + return environment; + } +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/ContextValueBatchMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/ContextValueBatchMethodArgumentResolver.java new file mode 100644 index 000000000..d72f7e9a2 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/ContextValueBatchMethodArgumentResolver.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.graphql.data.method.annotation.support.batch; + +import graphql.GraphQLContext; +import graphql.schema.DataFetchingEnvironment; +import org.dataloader.BatchLoaderEnvironment; +import org.springframework.core.MethodParameter; +import org.springframework.graphql.data.method.annotation.ContextValue; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Optional; + +/** + * Resolver for {@link ContextValue @ContextValue} annotated method parameters. + * + *

For access to a value from the GraphQLContext of BatchLoaderEnvironment, + * which is the same context as the one from the DataFetchingEnvironment. + * + * @author Genkui Du + * @since 1.0.0 + */ +public class ContextValueBatchMethodArgumentResolver extends BatchHandlerMethodArgumentResolverSupport { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(ContextValue.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + Collection keys, + Map keyContexts, + BatchLoaderEnvironment environment) throws Exception { + if(keyContexts.isEmpty()){ + return null; + } + + return resolveContextValue(parameter, null, environment.getContext()); + } + + @Nullable + static Object resolveContextValue( + MethodParameter parameter, @Nullable Object localContext, GraphQLContext graphQlContext) { + + ContextValue annotation = parameter.getParameterAnnotation(ContextValue.class); + Assert.state(annotation != null, "Expected @ContextValue annotation"); + String name = getValueName(parameter, annotation); + + Class parameterType = parameter.getParameterType(); + Object value = null; + + if (localContext instanceof GraphQLContext) { + value = ((GraphQLContext) localContext).get(name); + } + + if (value != null) { + return wrapAsOptionalIfNecessary(value, parameterType); + } + + value = graphQlContext.get(name); + if (value == null && annotation.required() && !parameterType.equals(Optional.class)) { + throw new IllegalStateException("Missing required context value for " + parameter); + } + + return wrapAsOptionalIfNecessary(value, parameterType); + } + + private static String getValueName(MethodParameter parameter, ContextValue annotation) { + if (StringUtils.hasText(annotation.name())) { + return annotation.name(); + } + String parameterName = parameter.getParameterName(); + if (parameterName != null) { + return parameterName; + } + throw new IllegalArgumentException("Name for @ContextValue argument " + + "of type [" + parameter.getNestedParameterType().getName() + "] not specified, " + + "and parameter name information not found in class file either."); + } + + @Nullable + private static Object wrapAsOptionalIfNecessary(@Nullable Object value, Class type) { + return (type.equals(Optional.class) ? Optional.ofNullable(value) : value); + } +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/ContinuationBatchMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/ContinuationBatchMethodArgumentResolver.java new file mode 100644 index 000000000..3a30c4276 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/ContinuationBatchMethodArgumentResolver.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.graphql.data.method.annotation.support.batch; + +import graphql.schema.DataFetchingEnvironment; +import org.dataloader.BatchLoaderEnvironment; +import org.springframework.core.MethodParameter; + +import java.util.Collection; +import java.util.Map; + +/** + * No-op resolver for method arguments of type {@link kotlin.coroutines.Continuation}. + * + * @author Genkui Du + * @since 1.0.0 + */ +public class ContinuationBatchMethodArgumentResolver extends BatchHandlerMethodArgumentResolverSupport { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + Class parameterType = parameter.getParameterType(); + return "kotlin.coroutines.Continuation".equals(parameterType.getName()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + Collection keys, + Map keyContexts, + BatchLoaderEnvironment environment) throws Exception { + return null; + } +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/GraphQLContextBatchMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/GraphQLContextBatchMethodArgumentResolver.java new file mode 100644 index 000000000..43379cf23 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/GraphQLContextBatchMethodArgumentResolver.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.graphql.data.method.annotation.support.batch; + +import graphql.GraphQLContext; +import graphql.schema.DataFetchingEnvironment; +import org.dataloader.BatchLoaderEnvironment; +import org.springframework.core.MethodParameter; + +import java.util.Collection; +import java.util.Map; + +/** + * Resolver for access to the context from the BatchLoaderEnvironment, + * which is the same context as the one from the DataFetchingEnvironment. + * + * @author Genkui Du + * @since 1.0.0 + */ +public class GraphQLContextBatchMethodArgumentResolver extends BatchHandlerMethodArgumentResolverSupport { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + Class parameterType = parameter.getParameterType(); + return parameterType.equals(GraphQLContext.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + Collection keys, + Map keyContexts, + BatchLoaderEnvironment environment) throws Exception { + return environment.getContext(); + } +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/PrincipalBatchMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/PrincipalBatchMethodArgumentResolver.java new file mode 100644 index 000000000..fd75f0d5e --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/PrincipalBatchMethodArgumentResolver.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.graphql.data.method.annotation.support.batch; + +import graphql.schema.DataFetchingEnvironment; +import org.dataloader.BatchLoaderEnvironment; +import org.springframework.core.MethodParameter; +import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.ClassUtils; + +import java.security.Principal; +import java.util.Collection; +import java.util.Map; + +/** + * Resolver to obtain {@link Principal} from Spring Security context via + * {@link SecurityContext#getAuthentication()}. + * + *

The resolver checks both ThreadLocal context via {@link SecurityContextHolder} + * for Spring MVC applications, and {@link ReactiveSecurityContextHolder} for + * Spring WebFlux applications. It returns . + * + * @author Genkui Du + * @since 1.0.0 + */ +public class PrincipalBatchMethodArgumentResolver extends BatchHandlerMethodArgumentResolverSupport { + + private final static boolean springSecurityPresent = ClassUtils.isPresent( + "org.springframework.security.core.context.SecurityContext", + AnnotatedControllerConfigurer.class.getClassLoader()); + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return springSecurityPresent && Principal.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + Collection keys, + Map keyContexts, + BatchLoaderEnvironment environment) throws Exception { + return doResolve(); + } + + static Object doResolve() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return (authentication != null ? authentication : + ReactiveSecurityContextHolder.getContext().map(SecurityContext::getAuthentication)); + } +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/SourceBatchMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/SourceBatchMethodArgumentResolver.java new file mode 100644 index 000000000..5b9d7d4ef --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/SourceBatchMethodArgumentResolver.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.graphql.data.method.annotation.support.batch; + +import graphql.schema.DataFetchingEnvironment; +import org.dataloader.BatchLoaderEnvironment; +import org.springframework.core.CollectionFactory; +import org.springframework.core.MethodParameter; + +import java.util.Collection; +import java.util.Map; + + +/** + * Resolver for access source/parent objects. + * + * @author GenKui Du + * @since 1.0.0 + */ +public class SourceBatchMethodArgumentResolver extends BatchHandlerMethodArgumentResolverSupport { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + Class parameterType = parameter.getParameterType(); + return Collection.class.isAssignableFrom(parameterType); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + Collection keys, + Map keyContexts, + BatchLoaderEnvironment environment) throws Exception { + Class parameterType = parameter.getParameterType(); + if (parameterType.isInstance(keys)) { + return keys; + } + + Class elementType = parameter.nested().getNestedParameterType(); + Collection collection = CollectionFactory.createCollection(parameterType, elementType, keys.size()); + collection.addAll(keys); + return collection; + } + +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/package-info.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/package-info.java new file mode 100644 index 000000000..7d8dde5e2 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Resolvers for method parameters of annotated batch handler methods. + */ +@NonNullApi +@NonNullFields +package org.springframework.graphql.data.method.annotation.support.batch; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingInvocationTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingInvocationTests.java index 6d23982d6..9f8db05b0 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingInvocationTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingInvocationTests.java @@ -25,6 +25,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.graphql.data.method.annotation.Argument; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -62,6 +63,7 @@ void oneToOne(CourseController controller) { " courses { " + " id" + " name" + + " classRoomNo(date:\"Monday\")"+ " instructor {" + " id" + " firstName" + @@ -81,6 +83,7 @@ void oneToOne(CourseController controller) { Course actualCourse = actualCourses.get(i); Course course = courses.get(i); assertThat(actualCourse).isEqualTo(course); + assertThat(actualCourse.classRoomNo()).isEqualTo("001"); Person actualInstructor = actualCourse.instructor(); assertThat(actualInstructor.firstName()).isEqualTo(course.instructor().firstName()); @@ -95,6 +98,7 @@ void oneToMany(CourseController controller) { " courses { " + " id" + " name" + + " classRoomNo(date:\"Monday\")"+ " students {" + " id" + " firstName" + @@ -114,6 +118,7 @@ void oneToMany(CourseController controller) { Course actualCourse = actualCourses.get(i); Course course = courses.get(i); assertThat(actualCourse.name()).isEqualTo(course.name()); + assertThat(actualCourse.classRoomNo()).isEqualTo("001"); List actualStudents = actualCourse.students(); List students = course.students(); @@ -130,6 +135,11 @@ void oneToMany(CourseController controller) { @Controller private static class BatchMonoMapController extends CourseController { + @BatchMapping + public Mono> classRoomNo(List courses, @Argument String date) { + return Flux.fromIterable(courses).collect(Collectors.toMap(Function.identity(), course -> course.classRoomNo(course.id(), date))); + } + @BatchMapping public Mono> instructor(List courses) { return Flux.fromIterable(courses).collect(Collectors.toMap(Function.identity(), Course::instructor)); @@ -144,6 +154,11 @@ public Mono>> students(Set courses) { @Controller private static class BatchMapController extends CourseController { + @BatchMapping + public Map classRoomNo(List courses, @Argument String date) { + return courses.stream().collect(Collectors.toMap(Function.identity(), course -> course.classRoomNo(course.id(), date))); + } + @BatchMapping public Map instructor(List courses) { return courses.stream().collect(Collectors.toMap(Function.identity(), Course::instructor)); @@ -158,6 +173,11 @@ public Map> students(List courses) { @Controller private static class BatchFluxController extends CourseController { + @BatchMapping + public Flux classRoomNo(List courses, @Argument String date) { + return Flux.fromIterable(courses).map(course -> course.classRoomNo(course.id(), date)); + } + @BatchMapping public Flux instructor(List courses) { return Flux.fromIterable(courses).map(Course::instructor); @@ -172,6 +192,11 @@ public Flux> students(List courses) { @Controller private static class BatchListController extends CourseController { + @BatchMapping + public List classRoomNo(List courses, @DefaultStringValue("Monday") String byDefault) { + return courses.stream().map(course -> course.classRoomNo(course.id(), byDefault)).collect(Collectors.toList()); + } + @BatchMapping public List instructor(List courses) { return courses.stream().map(Course::instructor).collect(Collectors.toList()); diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingTestSupport.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingTestSupport.java index a63e9b00a..c04759fcc 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingTestSupport.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingTestSupport.java @@ -15,6 +15,11 @@ */ package org.springframework.graphql.data.method.annotation.support; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -27,13 +32,18 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import graphql.schema.DataFetchingEnvironment; +import org.dataloader.BatchLoaderEnvironment; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.MethodParameter; import org.springframework.graphql.ExecutionGraphQlService; import org.springframework.graphql.GraphQlSetup; +import org.springframework.graphql.data.method.BatchHandlerMethodArgumentResolver; import org.springframework.graphql.data.method.annotation.QueryMapping; import org.springframework.graphql.execution.BatchLoaderRegistry; import org.springframework.graphql.execution.DefaultBatchLoaderRegistry; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Support class for {@code @BatchMapping}, and other batch loading tests, that @@ -48,9 +58,11 @@ public class BatchMappingTestSupport { static final Map personMap = new HashMap<>(); + static final Map> classRoomNoMap= new HashMap<>(); + static { - Course.save(11L, "Ethical Hacking", 15L, Arrays.asList(22L, 26L, 31L)); - Course.save(19L, "Docker and Kubernetes", 17L, Arrays.asList(31L, 39L, 44L, 45L)); + Course.save(11L, "Ethical Hacking", 15L, Arrays.asList(22L, 26L, 31L), Collections.singletonMap("Monday", "001")); + Course.save(19L, "Docker and Kubernetes", 17L, Arrays.asList(31L, 39L, 44L, 45L), Collections.singletonMap("Monday", "001")); Person.save(15L, "Josh", "Kelly"); Person.save(17L, "Albert", "Murray"); @@ -69,6 +81,7 @@ public class BatchMappingTestSupport { "type Course {" + " id: ID" + " name: String" + + " classRoomNo(date: String): String" + " instructor: Person" + " students: [Person]" + "}" + @@ -88,7 +101,7 @@ protected ExecutionGraphQlService createGraphQlService(CourseController controll context.refresh(); return GraphQlSetup.schemaContent(schema) - .runtimeWiringForAnnotatedControllers(context) + .runtimeWiringForAnnotatedControllers(context, Collections.singletonList(new DefaultValueResolver())) .dataLoaders(registry) .toGraphQlService(); } @@ -100,6 +113,8 @@ static class Course { private final String name; + private final String classRoomNo; + private final Long instructorId; private final List studentIds; @@ -107,28 +122,39 @@ static class Course { @JsonCreator public Course( @JsonProperty("id") Long id, @JsonProperty("name") String name, + @JsonProperty("classRoomNo") String classRoomNo, @JsonProperty("instructor") @Nullable Person instructor, @JsonProperty("students") @Nullable List students) { this.id = id; this.name = name; + this.classRoomNo = classRoomNo; this.instructorId = (instructor != null ? instructor.id() : -1); this.studentIds = (students != null ? students.stream().map(Person::id).collect(Collectors.toList()) : Collections.emptyList()); } - public Course(Long id, String name, Long instructorId, List studentIds) { + public Course(Long id, String name, String classRoomNo, Long instructorId, List studentIds) { this.id = id; this.name = name; + this.classRoomNo = classRoomNo; this.instructorId = instructorId; this.studentIds = studentIds; } + public Long id() { + return this.id; + } + public String name() { return this.name; } + public String classRoomNo() { + return this.classRoomNo; + } + public Long instructorId() { return this.instructorId; } @@ -141,13 +167,18 @@ public List students() { return this.studentIds.stream().map(personMap::get).collect(Collectors.toList()); } + public String classRoomNo(Long id, String date) { + return classRoomNoMap.getOrDefault(id, Collections.emptyMap()).get(date); + } + public Person instructor() { return personMap.get(this.instructorId); } - public static void save(Long id, String name, Long instructorId, List studentIds) { - Course course = new Course(id, name, instructorId, studentIds); + public static void save(Long id, String name, Long instructorId, List studentIds, Map classRoomNoByDate) { + Course course = new Course(id, name, "", instructorId, studentIds); courseMap.put(id, course); + classRoomNoMap.put(id, classRoomNoByDate); } public static List allCourses() { @@ -220,4 +251,29 @@ public Collection courses() { } } + private static class DefaultValueResolver implements BatchHandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(DefaultStringValue.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + Collection keys, + Map keyContexts, + BatchLoaderEnvironment environment) throws Exception { + DefaultStringValue annotation = parameter.getParameterAnnotation(DefaultStringValue.class); + Assert.state(annotation != null, "Expected @DefaultStringValue annotation"); + return annotation.value(); + } + } + + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @interface DefaultStringValue { + String value() default ""; + } + } diff --git a/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java b/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java index c17f88859..7b860aa97 100644 --- a/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java +++ b/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java @@ -18,6 +18,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import graphql.GraphQL; @@ -28,6 +29,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; +import org.springframework.graphql.data.method.BatchHandlerMethodArgumentResolver; import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer; import org.springframework.graphql.execution.DataFetcherExceptionResolver; import org.springframework.graphql.execution.DataLoaderRegistrar; @@ -86,8 +88,13 @@ public GraphQlSetup runtimeWiring(RuntimeWiringConfigurer configurer) { } public GraphQlSetup runtimeWiringForAnnotatedControllers(ApplicationContext context) { + return this.runtimeWiringForAnnotatedControllers(context, Collections.emptyList()); + } + + public GraphQlSetup runtimeWiringForAnnotatedControllers(ApplicationContext context, List customBatchMethodArgumentResolvers) { AnnotatedControllerConfigurer configurer = new AnnotatedControllerConfigurer(); configurer.setApplicationContext(context); + configurer.setCustomBatchMethodArgumentResolvers(customBatchMethodArgumentResolvers); configurer.afterPropertiesSet(); return runtimeWiring(configurer); }