From 1b87cef46dc051282d50e10e3b6d82cd3a0a33ff Mon Sep 17 00:00:00 2001 From: Jan Martiska Date: Fri, 15 Sep 2023 10:02:05 +0200 Subject: [PATCH] Support for dynamic headers on GraphQL clients --- .../api/TypesafeGraphQLClientBuilder.java | 3 ++ .../dynamic/VertxDynamicGraphQLClient.java | 25 ++++++++++- .../VertxDynamicGraphQLClientBuilder.java | 15 ++++++- .../VertxTypesafeGraphQLClientBuilder.java | 21 +++++++++- .../VertxTypesafeGraphQLClientProxy.java | 42 +++++++++++++++---- .../impl/GraphQLClientConfiguration.java | 20 +++++++++ .../impl/GraphQLClientsConfiguration.java | 6 +++ 7 files changed, 121 insertions(+), 11 deletions(-) diff --git a/client/api/src/main/java/io/smallrye/graphql/client/typesafe/api/TypesafeGraphQLClientBuilder.java b/client/api/src/main/java/io/smallrye/graphql/client/typesafe/api/TypesafeGraphQLClientBuilder.java index c6e17b883..85b38a11c 100644 --- a/client/api/src/main/java/io/smallrye/graphql/client/typesafe/api/TypesafeGraphQLClientBuilder.java +++ b/client/api/src/main/java/io/smallrye/graphql/client/typesafe/api/TypesafeGraphQLClientBuilder.java @@ -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. @@ -74,6 +75,8 @@ default TypesafeGraphQLClientBuilder headers(Map headers) { */ TypesafeGraphQLClientBuilder header(String name, String value); + TypesafeGraphQLClientBuilder dynamicHeader(String name, Uni value); + /** * Static payload to send with initialization method on subscription. */ diff --git a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/dynamic/VertxDynamicGraphQLClient.java b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/dynamic/VertxDynamicGraphQLClient.java index e1eab1089..d77e8d159 100644 --- a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/dynamic/VertxDynamicGraphQLClient.java +++ b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/dynamic/VertxDynamicGraphQLClient.java @@ -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; @@ -50,6 +51,7 @@ public class VertxDynamicGraphQLClient implements DynamicGraphQLClient { private final boolean executeSingleOperationsOverWebsocket; private final MultiMap headers; + private final Map> dynamicHeaders; private final Map initPayload; private final List subprotocols; private final Integer subscriptionInitializationTimeout; @@ -66,7 +68,8 @@ public class VertxDynamicGraphQLClient implements DynamicGraphQLClient { VertxDynamicGraphQLClient(Vertx vertx, WebClient webClient, String url, String websocketUrl, boolean executeSingleOperationsOverWebsocket, - MultiMap headers, Map initPayload, WebClientOptions options, + MultiMap headers, Map> dynamicHeaders, + Map initPayload, WebClientOptions options, List subprotocols, Integer subscriptionInitializationTimeout, boolean allowUnexpectedResponseFields) { if (options != null) { @@ -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")) { @@ -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> dynamicHeaderEntry : dynamicHeaders.entrySet()) { + allHeaders.add(dynamicHeaderEntry.getKey(), dynamicHeaderEntry.getValue().await().indefinitely()); + } return executeSingleResultOperationOverHttp(json, allHeaders).await().indefinitely(); } } @@ -306,7 +314,20 @@ private Uni executeAsync(JsonObject json, MultiMap additionalHeaders) if (additionalHeaders != null) { allHeaders.addAll(additionalHeaders); } - return executeSingleResultOperationOverHttp(json, allHeaders); + List> unis = new ArrayList<>(); + // append dynamic headers to the request + for (Map.Entry> 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)); + } } } diff --git a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/dynamic/VertxDynamicGraphQLClientBuilder.java b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/dynamic/VertxDynamicGraphQLClientBuilder.java index ad706c733..5f01c4458 100644 --- a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/dynamic/VertxDynamicGraphQLClientBuilder.java +++ b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/dynamic/VertxDynamicGraphQLClientBuilder.java @@ -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; @@ -33,6 +34,7 @@ public class VertxDynamicGraphQLClientBuilder implements DynamicGraphQLClientBui private Boolean executeSingleOperationsOverWebsocket; private String configKey; private final MultiMap headersMap; + private Map> dynamicHeaders; private final Map initPayload; private WebClientOptions options; private List subprotocols; @@ -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<>(); @@ -61,6 +64,11 @@ public VertxDynamicGraphQLClientBuilder header(String name, String value) { return this; } + public VertxDynamicGraphQLClientBuilder dynamicHeader(String name, Uni value) { + dynamicHeaders.put(name, value); + return this; + } + public VertxDynamicGraphQLClientBuilder headers(Map headers) { headersMap.setAll(headers); return this; @@ -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); } @@ -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); diff --git a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientBuilder.java b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientBuilder.java index a239e1bd9..8dca42cc1 100644 --- a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientBuilder.java +++ b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientBuilder.java @@ -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; @@ -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; @@ -37,6 +39,7 @@ public class VertxTypesafeGraphQLClientBuilder implements TypesafeGraphQLClientB private String websocketUrl; private Boolean executeSingleOperationsOverWebsocket; private Map headers; + private Map> dynamicHeaders; private Map initPayload; private List subprotocols; private Vertx vertx; @@ -99,6 +102,15 @@ public VertxTypesafeGraphQLClientBuilder header(String name, String value) { return this; } + @Override + public TypesafeGraphQLClientBuilder dynamicHeader(String name, Uni value) { + if (this.dynamicHeaders == null) { + this.dynamicHeaders = new LinkedHashMap<>(); + } + this.dynamicHeaders.put(name, value); + return this; + } + public VertxTypesafeGraphQLClientBuilder initPayload(Map initPayload) { if (this.initPayload == null) { this.initPayload = new LinkedHashMap<>(); @@ -152,8 +164,12 @@ public T build(Class 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, @@ -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(); } diff --git a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientProxy.java b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientProxy.java index b7c42dae0..68d018e4c 100644 --- a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientProxy.java +++ b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientProxy.java @@ -67,6 +67,7 @@ class VertxTypesafeGraphQLClientProxy { private final ConcurrentMap queryCache = new ConcurrentHashMap<>(); private final Map additionalHeaders; + private final Map> dynamicHeaders; private final Map initPayload; private final ServiceURLSupplier endpoint; private final ServiceURLSupplier websocketUrl; @@ -90,6 +91,7 @@ class VertxTypesafeGraphQLClientProxy { VertxTypesafeGraphQLClientProxy( Class api, Map additionalHeaders, + Map> dynamicHeaders, Map initPayload, URI endpoint, String websocketUrl, @@ -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")) { @@ -155,22 +158,47 @@ Object invoke(MethodInvocation method) { } private Object executeSingleResultOperationOverHttpSync(MethodInvocation method, JsonObject request, MultiMap headers) { - HttpResponse 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> dynamicHeaderEntry : dynamicHeaders.entrySet()) { + allHeaders.add(dynamicHeaderEntry.getKey(), dynamicHeaderEntry.getValue().await().indefinitely()); + } + HttpResponse 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 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> unis = new ArrayList<>(); + MultiMap allHeaders = new HeadersMultiMap(); + allHeaders.addAll(headers); + // obtain values of dynamic headers and add them to the request + for (Map.Entry> 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> convertHeaders(MultiMap input) { diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/GraphQLClientConfiguration.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/GraphQLClientConfiguration.java index 680e1ee15..d2f087ec8 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/GraphQLClientConfiguration.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/GraphQLClientConfiguration.java @@ -3,6 +3,8 @@ import java.util.List; import java.util.Map; +import io.smallrye.mutiny.Uni; + /** * The configuration of a single GraphQL client. */ @@ -23,6 +25,11 @@ public class GraphQLClientConfiguration { */ private Map headers; + /** + * HTTP headers that need to be resolved dynamically for each request. + */ + private Map> dynamicHeaders; + /** * Additional payload sent on subscription initialization. */ @@ -130,6 +137,14 @@ public void setHeaders(Map headers) { this.headers = headers; } + public Map> getDynamicHeaders() { + return dynamicHeaders; + } + + public void setDynamicHeaders(Map> dynamicHeaders) { + this.dynamicHeaders = dynamicHeaders; + } + public Map getInitPayload() { return initPayload; } @@ -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) { diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/GraphQLClientsConfiguration.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/GraphQLClientsConfiguration.java index 281755bda..c57004057 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/GraphQLClientsConfiguration.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/GraphQLClientsConfiguration.java @@ -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 @@ -157,6 +159,10 @@ public GraphQLClientConfiguration getClient(String key) { return clients.computeIfAbsent(key, this::readConfigurationByKey); } + public Map getClients() { + return clients; + } + /** this method is required by Quarkus */ public void addClient(String key, GraphQLClientConfiguration config) { clients.put(key, config);