diff --git a/spring-graphql-docs/src/docs/asciidoc/index.adoc b/spring-graphql-docs/src/docs/asciidoc/index.adoc index 0eaebcd1..22339f6f 100644 --- a/spring-graphql-docs/src/docs/asciidoc/index.adoc +++ b/spring-graphql-docs/src/docs/asciidoc/index.adoc @@ -1202,8 +1202,10 @@ See <>. See <>. | `@ContextValue` -| For access to a value from the localContext, if it is an instance of `GraphQLContext`, - or from the `GraphQLContext` of `DataFetchingEnvironment`. +| For access to an attribute from the main `GraphQLContext` in `DataFetchingEnvironment`. + +| `@LocalContextValue` +| For access to an attribute from the local `GraphQLContext` in `DataFetchingEnvironment`. | `GraphQLContext` | For access to the context from the `DataFetchingEnvironment`. diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/ContextValue.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/ContextValue.java index a4151c2c..0a2e1ef2 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/ContextValue.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/ContextValue.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. @@ -21,18 +21,17 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import graphql.GraphQLContext; import graphql.schema.DataFetchingEnvironment; import org.springframework.core.annotation.AliasFor; + /** - * Annotation for method parameters obtained from one of the following: - * + * Annotation to bind a method parameter to an attribute from the + * {@link DataFetchingEnvironment#getGraphQlContext() main} {@code GraphQLContext}. + * + *

To bind to an attribute from the local context instead, see + * {@link LocalContextValue @LocalContextValue}. * * @author Rossen Stoyanchev * @since 1.0.0 diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/LocalContextValue.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/LocalContextValue.java new file mode 100644 index 00000000..23bc1083 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/LocalContextValue.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2021 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; + +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 graphql.schema.DataFetchingEnvironment; + +import org.springframework.core.annotation.AliasFor; + + +/** + * Annotation to bind a method parameter to an attribute from the + * {@link DataFetchingEnvironment#getLocalContext() local} {@code GraphQLContext}. + * + *

To bind to an attribute from the main context instead, see + * {@link ContextValue @ContextValue}. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + * @see ContextValue + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface LocalContextValue { + + /** + * Alias for {@link #name}. + */ + @AliasFor("name") + String value() default ""; + + /** + * The name of the value to bind to. + */ + @AliasFor("value") + String name() default ""; + + /** + * Whether the value is required. + *

Defaults to "true", leading to an exception thrown if the value is + * missing. Switch to "false" if you prefer {@code null} if the value is + * not present, or use {@link java.util.Optional}. + */ + boolean required() default true; + +} 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 cab7f4ae..9518cfe2 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 @@ -162,6 +162,7 @@ public class AnnotatedControllerConfigurer resolvers.addResolver(new ArgumentMethodArgumentResolver(argumentBinder)); resolvers.addResolver(new ArgumentsMethodArgumentResolver(argumentBinder)); resolvers.addResolver(new ContextValueMethodArgumentResolver()); + resolvers.addResolver(new LocalContextValueMethodArgumentResolver()); // Type based resolvers.addResolver(new DataFetchingEnvironmentMethodArgumentResolver()); 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 cd582432..26ab88ac 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. @@ -31,6 +31,7 @@ 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.Assert; import org.springframework.util.ClassUtils; /** @@ -122,7 +123,7 @@ public class BatchLoaderHandlerMethod extends InvocableHandlerMethodSupport { return collection; } else if (parameter.hasParameterAnnotation(ContextValue.class)) { - return ContextValueMethodArgumentResolver.resolveContextValue(parameter, null, environment.getContext()); + return resolveContextValueArgument(parameter, environment); } else if (parameterType.equals(GraphQLContext.class)) { return environment.getContext(); @@ -141,6 +142,17 @@ public class BatchLoaderHandlerMethod extends InvocableHandlerMethodSupport { } } + @Nullable + private Object resolveContextValueArgument(MethodParameter parameter, BatchLoaderEnvironment environment) { + + ContextValue annotation = parameter.getParameterAnnotation(ContextValue.class); + Assert.state(annotation != null, "Expected @ContextValue annotation"); + String name = ContextValueMethodArgumentResolver.getContextValueName(parameter, annotation.name(), annotation); + + return ContextValueMethodArgumentResolver.resolveContextValue( + name, annotation.required(), parameter, environment.getContext()); + } + private boolean doesNotHaveAsyncArgs(Object[] args) { return Arrays.stream(args).noneMatch(arg -> arg instanceof Mono); } 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 c7b3a760..1335c00f 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 @@ -15,6 +15,7 @@ */ package org.springframework.graphql.data.method.annotation.support; +import java.lang.annotation.Annotation; import java.util.Optional; import graphql.GraphQLContext; @@ -28,14 +29,9 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; + /** - * Resolver for {@link ContextValue @ContextValue} annotated method parameters. - * Values are resolved through one of the following: - *

+ * Resolver for a {@link ContextValue @ContextValue} annotated method parameter. * * @author Rossen Stoyanchev * @since 1.0.0 @@ -49,32 +45,39 @@ public class ContextValueMethodArgumentResolver implements HandlerMethodArgument @Override public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) { - return resolveContextValue(parameter, environment.getLocalContext(), environment.getGraphQlContext()); + + ContextValue annotation = parameter.getParameterAnnotation(ContextValue.class); + Assert.state(annotation != null, "Expected @ContextValue annotation"); + String name = getContextValueName(parameter, annotation.name(), annotation); + + return resolveContextValue(name, annotation.required(), parameter, environment.getGraphQlContext()); + } + + static String getContextValueName(MethodParameter parameter, String nameFromAnnotation, Annotation annotation) { + if (StringUtils.hasText(nameFromAnnotation)) { + return nameFromAnnotation; + } + String parameterName = parameter.getParameterName(); + if (parameterName != null) { + return parameterName; + } + throw new IllegalArgumentException("Name for " + annotation.getClass().getSimpleName() + " argument " + + "of type [" + parameter.getNestedParameterType().getName() + "] not specified, " + + "and parameter name information not found in class file either."); } @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); + String contextValueName, boolean required, MethodParameter parameter, + @Nullable GraphQLContext graphQlContext) { Class parameterType = parameter.getParameterType(); - Object value = null; - - if (localContext instanceof GraphQLContext) { - value = ((GraphQLContext) localContext).get(name); - } - - if (value == null) { - value = graphQlContext.get(name); - } + Object value = (graphQlContext != null ? graphQlContext.get(contextValueName) : null); boolean isOptional = parameterType.equals(Optional.class); boolean isMono = parameterType.equals(Mono.class); - if (value == null && annotation.required() && !isOptional && !isMono) { + if (value == null && required && !isOptional && !isMono) { throw new IllegalStateException("Missing required context value for " + parameter); } @@ -95,22 +98,4 @@ public class ContextValueMethodArgumentResolver implements HandlerMethodArgument return value; } - 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/LocalContextValueMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/LocalContextValueMethodArgumentResolver.java new file mode 100644 index 00000000..26e6bcc3 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/LocalContextValueMethodArgumentResolver.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; + +import graphql.GraphQLContext; +import graphql.schema.DataFetchingEnvironment; + +import org.springframework.core.MethodParameter; +import org.springframework.graphql.data.method.HandlerMethodArgumentResolver; +import org.springframework.graphql.data.method.annotation.LocalContextValue; +import org.springframework.util.Assert; + + +/** + * Resolver for a {@link LocalContextValue @LocalContextValue} annotated method + * parameter. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +public class LocalContextValueMethodArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return (parameter.getParameterAnnotation(LocalContextValue.class) != null); + } + + @Override + public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) { + + LocalContextValue annotation = parameter.getParameterAnnotation(LocalContextValue.class); + Assert.state(annotation != null, "Expected @LocalContextValue annotation"); + String name = ContextValueMethodArgumentResolver.getContextValueName(parameter, annotation.name(), annotation); + + Object localContext = environment.getLocalContext(); + Assert.state(localContext == null || localContext instanceof GraphQLContext, + "Local context is not an instance of graphql.GraphQLContext"); + + return ContextValueMethodArgumentResolver.resolveContextValue( + name, annotation.required(), parameter, (GraphQLContext) localContext); + } + + +} diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ContextValueMethodArgumentResolverTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ContextValueMethodArgumentResolverTests.java index ef1e59b6..1ce6be64 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ContextValueMethodArgumentResolverTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ContextValueMethodArgumentResolverTests.java @@ -25,6 +25,7 @@ import graphql.schema.DataFetchingEnvironment; import graphql.schema.DataFetchingEnvironmentImpl; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; @@ -33,6 +34,7 @@ import org.springframework.graphql.Book; import org.springframework.graphql.data.method.HandlerMethod; import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite; import org.springframework.graphql.data.method.annotation.ContextValue; +import org.springframework.graphql.data.method.annotation.LocalContextValue; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; @@ -55,11 +57,14 @@ public class ContextValueMethodArgumentResolverTests { @Test void supportsParameter() { + assertThat(this.resolver.supportsParameter(methodParam(0))).isTrue(); assertThat(this.resolver.supportsParameter(methodParam(1))).isTrue(); assertThat(this.resolver.supportsParameter(methodParam(2))).isTrue(); assertThat(this.resolver.supportsParameter(methodParam(3))).isTrue(); + assertThat(this.resolver.supportsParameter(methodParam(4))).isFalse(); + assertThat(this.resolver.supportsParameter(methodParam(5))).isFalse(); } @Test @@ -73,17 +78,6 @@ public class ContextValueMethodArgumentResolverTests { tester.accept("customKey", 1); } - @Test - void resolveFromLocalContext() { - BiConsumer tester = (key, index) -> { - GraphQLContext context = GraphQLContext.newContext().of(key, this.book).build(); - Object actual = resolveValue(context, null, index); - assertThat(actual).isSameAs(this.book); - }; - tester.accept("book", 0); - tester.accept("customKey", 1); - } - @Test @SuppressWarnings({"unchecked", "ConstantConditions"}) void resolveMissing() { @@ -116,7 +110,7 @@ public class ContextValueMethodArgumentResolverTests { assertThat(actual).isNotPresent(); } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "ConstantConditions", "ReactiveStreamsUnusedPublisher"}) @Test // gh-355 void resolveMono() throws Exception { @@ -134,12 +128,10 @@ public class ContextValueMethodArgumentResolverTests { .build(); graphQLContext.put("stringMono", Mono.just("value A")); - String actual = ((Mono) handlerMethod.invoke(environment)).block(); - assertThat(actual).isEqualTo("value A"); + StepVerifier.create((Mono) handlerMethod.invoke(environment)).expectNext("value A").verifyComplete(); graphQLContext.delete("stringMono"); - actual = ((Mono) handlerMethod.invoke(environment)).block(); - assertThat(actual).isNull(); + StepVerifier.create((Mono) handlerMethod.invoke(environment)).verifyComplete(); } @Nullable @@ -161,12 +153,13 @@ public class ContextValueMethodArgumentResolverTests { } - @SuppressWarnings({"unused", "rawtypes", "OptionalUsedAsFieldOrParameterType"}) + @SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) public void handle( @ContextValue Book book, @ContextValue("customKey") Book customKeyBook, @ContextValue(required = false) Book notRequiredBook, @ContextValue Optional optionalBook, + @LocalContextValue Book localBook, Book otherBook) { } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/LocalContextValueMethodArgumentResolverTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/LocalContextValueMethodArgumentResolverTests.java new file mode 100644 index 00000000..eca93e3f --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/LocalContextValueMethodArgumentResolverTests.java @@ -0,0 +1,88 @@ +/* + * 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; + +import java.lang.reflect.Method; + +import graphql.GraphQLContext; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.DataFetchingEnvironmentImpl; +import org.junit.jupiter.api.Test; + +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.graphql.Book; +import org.springframework.graphql.data.method.annotation.ContextValue; +import org.springframework.graphql.data.method.annotation.LocalContextValue; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link ContextValueMethodArgumentResolver}. + * @author Rossen Stoyanchev + */ +public class LocalContextValueMethodArgumentResolverTests { + + private static final Method method = ClassUtils.getMethod( + LocalContextValueMethodArgumentResolverTests.class, "handle", (Class[]) null); + + private final LocalContextValueMethodArgumentResolver resolver = new LocalContextValueMethodArgumentResolver(); + + private final Book book = new Book(); + + + @Test + void supportsParameter() { + assertThat(this.resolver.supportsParameter(methodParam(0))).isFalse(); + assertThat(this.resolver.supportsParameter(methodParam(1))).isTrue(); + } + + @Test + void resolve() { + GraphQLContext context = GraphQLContext.newContext().of("localBook", this.book).build(); + Object actual = resolveValue(context, 1); + + assertThat(actual).isSameAs(this.book); + } + + @Nullable + private Object resolveValue(@Nullable GraphQLContext localContext, int index) { + + DataFetchingEnvironment environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment() + .localContext(localContext) + .graphQLContext(GraphQLContext.newContext().build()) + .build(); + + return this.resolver.resolveArgument(methodParam(index), environment); + } + + private MethodParameter methodParam(int index) { + MethodParameter methodParameter = new SynthesizingMethodParameter(method, index); + methodParameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); + return methodParameter; + } + + + @SuppressWarnings("unused") + public void handle( + @ContextValue Book book, + @LocalContextValue Book localBook) { + } + +}