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..35807a2b9 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/BatchHandlerMethodArgumentResolver.java @@ -0,0 +1,68 @@ +/* + * 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 type. + * + * @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 b0fa2de8b..6277b8af9 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; @@ -38,6 +40,14 @@ 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.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; @@ -74,6 +84,7 @@ * * @author Rossen Stoyanchev * @author Brian Clozel + * @author Genkui Du * @since 1.0.0 */ public class AnnotatedControllerConfigurer @@ -111,6 +122,12 @@ public class AnnotatedControllerConfigurer @Nullable private HandlerMethodArgumentResolverComposite argumentResolvers; + @Nullable + private List customBatchMethodArgumentResolvers; + + @Nullable + private BatchHandlerMethodArgumentResolverComposite batchMethodArgumentResolvers; + @Nullable private HandlerMethodInputValidator validator; @@ -138,6 +155,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 +184,69 @@ 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()); + resolvers.add(new ProjectedPayloadMethodArgumentResolver()); } - this.argumentResolvers.addResolver(new ArgumentMapMethodArgumentResolver()); + resolvers.add(new ArgumentMapMethodArgumentResolver()); GraphQlArgumentInitializer initializer = new GraphQlArgumentInitializer(this.conversionService); - this.argumentResolvers.addResolver(new ArgumentMethodArgumentResolver(initializer)); - this.argumentResolvers.addResolver(new ArgumentsMethodArgumentResolver(initializer)); - this.argumentResolvers.addResolver(new ContextValueMethodArgumentResolver()); + resolvers.add(new ArgumentMethodArgumentResolver(initializer)); + resolvers.add(new ArgumentsMethodArgumentResolver(initializer)); + 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 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 +420,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)) { @@ -465,7 +530,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..0a763e721 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,21 @@ */ 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.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,19 +38,48 @@ * {@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; + + /** + * 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 @@ -66,7 +93,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 +123,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 +143,36 @@ 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]; + 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) { @@ -167,4 +201,4 @@ else if (result instanceof Flux) { return Flux.error(new IllegalStateException("Unexpected return value: " + result)); } -} +} \ No newline at end of file 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..c14bb85db 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. @@ -52,7 +52,7 @@ public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment } @Nullable - static Object resolveContextValue( + public static Object resolveContextValue( MethodParameter parameter, @Nullable Object localContext, GraphQLContext graphQlContext) { ContextValue annotation = parameter.getParameterAnnotation(ContextValue.class); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/PrincipalMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/PrincipalMethodArgumentResolver.java index cc45ddbb6..4f3979739 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/PrincipalMethodArgumentResolver.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/PrincipalMethodArgumentResolver.java @@ -53,7 +53,7 @@ public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment return doResolve(); } - static Object doResolve() { + public 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/BatchLoaderEnvironmentBatchMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/BatchLoaderEnvironmentBatchMethodArgumentResolver.java new file mode 100644 index 000000000..8213bb332 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/BatchLoaderEnvironmentBatchMethodArgumentResolver.java @@ -0,0 +1,53 @@ +/* + * 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.BatchHandlerMethodArgumentResolver; + +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 implements BatchHandlerMethodArgumentResolver { + + 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..f1e492e2e --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/ContextValueBatchMethodArgumentResolver.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.MethodParameter; +import org.springframework.graphql.data.method.BatchHandlerMethodArgumentResolver; +import org.springframework.graphql.data.method.annotation.ContextValue; +import org.springframework.graphql.data.method.annotation.support.ContextValueMethodArgumentResolver; + +import java.util.Collection; +import java.util.Map; + + +/** + * 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 implements BatchHandlerMethodArgumentResolver { + + @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 ContextValueMethodArgumentResolver.resolveContextValue(parameter, null, environment.getContext()); + } +} 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..c0960f826 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/ContinuationBatchMethodArgumentResolver.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.schema.DataFetchingEnvironment; +import org.dataloader.BatchLoaderEnvironment; +import org.springframework.core.MethodParameter; +import org.springframework.graphql.data.method.BatchHandlerMethodArgumentResolver; + +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 implements BatchHandlerMethodArgumentResolver { + + @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..3149377c7 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/GraphQLContextBatchMethodArgumentResolver.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.GraphQLContext; +import graphql.schema.DataFetchingEnvironment; +import org.dataloader.BatchLoaderEnvironment; +import org.springframework.core.MethodParameter; +import org.springframework.graphql.data.method.BatchHandlerMethodArgumentResolver; + +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 implements BatchHandlerMethodArgumentResolver { + + @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..dda082216 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/PrincipalBatchMethodArgumentResolver.java @@ -0,0 +1,63 @@ +/* + * 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.BatchHandlerMethodArgumentResolver; +import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer; +import org.springframework.graphql.data.method.annotation.support.PrincipalMethodArgumentResolver; +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 implements BatchHandlerMethodArgumentResolver { + + 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 PrincipalMethodArgumentResolver.doResolve(); + } +} 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..dd52a0285 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/SourceBatchMethodArgumentResolver.java @@ -0,0 +1,58 @@ +/* + * 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 org.springframework.graphql.data.method.BatchHandlerMethodArgumentResolver; + +import java.util.Collection; +import java.util.Map; + + +/** + * Resolver for access source/parent objects. + * + * @author GenKui Du + * @since 1.0.0 + */ +public class SourceBatchMethodArgumentResolver implements BatchHandlerMethodArgumentResolver { + + @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..0de0589f1 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/batch/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ +package org.springframework.graphql.data.method.annotation.support.batch; \ No newline at end of file