Skip to content

Commit 36425af

Browse files
committed
Add interception for RSocket handler
See gh-339
1 parent 1462050 commit 36425af

File tree

10 files changed

+341
-94
lines changed

10 files changed

+341
-94
lines changed

spring-graphql/src/main/java/org/springframework/graphql/support/DefaultExecutionGraphQlResponse.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import graphql.ErrorClassification;
2424
import graphql.ExecutionInput;
2525
import graphql.ExecutionResult;
26+
import graphql.ExecutionResultImpl;
2627
import graphql.GraphQLError;
2728
import graphql.language.SourceLocation;
2829

@@ -153,4 +154,65 @@ public Map<String, Object> getExtensions() {
153154

154155
}
155156

157+
158+
/**
159+
* Builder to transform the response's {@link ExecutionResult}.
160+
*/
161+
public static abstract class Builder<B extends Builder<B, R>, R extends ExecutionGraphQlResponse> {
162+
163+
private final R original;
164+
165+
private final ExecutionResultImpl.Builder executionResultBuilder;
166+
167+
protected Builder(R original) {
168+
this.original = original;
169+
this.executionResultBuilder = ExecutionResultImpl.newExecutionResult().from(original.getExecutionResult());
170+
}
171+
172+
/**
173+
* Set the {@link ExecutionResult#getData() data} of the GraphQL execution result.
174+
* @param data the execution result data
175+
* @return the current builder
176+
*/
177+
public Builder<B, R> data(Object data) {
178+
this.executionResultBuilder.data(data);
179+
return this;
180+
}
181+
182+
/**
183+
* Set the {@link ExecutionResult#getErrors() errors} of the GraphQL execution
184+
* result.
185+
* @param errors the execution result errors
186+
* @return the current builder
187+
*/
188+
public Builder<B, R> errors(@Nullable List<GraphQLError> errors) {
189+
this.executionResultBuilder.errors(errors);
190+
return this;
191+
}
192+
193+
/**
194+
* Set the {@link ExecutionResult#getExtensions() extensions} of the GraphQL
195+
* execution result.
196+
* @param extensions the execution result extensions
197+
* @return the current builder
198+
*/
199+
public Builder<B, R> extensions(@Nullable Map<Object, Object> extensions) {
200+
this.executionResultBuilder.extensions(extensions);
201+
return this;
202+
}
203+
204+
/**
205+
* Build the response with the transformed {@code ExecutionResult}.
206+
*/
207+
public R build() {
208+
return build(this.original, this.executionResultBuilder.build());
209+
}
210+
211+
/**
212+
* Subclasses to create the specific response instance.
213+
*/
214+
protected abstract R build(R original, ExecutionResult newResult);
215+
216+
}
217+
156218
}

spring-graphql/src/main/java/org/springframework/graphql/web/DefaultWebGraphQlHandlerBuilder.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.graphql.ExecutionGraphQlService;
2626
import org.springframework.graphql.execution.ReactorContextManager;
2727
import org.springframework.graphql.execution.ThreadLocalAccessor;
28+
import org.springframework.graphql.web.WebGraphQlHandlerInterceptor.Chain;
2829
import org.springframework.lang.Nullable;
2930
import org.springframework.util.Assert;
3031
import org.springframework.util.CollectionUtils;
@@ -88,12 +89,11 @@ public WebGraphQlHandler.Builder threadLocalAccessors(List<ThreadLocalAccessor>
8889
@Override
8990
public WebGraphQlHandler build() {
9091

91-
WebGraphQlHandlerInterceptor.Chain endOfChain =
92-
request -> this.service.execute(request).map(WebGraphQlResponse::new);
92+
Chain endOfChain = request -> this.service.execute(request).map(WebGraphQlResponse::new);
9393

94-
WebGraphQlHandlerInterceptor.Chain chain = this.interceptors.stream()
94+
Chain chain = this.interceptors.stream()
9595
.reduce(WebGraphQlHandlerInterceptor::andThen)
96-
.map(interceptor -> (WebGraphQlHandlerInterceptor.Chain) (request) -> interceptor.intercept(request, endOfChain))
96+
.map(interceptor -> (Chain) (request) -> interceptor.intercept(request, endOfChain))
9797
.orElse(endOfChain);
9898

9999
return new WebGraphQlHandler() {
@@ -112,7 +112,8 @@ public Mono<WebGraphQlResponse> handleRequest(WebGraphQlRequest request) {
112112

113113
@Override
114114
public WebSocketGraphQlHandlerInterceptor webSocketInterceptor() {
115-
return (webSocketInterceptor != null ? webSocketInterceptor : new WebSocketGraphQlHandlerInterceptor() {});
115+
return (webSocketInterceptor != null ?
116+
webSocketInterceptor : new WebSocketGraphQlHandlerInterceptor() {});
116117
}
117118

118119
};

spring-graphql/src/main/java/org/springframework/graphql/web/GraphQlRSocketHandler.java

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.graphql.web;
1818

1919

20+
import java.util.List;
2021
import java.util.Map;
2122

2223
import graphql.ExecutionResult;
@@ -25,38 +26,39 @@
2526
import reactor.core.publisher.Flux;
2627
import reactor.core.publisher.Mono;
2728

28-
import org.springframework.graphql.ExecutionGraphQlRequest;
2929
import org.springframework.graphql.ExecutionGraphQlResponse;
3030
import org.springframework.graphql.ExecutionGraphQlService;
31-
import org.springframework.graphql.support.DefaultExecutionGraphQlRequest;
31+
import org.springframework.graphql.web.RSocketGraphQlHandlerInterceptor.Chain;
32+
import org.springframework.util.AlternativeJdkIdGenerator;
33+
import org.springframework.util.IdGenerator;
3234

3335

3436
/**
3537
* Handler for GraphQL over RSocket requests.
3638
*
37-
* <p>This class can be extended from an {@code @Controller} that overrides
38-
* {@link #handle(Map)} and {@link #handleSubscription(Map)} in order to add
39+
* <p>This class can be extended or wrapped from an {@code @Controller} in order
40+
* to re-declare {@link #handle(Map)} and {@link #handleSubscription(Map)} with
3941
* {@link org.springframework.messaging.handler.annotation.MessageMapping @MessageMapping}
40-
* annotations with the route.
42+
* annotations including the GraphQL endpoint route.
4143
*
4244
* <pre style="class">
4345
* &#064;Controller
44-
* private static class GraphQlRSocketController extends GraphQlRSocketHandler {
46+
* private static class GraphQlRSocketController {
4547
*
46-
* GraphQlRSocketController(ExecutionGraphQlService graphQlService) {
47-
* super(graphQlService);
48+
* private final GraphQlRSocketHandler handler;
49+
*
50+
* GraphQlRSocketController(GraphQlRSocketHandler handler) {
51+
* this.handler = handler;
4852
* }
4953
*
50-
* &#064;Override
5154
* &#064;MessageMapping("graphql")
5255
* public Mono<Map<String, Object>> handle(Map<String, Object> payload) {
53-
* return super.handle(payload);
56+
* return this.handler.handle(payload);
5457
* }
5558
*
56-
* &#064;Override
5759
* &#064;MessageMapping("graphql")
5860
* public Flux<Map<String, Object>> handleSubscription(Map<String, Object> payload) {
59-
* return super.handleSubscription(payload);
61+
* return this.handler.handleSubscription(payload);
6062
* }
6163
* }
6264
* </pre>
@@ -66,26 +68,40 @@
6668
*/
6769
public class GraphQlRSocketHandler {
6870

69-
private final ExecutionGraphQlService service;
71+
private final Chain executionChain;
72+
73+
private final IdGenerator idGenerator = new AlternativeJdkIdGenerator();
74+
75+
76+
/**
77+
* Create a new instance that handles requests through a chain of interceptors
78+
* followed by the given {@link ExecutionGraphQlService}.
79+
*/
80+
public GraphQlRSocketHandler(
81+
ExecutionGraphQlService service, List<RSocketGraphQlHandlerInterceptor> interceptors) {
7082

83+
Chain endOfChain = request -> service.execute(request).map(RSocketGraphQlResponse::new);
7184

72-
public GraphQlRSocketHandler(ExecutionGraphQlService service) {
73-
this.service = service;
85+
this.executionChain = (interceptors.isEmpty() ? endOfChain :
86+
interceptors.stream()
87+
.reduce(RSocketGraphQlHandlerInterceptor::andThen)
88+
.map(interceptor -> (Chain) request -> interceptor.intercept(request, endOfChain))
89+
.orElse(endOfChain));
7490
}
7591

7692

7793
/**
7894
* Handle a {@code Request-Response} interaction. For queries and mutations.
7995
*/
8096
public Mono<Map<String, Object>> handle(Map<String, Object> payload) {
81-
return this.service.execute(initRequest(payload)).map(ExecutionGraphQlResponse::toMap);
97+
return handleInternal(payload).map(ExecutionGraphQlResponse::toMap);
8298
}
8399

84100
/**
85101
* Handle a {@code Request-Stream} interaction. For subscriptions.
86102
*/
87103
public Flux<Map<String, Object>> handleSubscription(Map<String, Object> payload) {
88-
return this.service.execute(initRequest(payload))
104+
return handleInternal(payload)
89105
.flatMapMany(response -> {
90106
if (response.getData() instanceof Publisher) {
91107
Publisher<ExecutionResult> publisher = response.getData();
@@ -100,12 +116,9 @@ public Flux<Map<String, Object>> handleSubscription(Map<String, Object> payload)
100116
});
101117
}
102118

103-
@SuppressWarnings("unchecked")
104-
private ExecutionGraphQlRequest initRequest(Map<String, Object> payload) {
105-
String query = (String) payload.get("query");
106-
String operationName = (String) payload.get("operationName");
107-
Map<String, Object> variables = (Map<String, Object>) payload.get("variables");
108-
return new DefaultExecutionGraphQlRequest(query, operationName, variables, "1", null);
119+
private Mono<RSocketGraphQlResponse> handleInternal(Map<String, Object> payload) {
120+
String requestId = this.idGenerator.generateId().toString();
121+
return this.executionChain.next(new RSocketGraphQlRequest(payload, requestId, null));
109122
}
110123

111124
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2020-2022 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+
17+
package org.springframework.graphql.web;
18+
19+
import graphql.ExecutionInput;
20+
import graphql.ExecutionResult;
21+
import reactor.core.publisher.Mono;
22+
23+
import org.springframework.beans.factory.ObjectProvider;
24+
import org.springframework.graphql.ExecutionGraphQlService;
25+
26+
27+
/**
28+
* Interceptor for server handling of GraphQL over RSocket requests,
29+
* allowing customization of the {@link ExecutionInput} and
30+
* the {@link ExecutionResult}.
31+
*
32+
* <p>Interceptors are typically declared as beans in Spring configuration and
33+
* ordered as defined in {@link ObjectProvider#orderedStream()}.
34+
*
35+
* @author Rossen Stoyanchev
36+
* @since 1.0.0
37+
*/
38+
public interface RSocketGraphQlHandlerInterceptor {
39+
40+
/**
41+
* Intercept a request and delegate to the rest of the chain including other
42+
* interceptors and a {@link ExecutionGraphQlService}.
43+
* @param request the request to execute
44+
* @param chain the rest of the chain to execute the request
45+
* @return a {@link Mono} with the response
46+
*/
47+
Mono<RSocketGraphQlResponse> intercept(RSocketGraphQlRequest request, Chain chain);
48+
49+
/**
50+
* Return a new {@link RSocketGraphQlHandlerInterceptor} that invokes the current
51+
* interceptor first and then the one that is passed in.
52+
* @param nextInterceptor the interceptor to delegate to after the current
53+
* @return a new interceptor that chains the two
54+
*/
55+
default RSocketGraphQlHandlerInterceptor andThen(RSocketGraphQlHandlerInterceptor nextInterceptor) {
56+
return (request, chain) -> intercept(request, nextRequest -> nextInterceptor.intercept(nextRequest, chain));
57+
}
58+
59+
60+
/**
61+
* Contract for delegation to the rest of the chain.
62+
*/
63+
interface Chain {
64+
65+
/**
66+
* Delegate to the rest of the chain to execute the request.
67+
* @param request the request to execute
68+
* the {@link ExecutionInput} for {@link graphql.GraphQL}.
69+
* @return {@code Mono} with the response
70+
*/
71+
Mono<RSocketGraphQlResponse> next(RSocketGraphQlRequest request);
72+
73+
}
74+
75+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2020-2022 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+
17+
package org.springframework.graphql.web;
18+
19+
import java.util.Locale;
20+
import java.util.Map;
21+
22+
import io.rsocket.exceptions.RejectedException;
23+
24+
import org.springframework.graphql.ExecutionGraphQlRequest;
25+
import org.springframework.graphql.support.DefaultExecutionGraphQlRequest;
26+
import org.springframework.lang.Nullable;
27+
import org.springframework.util.StringUtils;
28+
29+
/**
30+
* {@link org.springframework.graphql.GraphQlRequest} implementation for server
31+
* handling over RSocket.
32+
*
33+
* @author Rossen Stoyanchev
34+
* @since 1.0.0
35+
*/
36+
public class RSocketGraphQlRequest extends DefaultExecutionGraphQlRequest implements ExecutionGraphQlRequest {
37+
38+
39+
/**
40+
* Create an instance.
41+
* @param body the deserialized content of the GraphQL request
42+
* @param id an identifier for the GraphQL request
43+
* @param locale the locale from the HTTP request, if any
44+
*/
45+
public RSocketGraphQlRequest(Map<String, Object> body, String id, @Nullable Locale locale) {
46+
super(getKey("query", body), getKey("operationName", body), getKey("variables", body), id, locale);
47+
}
48+
49+
@SuppressWarnings("unchecked")
50+
private static <T> T getKey(String key, Map<String, Object> body) {
51+
if (key.equals("query") && !StringUtils.hasText((String) body.get(key))) {
52+
throw new RejectedException("No \"query\" in the request document");
53+
}
54+
return (T) body.get(key);
55+
}
56+
57+
}

0 commit comments

Comments
 (0)