Add support for @ContextValue method parameters
See gh-172
This commit is contained in:
@@ -683,6 +683,10 @@ See <<controllers-schema-mapping-source>>.
|
||||
| For access to a `DataLoader` in the `DataLoaderRegistry`.
|
||||
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`.
|
||||
|
||||
| `GraphQLContext`
|
||||
| For access to the context from the `DataFetchingEnvironment`.
|
||||
|
||||
|
||||
@@ -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.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>
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface ContextValue {
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
}
|
||||
@@ -121,23 +121,27 @@ public class AnnotatedControllerConfigurer
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
this.argumentResolvers = new HandlerMethodArgumentResolverComposite();
|
||||
|
||||
// Annotation based
|
||||
if (springDataPresent) {
|
||||
// This must be ahead of ArgumentMethodArgumentResolver
|
||||
// Must be ahead of ArgumentMethodArgumentResolver
|
||||
this.argumentResolvers.addResolver(new ProjectedPayloadMethodArgumentResolver());
|
||||
}
|
||||
this.argumentResolvers.addResolver(new ArgumentMapMethodArgumentResolver());
|
||||
this.argumentResolvers.addResolver(new ArgumentMethodArgumentResolver(this.conversionService));
|
||||
this.argumentResolvers.addResolver(new ContextValueMethodArgumentResolver());
|
||||
|
||||
// Type based
|
||||
this.argumentResolvers.addResolver(new DataFetchingEnvironmentMethodArgumentResolver());
|
||||
this.argumentResolvers.addResolver(new DataLoaderMethodArgumentResolver());
|
||||
if (springSecurityPresent) {
|
||||
this.argumentResolvers.addResolver(new PrincipalMethodArgumentResolver());
|
||||
}
|
||||
|
||||
if (KotlinDetector.isKotlinPresent()) {
|
||||
this.argumentResolvers.addResolver(new ContinuationHandlerMethodArgumentResolver());
|
||||
}
|
||||
|
||||
// This works as a fallback, after all other resolvers
|
||||
// This works as a fallback, after other resolvers
|
||||
this.argumentResolvers.addResolver(new SourceMethodArgumentResolver());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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.support;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
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.ContextValue;
|
||||
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>
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class ContextValueMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return (parameter.getParameterAnnotation(ContextValue.class) != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) {
|
||||
return resolveContextValue(parameter, environment.getLocalContext(), environment.getGraphQlContext());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object resolveContextValue(
|
||||
MethodParameter parameter, @Nullable Object localContext, GraphQLContext graphQlContext) {
|
||||
|
||||
ContextValue annotation = parameter.getParameterAnnotation(ContextValue.class);
|
||||
Assert.state(annotation != null, "Expected @ContextValue annotation");
|
||||
String name = getValueName(parameter, annotation);
|
||||
|
||||
Class<?> parameterType = parameter.getParameterType();
|
||||
Object value = null;
|
||||
|
||||
if (localContext instanceof GraphQLContext) {
|
||||
value = ((GraphQLContext) localContext).get(name);
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
return wrapAsOptionalIfNecessary(value, parameterType);
|
||||
}
|
||||
|
||||
value = graphQlContext.get(name);
|
||||
if (value == null && annotation.required() && !parameterType.equals(Optional.class)) {
|
||||
throw new IllegalStateException("Missing required context value for " + parameter);
|
||||
}
|
||||
|
||||
return wrapAsOptionalIfNecessary(value, parameterType);
|
||||
}
|
||||
|
||||
private 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 Object wrapAsOptionalIfNecessary(@Nullable Object value, Class<?> type) {
|
||||
return (type.equals(Optional.class) ? Optional.ofNullable(value) : value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,6 +30,7 @@ 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.format.support.DefaultFormattingConversionService;
|
||||
import org.springframework.graphql.Book;
|
||||
import org.springframework.graphql.data.method.annotation.Argument;
|
||||
@@ -111,7 +112,7 @@ class ArgumentMethodArgumentResolverTests {
|
||||
}
|
||||
|
||||
private MethodParameter methodParam(Method method, int index) {
|
||||
MethodParameter methodParameter = new MethodParameter(method, index);
|
||||
MethodParameter methodParameter = new SynthesizingMethodParameter(method, index);
|
||||
methodParameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
|
||||
return methodParameter;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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.support;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
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.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ContextValueMethodArgumentResolver}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class ContextValueMethodArgumentResolverTests {
|
||||
|
||||
private static final Method method = ClassUtils.getMethod(
|
||||
ContextValueMethodArgumentResolverTests.class, "handle", (Class<?>[]) null);
|
||||
|
||||
private final ContextValueMethodArgumentResolver resolver = new ContextValueMethodArgumentResolver();
|
||||
|
||||
private final Book book = new Book();
|
||||
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve() {
|
||||
BiConsumer<String, Integer> tester = (key, index) -> {
|
||||
GraphQLContext context = GraphQLContext.newContext().of(key, this.book).build();
|
||||
Object actual = resolveValue(null, context, index);
|
||||
assertThat(actual).isSameAs(this.book);
|
||||
};
|
||||
tester.accept("book", 0);
|
||||
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() {
|
||||
GraphQLContext context = GraphQLContext.newContext().build();
|
||||
|
||||
// Required
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> resolveValue(context, context, 0))
|
||||
.withMessage("Missing required context value for method 'handle' parameter 0");
|
||||
|
||||
// Not required
|
||||
assertThat(resolveValue(context, context, 2)).isNull();
|
||||
|
||||
// Optional
|
||||
Optional<Book> actual = (Optional<Book>) resolveValue(context, context, 3);
|
||||
assertThat(actual.isPresent()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"unchecked", "ConstantConditions", "OptionalGetWithoutIsPresent"})
|
||||
void resolveOptional() {
|
||||
GraphQLContext context = GraphQLContext.newContext().of("optionalBook", this.book).build();
|
||||
Optional<Book> actual = (Optional<Book>) resolveValue(context, context, 3);
|
||||
|
||||
assertThat(actual.get()).isSameAs(this.book);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object resolveValue(
|
||||
@Nullable GraphQLContext localContext, @Nullable GraphQLContext graphQLContext, int index) {
|
||||
|
||||
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment()
|
||||
.localContext(localContext)
|
||||
.graphQLContext(graphQLContext)
|
||||
.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", "rawtypes", "OptionalUsedAsFieldOrParameterType"})
|
||||
public void handle(
|
||||
@ContextValue Book book,
|
||||
@ContextValue("customKey") Book customKeyBook,
|
||||
@ContextValue(required = false) Book notRequiredBook,
|
||||
@ContextValue Optional<Book> optionalBook,
|
||||
Book otherBook) {
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user