Add support for @ContextValue method parameters

See gh-172
This commit is contained in:
Rossen Stoyanchev
2021-12-08 09:47:49 +00:00
parent e6453ef9b0
commit 50aeaba63e
6 changed files with 315 additions and 4 deletions

View File

@@ -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`.

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.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;
}

View File

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

View File

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

View File

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

View File

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