Skip to content

Commit a38fac0

Browse files
committed
Introduce blocking execution in GraphQlClient
See gh-771
1 parent 117445b commit a38fac0

11 files changed

+901
-266
lines changed

Diff for: spring-graphql/src/main/java/org/springframework/graphql/client/AbstractGraphQlClientBuilder.java

+21-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.graphql.client;
1818

19+
import java.time.Duration;
1920
import java.util.ArrayList;
2021
import java.util.Arrays;
2122
import java.util.Collections;
@@ -66,6 +67,9 @@ public abstract class AbstractGraphQlClientBuilder<B extends AbstractGraphQlClie
6667
@Nullable
6768
private Decoder<?> jsonDecoder;
6869

70+
@Nullable
71+
private Duration blockingTimeout;
72+
6973

7074
/**
7175
* Default constructor for use from subclasses.
@@ -82,7 +86,6 @@ private static DocumentSource initDocumentSource() {
8286
ResourceDocumentSource.FILE_EXTENSIONS));
8387
}
8488

85-
8689
@Override
8790
public B interceptor(GraphQlClientInterceptor... interceptors) {
8891
this.interceptors.addAll(Arrays.asList(interceptors));
@@ -101,6 +104,12 @@ public B documentSource(DocumentSource contentLoader) {
101104
return self();
102105
}
103106

107+
@Override
108+
public B blockingTimeout(@Nullable Duration blockingTimeout) {
109+
this.blockingTimeout = blockingTimeout;
110+
return self();
111+
}
112+
104113
@SuppressWarnings("unchecked")
105114
private <T extends B> T self() {
106115
return (T) this;
@@ -169,8 +178,8 @@ protected GraphQlClient buildGraphQlClient(GraphQlTransport transport) {
169178
this.jsonDecoder = (this.jsonDecoder == null ? DefaultJackson2Codecs.decoder() : this.jsonDecoder);
170179
}
171180

172-
return new DefaultGraphQlClient(
173-
this.documentSource, createExecuteChain(transport), createExecuteSubscriptionChain(transport));
181+
return new DefaultGraphQlClient(this.documentSource,
182+
createExecuteChain(transport), createSubscriptionChain(transport), this.blockingTimeout);
174183
}
175184

176185
/**
@@ -186,23 +195,25 @@ protected Consumer<AbstractGraphQlClientBuilder<?>> getBuilderInitializer() {
186195

187196
private Chain createExecuteChain(GraphQlTransport transport) {
188197

189-
Chain chain = request -> transport.execute(request).map(response ->
190-
new DefaultClientGraphQlResponse(request, response, getEncoder(), getDecoder()));
198+
Chain chain = request -> transport
199+
.execute(request)
200+
.map(response -> new DefaultClientGraphQlResponse(request, response, getEncoder(), getDecoder()));
191201

192202
return this.interceptors.stream()
193203
.reduce(GraphQlClientInterceptor::andThen)
194-
.map(interceptor -> (Chain) (request) -> interceptor.intercept(request, chain))
204+
.map(i -> (Chain) (request) -> i.intercept(request, chain))
195205
.orElse(chain);
196206
}
197207

198-
private SubscriptionChain createExecuteSubscriptionChain(GraphQlTransport transport) {
208+
private SubscriptionChain createSubscriptionChain(GraphQlTransport transport) {
199209

200-
SubscriptionChain chain = request -> transport.executeSubscription(request)
210+
SubscriptionChain chain = request -> transport
211+
.executeSubscription(request)
201212
.map(response -> new DefaultClientGraphQlResponse(request, response, getEncoder(), getDecoder()));
202213

203214
return this.interceptors.stream()
204215
.reduce(GraphQlClientInterceptor::andThen)
205-
.map(interceptor -> (SubscriptionChain) (request) -> interceptor.interceptSubscription(request, chain))
216+
.map(i -> (SubscriptionChain) (request) -> i.interceptSubscription(request, chain))
206217
.orElse(chain);
207218
}
208219

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/*
2+
* Copyright 2002-2024 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.client;
18+
19+
import java.time.Duration;
20+
import java.util.ArrayList;
21+
import java.util.Collections;
22+
import java.util.List;
23+
import java.util.function.Consumer;
24+
25+
import reactor.core.scheduler.Scheduler;
26+
import reactor.core.scheduler.Schedulers;
27+
28+
import org.springframework.core.codec.Decoder;
29+
import org.springframework.core.codec.Encoder;
30+
import org.springframework.core.io.ClassPathResource;
31+
import org.springframework.graphql.GraphQlResponse;
32+
import org.springframework.graphql.client.SyncGraphQlClientInterceptor.Chain;
33+
import org.springframework.graphql.support.CachingDocumentSource;
34+
import org.springframework.graphql.support.DocumentSource;
35+
import org.springframework.graphql.support.ResourceDocumentSource;
36+
import org.springframework.http.converter.HttpMessageConverter;
37+
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
38+
import org.springframework.lang.Nullable;
39+
import org.springframework.util.Assert;
40+
import org.springframework.util.ClassUtils;
41+
42+
43+
/**
44+
* Abstract, base class for transport specific {@link GraphQlClient.SyncBuilder}
45+
* implementations.
46+
*
47+
* <p>Subclasses must implement {@link #build()} and call
48+
* {@link #buildGraphQlClient(SyncGraphQlTransport)} to obtain a default, transport
49+
* agnostic {@code GraphQlClient}. A transport specific extension can then wrap
50+
* this default tester by extending {@link AbstractDelegatingGraphQlClient}.
51+
*
52+
* @author Rossen Stoyanchev
53+
* @since 1.3
54+
* @see AbstractDelegatingGraphQlClient
55+
*/
56+
public abstract class AbstractGraphQlClientSyncBuilder<B extends AbstractGraphQlClientSyncBuilder<B>>
57+
implements GraphQlClient.SyncBuilder<B> {
58+
59+
protected static final boolean jackson2Present = ClassUtils.isPresent(
60+
"com.fasterxml.jackson.databind.ObjectMapper", AbstractGraphQlClientSyncBuilder.class.getClassLoader());
61+
62+
63+
private final List<SyncGraphQlClientInterceptor> interceptors = new ArrayList<>();
64+
65+
private DocumentSource documentSource;
66+
67+
@Nullable
68+
private HttpMessageConverter<Object> jsonConverter;
69+
70+
private Scheduler scheduler = Schedulers.boundedElastic();
71+
72+
@Nullable
73+
private Duration blockingTimeout;
74+
75+
/**
76+
* Default constructor for use from subclasses.
77+
* <p>Subclasses must set the transport to use before {@link #build()} or
78+
* during, by overriding {@link #build()}.
79+
*/
80+
protected AbstractGraphQlClientSyncBuilder() {
81+
this.documentSource = initDocumentSource();
82+
}
83+
84+
private static DocumentSource initDocumentSource() {
85+
return new CachingDocumentSource(new ResourceDocumentSource(
86+
Collections.singletonList(new ClassPathResource("graphql-documents/")),
87+
ResourceDocumentSource.FILE_EXTENSIONS));
88+
}
89+
90+
@Override
91+
public B interceptor(SyncGraphQlClientInterceptor... interceptors) {
92+
Collections.addAll(this.interceptors, interceptors);
93+
return self();
94+
}
95+
96+
@Override
97+
public B interceptors(Consumer<List<SyncGraphQlClientInterceptor>> interceptorsConsumer) {
98+
interceptorsConsumer.accept(this.interceptors);
99+
return self();
100+
}
101+
102+
@Override
103+
public B documentSource(DocumentSource contentLoader) {
104+
this.documentSource = contentLoader;
105+
return self();
106+
}
107+
108+
@Override
109+
public B scheduler(Scheduler scheduler) {
110+
Assert.notNull(scheduler, "Scheduler is required");
111+
this.scheduler = scheduler;
112+
return self();
113+
}
114+
115+
@Override
116+
public B blockingTimeout(@Nullable Duration blockingTimeout) {
117+
this.blockingTimeout = blockingTimeout;
118+
return self();
119+
}
120+
121+
@SuppressWarnings("unchecked")
122+
private <T extends B> T self() {
123+
return (T) this;
124+
}
125+
126+
127+
// Protected methods for use from build() in subclasses
128+
129+
130+
/**
131+
* Transport-specific subclasses can provide their JSON {@code Encoder} and
132+
* {@code Decoder} for use at the client level, for mapping response data
133+
* to some target entity type.
134+
*/
135+
protected void setJsonConverter(HttpMessageConverter<Object> converter) {
136+
this.jsonConverter = converter;
137+
}
138+
139+
140+
/**
141+
* Build the default transport-agnostic client that subclasses can then wrap
142+
* with {@link AbstractDelegatingGraphQlClient}.
143+
*/
144+
protected GraphQlClient buildGraphQlClient(SyncGraphQlTransport transport) {
145+
146+
if (jackson2Present) {
147+
this.jsonConverter = (this.jsonConverter == null ?
148+
DefaultJacksonConverter.initialize() : this.jsonConverter);
149+
}
150+
151+
return new DefaultGraphQlClient(
152+
this.documentSource, createExecuteChain(transport), this.scheduler, this.blockingTimeout);
153+
}
154+
155+
/**
156+
* Return a {@code Consumer} to initialize new builders from "this" builder.
157+
*/
158+
protected Consumer<AbstractGraphQlClientSyncBuilder<?>> getBuilderInitializer() {
159+
return builder -> {
160+
builder.interceptors(interceptorList -> interceptorList.addAll(interceptors));
161+
builder.documentSource(documentSource);
162+
builder.setJsonConverter(getJsonConverter());
163+
};
164+
}
165+
166+
private Chain createExecuteChain(SyncGraphQlTransport transport) {
167+
168+
Encoder<?> encoder = HttpMessageConverterDelegate.asEncoder(getJsonConverter());
169+
Decoder<?> decoder = HttpMessageConverterDelegate.asDecoder(getJsonConverter());
170+
171+
Chain chain = request -> {
172+
GraphQlResponse response = transport.execute(request);
173+
return new DefaultClientGraphQlResponse(request, response, encoder, decoder);
174+
};
175+
176+
return this.interceptors.stream()
177+
.reduce(SyncGraphQlClientInterceptor::andThen)
178+
.map(i -> (Chain) (request) -> i.intercept(request, chain))
179+
.orElse(chain);
180+
}
181+
182+
private HttpMessageConverter<Object> getJsonConverter() {
183+
Assert.notNull(this.jsonConverter, "jsonConverter has not been set");
184+
return this.jsonConverter;
185+
}
186+
187+
188+
private static class DefaultJacksonConverter {
189+
190+
static HttpMessageConverter<Object> initialize() {
191+
return new MappingJackson2HttpMessageConverter();
192+
}
193+
}
194+
195+
}

0 commit comments

Comments
 (0)