Add @LocalContextValue and resolver

@ContextValue checks in the main context.
@LocalContextValue checks in the local context.

Closes gh-353
This commit is contained in:
rstoyanchev
2022-05-06 07:14:06 +01:00
parent 07293abf35
commit 5b59e51bab
9 changed files with 272 additions and 70 deletions

View File

@@ -1202,8 +1202,10 @@ See <<controllers-schema-mapping-source>>.
See <<controllers-schema-mapping-data-loader>>.
| `@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`.

View File

@@ -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:
* <ul>
* <li>{@link DataFetchingEnvironment#getLocalContext()} -- if it is an
* instance of {@link GraphQLContext}.
* <li>{@link DataFetchingEnvironment#getGraphQlContext()}
* </ul>
* Annotation to bind a method parameter to an attribute from the
* {@link DataFetchingEnvironment#getGraphQlContext() main} {@code GraphQLContext}.
*
* <p>To bind to an attribute from the local context instead, see
* {@link LocalContextValue @LocalContextValue}.
*
* @author Rossen Stoyanchev
* @since 1.0.0

View File

@@ -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}.
*
* <p>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.
* <p>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;
}

View File

@@ -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());

View File

@@ -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);
}

View File

@@ -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:
* <ul>
* <li>{@link DataFetchingEnvironment#getLocalContext()} -- if it is an
* instance of {@link GraphQLContext}.
* <li>{@link DataFetchingEnvironment#getGraphQlContext()}
* </ul>
* 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<String, Integer> 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<String>) handlerMethod.invoke(environment)).block();
assertThat(actual).isEqualTo("value A");
StepVerifier.create((Mono<String>) handlerMethod.invoke(environment)).expectNext("value A").verifyComplete();
graphQLContext.delete("stringMono");
actual = ((Mono<String>) handlerMethod.invoke(environment)).block();
assertThat(actual).isNull();
StepVerifier.create((Mono<String>) 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<Book> optionalBook,
@LocalContextValue Book localBook,
Book otherBook) {
}

View File

@@ -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) {
}
}