Skip to content

Commit d8b3dad

Browse files
committed
Polishing ProjectedPayloadMethodArgumentResolver
See gh-550
1 parent 0baceda commit d8b3dad

File tree

2 files changed

+69
-33
lines changed

2 files changed

+69
-33
lines changed

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

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -30,32 +30,34 @@
3030
import org.springframework.util.Assert;
3131

3232
/**
33-
* Resolver to obtain an {@link ProjectedPayload @ProjectedPayload},
34-
* either based on the complete {@link DataFetchingEnvironment#getArguments()}
35-
* map, or based on a specific argument within the map when the method
36-
* parameter is annotated with {@code @Argument}.
33+
* Resolver for a method parameter that is an interface annotated with
34+
* {@link ProjectedPayload @ProjectedPayload}.
3735
*
38-
* <p>Projected payloads consist of the projection interface and accessor
39-
* methods. Projections can be closed or open projections. Closed projections
40-
* use interface getter methods to access underlying properties directly.
41-
* Open projection methods make use of the {@code @Value} annotation to
36+
* <p>By default, the projection is prepared by using the complete
37+
* {@link DataFetchingEnvironment#getArguments() arguments map} as its source.
38+
* Add {@link Argument @Argument} with a name, if you to prepare it by using a
39+
* specific argument value instead as its source.
40+
*
41+
* <p>An {@code @ProjectedPayload} interface has accessor methods. In a closed
42+
* projection, getter methods access underlying properties directly. In an open
43+
* projection, getter methods make use of the {@code @Value} annotation to
4244
* evaluate SpEL expressions against the underlying {@code target} object.
4345
*
4446
* <p>For example:
4547
* <pre class="code">
4648
* &#064;ProjectedPayload
4749
* interface BookProjection {
50+
*
4851
* String getName();
49-
* }
5052
*
51-
* &#064;ProjectedPayload
52-
* interface BookProjection {
5353
* &#064;Value("#{target.author + ' ' + target.name}")
5454
* String getAuthorAndName();
5555
* }
5656
* </pre>
5757
*
5858
* @author Mark Paluch
59+
* @author Rossen Stoyanchev
60+
*
5961
* @since 1.0.0
6062
*/
6163
public class ProjectedPayloadMethodArgumentResolver implements HandlerMethodArgumentResolver {
@@ -77,11 +79,19 @@ public ProjectedPayloadMethodArgumentResolver(ApplicationContext applicationCont
7779
}
7880

7981

82+
/**
83+
* Return underlying projection factory used by the resolver.
84+
* @since 1.1.1
85+
*/
86+
protected SpelAwareProxyProjectionFactory getProjectionFactory() {
87+
return this.projectionFactory;
88+
}
89+
90+
8091
@Override
8192
public boolean supportsParameter(MethodParameter parameter) {
8293
Class<?> type = parameter.nestedIfOptional().getNestedParameterType();
83-
return (type.isInterface() &&
84-
AnnotatedElementUtils.findMergedAnnotation(type, ProjectedPayload.class) != null);
94+
return (type.isInterface() && AnnotatedElementUtils.findMergedAnnotation(type, ProjectedPayload.class) != null);
8595
}
8696

8797
@Override
@@ -90,26 +100,22 @@ public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment
90100
String name = (parameter.hasParameterAnnotation(Argument.class) ?
91101
ArgumentMethodArgumentResolver.getArgumentName(parameter) : null);
92102

93-
Class<?> projectionType = parameter.getParameterType();
94-
95-
boolean isOptional = parameter.isOptional();
96-
if (isOptional) {
97-
projectionType = parameter.nestedIfOptional().getNestedParameterType();
98-
}
99-
100-
Object projectionSource = (name != null ?
101-
environment.getArgument(name) : environment.getArguments());
102-
103-
Object value = null;
104-
if (!isOptional || projectionSource != null) {
105-
value = project(projectionType, projectionSource);
106-
}
107-
108-
return (isOptional ? Optional.ofNullable(value) : value);
103+
Class<?> targetType = parameter.nestedIfOptional().getNestedParameterType();
104+
Object rawValue = (name != null ? environment.getArgument(name) : environment.getArguments());
105+
Object value = (!parameter.isOptional() || rawValue != null ? createProjection(targetType, rawValue) : null);
106+
return (parameter.isOptional() ? Optional.ofNullable(value) : value);
109107
}
110108

111-
protected Object project(Class<?> projectionType, Object projectionSource){
112-
return this.projectionFactory.createProjection(projectionType, projectionSource);
109+
/**
110+
* Protected method to create the projection. The default implementation
111+
* delegates to the underlying {@link #getProjectionFactory() projectionFactory}.
112+
* @param targetType the type to create
113+
* @param rawValue a specific argument (if named via {@link Argument}
114+
* or the map of arguments
115+
* @return the created project instance
116+
*/
117+
protected Object createProjection(Class<?> targetType, Object rawValue){
118+
return this.projectionFactory.createProjection(targetType, rawValue);
113119
}
114120

115121
}

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

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Optional;
2020

2121
import org.junit.jupiter.api.BeforeEach;
22+
import org.junit.jupiter.api.Disabled;
2223
import org.junit.jupiter.api.Test;
2324

2425
import org.springframework.context.support.StaticApplicationContext;
@@ -32,7 +33,8 @@
3233
import static org.assertj.core.api.Assertions.assertThat;
3334

3435
/**
35-
*
36+
* Unit tests for {@link ProjectedPayloadMethodArgumentResolver}.
37+
* @author Rossen Stoyanchev
3638
*/
3739
public class ProjectedPayloadMethodArgumentResolverTests extends ArgumentResolverTestSupport {
3840

@@ -56,7 +58,19 @@ void supports() {
5658
}
5759

5860
@Test
59-
void optionalWrapper() throws Exception {
61+
void optionalPresent() throws Exception {
62+
63+
Object result = this.resolver.resolveArgument(
64+
methodParam(BookController.class, "optionalProjection", Optional.class),
65+
environment("{ \"where\" : { \"author\" : \"Orwell\" }}"));
66+
67+
assertThat(result).isNotNull().isInstanceOf(Optional.class);
68+
BookProjection book = ((Optional<BookProjection>) result).get();
69+
assertThat(book.getAuthor()).isEqualTo("Orwell");
70+
}
71+
72+
@Test
73+
void optionalNotPresent() throws Exception {
6074

6175
Object result = this.resolver.resolveArgument(
6276
methodParam(BookController.class, "optionalProjection", Optional.class),
@@ -66,11 +80,27 @@ void optionalWrapper() throws Exception {
6680
assertThat((Optional<?>) result).isNotPresent();
6781
}
6882

83+
@Test
84+
@Disabled // pending decision under gh-550
85+
void nullValue() throws Exception {
86+
87+
Object result = this.resolver.resolveArgument(
88+
methodParam(BookController.class, "projection", BookProjection.class),
89+
environment("{}"));
90+
91+
assertThat(result).isNull();
92+
}
93+
6994

7095
@SuppressWarnings({"ConstantConditions", "unused"})
7196
@Controller
7297
static class BookController {
7398

99+
@QueryMapping
100+
public List<Book> projection(@Argument(name = "where") BookProjection projection) {
101+
return null;
102+
}
103+
74104
@QueryMapping
75105
public List<Book> optionalProjection(@Argument(name = "where") Optional<BookProjection> projection) {
76106
return null;

0 commit comments

Comments
 (0)