From 000b297d2b264a1d147040495bcc23d4c7d89e88 Mon Sep 17 00:00:00 2001 From: Sashir Estela <sashirestela@yahoo.com> Date: Mon, 12 Feb 2024 22:26:11 +0000 Subject: [PATCH] Extend interceptor to handle body request --- .../cleverclient/CleverClient.java | 27 ++++--- .../cleverclient/http/HttpConnector.java | 77 +++++++++++++++---- .../cleverclient/http/HttpProcessor.java | 63 +++++++-------- .../cleverclient/http/HttpRequestData.java | 18 +++++ .../metadata/InterfaceMetadata.java | 11 ++- .../cleverclient/support/ContentType.java | 28 +++++++ .../cleverclient/util/CommonUtil.java | 5 ++ .../cleverclient/util/Constant.java | 9 --- .../cleverclient/CleverClientTest.java | 59 +++++++++++--- .../cleverclient/util/CommonUtilTest.java | 10 +++ .../util/HttpRequestBodyTestUtility.java | 48 ++++++++++++ 11 files changed, 273 insertions(+), 82 deletions(-) create mode 100644 src/main/java/io/github/sashirestela/cleverclient/http/HttpRequestData.java create mode 100644 src/main/java/io/github/sashirestela/cleverclient/support/ContentType.java create mode 100644 src/test/java/io/github/sashirestela/cleverclient/util/HttpRequestBodyTestUtility.java diff --git a/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java b/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java index 77e1fd2..a727950 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java +++ b/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java @@ -3,12 +3,13 @@ import java.net.http.HttpClient; import java.util.Map; import java.util.Optional; -import java.util.function.Function; +import java.util.function.UnaryOperator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.github.sashirestela.cleverclient.http.HttpProcessor; +import io.github.sashirestela.cleverclient.http.HttpRequestData; import io.github.sashirestela.cleverclient.support.CleverClientSSE; import io.github.sashirestela.cleverclient.util.CommonUtil; import lombok.Builder; @@ -29,33 +30,35 @@ public class CleverClient { private final String baseUrl; private final Map<String, String> headers; private final HttpClient httpClient; - private final Function<String, String> urlInterceptor; + private final UnaryOperator<HttpRequestData> requestInterceptor; private final HttpProcessor httpProcessor; /** * Constructor to create an instance of CleverClient. * - * @param baseUrl Root of the url of the API service to call. Mandatory. - * @param headers Http headers for all the API service. Optional. - * @param httpClient Custom Java's HttpClient component. One is created by - * default if none is passed. Optional. - * @param urlInterceptor Function to modify the url once it has been built. - * @param endOfStream Text used to mark the final of streams when handling - * server sent events (SSE). Optional. + * @param baseUrl Root of the url of the API service to call. + * Mandatory. + * @param headers Http headers for all the API service. Optional. + * @param httpClient Custom Java's HttpClient component. One is created + * by default if none is passed. Optional. + * @param requestInterceptor Function to modify the request once it has been + * built. + * @param endOfStream Text used to mark the final of streams when + * handling server sent events (SSE). Optional. */ @Builder public CleverClient(@NonNull String baseUrl, @Singular Map<String, String> headers, HttpClient httpClient, - Function<String, String> urlInterceptor, String endOfStream) { + UnaryOperator<HttpRequestData> requestInterceptor, String endOfStream) { this.baseUrl = baseUrl; this.headers = Optional.ofNullable(headers).orElse(Map.of()); this.httpClient = Optional.ofNullable(httpClient).orElse(HttpClient.newHttpClient()); - this.urlInterceptor = urlInterceptor; + this.requestInterceptor = requestInterceptor; CleverClientSSE.setEndOfStream(endOfStream); this.httpProcessor = HttpProcessor.builder() .baseUrl(this.baseUrl) .headers(CommonUtil.mapToListOfString(this.headers)) .httpClient(this.httpClient) - .urlInterceptor(this.urlInterceptor) + .requestInterceptor(this.requestInterceptor) .build(); logger.debug("CleverClient has been created."); } diff --git a/src/main/java/io/github/sashirestela/cleverclient/http/HttpConnector.java b/src/main/java/io/github/sashirestela/cleverclient/http/HttpConnector.java index e4c4807..b3b5825 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/http/HttpConnector.java +++ b/src/main/java/io/github/sashirestela/cleverclient/http/HttpConnector.java @@ -5,14 +5,18 @@ import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublisher; import java.net.http.HttpRequest.BodyPublishers; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.github.sashirestela.cleverclient.sender.HttpSenderFactory; +import io.github.sashirestela.cleverclient.support.ContentType; import io.github.sashirestela.cleverclient.support.HttpMultipart; import io.github.sashirestela.cleverclient.support.ReturnType; -import io.github.sashirestela.cleverclient.util.JsonUtil; +import io.github.sashirestela.cleverclient.util.CommonUtil; import lombok.AllArgsConstructor; import lombok.Builder; @@ -23,15 +27,16 @@ @AllArgsConstructor @Builder public class HttpConnector { - private static Logger logger = LoggerFactory.getLogger(HttpConnector.class); + private static final Logger logger = LoggerFactory.getLogger(HttpConnector.class); private HttpClient httpClient; private String url; private String httpMethod; private ReturnType returnType; private Object bodyObject; - private boolean isMultipart; - private String[] headersArray; + private ContentType contentType; + private List<String> headers; + private UnaryOperator<HttpRequestData> requestInterceptor; /** * Prepares the request to call Java's HttpClient and delegates it to a @@ -40,9 +45,16 @@ public class HttpConnector { * @return The response coming from the HttpSender's sendRequest method. */ public Object sendRequest() { - var bodyPublisher = createBodyPublisher(bodyObject, isMultipart); + if (requestInterceptor != null) { + interceptRequest(); + } + logger.debug("Http Call : {} {}", httpMethod, url); + logger.debug("Request Headers : {}", printHeaders(headers)); + + var bodyPublisher = createBodyPublisher(bodyObject, contentType); var responseClass = returnType.getBaseClass(); var genericClass = returnType.getGenericClassIfExists(); + var headersArray = headers.toArray(new String[0]); HttpRequest httpRequest = null; if (headersArray.length > 0) { httpRequest = HttpRequest.newBuilder() @@ -60,21 +72,56 @@ public Object sendRequest() { return httpSender.sendRequest(httpClient, httpRequest, responseClass, genericClass); } - private BodyPublisher createBodyPublisher(Object bodyObject, boolean isMultipart) { + private void interceptRequest() { + var httpRequestData = HttpRequestData.builder() + .url(url) + .body(bodyObject) + .headers(CommonUtil.listToMapOfString(headers)) + .httpMethod(httpMethod) + .contentType(contentType) + .build(); + + httpRequestData = requestInterceptor.apply(httpRequestData); + + url = httpRequestData.getUrl(); + bodyObject = httpRequestData.getBody(); + headers = CommonUtil.mapToListOfString(httpRequestData.getHeaders()); + } + + @SuppressWarnings("unchecked") + private BodyPublisher createBodyPublisher(Object bodyObject, ContentType contentType) { BodyPublisher bodyPublisher = null; - if (bodyObject == null) { + if (contentType == null) { logger.debug("Request Body : (Empty)"); bodyPublisher = BodyPublishers.noBody(); - } else if (isMultipart) { - var data = JsonUtil.objectToMap(bodyObject); - var requestBytes = HttpMultipart.toByteArrays(data); - logger.debug("Request Body : {}", data); - bodyPublisher = BodyPublishers.ofByteArrays(requestBytes); } else { - var requestString = JsonUtil.objectToJson(bodyObject); - logger.debug("Request Body : {}", requestString); - bodyPublisher = BodyPublishers.ofString(requestString); + switch (contentType) { + case MULTIPART_FORMDATA: + logger.debug("Request Body : {}", (Map<String, Object>) bodyObject); + var bodyBytes = HttpMultipart.toByteArrays((Map<String, Object>) bodyObject); + bodyPublisher = BodyPublishers.ofByteArrays(bodyBytes); + break; + + case APPLICATION_JSON: + logger.debug("Request Body : {}", (String) bodyObject); + bodyPublisher = BodyPublishers.ofString((String) bodyObject); + break; + } } return bodyPublisher; } + + private String printHeaders(List<String> headers) { + var print = "{"; + for (var i = 0; i < headers.size(); i++) { + if (i > 1) { + print += ", "; + } + var headerKey = headers.get(i++); + var headerVal = headerKey.equals("Authorization") ? "*".repeat(10) : headers.get(i); + print += headerKey + " = " + headerVal; + } + print += "}"; + return print; + } } \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/cleverclient/http/HttpProcessor.java b/src/main/java/io/github/sashirestela/cleverclient/http/HttpProcessor.java index 77070fe..0469a52 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/http/HttpProcessor.java +++ b/src/main/java/io/github/sashirestela/cleverclient/http/HttpProcessor.java @@ -7,14 +7,15 @@ import java.net.http.HttpClient; import java.util.ArrayList; import java.util.List; -import java.util.function.Function; +import java.util.function.UnaryOperator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.github.sashirestela.cleverclient.metadata.InterfaceMetadata.MethodMetadata; +import io.github.sashirestela.cleverclient.support.ContentType; import io.github.sashirestela.cleverclient.metadata.InterfaceMetadataStore; -import io.github.sashirestela.cleverclient.util.Constant; +import io.github.sashirestela.cleverclient.util.JsonUtil; import io.github.sashirestela.cleverclient.util.ReflectUtil; import lombok.Builder; @@ -28,7 +29,7 @@ public class HttpProcessor implements InvocationHandler { private final String baseUrl; private final List<String> headers; private final HttpClient httpClient; - private final Function<String, String> urlInterceptor; + private final UnaryOperator<HttpRequestData> requestInterceptor; /** * Creates a generic dynamic proxy with this HttpProcessor object acting as an @@ -98,59 +99,49 @@ private Object resolve(Method method, Object[] arguments) { var methodMetadata = interfaceMetadata.getMethodBySignature().get(method.toString()); var urlMethod = interfaceMetadata.getFullUrlByMethod(methodMetadata); var url = baseUrl + URLBuilder.one().build(urlMethod, methodMetadata, arguments); - if (urlInterceptor != null) { - url = urlInterceptor.apply(url); - } var httpMethod = methodMetadata.getHttpAnnotationName(); var returnType = methodMetadata.getReturnType(); - var isMultipart = methodMetadata.isMultipart(); var bodyObject = calculateBodyObject(methodMetadata, arguments); + var contentType = methodMetadata.getContentType(); var fullHeaders = new ArrayList<>(this.headers); - fullHeaders.addAll(calculateHeaderContentType(bodyObject, isMultipart)); + fullHeaders.addAll(calculateHeaderContentType(contentType)); fullHeaders.addAll(interfaceMetadata.getFullHeadersByMethod(methodMetadata)); - var fullHeadersArray = fullHeaders.toArray(new String[0]); var httpConnector = HttpConnector.builder() .httpClient(httpClient) .url(url) .httpMethod(httpMethod) .returnType(returnType) .bodyObject(bodyObject) - .isMultipart(isMultipart) - .headersArray(fullHeadersArray) + .contentType(contentType) + .headers(fullHeaders) + .requestInterceptor(requestInterceptor) .build(); - logger.debug("Http Call : {} {}", httpMethod, url); - logger.debug("Request Headers : {}", printHeaders(fullHeaders)); return httpConnector.sendRequest(); } private Object calculateBodyObject(MethodMetadata methodMetadata, Object[] arguments) { - var indexBody = methodMetadata.getBodyIndex(); - return indexBody >= 0 ? arguments[indexBody] : null; - } - - private List<String> calculateHeaderContentType(Object bodyObject, boolean isMultipart) { - List<String> headerContentType = new ArrayList<>(); + var bodyIndex = methodMetadata.getBodyIndex(); + var bodyObject = bodyIndex >= 0 ? arguments[bodyIndex] : null; if (bodyObject != null) { - headerContentType.add(Constant.HEADER_CONTENT_TYPE); - var contentType = isMultipart - ? Constant.TYPE_MULTIPART + Constant.BOUNDARY_TITLE + "\"" + Constant.BOUNDARY_VALUE + "\"" - : Constant.TYPE_APP_JSON; - headerContentType.add(contentType); + switch (methodMetadata.getContentType()) { + case MULTIPART_FORMDATA: + bodyObject = JsonUtil.objectToMap(bodyObject); + break; + case APPLICATION_JSON: + bodyObject = JsonUtil.objectToJson(bodyObject); + break; + } } - return headerContentType; + return bodyObject; } - private String printHeaders(List<String> headers) { - var print = "{"; - for (var i = 0; i < headers.size(); i++) { - if (i > 1) { - print += ", "; - } - var headerKey = headers.get(i++); - var headerVal = headerKey.equals("Authorization") ? "*".repeat(10) : headers.get(i); - print += headerKey + " = " + headerVal; + private List<String> calculateHeaderContentType(ContentType contentType) { + final String HEADER_CONTENT_TYPE = "Content-Type"; + List<String> headerContentType = new ArrayList<>(); + if (contentType != null) { + headerContentType.add(HEADER_CONTENT_TYPE); + headerContentType.add(contentType.getMimeType() + contentType.getDetails()); } - print += "}"; - return print; + return headerContentType; } } \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/cleverclient/http/HttpRequestData.java b/src/main/java/io/github/sashirestela/cleverclient/http/HttpRequestData.java new file mode 100644 index 0000000..eb4808a --- /dev/null +++ b/src/main/java/io/github/sashirestela/cleverclient/http/HttpRequestData.java @@ -0,0 +1,18 @@ +package io.github.sashirestela.cleverclient.http; + +import java.util.Map; + +import io.github.sashirestela.cleverclient.support.ContentType; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Builder +@Getter +public class HttpRequestData { + @Setter private String url; + @Setter private Object body; + @Setter private Map<String, String> headers; + private String httpMethod; + private ContentType contentType; +} diff --git a/src/main/java/io/github/sashirestela/cleverclient/metadata/InterfaceMetadata.java b/src/main/java/io/github/sashirestela/cleverclient/metadata/InterfaceMetadata.java index a5b1355..a972822 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/metadata/InterfaceMetadata.java +++ b/src/main/java/io/github/sashirestela/cleverclient/metadata/InterfaceMetadata.java @@ -5,6 +5,7 @@ import java.util.Map; import java.util.stream.Collectors; +import io.github.sashirestela.cleverclient.support.ContentType; import io.github.sashirestela.cleverclient.support.ReturnType; import lombok.Builder; import lombok.Value; @@ -75,7 +76,15 @@ public String getHttpAnnotationName() { .getName(); } - public boolean isMultipart() { + public ContentType getContentType() { + return getBodyIndex() == -1 + ? null + : isMultipart() + ? ContentType.MULTIPART_FORMDATA + : ContentType.APPLICATION_JSON; + } + + private boolean isMultipart() { return annotations.stream() .anyMatch(annot -> annot.getName().equals(ANNOT_MULTIPART)); } diff --git a/src/main/java/io/github/sashirestela/cleverclient/support/ContentType.java b/src/main/java/io/github/sashirestela/cleverclient/support/ContentType.java new file mode 100644 index 0000000..c8c735e --- /dev/null +++ b/src/main/java/io/github/sashirestela/cleverclient/support/ContentType.java @@ -0,0 +1,28 @@ +package io.github.sashirestela.cleverclient.support; + +import io.github.sashirestela.cleverclient.util.Constant; + +public enum ContentType { + MULTIPART_FORMDATA( + "multipart/form-data", + "; boundary=\"" + Constant.BOUNDARY_VALUE + "\""), + APPLICATION_JSON( + "application/json", + ""); + + private String mimeType; + private String details; + + ContentType(String mimeType, String details) { + this.mimeType = mimeType; + this.details = details; + } + + public String getMimeType() { + return this.mimeType; + } + + public String getDetails() { + return this.details; + } +} diff --git a/src/main/java/io/github/sashirestela/cleverclient/util/CommonUtil.java b/src/main/java/io/github/sashirestela/cleverclient/util/CommonUtil.java index 79d7641..ae302b8 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/util/CommonUtil.java +++ b/src/main/java/io/github/sashirestela/cleverclient/util/CommonUtil.java @@ -68,4 +68,9 @@ public static List<String> mapToListOfString(Map<String, String> map) { }).collect(Collectors.counting()); return list; } + + public static Map<String, String> listToMapOfString(List<String> list) { + var array = list.toArray(new String[0]); + return createMapString(array); + } } \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/cleverclient/util/Constant.java b/src/main/java/io/github/sashirestela/cleverclient/util/Constant.java index 64c9c12..0a21655 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/util/Constant.java +++ b/src/main/java/io/github/sashirestela/cleverclient/util/Constant.java @@ -7,15 +7,6 @@ public class Constant { private Constant() { } - - public static final String JSON_EMPTY_CLASS = "{\"type\":\"object\",\"properties\":{}}"; - - public static final String HEADER_CONTENT_TYPE = "Content-Type"; - - public static final String TYPE_APP_JSON = "application/json"; - public static final String TYPE_MULTIPART = "multipart/form-data"; - - public static final String BOUNDARY_TITLE = "; boundary="; public static final String BOUNDARY_VALUE = new BigInteger(256, new Random()).toString(); public static final String REGEX_PATH_PARAM_URL = "\\{(.*?)\\}"; diff --git a/src/test/java/io/github/sashirestela/cleverclient/CleverClientTest.java b/src/test/java/io/github/sashirestela/cleverclient/CleverClientTest.java index e4f6342..957d422 100644 --- a/src/test/java/io/github/sashirestela/cleverclient/CleverClientTest.java +++ b/src/test/java/io/github/sashirestela/cleverclient/CleverClientTest.java @@ -15,14 +15,20 @@ import java.net.http.HttpResponse; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.function.Function; +import java.util.function.UnaryOperator; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import io.github.sashirestela.cleverclient.annotation.Body; import io.github.sashirestela.cleverclient.annotation.GET; import io.github.sashirestela.cleverclient.annotation.Query; import io.github.sashirestela.cleverclient.annotation.Resource; +import io.github.sashirestela.cleverclient.http.HttpRequestData; +import io.github.sashirestela.cleverclient.support.ContentType; +import io.github.sashirestela.cleverclient.util.HttpRequestBodyTestUtility; +import lombok.Builder; +import lombok.Value; class CleverClientTest { @@ -35,7 +41,7 @@ void shouldSetPropertiesToDefaultValuesWhenBuilderIsCalledWithoutThoseProperties assertEquals(HttpClient.Version.HTTP_2, cleverClient.getHttpClient().version()); assertNotNull(cleverClient.getBaseUrl()); assertNotNull(cleverClient.getHttpProcessor()); - assertNull(cleverClient.getUrlInterceptor()); + assertNull(cleverClient.getRequestInterceptor()); } @Test @@ -62,38 +68,73 @@ void shouldThrownExceptionWhenTryingToPassAnEmptyBaseUrl() { @SuppressWarnings("unchecked") @Test - void shouldModifyUrlWhenPassingUrlInterceptorFunction() { + void shouldModifyRequestWhenPassingInterceptorFunction() { var httpClient = mock(HttpClient.class); - Function<String, String> customUrlInterceptor = url -> { + UnaryOperator<HttpRequestData> requestInterceptor = request -> { + var url = request.getUrl(); + var contentType = request.getContentType(); + var body = request.getBody(); + // add a query parameter to url url = url + (url.contains("?") ? "&" : "?") + "api-version=2024-01-31"; // remove '/vN' or '/vN.M' from url url = url.replaceFirst("(\\/v\\d+\\.*\\d*)", ""); - return url; + request.setUrl(url); + + if (contentType != null) { + if (contentType.equals(ContentType.APPLICATION_JSON)) { + var bodyJson = (String) request.getBody(); + // remove a field from body (as Json) + bodyJson = bodyJson.replaceFirst(",?\"model\":\"[^\"]*\",?", ""); + bodyJson = bodyJson.replaceFirst("\"\"", "\",\""); + body = bodyJson; + } + if (contentType.equals(ContentType.MULTIPART_FORMDATA)) { + Map<String, Object> bodyMap = (Map<String, Object>) request.getBody(); + // remove a field from body (as Map) + bodyMap.remove("model"); + body = bodyMap; + } + request.setBody(body); + } + + return request; }; var cleverClient = CleverClient.builder() .baseUrl("https://test") - .urlInterceptor(customUrlInterceptor) + .requestInterceptor(requestInterceptor) .httpClient(httpClient) .build(); when(httpClient.sendAsync(any(), any())) .thenReturn(CompletableFuture.completedFuture(mock(HttpResponse.class))); var test = cleverClient.create(TestCleverClient.class); - test.getText("geo"); + test.getText(Sample.builder().id("1").model("abc").description("sample").build(), "geo"); ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); verify(httpClient, times(1)).sendAsync(requestCaptor.capture(), any()); - var actualUrl = requestCaptor.getValue().uri().toString(); + var httpRequestCaptor = requestCaptor.getValue(); + var actualUrl = httpRequestCaptor.uri().toString(); var expectedUrl = "https://test/api/text?prefix=geo&api-version=2024-01-31"; + var actualBody = HttpRequestBodyTestUtility.extractBody(httpRequestCaptor); + var expectedBody = "{\"id\":\"1\",\"description\":\"sample\"}"; assertEquals(expectedUrl, actualUrl); + assertEquals(expectedBody, actualBody); + } + + @Value + @Builder + static class Sample { + String id; + String model; + String description; } @Resource("/v1.2/api") interface TestCleverClient { @GET("/text") - CompletableFuture<String> getText(@Query("prefix") String prefix); + CompletableFuture<String> getText(@Body Sample sample, @Query("prefix") String prefix); } } diff --git a/src/test/java/io/github/sashirestela/cleverclient/util/CommonUtilTest.java b/src/test/java/io/github/sashirestela/cleverclient/util/CommonUtilTest.java index b2052e1..b318fec 100644 --- a/src/test/java/io/github/sashirestela/cleverclient/util/CommonUtilTest.java +++ b/src/test/java/io/github/sashirestela/cleverclient/util/CommonUtilTest.java @@ -144,4 +144,14 @@ void shouldReturnAnEmptyListWhenCallingMapToListAnEmptyMapIsPassed() { List<String> actualList = CommonUtil.mapToListOfString(map); assertLinesMatch(expectedList, actualList); } + + @Test + void shouldConvertListToMapOfStringWhenAListIsPassed() { + List<String> list = List.of("key1", "val1", "key2", "val2"); + Map<String, String> expectedMap = new HashMap<>(); + expectedMap.put("key1", "val1"); + expectedMap.put("key2", "val2"); + Map<String, String> actualMap = CommonUtil.listToMapOfString(list); + assertEquals(expectedMap, actualMap); + } } \ No newline at end of file diff --git a/src/test/java/io/github/sashirestela/cleverclient/util/HttpRequestBodyTestUtility.java b/src/test/java/io/github/sashirestela/cleverclient/util/HttpRequestBodyTestUtility.java new file mode 100644 index 0000000..7ec8c44 --- /dev/null +++ b/src/test/java/io/github/sashirestela/cleverclient/util/HttpRequestBodyTestUtility.java @@ -0,0 +1,48 @@ +package io.github.sashirestela.cleverclient.util; + +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.Flow; + +public class HttpRequestBodyTestUtility { + + public static String extractBody(HttpRequest httpRequest) { + return httpRequest.bodyPublisher().map(p -> { + var bodySubscriber = HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8); + var flowSubscriber = new HttpRequestBodyTestUtility.StringSubscriber(bodySubscriber); + p.subscribe(flowSubscriber); + return bodySubscriber.getBody().toCompletableFuture().join(); + }).orElseThrow(); + } + + static final class StringSubscriber implements Flow.Subscriber<ByteBuffer> { + final HttpResponse.BodySubscriber<String> wrapped; + + StringSubscriber(HttpResponse.BodySubscriber<String> wrapped) { + this.wrapped = wrapped; + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + wrapped.onSubscribe(subscription); + } + + @Override + public void onNext(ByteBuffer item) { + wrapped.onNext(List.of(item)); + } + + @Override + public void onError(Throwable throwable) { + wrapped.onError(throwable); + } + + @Override + public void onComplete() { + wrapped.onComplete(); + } + } +}