From 84f9159134dc29228b89564b7f611cb29cf365f7 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 18 Jun 2021 16:23:30 +1200 Subject: [PATCH 1/8] Improve javadoc for HttpException --- .../io/avaje/http/client/HttpException.java | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/io/avaje/http/client/HttpException.java b/client/src/main/java/io/avaje/http/client/HttpException.java index 21b183f..8d5a82f 100644 --- a/client/src/main/java/io/avaje/http/client/HttpException.java +++ b/client/src/main/java/io/avaje/http/client/HttpException.java @@ -8,7 +8,37 @@ *

* Wraps an underlying HttpResponse with helper methods to get the response body * as string or as a bean. - *

+ * + *

Example catching HttpException

+ *
{@code
+ *
+ *   try {
+ *       clientContext.request()
+ *         .path("hello/saveForm")
+ *         .formParam("email", "user@foo.com")
+ *         .formParam("url", "notAValidUrl")
+ *         .POST()
+ *         .asVoid();
+ *
+ *     } catch (HttpException e) {
+ *
+ *       // obtain the statusCode from the exception ...
+ *       int statusCode = e.getStatusCode());
+ *
+ *       HttpResponse httpResponse = e.getHttpResponse();
+ *
+ *       // obtain the statusCode from httpResponse ...
+ *       int statusCode = httpResponse.statusCode();
+ *
+ *       // convert error response body into a bean (typically Jackson/Gson)
+ *       final MyErrorBean errorResponse = e.bean(MyErrorBean.class);
+ *
+ *       final Map errorMap = errorResponse.getErrors();
+ *       assertThat(errorMap.get("url")).isEqualTo("must be a valid URL");
+ *       assertThat(errorMap.get("name")).isEqualTo("must not be null");
+ *     }
+ *
+ * }
*/ public class HttpException extends RuntimeException { From 34fa253eed19bdd76170513de850a99cc957463f Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 18 Jun 2021 16:51:26 +1200 Subject: [PATCH 2/8] Add async() execution for Void and String --- .../java/io/avaje/http/client/DHttpAsync.java | 28 +++++++++++++++++ .../avaje/http/client/DHttpClientContext.java | 9 ++++++ .../avaje/http/client/DHttpClientRequest.java | 30 +++++++++++++++---- .../avaje/http/client/HttpAsyncResponse.java | 21 +++++++++++++ .../avaje/http/client/HttpClientResponse.java | 5 ++++ .../http/client/HelloControllerTest.java | 27 +++++++++++++++++ .../java/org/example/github/GithubTest.java | 18 ++++++++++- 7 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 client/src/main/java/io/avaje/http/client/DHttpAsync.java create mode 100644 client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java diff --git a/client/src/main/java/io/avaje/http/client/DHttpAsync.java b/client/src/main/java/io/avaje/http/client/DHttpAsync.java new file mode 100644 index 0000000..1042bdf --- /dev/null +++ b/client/src/main/java/io/avaje/http/client/DHttpAsync.java @@ -0,0 +1,28 @@ +package io.avaje.http.client; + +import java.net.http.HttpResponse; +import java.util.concurrent.CompletableFuture; + +class DHttpAsync implements HttpAsyncResponse { + + private final DHttpClientRequest request; + + DHttpAsync(DHttpClientRequest request) { + this.request = request; + } + + @Override + public CompletableFuture> asDiscarding() { + return request + .performSendAsync(false, HttpResponse.BodyHandlers.discarding()) + .thenApply(request::afterAsync); + } + + @Override + public CompletableFuture> asString() { + return request + .performSendAsync(true, HttpResponse.BodyHandlers.ofString()) + .thenApply(request::afterAsync); + } + +} diff --git a/client/src/main/java/io/avaje/http/client/DHttpClientContext.java b/client/src/main/java/io/avaje/http/client/DHttpClientContext.java index bf64681..ff3003d 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientContext.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientContext.java @@ -9,6 +9,7 @@ import java.time.Duration; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; class DHttpClientContext implements HttpClientContext { @@ -29,6 +30,7 @@ class DHttpClientContext implements HttpClientContext { private final boolean withAuthToken; private final AuthTokenProvider authTokenProvider; private final AtomicReference tokenRef = new AtomicReference<>(); + private int loggingMaxBody = 1_000; DHttpClientContext(HttpClient httpClient, String baseUrl, Duration requestTimeout, BodyAdapter bodyAdapter, RetryHandler retryHandler, RequestListener requestListener, AuthTokenProvider authTokenProvider, RequestIntercept intercept) { this.httpClient = httpClient; @@ -155,6 +157,10 @@ HttpResponse send(HttpRequest.Builder requestBuilder, HttpResponse.BodyHa } } + CompletableFuture> sendAsync(HttpRequest.Builder requestBuilder, HttpResponse.BodyHandler bodyHandler) { + return httpClient.sendAsync(requestBuilder.build(), bodyHandler); + } + BodyContent write(Object bean, String contentType) { return bodyAdapter.beanWriter(bean.getClass()).write(bean, contentType); } @@ -198,4 +204,7 @@ private String authToken() { return authToken.token(); } + String maxResponseBody(String body) { + return body.length() > loggingMaxBody ? body.substring(0, loggingMaxBody) + " ..." : body; + } } diff --git a/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java b/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java index 95f907d..27bac10 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java @@ -13,6 +13,7 @@ import java.nio.file.Path; import java.time.*; import java.util.*; +import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; import java.util.stream.Stream; @@ -49,6 +50,7 @@ class DHttpClientRequest implements HttpClientRequest, HttpClientResponse { private boolean loggableResponseBody; private boolean skipAuthToken; private boolean suppressLogging; + private long startAsyncNanos; DHttpClientRequest(DHttpClientContext context, Duration requestTimeout) { this.context = context; @@ -276,6 +278,11 @@ private void addHeaders() { } } + @Override + public HttpAsyncResponse async() { + return new DHttpAsync(this); + } + @Override public HttpClientResponse HEAD() { httpRequest = newHead(url.build()); @@ -378,6 +385,21 @@ protected HttpResponse performSend(HttpResponse.BodyHandler responseHa } } + protected CompletableFuture> performSendAsync(boolean loggable, HttpResponse.BodyHandler responseHandler) { + loggableResponseBody = loggable; + context.beforeRequest(this); + addHeaders(); + startAsyncNanos = System.nanoTime(); + return context.sendAsync(httpRequest, responseHandler); + } + + public HttpResponse afterAsync(HttpResponse response) { + requestTimeNanos = System.nanoTime() - startAsyncNanos; + httpResponse = response; + context.afterResponse(this); + return response; + } + @Override public HttpResponse asByteArray() { return withResponseHandler(HttpResponse.BodyHandlers.ofByteArray()); @@ -511,13 +533,9 @@ public String responseBody() { return ""; } if (encodedResponseBody != null) { - return new String(encodedResponseBody.content(), StandardCharsets.UTF_8); + return context.maxResponseBody(new String(encodedResponseBody.content(), StandardCharsets.UTF_8)); } else if (httpResponse != null && loggableResponseBody) { - String strBody = httpResponse.body().toString(); - if (strBody.length() > 1_000) { - return strBody.substring(0, 1_000) + "..."; - } - return strBody; + return context.maxResponseBody(httpResponse.body().toString()); } return null; } diff --git a/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java b/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java new file mode 100644 index 0000000..bad4d0d --- /dev/null +++ b/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java @@ -0,0 +1,21 @@ +package io.avaje.http.client; + +import java.net.http.HttpResponse; +import java.util.concurrent.CompletableFuture; + +/** + * Async responses as CompletableFuture. + */ +public interface HttpAsyncResponse { + + /** + * Process discarding response body as {@literal HttpResponse}. + */ + CompletableFuture> asDiscarding(); + + /** + * Process as String response body {@literal HttpResponse}. + */ + CompletableFuture> asString(); + +} diff --git a/client/src/main/java/io/avaje/http/client/HttpClientResponse.java b/client/src/main/java/io/avaje/http/client/HttpClientResponse.java index 8d10820..186d23d 100644 --- a/client/src/main/java/io/avaje/http/client/HttpClientResponse.java +++ b/client/src/main/java/io/avaje/http/client/HttpClientResponse.java @@ -12,6 +12,11 @@ */ public interface HttpClientResponse { + /** + * Send the request async using CompletableFuture. + */ + HttpAsyncResponse async(); + /** * Returning the response using the given response reader. * diff --git a/client/src/test/java/io/avaje/http/client/HelloControllerTest.java b/client/src/test/java/io/avaje/http/client/HelloControllerTest.java index 82180d4..ce85c03 100644 --- a/client/src/test/java/io/avaje/http/client/HelloControllerTest.java +++ b/client/src/test/java/io/avaje/http/client/HelloControllerTest.java @@ -7,6 +7,8 @@ import java.net.http.HttpResponse; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -44,6 +46,31 @@ void get_helloMessage() { assertThat(hres.statusCode()).isEqualTo(200); } + @Test + void async_get_asString() throws ExecutionException, InterruptedException { + + final CompletableFuture> future = clientContext.request() + .path("hello").path("message") + .GET() + .async().asString(); + + final HttpResponse hres = future.get(); + assertThat(hres.body()).contains("hello world"); + assertThat(hres.statusCode()).isEqualTo(200); + } + + @Test + void async_get_asDiscarding() throws ExecutionException, InterruptedException { + + final CompletableFuture> future = clientContext.request() + .path("hello").path("message") + .GET() + .async().asDiscarding(); + + final HttpResponse hres = future.get(); + assertThat(hres.statusCode()).isEqualTo(200); + } + @Test void get_helloMessage_via_url() { diff --git a/client/src/test/java/org/example/github/GithubTest.java b/client/src/test/java/org/example/github/GithubTest.java index 4709ba2..238fb28 100644 --- a/client/src/test/java/org/example/github/GithubTest.java +++ b/client/src/test/java/org/example/github/GithubTest.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.avaje.http.client.HttpClientContext; import io.avaje.http.client.JacksonBodyAdapter; +import io.avaje.http.client.RequestLogger; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -18,12 +19,27 @@ public class GithubTest { @Test @Disabled - void test() { + void test() throws InterruptedException { + final HttpClientContext clientContext = HttpClientContext.newBuilder() .withBaseUrl("https://api.github.com") .withBodyAdapter(bodyAdapter) + .withRequestListener(new RequestLogger()) .build(); + clientContext.request() + .path("users").path("rbygrave").path("repos") + .GET() + .async() + .asString() + .thenAccept(res -> { + + System.out.println("RES: "+res.statusCode()); + System.out.println("BODY: "+res.body()); + }); + + Thread.sleep(1_000); + // will not work under module classpath without registering the HttpApiProvider final Simple simple = clientContext.create(Simple.class); From b2267d01dc5f8636d811b8de711d91a4ae082e3c Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 18 Jun 2021 20:59:22 +1200 Subject: [PATCH 3/8] Tidy HttpException and JacksonBodyAdapter --- .../src/main/java/io/avaje/http/client/HttpException.java | 8 ++++---- .../java/io/avaje/http/client/JacksonBodyAdapter.java | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/src/main/java/io/avaje/http/client/HttpException.java b/client/src/main/java/io/avaje/http/client/HttpException.java index 8d5a82f..576062f 100644 --- a/client/src/main/java/io/avaje/http/client/HttpException.java +++ b/client/src/main/java/io/avaje/http/client/HttpException.java @@ -43,7 +43,7 @@ public class HttpException extends RuntimeException { private final int statusCode; - private HttpClientContext context; + private DHttpClientContext context; private HttpResponse httpResponse; /** @@ -70,14 +70,14 @@ public HttpException(int statusCode, Throwable cause) { this.statusCode = statusCode; } - HttpException(HttpResponse httpResponse, HttpClientContext context) { + HttpException(HttpResponse httpResponse, DHttpClientContext context) { super(); this.httpResponse = httpResponse; this.statusCode = httpResponse.statusCode(); this.context = context; } - HttpException(HttpClientContext context, HttpResponse httpResponse) { + HttpException(DHttpClientContext context, HttpResponse httpResponse) { super(); this.httpResponse = httpResponse; this.statusCode = httpResponse.statusCode(); @@ -93,7 +93,7 @@ public HttpException(int statusCode, Throwable cause) { @SuppressWarnings("unchecked") public T bean(Class cls) { final BodyContent body = context.readContent((HttpResponse) httpResponse); - return context.converters().beanReader(cls).read(body); + return context.readBean(cls, body); } /** diff --git a/client/src/main/java/io/avaje/http/client/JacksonBodyAdapter.java b/client/src/main/java/io/avaje/http/client/JacksonBodyAdapter.java index 702ee48..049aee1 100644 --- a/client/src/main/java/io/avaje/http/client/JacksonBodyAdapter.java +++ b/client/src/main/java/io/avaje/http/client/JacksonBodyAdapter.java @@ -93,9 +93,9 @@ public T readBody(String content) { } @Override - public T read(BodyContent s) { + public T read(BodyContent bodyContent) { try { - return reader.readValue(s.content()); + return reader.readValue(bodyContent.content()); } catch (IOException e) { throw new RuntimeException(e); } From a959dbc04c91ad3515b08bd5b3b4dbc6907c35ba Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 18 Jun 2021 21:17:41 +1200 Subject: [PATCH 4/8] Add Async bean() support --- .../java/io/avaje/http/client/DHttpAsync.java | 6 + .../avaje/http/client/DHttpClientContext.java | 2 +- .../avaje/http/client/DHttpClientRequest.java | 11 +- .../avaje/http/client/HttpAsyncResponse.java | 33 ++++++ .../http/client/HelloControllerTest.java | 105 ++++++++++++++++++ 5 files changed, 155 insertions(+), 2 deletions(-) diff --git a/client/src/main/java/io/avaje/http/client/DHttpAsync.java b/client/src/main/java/io/avaje/http/client/DHttpAsync.java index 1042bdf..7fb6da8 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpAsync.java +++ b/client/src/main/java/io/avaje/http/client/DHttpAsync.java @@ -25,4 +25,10 @@ public CompletableFuture> asString() { .thenApply(request::afterAsync); } + @Override + public CompletableFuture bean(Class type) { + return request + .performSendAsync(true, HttpResponse.BodyHandlers.ofByteArray()) + .thenApply(httpResponse -> request.asyncBean(type, httpResponse)); + } } diff --git a/client/src/main/java/io/avaje/http/client/DHttpClientContext.java b/client/src/main/java/io/avaje/http/client/DHttpClientContext.java index ff3003d..496a482 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientContext.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientContext.java @@ -100,7 +100,7 @@ public void checkResponse(HttpResponse response) { } } - void check(HttpResponse response) { + void checkMaybeThrow(HttpResponse response) { if (response.statusCode() >= 300) { throw new HttpException(this, response); } diff --git a/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java b/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java index 27bac10..0fe0022 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java @@ -327,7 +327,7 @@ public HttpClientResponse TRACE() { private void readResponseContent() { final HttpResponse response = asByteArray(); this.httpResponse = response; - context.check(response); + context.checkMaybeThrow(response); encodedResponseBody = context.readContent(response); } @@ -393,6 +393,15 @@ protected CompletableFuture> performSendAsync(boolean loggab return context.sendAsync(httpRequest, responseHandler); } + protected E asyncBean(Class type, HttpResponse response) { + requestTimeNanos = System.nanoTime() - startAsyncNanos; + httpResponse = response; + encodedResponseBody = context.readContent(response); + context.afterResponse(this); + context.checkMaybeThrow(response); + return context.readBean(type, encodedResponseBody); + } + public HttpResponse afterAsync(HttpResponse response) { requestTimeNanos = System.nanoTime() - startAsyncNanos; httpResponse = response; diff --git a/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java b/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java index bad4d0d..c9e583e 100644 --- a/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java +++ b/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java @@ -18,4 +18,37 @@ public interface HttpAsyncResponse { */ CompletableFuture> asString(); + /** + * Process expecting a (json) bean response body. + *

+ * If the HTTP statusCode is 300 or above a HttpException is throw which + * contains the HttpResponse. + * + *

{@code
+   *
+   *    clientContext.request()
+   *       ...
+   *       .POST().async()
+   *       .bean(HelloDto.class)
+   *       .whenComplete((helloDto, throwable) -> {
+   *
+   *         if (throwable != null) {
+   *           HttpException httpException = (HttpException) throwable.getCause();
+   *           int statusCode = httpException.getStatusCode();
+   *
+   *           // maybe convert json error response body to a bean (using Jackson/Gson)
+   *           MyErrorBean errorResponse = httpException.bean(MyErrorBean.class);
+   *           ..
+   *
+   *         } else {
+   *           // use helloDto
+   *           ...
+   *         }
+   *
+   *       });
+   *
+   *
+   * }
+ */ + CompletableFuture bean(Class type); } diff --git a/client/src/test/java/io/avaje/http/client/HelloControllerTest.java b/client/src/test/java/io/avaje/http/client/HelloControllerTest.java index ce85c03..5c8e8fd 100644 --- a/client/src/test/java/io/avaje/http/client/HelloControllerTest.java +++ b/client/src/test/java/io/avaje/http/client/HelloControllerTest.java @@ -8,7 +8,10 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -106,6 +109,108 @@ void get_withPathParamAndQueryParam_returningBean() { assertThat(dto.otherParam).isEqualTo("other"); } + @Test + void async_whenComplete_returningBean() throws ExecutionException, InterruptedException { + + final AtomicInteger counter = new AtomicInteger(); + final AtomicReference ref = new AtomicReference<>(); + + final CompletableFuture future = clientContext.request() + .path("hello/43/2020-03-05").queryParam("otherParam", "other").queryParam("foo", null) + .GET() + .async().bean(HelloDto.class); + + future.whenComplete((dto, throwable) -> { + counter.incrementAndGet(); + ref.set(dto); + + assertThat(throwable).isNull(); + assertThat(dto.id).isEqualTo(43L); + assertThat(dto.name).isEqualTo("2020-03-05"); + assertThat(dto.otherParam).isEqualTo("other"); + }); + + // wait ... + final HelloDto dto = future.get(); + assertThat(counter.incrementAndGet()).isEqualTo(2); + assertThat(dto).isSameAs(ref.get()); + + assertThat(dto.id).isEqualTo(43L); + assertThat(dto.name).isEqualTo("2020-03-05"); + assertThat(dto.otherParam).isEqualTo("other"); + } + + @Test + void async_whenComplete_throwingHttpException() { + + AtomicReference causeRef = new AtomicReference<>(); + + final CompletableFuture future = clientContext.request() + .path("hello/saveform3") + .formParam("name", "Bax") + .formParam("email", "notValidEmail") + .formParam("url", "notValidUrl") + .formParam("startDate", "2030-12-03") + .POST() + .async() + .bean(HelloDto.class) + .whenComplete((helloDto, throwable) -> { + // we get a throwable + assertThat(throwable.getCause()).isInstanceOf(HttpException.class); + assertThat(helloDto).isNull(); + + final HttpException httpException = (HttpException) throwable.getCause(); + causeRef.set(httpException); + assertThat(httpException.getStatusCode()).isEqualTo(422); + + // convert json error response body to a bean + final ErrorResponse errorResponse = httpException.bean(ErrorResponse.class); + + final Map errorMap = errorResponse.getErrors(); + assertThat(errorMap.get("url")).isEqualTo("must be a valid URL"); + assertThat(errorMap.get("email")).isEqualTo("must be a well-formed email address"); + }); + + try { + future.join(); + } catch (CompletionException e) { + assertThat(e.getCause()).isSameAs(causeRef.get()); + } + } + + @Test + void async_exceptionally_style() { + + AtomicReference causeRef = new AtomicReference<>(); + + final CompletableFuture future = clientContext.request() + .path("hello/saveform3") + .formParam("name", "Bax") + .formParam("email", "notValidEmail") + .formParam("url", "notValidUrl") + .formParam("startDate", "2030-12-03") + .POST() + .async() + .bean(HelloDto.class); + + future.exceptionally(throwable -> { + final HttpException httpException = (HttpException) throwable.getCause(); + causeRef.set(httpException); + assertThat(httpException.getStatusCode()).isEqualTo(422); + + return new HelloDto(0, "ErrorResponse", ""); + + }).thenAccept(helloDto -> { + assertThat(helloDto.name).isEqualTo("ErrorResponse"); + }); + + try { + future.join(); + } catch (CompletionException e) { + assertThat(e.getCause()).isSameAs(causeRef.get()); + } + } + @Test void post_bean_returningBean_usingExplicitConverters() { From 24704fb3b7c7c032e841738c4f5f229124ebe2a0 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 18 Jun 2021 21:25:20 +1200 Subject: [PATCH 5/8] Improve javadoc for HttpAsyncResponse --- .../avaje/http/client/HttpAsyncResponse.java | 39 ++++++++++++++++++- .../http/client/HelloControllerTest.java | 10 ++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java b/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java index c9e583e..6eaccc2 100644 --- a/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java +++ b/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java @@ -10,16 +10,53 @@ public interface HttpAsyncResponse { /** * Process discarding response body as {@literal HttpResponse}. + * + *
{@code
+   *
+   *   clientContext.request()
+   *       .path("hello/world")
+   *       .GET()
+   *       .async().asDiscarding()
+   *       .whenComplete((hres, throwable) -> {
+   *
+   *         if (throwable != null) {
+   *           ...
+   *         } else {
+   *           int statusCode = hres.statusCode();
+   *           ...
+   *         }
+   *       });
+   *
+   * }
*/ CompletableFuture> asDiscarding(); /** * Process as String response body {@literal HttpResponse}. + * + *
{@code
+   *
+   *   clientContext.request()
+   *       .path("hello/world")
+   *       .GET()
+   *       .async().asString()
+   *       .whenComplete((hres, throwable) -> {
+   *
+   *         if (throwable != null) {
+   *           ...
+   *         } else {
+   *           int statusCode = hres.statusCode();
+   *           String body = hres.body();
+   *           ...
+   *         }
+   *       });
+   *
+   * }
*/ CompletableFuture> asString(); /** - * Process expecting a (json) bean response body. + * Process expecting a bean response body (typically from json content). *

* If the HTTP statusCode is 300 or above a HttpException is throw which * contains the HttpResponse. diff --git a/client/src/test/java/io/avaje/http/client/HelloControllerTest.java b/client/src/test/java/io/avaje/http/client/HelloControllerTest.java index 5c8e8fd..9974d44 100644 --- a/client/src/test/java/io/avaje/http/client/HelloControllerTest.java +++ b/client/src/test/java/io/avaje/http/client/HelloControllerTest.java @@ -52,12 +52,20 @@ void get_helloMessage() { @Test void async_get_asString() throws ExecutionException, InterruptedException { + AtomicReference> ref = new AtomicReference<>(); + final CompletableFuture> future = clientContext.request() .path("hello").path("message") .GET() - .async().asString(); + .async().asString() + .whenComplete((hres, throwable) -> { + ref.set(hres); + assertThat(hres.statusCode()).isEqualTo(200); + assertThat(hres.body()).contains("hello world"); + }); final HttpResponse hres = future.get(); + assertThat(hres).isSameAs(ref.get()); assertThat(hres.body()).contains("hello world"); assertThat(hres.statusCode()).isEqualTo(200); } From baf191b2faca43c017c965b5bcc8b498ffdb39d9 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 18 Jun 2021 21:37:46 +1200 Subject: [PATCH 6/8] Add Async list() of beans support --- .../java/io/avaje/http/client/DHttpAsync.java | 8 +++++ .../avaje/http/client/DHttpClientRequest.java | 13 ++++++-- .../avaje/http/client/HttpAsyncResponse.java | 31 +++++++++++++++++-- .../http/client/HelloControllerTest.java | 21 +++++++++++++ 4 files changed, 69 insertions(+), 4 deletions(-) diff --git a/client/src/main/java/io/avaje/http/client/DHttpAsync.java b/client/src/main/java/io/avaje/http/client/DHttpAsync.java index 7fb6da8..733c00d 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpAsync.java +++ b/client/src/main/java/io/avaje/http/client/DHttpAsync.java @@ -1,6 +1,7 @@ package io.avaje.http.client; import java.net.http.HttpResponse; +import java.util.List; import java.util.concurrent.CompletableFuture; class DHttpAsync implements HttpAsyncResponse { @@ -31,4 +32,11 @@ public CompletableFuture bean(Class type) { .performSendAsync(true, HttpResponse.BodyHandlers.ofByteArray()) .thenApply(httpResponse -> request.asyncBean(type, httpResponse)); } + + @Override + public CompletableFuture> list(Class type) { + return request + .performSendAsync(true, HttpResponse.BodyHandlers.ofByteArray()) + .thenApply(httpResponse -> request.asyncList(type, httpResponse)); + } } diff --git a/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java b/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java index 0fe0022..ada58ec 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java @@ -394,15 +394,24 @@ protected CompletableFuture> performSendAsync(boolean loggab } protected E asyncBean(Class type, HttpResponse response) { + afterAsyncEncoded(response); + return context.readBean(type, encodedResponseBody); + } + + protected List asyncList(Class type, HttpResponse response) { + afterAsyncEncoded(response); + return context.readList(type, encodedResponseBody); + } + + private void afterAsyncEncoded(HttpResponse response) { requestTimeNanos = System.nanoTime() - startAsyncNanos; httpResponse = response; encodedResponseBody = context.readContent(response); context.afterResponse(this); context.checkMaybeThrow(response); - return context.readBean(type, encodedResponseBody); } - public HttpResponse afterAsync(HttpResponse response) { + protected HttpResponse afterAsync(HttpResponse response) { requestTimeNanos = System.nanoTime() - startAsyncNanos; httpResponse = response; context.afterResponse(this); diff --git a/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java b/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java index 6eaccc2..6b6442d 100644 --- a/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java +++ b/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java @@ -1,6 +1,7 @@ package io.avaje.http.client; import java.net.http.HttpResponse; +import java.util.List; import java.util.concurrent.CompletableFuture; /** @@ -81,11 +82,37 @@ public interface HttpAsyncResponse { * // use helloDto * ... * } - * * }); + * } + */ + CompletableFuture bean(Class type); + + /** + * Process expecting a list of beans response body (typically from json content). + *

+ * If the HTTP statusCode is 300 or above a HttpException is throw which + * contains the HttpResponse. + * + *

{@code
+   *
+   *    clientContext.request()
+   *       ...
+   *       .GET().async()
+   *       .list(HelloDto.class)
+   *       .whenComplete((helloDtos, throwable) -> {
    *
+   *         if (throwable != null) {
+   *           HttpException httpException = (HttpException) throwable.getCause();
+   *           int statusCode = httpException.getStatusCode();
+   *           ...
    *
+   *         } else {
+   *           // use list of helloDto
+   *           ...
+   *         }
+   *       });
    * }
*/ - CompletableFuture bean(Class type); + CompletableFuture> list(Class type); + } diff --git a/client/src/test/java/io/avaje/http/client/HelloControllerTest.java b/client/src/test/java/io/avaje/http/client/HelloControllerTest.java index 9974d44..fc6ea8d 100644 --- a/client/src/test/java/io/avaje/http/client/HelloControllerTest.java +++ b/client/src/test/java/io/avaje/http/client/HelloControllerTest.java @@ -104,6 +104,27 @@ void get_hello_returningListOfBeans() { assertThat(helloDtos).hasSize(2); } + @Test + void async_list() throws ExecutionException, InterruptedException { + + AtomicReference> ref = new AtomicReference<>(); + + final CompletableFuture> future = clientContext.request() + .path("hello") + .GET().async() + .list(HelloDto.class); + + future.whenComplete((helloDtos, throwable) -> { + assertThat(throwable).isNull(); + assertThat(helloDtos).hasSize(2); + ref.set(helloDtos); + }); + + final List helloDtos = future.get(); + assertThat(helloDtos).hasSize(2); + assertThat(helloDtos).isSameAs(ref.get()); + } + @Test void get_withPathParamAndQueryParam_returningBean() { From 5739cbb6e67c53ab0e9c416b258b103aa0ce313d Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 18 Jun 2021 22:31:16 +1200 Subject: [PATCH 7/8] Add Async withHandler() support for any response body handler --- .../java/io/avaje/http/client/DHttpAsync.java | 9 +++- .../avaje/http/client/DHttpClientRequest.java | 3 +- .../avaje/http/client/HttpAsyncResponse.java | 54 ++++++++++++++++++- .../http/client/HelloControllerTest.java | 51 ++++++++++++++++++ 4 files changed, 113 insertions(+), 4 deletions(-) diff --git a/client/src/main/java/io/avaje/http/client/DHttpAsync.java b/client/src/main/java/io/avaje/http/client/DHttpAsync.java index 733c00d..e7fb69e 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpAsync.java +++ b/client/src/main/java/io/avaje/http/client/DHttpAsync.java @@ -13,12 +13,17 @@ class DHttpAsync implements HttpAsyncResponse { } @Override - public CompletableFuture> asDiscarding() { + public CompletableFuture> withHandler(HttpResponse.BodyHandler handler) { return request - .performSendAsync(false, HttpResponse.BodyHandlers.discarding()) + .performSendAsync(false, handler) .thenApply(request::afterAsync); } + @Override + public CompletableFuture> asDiscarding() { + return withHandler(HttpResponse.BodyHandlers.discarding()); + } + @Override public CompletableFuture> asString() { return request diff --git a/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java b/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java index ada58ec..aa7a262 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java @@ -553,7 +553,8 @@ public String responseBody() { if (encodedResponseBody != null) { return context.maxResponseBody(new String(encodedResponseBody.content(), StandardCharsets.UTF_8)); } else if (httpResponse != null && loggableResponseBody) { - return context.maxResponseBody(httpResponse.body().toString()); + final Object body = httpResponse.body(); + return (body == null) ? null : context.maxResponseBody(body.toString()); } return null; } diff --git a/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java b/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java index 6b6442d..527ca91 100644 --- a/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java +++ b/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java @@ -5,7 +5,7 @@ import java.util.concurrent.CompletableFuture; /** - * Async responses as CompletableFuture. + * Async processing of the request with responses as CompletableFuture. */ public interface HttpAsyncResponse { @@ -29,6 +29,8 @@ public interface HttpAsyncResponse { * }); * * } + * + * @return The CompletableFuture of the response */ CompletableFuture> asDiscarding(); @@ -53,9 +55,53 @@ public interface HttpAsyncResponse { * }); * * } + * + * @return The CompletableFuture of the response */ CompletableFuture> asString(); + /** + * Process with any given {@code HttpResponse.BodyHandler}. + * + *

Example: line subscriber

+ *

+ * Subscribe line by line to the response. + *

+ *
{@code
+   *
+   *    CompletableFuture> future = clientContext.request()
+   *       .path("hello/lineStream")
+   *       .GET().async()
+   *       .withHandler(HttpResponse.BodyHandlers.fromLineSubscriber(new Flow.Subscriber<>() {
+   *
+   *         @Override
+   *         public void onSubscribe(Flow.Subscription subscription) {
+   *           subscription.request(Long.MAX_VALUE);
+   *         }
+   *         @Override
+   *         public void onNext(String item) {
+   *           ...
+   *         }
+   *         @Override
+   *         public void onError(Throwable throwable) {
+   *           ...
+   *         }
+   *         @Override
+   *         public void onComplete() {
+   *           ...
+   *         }
+   *       }))
+   *       .whenComplete((hres, throwable) -> {
+   *         int statusCode = hres.statusCode();
+   *         ...
+   *       });
+   * }
+ * + * @param bodyHandlers The body handler to use to process the response + * @return The CompletableFuture of the response + */ + CompletableFuture> withHandler(HttpResponse.BodyHandler bodyHandlers); + /** * Process expecting a bean response body (typically from json content). *

@@ -84,6 +130,9 @@ public interface HttpAsyncResponse { * } * }); * } + * + * @param type The bean type to convert the content to + * @return The CompletableFuture of the response */ CompletableFuture bean(Class type); @@ -112,6 +161,9 @@ public interface HttpAsyncResponse { * } * }); * } + * + * @param type The bean type to convert the content to + * @return The CompletableFuture of the response */ CompletableFuture> list(Class type); diff --git a/client/src/test/java/io/avaje/http/client/HelloControllerTest.java b/client/src/test/java/io/avaje/http/client/HelloControllerTest.java index fc6ea8d..bebc681 100644 --- a/client/src/test/java/io/avaje/http/client/HelloControllerTest.java +++ b/client/src/test/java/io/avaje/http/client/HelloControllerTest.java @@ -5,11 +5,13 @@ import org.junit.jupiter.api.Test; import java.net.http.HttpResponse; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Flow; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -38,6 +40,55 @@ void get_stream() { assertThat(first.name).isEqualTo("one"); } + @Test + void async_stream() throws ExecutionException, InterruptedException { + + AtomicReference> hresRef = new AtomicReference<>(); + AtomicReference errRef = new AtomicReference<>(); + AtomicReference completeRef = new AtomicReference<>(); + AtomicReference onSubscribeRef = new AtomicReference<>(); + + final List lines = new ArrayList<>(); + + final CompletableFuture> future = clientContext.request() + .path("hello/stream") + .GET() + .async().withHandler(HttpResponse.BodyHandlers.fromLineSubscriber(new Flow.Subscriber<>() { + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscription.request(Long.MAX_VALUE); + onSubscribeRef.set(true); + } + @Override + public void onNext(String item) { + lines.add(item); + } + @Override + public void onError(Throwable throwable) { + errRef.set(throwable); + } + @Override + public void onComplete() { + completeRef.set(true); + } + })).whenComplete((hres, throwable) -> { + hresRef.set(hres); + assertThat(hres.statusCode()).isEqualTo(200); + assertThat(throwable).isNull(); + }); + + // just wait + assertThat(future.get()).isSameAs(hresRef.get()); + + assertThat(onSubscribeRef.get()).isTrue(); + assertThat(completeRef.get()).isTrue(); + assertThat(errRef.get()).isNull(); + assertThat(lines).hasSize(4); + + final String first = lines.get(0); + assertThat(first).isEqualTo("{\"id\":1, \"name\":\"one\"}"); + } + @Test void get_helloMessage() { From 3333bb226e44a991a816ba96f7b93a7fea70c1eb Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 18 Jun 2021 22:35:53 +1200 Subject: [PATCH 8/8] Refactor rename method withResponseHandler() to withHandler() with deprecation --- .../avaje/http/client/DHttpClientRequest.java | 18 +++++++++--------- .../avaje/http/client/HttpClientResponse.java | 10 +++++++++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java b/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java index aa7a262..8340cbe 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java @@ -357,7 +357,7 @@ public List list(Class cls) { @Override public Stream stream(Class cls) { - final HttpResponse> res = withResponseHandler(HttpResponse.BodyHandlers.ofLines()); + final HttpResponse> res = withHandler(HttpResponse.BodyHandlers.ofLines()); this.httpResponse = res; if (res.statusCode() >= 300) { throw new HttpException(res, context); @@ -367,7 +367,7 @@ public Stream stream(Class cls) { } @Override - public HttpResponse withResponseHandler(HttpResponse.BodyHandler responseHandler) { + public HttpResponse withHandler(HttpResponse.BodyHandler responseHandler) { context.beforeRequest(this); addHeaders(); HttpResponse response = performSend(responseHandler); @@ -420,33 +420,33 @@ protected HttpResponse afterAsync(HttpResponse response) { @Override public HttpResponse asByteArray() { - return withResponseHandler(HttpResponse.BodyHandlers.ofByteArray()); + return withHandler(HttpResponse.BodyHandlers.ofByteArray()); } @Override public HttpResponse asString() { loggableResponseBody = true; - return withResponseHandler(HttpResponse.BodyHandlers.ofString()); + return withHandler(HttpResponse.BodyHandlers.ofString()); } @Override public HttpResponse asDiscarding() { - return withResponseHandler(discarding()); + return withHandler(discarding()); } @Override public HttpResponse asInputStream() { - return withResponseHandler(HttpResponse.BodyHandlers.ofInputStream()); + return withHandler(HttpResponse.BodyHandlers.ofInputStream()); } @Override public HttpResponse asFile(Path file) { - return withResponseHandler(HttpResponse.BodyHandlers.ofFile(file)); + return withHandler(HttpResponse.BodyHandlers.ofFile(file)); } @Override public HttpResponse> asLines() { - return withResponseHandler(HttpResponse.BodyHandlers.ofLines()); + return withHandler(HttpResponse.BodyHandlers.ofLines()); } private HttpRequest.Builder newReq(String url) { @@ -564,7 +564,7 @@ static class HttpVoidResponse implements HttpResponse { private final HttpResponse orig; - @SuppressWarnings({"unchecked", "raw"}) + @SuppressWarnings({"raw"}) HttpVoidResponse(HttpResponse orig) { this.orig = orig; } diff --git a/client/src/main/java/io/avaje/http/client/HttpClientResponse.java b/client/src/main/java/io/avaje/http/client/HttpClientResponse.java index 186d23d..90a75e3 100644 --- a/client/src/main/java/io/avaje/http/client/HttpClientResponse.java +++ b/client/src/main/java/io/avaje/http/client/HttpClientResponse.java @@ -108,6 +108,14 @@ public interface HttpClientResponse { /** * Return the response using the given response body handler. */ - HttpResponse withResponseHandler(HttpResponse.BodyHandler responseHandler); + HttpResponse withHandler(HttpResponse.BodyHandler responseHandler); + + /** + * Deprecated migrate to {@link #withHandler(HttpResponse.BodyHandler)} + */ + @Deprecated + default HttpResponse withResponseHandler(HttpResponse.BodyHandler responseHandler) { + return withHandler(responseHandler); + } }