Add @LocalContextValue and resolver
@ContextValue checks in the main context. @LocalContextValue checks in the local context. Closes gh-353
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user