Skip to content

Commit 50aeaba

Browse files
committed
Add support for @ContextValue method parameters
See gh-172
1 parent e6453ef commit 50aeaba

File tree

6 files changed

+315
-4
lines changed

6 files changed

+315
-4
lines changed

spring-graphql-docs/src/docs/asciidoc/index.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,10 @@ See <<controllers-schema-mapping-source>>.
683683
| For access to a `DataLoader` in the `DataLoaderRegistry`.
684684
See <<controllers-schema-mapping-data-loader>>.
685685

686+
| `@ContextValue`
687+
| For access to a value from the localContext, if it is an instance of `GraphQLContext`,
688+
or from the `GraphQLContext` of `DataFetchingEnvironment`.
689+
686690
| `GraphQLContext`
687691
| For access to the context from the `DataFetchingEnvironment`.
688692

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2002-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.graphql.data.method.annotation;
17+
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
import graphql.GraphQLContext;
25+
import graphql.schema.DataFetchingEnvironment;
26+
27+
import org.springframework.core.annotation.AliasFor;
28+
29+
/**
30+
* Annotation for method parameters obtained from one of the following:
31+
* <ul>
32+
* <li>{@link DataFetchingEnvironment#getLocalContext()} -- if it is an
33+
* instance of {@link GraphQLContext}.
34+
* <li>{@link DataFetchingEnvironment#getGraphQlContext()}
35+
* </ul>
36+
*
37+
* @author Rossen Stoyanchev
38+
* @since 1.0.0
39+
*/
40+
@Target(ElementType.PARAMETER)
41+
@Retention(RetentionPolicy.RUNTIME)
42+
@Documented
43+
public @interface ContextValue {
44+
45+
/**
46+
* Alias for {@link #name}.
47+
*/
48+
@AliasFor("name")
49+
String value() default "";
50+
51+
/**
52+
* The name of the value to bind to.
53+
*/
54+
@AliasFor("value")
55+
String name() default "";
56+
57+
/**
58+
* Whether the value is required.
59+
* <p>Defaults to "true", leading to an exception thrown if the value is
60+
* missing. Switch to "false" if you prefer {@code null} if the value is
61+
* not present, or use {@link java.util.Optional}.
62+
*/
63+
boolean required() default true;
64+
65+
}

spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,23 +121,27 @@ protected final ApplicationContext obtainApplicationContext() {
121121
@Override
122122
public void afterPropertiesSet() {
123123
this.argumentResolvers = new HandlerMethodArgumentResolverComposite();
124+
125+
// Annotation based
124126
if (springDataPresent) {
125-
// This must be ahead of ArgumentMethodArgumentResolver
127+
// Must be ahead of ArgumentMethodArgumentResolver
126128
this.argumentResolvers.addResolver(new ProjectedPayloadMethodArgumentResolver());
127129
}
128130
this.argumentResolvers.addResolver(new ArgumentMapMethodArgumentResolver());
129131
this.argumentResolvers.addResolver(new ArgumentMethodArgumentResolver(this.conversionService));
132+
this.argumentResolvers.addResolver(new ContextValueMethodArgumentResolver());
133+
134+
// Type based
130135
this.argumentResolvers.addResolver(new DataFetchingEnvironmentMethodArgumentResolver());
131136
this.argumentResolvers.addResolver(new DataLoaderMethodArgumentResolver());
132137
if (springSecurityPresent) {
133138
this.argumentResolvers.addResolver(new PrincipalMethodArgumentResolver());
134139
}
135-
136140
if (KotlinDetector.isKotlinPresent()) {
137141
this.argumentResolvers.addResolver(new ContinuationHandlerMethodArgumentResolver());
138142
}
139143

140-
// This works as a fallback, after all other resolvers
144+
// This works as a fallback, after other resolvers
141145
this.argumentResolvers.addResolver(new SourceMethodArgumentResolver());
142146
}
143147

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2002-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.graphql.data.method.annotation.support;
17+
18+
import java.util.Optional;
19+
20+
import graphql.GraphQLContext;
21+
import graphql.schema.DataFetchingEnvironment;
22+
23+
import org.springframework.core.MethodParameter;
24+
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
25+
import org.springframework.graphql.data.method.annotation.ContextValue;
26+
import org.springframework.lang.Nullable;
27+
import org.springframework.util.Assert;
28+
import org.springframework.util.StringUtils;
29+
30+
/**
31+
* Resolver for {@link ContextValue @ContextValue} annotated method parameters.
32+
* Values are resolved through one of the following:
33+
* <ul>
34+
* <li>{@link DataFetchingEnvironment#getLocalContext()} -- if it is an
35+
* instance of {@link GraphQLContext}.
36+
* <li>{@link DataFetchingEnvironment#getGraphQlContext()}
37+
* </ul>
38+
*
39+
* @author Rossen Stoyanchev
40+
* @since 1.0.0
41+
*/
42+
public class ContextValueMethodArgumentResolver implements HandlerMethodArgumentResolver {
43+
44+
@Override
45+
public boolean supportsParameter(MethodParameter parameter) {
46+
return (parameter.getParameterAnnotation(ContextValue.class) != null);
47+
}
48+
49+
@Override
50+
public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) {
51+
return resolveContextValue(parameter, environment.getLocalContext(), environment.getGraphQlContext());
52+
}
53+
54+
@Nullable
55+
private Object resolveContextValue(
56+
MethodParameter parameter, @Nullable Object localContext, GraphQLContext graphQlContext) {
57+
58+
ContextValue annotation = parameter.getParameterAnnotation(ContextValue.class);
59+
Assert.state(annotation != null, "Expected @ContextValue annotation");
60+
String name = getValueName(parameter, annotation);
61+
62+
Class<?> parameterType = parameter.getParameterType();
63+
Object value = null;
64+
65+
if (localContext instanceof GraphQLContext) {
66+
value = ((GraphQLContext) localContext).get(name);
67+
}
68+
69+
if (value != null) {
70+
return wrapAsOptionalIfNecessary(value, parameterType);
71+
}
72+
73+
value = graphQlContext.get(name);
74+
if (value == null && annotation.required() && !parameterType.equals(Optional.class)) {
75+
throw new IllegalStateException("Missing required context value for " + parameter);
76+
}
77+
78+
return wrapAsOptionalIfNecessary(value, parameterType);
79+
}
80+
81+
private String getValueName(MethodParameter parameter, ContextValue annotation) {
82+
if (StringUtils.hasText(annotation.name())) {
83+
return annotation.name();
84+
}
85+
String parameterName = parameter.getParameterName();
86+
if (parameterName != null) {
87+
return parameterName;
88+
}
89+
throw new IllegalArgumentException("Name for @ContextValue argument " +
90+
"of type [" + parameter.getNestedParameterType().getName() + "] not specified, " +
91+
"and parameter name information not found in class file either.");
92+
}
93+
94+
@Nullable
95+
private Object wrapAsOptionalIfNecessary(@Nullable Object value, Class<?> type) {
96+
return (type.equals(Optional.class) ? Optional.ofNullable(value) : value);
97+
}
98+
99+
}

spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolverTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
import org.springframework.core.DefaultParameterNameDiscoverer;
3232
import org.springframework.core.MethodParameter;
33+
import org.springframework.core.annotation.SynthesizingMethodParameter;
3334
import org.springframework.format.support.DefaultFormattingConversionService;
3435
import org.springframework.graphql.Book;
3536
import org.springframework.graphql.data.method.annotation.Argument;
@@ -111,7 +112,7 @@ void shouldResolveArgumentWithConversionService() throws Exception {
111112
}
112113

113114
private MethodParameter methodParam(Method method, int index) {
114-
MethodParameter methodParameter = new MethodParameter(method, index);
115+
MethodParameter methodParameter = new SynthesizingMethodParameter(method, index);
115116
methodParameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
116117
return methodParameter;
117118
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* Copyright 2002-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.graphql.data.method.annotation.support;
17+
18+
import java.lang.reflect.Method;
19+
import java.util.Optional;
20+
import java.util.function.BiConsumer;
21+
22+
import graphql.GraphQLContext;
23+
import graphql.schema.DataFetchingEnvironment;
24+
import graphql.schema.DataFetchingEnvironmentImpl;
25+
import org.junit.jupiter.api.Test;
26+
27+
import org.springframework.core.DefaultParameterNameDiscoverer;
28+
import org.springframework.core.MethodParameter;
29+
import org.springframework.core.annotation.SynthesizingMethodParameter;
30+
import org.springframework.graphql.Book;
31+
import org.springframework.graphql.data.method.annotation.ContextValue;
32+
import org.springframework.lang.Nullable;
33+
import org.springframework.util.ClassUtils;
34+
35+
import static org.assertj.core.api.Assertions.assertThat;
36+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
37+
38+
/**
39+
* Unit tests for {@link ContextValueMethodArgumentResolver}.
40+
* @author Rossen Stoyanchev
41+
*/
42+
public class ContextValueMethodArgumentResolverTests {
43+
44+
private static final Method method = ClassUtils.getMethod(
45+
ContextValueMethodArgumentResolverTests.class, "handle", (Class<?>[]) null);
46+
47+
private final ContextValueMethodArgumentResolver resolver = new ContextValueMethodArgumentResolver();
48+
49+
private final Book book = new Book();
50+
51+
52+
@Test
53+
void supportsParameter() {
54+
assertThat(this.resolver.supportsParameter(methodParam(0))).isTrue();
55+
assertThat(this.resolver.supportsParameter(methodParam(1))).isTrue();
56+
assertThat(this.resolver.supportsParameter(methodParam(2))).isTrue();
57+
assertThat(this.resolver.supportsParameter(methodParam(3))).isTrue();
58+
assertThat(this.resolver.supportsParameter(methodParam(4))).isFalse();
59+
}
60+
61+
@Test
62+
void resolve() {
63+
BiConsumer<String, Integer> tester = (key, index) -> {
64+
GraphQLContext context = GraphQLContext.newContext().of(key, this.book).build();
65+
Object actual = resolveValue(null, context, index);
66+
assertThat(actual).isSameAs(this.book);
67+
};
68+
tester.accept("book", 0);
69+
tester.accept("customKey", 1);
70+
}
71+
72+
@Test
73+
void resolveFromLocalContext() {
74+
BiConsumer<String, Integer> tester = (key, index) -> {
75+
GraphQLContext context = GraphQLContext.newContext().of(key, this.book).build();
76+
Object actual = resolveValue(context, null, index);
77+
assertThat(actual).isSameAs(this.book);
78+
};
79+
tester.accept("book", 0);
80+
tester.accept("customKey", 1);
81+
}
82+
83+
@Test
84+
@SuppressWarnings({"unchecked", "ConstantConditions"})
85+
void resolveMissing() {
86+
GraphQLContext context = GraphQLContext.newContext().build();
87+
88+
// Required
89+
assertThatIllegalStateException()
90+
.isThrownBy(() -> resolveValue(context, context, 0))
91+
.withMessage("Missing required context value for method 'handle' parameter 0");
92+
93+
// Not required
94+
assertThat(resolveValue(context, context, 2)).isNull();
95+
96+
// Optional
97+
Optional<Book> actual = (Optional<Book>) resolveValue(context, context, 3);
98+
assertThat(actual.isPresent()).isFalse();
99+
}
100+
101+
@Test
102+
@SuppressWarnings({"unchecked", "ConstantConditions", "OptionalGetWithoutIsPresent"})
103+
void resolveOptional() {
104+
GraphQLContext context = GraphQLContext.newContext().of("optionalBook", this.book).build();
105+
Optional<Book> actual = (Optional<Book>) resolveValue(context, context, 3);
106+
107+
assertThat(actual.get()).isSameAs(this.book);
108+
}
109+
110+
@Nullable
111+
private Object resolveValue(
112+
@Nullable GraphQLContext localContext, @Nullable GraphQLContext graphQLContext, int index) {
113+
114+
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment()
115+
.localContext(localContext)
116+
.graphQLContext(graphQLContext)
117+
.build();
118+
119+
return this.resolver.resolveArgument(methodParam(index), environment);
120+
}
121+
122+
private MethodParameter methodParam(int index) {
123+
MethodParameter methodParameter = new SynthesizingMethodParameter(method, index);
124+
methodParameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
125+
return methodParameter;
126+
}
127+
128+
129+
@SuppressWarnings({"unused", "rawtypes", "OptionalUsedAsFieldOrParameterType"})
130+
public void handle(
131+
@ContextValue Book book,
132+
@ContextValue("customKey") Book customKeyBook,
133+
@ContextValue(required = false) Book notRequiredBook,
134+
@ContextValue Optional<Book> optionalBook,
135+
Book otherBook) {
136+
}
137+
138+
}

0 commit comments

Comments
 (0)