Skip to content

Commit

Permalink
Support for dynamic headers on GraphQL clients
Browse files Browse the repository at this point in the history
  • Loading branch information
jmartisk committed Sep 21, 2023
1 parent 87e42fb commit 00154aa
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.ServiceLoader;

import io.smallrye.graphql.client.websocket.WebsocketSubprotocol;
import io.smallrye.mutiny.Uni;

/**
* Use this builder, when you are not in a CDI context, i.e. when working with Java SE.
Expand Down Expand Up @@ -74,6 +75,8 @@ default TypesafeGraphQLClientBuilder headers(Map<String, String> headers) {
*/
TypesafeGraphQLClientBuilder header(String name, String value);

TypesafeGraphQLClientBuilder dynamicHeader(String name, Uni<String> value);

/**
* Static payload to send with initialization method on subscription.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static java.util.stream.Collectors.toList;

import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -50,6 +51,7 @@ public class VertxDynamicGraphQLClient implements DynamicGraphQLClient {

private final boolean executeSingleOperationsOverWebsocket;
private final MultiMap headers;
private final Map<String, Uni<String>> dynamicHeaders;
private final Map<String, Object> initPayload;
private final List<WebsocketSubprotocol> subprotocols;
private final Integer subscriptionInitializationTimeout;
Expand All @@ -66,7 +68,8 @@ public class VertxDynamicGraphQLClient implements DynamicGraphQLClient {

VertxDynamicGraphQLClient(Vertx vertx, WebClient webClient,
String url, String websocketUrl, boolean executeSingleOperationsOverWebsocket,
MultiMap headers, Map<String, Object> initPayload, WebClientOptions options,
MultiMap headers, Map<String, Uni<String>> dynamicHeaders,
Map<String, Object> initPayload, WebClientOptions options,
List<WebsocketSubprotocol> subprotocols, Integer subscriptionInitializationTimeout,
boolean allowUnexpectedResponseFields) {
if (options != null) {
Expand All @@ -80,6 +83,7 @@ public class VertxDynamicGraphQLClient implements DynamicGraphQLClient {
this.webClient = webClient;
}
this.headers = headers;
this.dynamicHeaders = dynamicHeaders;
this.initPayload = initPayload;
if (url != null) {
if (url.startsWith("stork")) {
Expand Down Expand Up @@ -204,6 +208,10 @@ private Response executeSync(JsonObject json, MultiMap additionalHeaders) {
if (additionalHeaders != null) {
allHeaders.addAll(additionalHeaders);
}
// obtain values of dynamic headers and add them to the request
for (Map.Entry<String, Uni<String>> dynamicHeaderEntry : dynamicHeaders.entrySet()) {
allHeaders.add(dynamicHeaderEntry.getKey(), dynamicHeaderEntry.getValue().await().indefinitely());
}
return executeSingleResultOperationOverHttp(json, allHeaders).await().indefinitely();
}
}
Expand Down Expand Up @@ -306,7 +314,20 @@ private Uni<Response> executeAsync(JsonObject json, MultiMap additionalHeaders)
if (additionalHeaders != null) {
allHeaders.addAll(additionalHeaders);
}
return executeSingleResultOperationOverHttp(json, allHeaders);
List<Uni<Void>> unis = new ArrayList<>();
// append dynamic headers to the request
for (Map.Entry<String, Uni<String>> stringUniEntry : dynamicHeaders.entrySet()) {
unis.add(stringUniEntry.getValue().onItem().invoke(headerValue -> {
allHeaders.add(stringUniEntry.getKey(), headerValue);
}).replaceWithVoid());
}
if (unis.isEmpty()) {
return executeSingleResultOperationOverHttp(json, allHeaders);
} else {
return Uni.combine().all().unis(unis)
.combinedWith(f -> f).onItem()
.transformToUni(f -> executeSingleResultOperationOverHttp(json, allHeaders));
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.smallrye.graphql.client.vertx.VertxClientOptionsHelper;
import io.smallrye.graphql.client.vertx.VertxManager;
import io.smallrye.graphql.client.websocket.WebsocketSubprotocol;
import io.smallrye.mutiny.Uni;
import io.vertx.core.MultiMap;
import io.vertx.core.Vertx;
import io.vertx.core.http.impl.headers.HeadersMultiMap;
Expand All @@ -33,6 +34,7 @@ public class VertxDynamicGraphQLClientBuilder implements DynamicGraphQLClientBui
private Boolean executeSingleOperationsOverWebsocket;
private String configKey;
private final MultiMap headersMap;
private Map<String, Uni<String>> dynamicHeaders;
private final Map<String, Object> initPayload;
private WebClientOptions options;
private List<WebsocketSubprotocol> subprotocols;
Expand All @@ -41,6 +43,7 @@ public class VertxDynamicGraphQLClientBuilder implements DynamicGraphQLClientBui

public VertxDynamicGraphQLClientBuilder() {
headersMap = new HeadersMultiMap();
dynamicHeaders = new HashMap<>();
initPayload = new HashMap<>();
headersMap.set("Content-Type", "application/json");
subprotocols = new ArrayList<>();
Expand All @@ -61,6 +64,11 @@ public VertxDynamicGraphQLClientBuilder header(String name, String value) {
return this;
}

public VertxDynamicGraphQLClientBuilder dynamicHeader(String name, Uni<String> value) {
dynamicHeaders.put(name, value);
return this;
}

public VertxDynamicGraphQLClientBuilder headers(Map<String, String> headers) {
headersMap.setAll(headers);
return this;
Expand Down Expand Up @@ -149,7 +157,7 @@ public DynamicGraphQLClient build() {
allowUnexpectedResponseFields = false;
}
return new VertxDynamicGraphQLClient(toUseVertx, webClient, url, websocketUrl,
executeSingleOperationsOverWebsocket, headersMap, initPayload, options, subprotocols,
executeSingleOperationsOverWebsocket, headersMap, dynamicHeaders, initPayload, options, subprotocols,
subscriptionInitializationTimeout, allowUnexpectedResponseFields);
}

Expand All @@ -169,6 +177,11 @@ private void applyConfig(GraphQLClientConfiguration configuration) {
this.headersMap.set(k, v);
}
});
configuration.getDynamicHeaders().forEach((k, v) -> {
if (!this.dynamicHeaders.containsKey(k)) {
this.dynamicHeaders.put(k, v);
}
});
configuration.getInitPayload().forEach((k, v) -> {
if (!this.initPayload.containsKey(k)) {
this.initPayload.put(k, v);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -22,6 +23,7 @@
import io.smallrye.graphql.client.vertx.VertxClientOptionsHelper;
import io.smallrye.graphql.client.vertx.VertxManager;
import io.smallrye.graphql.client.websocket.WebsocketSubprotocol;
import io.smallrye.mutiny.Uni;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientOptions;
Expand All @@ -37,6 +39,7 @@ public class VertxTypesafeGraphQLClientBuilder implements TypesafeGraphQLClientB
private String websocketUrl;
private Boolean executeSingleOperationsOverWebsocket;
private Map<String, String> headers;
private Map<String, Uni<String>> dynamicHeaders;
private Map<String, Object> initPayload;
private List<WebsocketSubprotocol> subprotocols;
private Vertx vertx;
Expand Down Expand Up @@ -99,6 +102,15 @@ public VertxTypesafeGraphQLClientBuilder header(String name, String value) {
return this;
}

@Override
public TypesafeGraphQLClientBuilder dynamicHeader(String name, Uni<String> value) {
if (this.dynamicHeaders == null) {
this.dynamicHeaders = new LinkedHashMap<>();
}
this.dynamicHeaders.put(name, value);
return this;
}

public VertxTypesafeGraphQLClientBuilder initPayload(Map<String, Object> initPayload) {
if (this.initPayload == null) {
this.initPayload = new LinkedHashMap<>();
Expand Down Expand Up @@ -152,8 +164,12 @@ public <T> T build(Class<T> apiClass) {
if (allowUnexpectedResponseFields == null) {
allowUnexpectedResponseFields = false;
}
if (dynamicHeaders == null) {
dynamicHeaders = new HashMap<>();
}

VertxTypesafeGraphQLClientProxy graphQlClient = new VertxTypesafeGraphQLClientProxy(apiClass, headers, initPayload,
VertxTypesafeGraphQLClientProxy graphQlClient = new VertxTypesafeGraphQLClientProxy(apiClass, headers,
dynamicHeaders, initPayload,
endpoint,
websocketUrl, executeSingleOperationsOverWebsocket, httpClient, webClient, subprotocols,
websocketInitializationTimeout,
Expand Down Expand Up @@ -222,6 +238,9 @@ private void applyConfig(GraphQLClientConfiguration configuration) {
if (this.headers == null && configuration.getHeaders() != null) {
this.headers = configuration.getHeaders();
}
if (this.dynamicHeaders == null && configuration.getDynamicHeaders() != null) {
this.dynamicHeaders = configuration.getDynamicHeaders();
}
if (this.initPayload == null && configuration.getInitPayload() != null) {
this.initPayload = configuration.getInitPayload();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class VertxTypesafeGraphQLClientProxy {
private final ConcurrentMap<String, String> queryCache = new ConcurrentHashMap<>();

private final Map<String, String> additionalHeaders;
private final Map<String, Uni<String>> dynamicHeaders;
private final Map<String, Object> initPayload;
private final ServiceURLSupplier endpoint;
private final ServiceURLSupplier websocketUrl;
Expand All @@ -90,6 +91,7 @@ class VertxTypesafeGraphQLClientProxy {
VertxTypesafeGraphQLClientProxy(
Class<?> api,
Map<String, String> additionalHeaders,
Map<String, Uni<String>> dynamicHeaders,
Map<String, Object> initPayload,
URI endpoint,
String websocketUrl,
Expand All @@ -101,6 +103,7 @@ class VertxTypesafeGraphQLClientProxy {
boolean allowUnexpectedResponseFields) {
this.api = api;
this.additionalHeaders = additionalHeaders;
this.dynamicHeaders = dynamicHeaders;
this.initPayload = initPayload;
if (endpoint != null) {
if (endpoint.getScheme().startsWith("stork")) {
Expand Down Expand Up @@ -155,22 +158,47 @@ Object invoke(MethodInvocation method) {
}

private Object executeSingleResultOperationOverHttpSync(MethodInvocation method, JsonObject request, MultiMap headers) {
HttpResponse<Buffer> response = postSync(request.toString(), headers);
MultiMap allHeaders = new HeadersMultiMap();
allHeaders.addAll(headers);
// obtain values of dynamic headers and add them to the request
for (Map.Entry<String, Uni<String>> dynamicHeaderEntry : dynamicHeaders.entrySet()) {
allHeaders.add(dynamicHeaderEntry.getKey(), dynamicHeaderEntry.getValue().await().indefinitely());
}
HttpResponse<Buffer> response = postSync(request.toString(), allHeaders);
if (log.isTraceEnabled() && response != null) {
log.tracef("response graphql: %s", response.bodyAsString());
}
return new ResultBuilder(method, response.bodyAsString(),
response.statusCode(), response.statusMessage(), convertHeaders(headers),
response.statusCode(), response.statusMessage(), convertHeaders(allHeaders),
allowUnexpectedResponseFields).read();
}

private Uni<Object> executeSingleResultOperationOverHttpAsync(MethodInvocation method, JsonObject request,
MultiMap headers) {
return Uni.createFrom()
.completionStage(postAsync(request.toString(), headers))
.map(response -> new ResultBuilder(method, response.bodyAsString(),
response.statusCode(), response.statusMessage(), convertHeaders(headers),
allowUnexpectedResponseFields).read());
List<Uni<Void>> unis = new ArrayList<>();
MultiMap allHeaders = new HeadersMultiMap();
allHeaders.addAll(headers);
// obtain values of dynamic headers and add them to the request
for (Map.Entry<String, Uni<String>> stringUniEntry : dynamicHeaders.entrySet()) {
unis.add(stringUniEntry.getValue().onItem().invoke(headerValue -> {
allHeaders.add(stringUniEntry.getKey(), headerValue);
}).replaceWithVoid());
}
if (unis.isEmpty()) {
return Uni.createFrom().completionStage(postAsync(request.toString(), allHeaders))
.map(response -> new ResultBuilder(method, response.bodyAsString(),
response.statusCode(), response.statusMessage(), convertHeaders(allHeaders),
allowUnexpectedResponseFields).read());
} else {
// when all dynamic headers have been obtained, proceed with the request
return Uni.combine().all().unis(unis)
.combinedWith(f -> f)
.onItem().transformToUni(g -> Uni.createFrom()
.completionStage(postAsync(request.toString(), allHeaders))
.map(response -> new ResultBuilder(method, response.bodyAsString(),
response.statusCode(), response.statusMessage(), convertHeaders(allHeaders),
allowUnexpectedResponseFields).read()));
}
}

private Map<String, List<String>> convertHeaders(MultiMap input) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.util.List;
import java.util.Map;

import io.smallrye.mutiny.Uni;

/**
* The configuration of a single GraphQL client.
*/
Expand All @@ -23,6 +25,11 @@ public class GraphQLClientConfiguration {
*/
private Map<String, String> headers;

/**
* HTTP headers that need to be resolved dynamically for each request.
*/
private Map<String, Uni<String>> dynamicHeaders;

/**
* Additional payload sent on subscription initialization.
*/
Expand Down Expand Up @@ -130,6 +137,14 @@ public void setHeaders(Map<String, String> headers) {
this.headers = headers;
}

public Map<String, Uni<String>> getDynamicHeaders() {
return dynamicHeaders;
}

public void setDynamicHeaders(Map<String, Uni<String>> dynamicHeaders) {
this.dynamicHeaders = dynamicHeaders;
}

public Map<String, Object> getInitPayload() {
return initPayload;
}
Expand Down Expand Up @@ -274,6 +289,11 @@ public GraphQLClientConfiguration merge(GraphQLClientConfiguration other) {
} else if (other.headers != null) {
other.headers.forEach((key, value) -> this.headers.put(key, value));
}
if (this.dynamicHeaders == null) {
this.dynamicHeaders = other.dynamicHeaders;
} else if (other.dynamicHeaders != null) {
other.dynamicHeaders.forEach((key, value) -> this.dynamicHeaders.put(key, value));
}
if (this.initPayload == null) {
this.initPayload = other.initPayload;
} else if (other.initPayload != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ private GraphQLClientConfiguration readConfigurationByKey(String clientName) {

// HTTP headers
configuration.setHeaders(getConfiguredHeaders(clientName, mpConfig));
// dynamic headers can't be configured via config properties
configuration.setDynamicHeaders(new HashMap<>());

configuration.setInitPayload(getConfiguredInitPayload(clientName, mpConfig));
// websocket subprotocols
Expand Down Expand Up @@ -157,6 +159,10 @@ public GraphQLClientConfiguration getClient(String key) {
return clients.computeIfAbsent(key, this::readConfigurationByKey);
}

public Map<String, GraphQLClientConfiguration> getClients() {
return clients;
}

/** this method is required by Quarkus */
public void addClient(String key, GraphQLClientConfiguration config) {
clients.put(key, config);
Expand Down

0 comments on commit 00154aa

Please sign in to comment.