diff --git a/README.md b/README.md index f87cec7..5513b53 100644 --- a/README.md +++ b/README.md @@ -310,7 +310,7 @@ public final class ExampleRetry implements RetryHandler { final var code = response.statusCode(); - if (retryCount >= MAX_RETRIES || code >= 400) { + if (retryCount >= MAX_RETRIES || code <= 400) { return false; } diff --git a/client/pom.xml b/client/pom.xml index bf5c54f..cb42865 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -39,14 +39,14 @@ io.avaje avaje-jsonb - 1.0-RC1 + 1.1-RC2 true io.avaje avaje-inject - 8.6 + 8.10 true @@ -76,14 +76,14 @@ io.javalin javalin - 4.1.1 + 5.2.0 test io.avaje avaje-http-api - 1.16 + 1.20 test @@ -125,7 +125,7 @@ io.avaje avaje-inject-generator - 8.6 + 8.10 diff --git a/client/src/main/java/io/avaje/http/client/BodyAdapter.java b/client/src/main/java/io/avaje/http/client/BodyAdapter.java index 3c0ffa4..012e503 100644 --- a/client/src/main/java/io/avaje/http/client/BodyAdapter.java +++ b/client/src/main/java/io/avaje/http/client/BodyAdapter.java @@ -1,5 +1,6 @@ package io.avaje.http.client; +import java.lang.reflect.ParameterizedType; import java.util.List; /** @@ -23,6 +24,16 @@ public interface BodyAdapter { */ BodyReader beanReader(Class type); + /** + * Return a BodyReader to read response content and convert to a bean. + * + * @param type The bean type to convert the content to. + */ + default BodyReader beanReader(ParameterizedType type) { + throw new UnsupportedOperationException("Parameterized types not supported for this adapter"); + } + + /** * Return a BodyReader to read response content and convert to a list of beans. * @@ -30,4 +41,12 @@ public interface BodyAdapter { */ BodyReader> listReader(Class type); + /** + * Return a BodyReader to read response content and convert to a list of beans. + * + * @param type The bean type to convert the content to. + */ + default BodyReader> listReader(ParameterizedType type) { + throw new UnsupportedOperationException("Parameterized types not supported for this adapter"); + } } 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 3ab02cb..770f6d3 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.io.InputStream; +import java.lang.reflect.ParameterizedType; import java.net.http.HttpResponse; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -78,4 +79,25 @@ public CompletableFuture> stream(Class type) { .performSendAsync(false, HttpResponse.BodyHandlers.ofLines()) .thenApply(httpResponse -> request.asyncStream(type, httpResponse)); } + + @Override + public CompletableFuture bean(ParameterizedType type) { + return request + .performSendAsync(true, HttpResponse.BodyHandlers.ofByteArray()) + .thenApply(httpResponse -> request.asyncBean(type, httpResponse)); + } + + @Override + public CompletableFuture> list(ParameterizedType type) { + return request + .performSendAsync(true, HttpResponse.BodyHandlers.ofByteArray()) + .thenApply(httpResponse -> request.asyncList(type, httpResponse)); + } + + @Override + public CompletableFuture> stream(ParameterizedType type) { + return request + .performSendAsync(false, HttpResponse.BodyHandlers.ofLines()) + .thenApply(httpResponse -> request.asyncStream(type, httpResponse)); + } } diff --git a/client/src/main/java/io/avaje/http/client/DHttpCall.java b/client/src/main/java/io/avaje/http/client/DHttpCall.java index 3ee7a8e..31c94e9 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpCall.java +++ b/client/src/main/java/io/avaje/http/client/DHttpCall.java @@ -1,6 +1,7 @@ package io.avaje.http.client; import java.io.InputStream; +import java.lang.reflect.ParameterizedType; import java.net.http.HttpResponse; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -64,6 +65,21 @@ public HttpCall> stream(Class type) { return new CallStream<>(type); } + @Override + public HttpCall bean(ParameterizedType type) { + return new CallBean<>(type); + } + + @Override + public HttpCall> list(ParameterizedType type) { + return new CallList<>(type); + } + + @Override + public HttpCall> stream(ParameterizedType type) { + return new CallStream<>(type); + } + private class CallVoid implements HttpCall> { @Override public HttpResponse execute() { @@ -132,46 +148,85 @@ public CompletableFuture> async() { private class CallBean implements HttpCall { private final Class type; + private final ParameterizedType genericType; + private final boolean isGeneric; + CallBean(Class type) { + this.isGeneric = false; this.type = type; + this.genericType = null; } + + CallBean(ParameterizedType type) { + this.isGeneric = true; + this.type = null; + this.genericType = type; + } + @Override public E execute() { - return request.bean(type); + return isGeneric ? request.bean(genericType) : request.bean(type); } + @Override public CompletableFuture async() { - return request.async().bean(type); + return isGeneric ? request.async().bean(genericType) : request.async().bean(type); } } private class CallList implements HttpCall> { private final Class type; + private final ParameterizedType genericType; + private final boolean isGeneric; + CallList(Class type) { + this.isGeneric = false; this.type = type; + this.genericType = null; } + + CallList(ParameterizedType type) { + this.isGeneric = true; + this.type = null; + this.genericType = type; + } + @Override public List execute() { - return request.list(type); + return isGeneric ? request.list(genericType) : request.list(type); } + @Override public CompletableFuture> async() { - return request.async().list(type); + return isGeneric ? request.async().list(genericType) : request.async().list(type); } } private class CallStream implements HttpCall> { private final Class type; + private final ParameterizedType genericType; + private final boolean isGeneric; + CallStream(Class type) { + this.isGeneric = false; this.type = type; + this.genericType = null; + } + + CallStream(ParameterizedType type) { + this.isGeneric = true; + this.type = null; + this.genericType = type; } + @Override public Stream execute() { - return request.stream(type); + return isGeneric ? request.stream(genericType) : request.stream(type); } + @Override public CompletableFuture> async() { - return request.async().stream(type); + return isGeneric ? request.async().stream(genericType) : request.async().stream(type); } } 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 afa764f..d2c1064 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientContext.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientContext.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.lang.reflect.Constructor; +import java.lang.reflect.ParameterizedType; import java.net.http.HttpClient; import java.net.http.HttpHeaders; import java.net.http.HttpRequest; @@ -273,6 +274,10 @@ BodyReader beanReader(Class cls) { return bodyAdapter.beanReader(cls); } + BodyReader beanReader(ParameterizedType cls) { + return bodyAdapter.beanReader(cls); + } + T readBean(Class cls, BodyContent content) { return bodyAdapter.beanReader(cls).read(content); } @@ -281,6 +286,15 @@ List readList(Class cls, BodyContent content) { return bodyAdapter.listReader(cls).read(content); } + @SuppressWarnings("unchecked") + T readBean(ParameterizedType cls, BodyContent content) { + return (T) bodyAdapter.beanReader(cls).read(content); + } + + List readList(ParameterizedType cls, BodyContent content) { + return (List) bodyAdapter.listReader(cls).read(content); + } + void afterResponse(DHttpClientRequest request) { metricResTotal.add(1); metricResMicros.add(request.responseTimeMicros()); 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 8d7fed7..22d885f 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java @@ -3,6 +3,7 @@ import javax.net.ssl.SSLSession; import java.io.FileNotFoundException; import java.io.InputStream; +import java.lang.reflect.ParameterizedType; import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; @@ -433,7 +434,7 @@ public List list(Class cls) { readResponseContent(); return context.readList(cls, encodedResponseBody); } - + @Override public Stream stream(Class cls) { final HttpResponse> res = handler(HttpResponse.BodyHandlers.ofLines()); @@ -445,6 +446,31 @@ public Stream stream(Class cls) { return res.body().map(bodyReader::readBody); } + + @Override + public T bean(ParameterizedType cls) { + readResponseContent(); + return context.readBean(cls, encodedResponseBody); + } + + @Override + public List list(ParameterizedType cls) { + readResponseContent(); + return context.readList(cls, encodedResponseBody); + } + + + @Override + public Stream stream(ParameterizedType cls) { + final HttpResponse> res = handler(HttpResponse.BodyHandlers.ofLines()); + this.httpResponse = res; + if (res.statusCode() >= 300) { + throw new HttpException(res, context); + } + final BodyReader bodyReader = context.beanReader(cls); + return res.body().map(bodyReader::readBody); + } + @Override public HttpResponse handler(HttpResponse.BodyHandler responseHandler) { final HttpResponse response = sendWith(responseHandler); @@ -506,6 +532,28 @@ protected Stream asyncStream(Class type, HttpResponse> return response.body().map(bodyReader::readBody); } + protected E asyncBean(ParameterizedType type, HttpResponse response) { + afterAsyncEncoded(response); + return context.readBean(type, encodedResponseBody); + } + + protected List asyncList(ParameterizedType type, HttpResponse response) { + afterAsyncEncoded(response); + return context.readList(type, encodedResponseBody); + } + + protected Stream asyncStream( + ParameterizedType type, HttpResponse> response) { + responseTimeNanos = System.nanoTime() - startAsyncNanos; + httpResponse = response; + context.afterResponse(this); + if (response.statusCode() >= 300) { + throw new HttpException(response, context); + } + final BodyReader bodyReader = context.beanReader(type); + return response.body().map(bodyReader::readBody); + } + private void afterAsyncEncoded(HttpResponse response) { responseTimeNanos = 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 45cea96..7e6dee8 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.io.InputStream; +import java.lang.reflect.ParameterizedType; import java.net.http.HttpResponse; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -334,4 +335,29 @@ default CompletableFuture> withHandler(HttpResponse.BodyHand * @return The CompletableFuture of the response */ CompletableFuture> stream(Class type); + + /** + * Process expecting a bean response body (typically from json content). + * + * @param type The parameterized type to convert the content to + * @return The CompletableFuture of the response + */ + CompletableFuture bean(ParameterizedType type); + + /** + * Process expecting a list of beans response body (typically from json content). + * + * @param type The parameterized type to convert the content to + * @return The CompletableFuture of the response + */ + CompletableFuture> list(ParameterizedType type); + + /** + * Process response as a stream of beans (x-json-stream). + * + * @param type The parameterized type to convert the content to + * @return The CompletableFuture of the response + */ + CompletableFuture> stream(ParameterizedType type); + } diff --git a/client/src/main/java/io/avaje/http/client/HttpCallResponse.java b/client/src/main/java/io/avaje/http/client/HttpCallResponse.java index d081a32..96cccbe 100644 --- a/client/src/main/java/io/avaje/http/client/HttpCallResponse.java +++ b/client/src/main/java/io/avaje/http/client/HttpCallResponse.java @@ -1,6 +1,7 @@ package io.avaje.http.client; import java.io.InputStream; +import java.lang.reflect.ParameterizedType; import java.net.http.HttpResponse; import java.util.List; import java.util.stream.Stream; @@ -186,4 +187,28 @@ default HttpCall> withHandler(HttpResponse.BodyHandler bo */ HttpCall> stream(Class type); + /** + * A bean response to execute async or sync. + * + * @param type The parameterized type to convert the content to + * @return The HttpCall to allow sync or async execution + */ + HttpCall bean(ParameterizedType type); + + /** + * Process expecting a list of beans response body (typically from json content). + * + * @param type The parameterized type to convert the content to + * @return The HttpCall to execute sync or async + */ + HttpCall> list(ParameterizedType type); + + /** + * Process expecting a stream of beans response body (typically from json content). + * + * @param type The parameterized type to convert the content to + * @return The HttpCall to execute sync or async + */ + HttpCall> stream(ParameterizedType type); + } 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 10c2d82..13f4e65 100644 --- a/client/src/main/java/io/avaje/http/client/HttpClientResponse.java +++ b/client/src/main/java/io/avaje/http/client/HttpClientResponse.java @@ -1,6 +1,7 @@ package io.avaje.http.client; import java.io.InputStream; +import java.lang.reflect.ParameterizedType; import java.net.http.HttpResponse; import java.nio.file.Path; import java.util.List; @@ -85,6 +86,7 @@ public interface HttpClientResponse { */ List list(Class type); + /** * Return the response as a stream of beans. *

@@ -106,6 +108,51 @@ public interface HttpClientResponse { */ Stream stream(Class type); + /** + * Return the response as a single bean. + *

+ * If the HTTP statusCode is not in the 2XX range a HttpException is throw which contains + * the HttpResponse. This is the cause in the CompletionException. + * + * @param type The parameterized type of the bean to convert the response content into. + * @return The bean the response is converted into. + * @throws HttpException when the response has error status codes + */ + T bean(ParameterizedType type); + + /** + * Return the response as a list of beans. + *

+ * If the HTTP statusCode is not in the 2XX range a HttpException is throw which contains + * the HttpResponse. This is the cause in the CompletionException. + * + * @param type The parameterized type of the bean to convert the response content into. + * @return The list of beans the response is converted into. + * @throws HttpException when the response has error status codes + */ + List list(ParameterizedType type); + + /** + * Return the response as a stream of beans. + *

+ * Typically the response is expected to be {@literal application/x-json-stream} + * newline delimited json payload. + *

+ * Note that for this stream request the response content is not deemed + * 'loggable' by avaje-http-client. This is because the entire response + * may not be available at the time of the callback. As such {@link RequestLogger} + * will not include response content when logging stream request/response + *

+ * If the HTTP statusCode is not in the 2XX range a HttpException is throw which contains + * the HttpResponse. This is the cause in the CompletionException. + * + * @param type The parameterized type of the bean to convert the response content into. + * @return The stream of beans from the response + * @throws HttpException when the response has error status codes + */ + Stream stream(ParameterizedType type); + + /** * Return the response with check for 200 range status code. *

diff --git a/client/src/main/java/io/avaje/http/client/JsonbBodyAdapter.java b/client/src/main/java/io/avaje/http/client/JsonbBodyAdapter.java index b678072..610c877 100644 --- a/client/src/main/java/io/avaje/http/client/JsonbBodyAdapter.java +++ b/client/src/main/java/io/avaje/http/client/JsonbBodyAdapter.java @@ -1,13 +1,15 @@ package io.avaje.http.client; -import io.avaje.jsonb.JsonType; -import io.avaje.jsonb.Jsonb; - +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.List; import java.util.concurrent.ConcurrentHashMap; +import io.avaje.jsonb.JsonType; +import io.avaje.jsonb.Jsonb; + /** - * avaje jsonb BodyAdapter to read and write beans as JSON. + * Avaje Jsonb BodyAdapter to read and write beans as JSON. * *

{@code
  *
@@ -21,9 +23,9 @@
 public final class JsonbBodyAdapter implements BodyAdapter {
 
   private final Jsonb jsonb;
-  private final ConcurrentHashMap, BodyWriter> beanWriterCache = new ConcurrentHashMap<>();
-  private final ConcurrentHashMap, BodyReader> beanReaderCache = new ConcurrentHashMap<>();
-  private final ConcurrentHashMap, BodyReader> listReaderCache = new ConcurrentHashMap<>();
+  private final ConcurrentHashMap> beanWriterCache = new ConcurrentHashMap<>();
+  private final ConcurrentHashMap> beanReaderCache = new ConcurrentHashMap<>();
+  private final ConcurrentHashMap> listReaderCache = new ConcurrentHashMap<>();
 
   /**
    * Create passing the Jsonb to use.
@@ -36,7 +38,7 @@ public JsonbBodyAdapter(Jsonb jsonb) {
    * Create with a default Jsonb that allows unknown properties.
    */
   public JsonbBodyAdapter() {
-    this.jsonb = Jsonb.newBuilder().build();
+    this.jsonb = Jsonb.builder().build();
   }
 
   @SuppressWarnings("unchecked")
@@ -51,6 +53,18 @@ public  BodyReader beanReader(Class cls) {
     return (BodyReader) beanReaderCache.computeIfAbsent(cls, aClass -> new JReader<>(jsonb.type(cls)));
   }
 
+  @SuppressWarnings("unchecked")
+  @Override
+  public  BodyReader beanReader(ParameterizedType cls) {
+    return (BodyReader) beanReaderCache.computeIfAbsent(cls, aClass -> new JReader<>(jsonb.type(cls)));
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public  BodyReader> listReader(ParameterizedType cls) {
+    return (BodyReader>) listReaderCache.computeIfAbsent(cls, aClass -> new JReader<>(jsonb.type(cls).list()));
+  }
+
   @SuppressWarnings("unchecked")
   @Override
   public  BodyReader> listReader(Class cls) {
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 516b118..6b308ad 100644
--- a/client/src/test/java/io/avaje/http/client/HelloControllerTest.java
+++ b/client/src/test/java/io/avaje/http/client/HelloControllerTest.java
@@ -394,7 +394,7 @@ void get_notFound() {
     final HttpResponse hres = request.GET().asString();
 
     assertThat(hres.statusCode()).isEqualTo(404);
-    assertThat(hres.body()).contains("Not found");
+    assertThat(hres.body()).contains("Not Found");
     HttpClientContext.Metrics metrics = clientContext.metrics(true);
     assertThat(metrics.totalCount()).isEqualTo(1);
     assertThat(metrics.errorCount()).isEqualTo(1);