From 9c581fc52a88bcd2419e34fb112511ee67d5e900 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Fri, 25 Feb 2022 13:59:25 +1300 Subject: [PATCH 01/67] [maven-release-plugin] prepare for next development iteration --- client/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index 4fe011e..b075525 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -9,11 +9,11 @@ io.avaje avaje-http-client - 1.14 + 1.15-SNAPSHOT scm:git:git@github.com:avaje/avaje-http-client.git - avaje-http-client-1.14 + HEAD From 1a19a1a3c364eb0b80ae694ba475392e53abcc7e Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Fri, 25 Feb 2022 14:34:04 +1300 Subject: [PATCH 02/67] Bump Gson dependency etc --- gson-adapter/pom.xml | 8 ++++---- test/pom.xml | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gson-adapter/pom.xml b/gson-adapter/pom.xml index 21a3a61..2385c02 100644 --- a/gson-adapter/pom.xml +++ b/gson-adapter/pom.xml @@ -4,12 +4,12 @@ java11-oss org.avaje - 3.3 + 3.5 io.avaje avaje-http-client-gson - 1.14-SNAPSHOT + 1.15-SNAPSHOT scm:git:git@github.com:avaje/avaje-http-client.git @@ -21,13 +21,13 @@ com.google.code.gson gson - 2.8.9 + 2.9.0 io.avaje avaje-http-client - 1.14-SNAPSHOT + 1.15-SNAPSHOT provided diff --git a/test/pom.xml b/test/pom.xml index a0a80cb..4d4ab73 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -16,13 +16,13 @@ io.avaje avaje-http-client - 1.14-SNAPSHOT + 1.15-SNAPSHOT io.avaje avaje-http-client-gson - 1.14-SNAPSHOT + 1.15-SNAPSHOT @@ -34,7 +34,7 @@ com.fasterxml.jackson.core jackson-databind - 2.12.5 + 2.13.1 From 761da88d820c83b18999d12523bb1c8f640c9cde Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Fri, 25 Feb 2022 17:06:12 +1300 Subject: [PATCH 03/67] #43 - ENH: Add gitter support to SimpleRetryHandler --- .../avaje/http/client/SimpleRetryHandler.java | 21 ++++++++++++++++-- .../java/io/avaje/http/client/RetryTest.java | 22 ++++++++++++++----- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/client/src/main/java/io/avaje/http/client/SimpleRetryHandler.java b/client/src/main/java/io/avaje/http/client/SimpleRetryHandler.java index 3b0ea55..9089046 100644 --- a/client/src/main/java/io/avaje/http/client/SimpleRetryHandler.java +++ b/client/src/main/java/io/avaje/http/client/SimpleRetryHandler.java @@ -4,6 +4,7 @@ import org.slf4j.LoggerFactory; import java.net.http.HttpResponse; +import java.util.Random; /** * Simple retry with max attempts and linear backoff. @@ -14,16 +15,31 @@ public class SimpleRetryHandler implements RetryHandler { private final int maxRetries; private final long backoffMillis; + private final int gitterMillis; + private final Random random; /** * Create with maximum number of retries and linear backoff time. * * @param maxRetries The maximum number of retry attempts * @param backoffMillis The linear backoff between attempts in milliseconds + * @param gitterMillis The maximum amount of gitter that gets added to backoffMillis */ - public SimpleRetryHandler(int maxRetries, long backoffMillis) { + public SimpleRetryHandler(int maxRetries, int backoffMillis, int gitterMillis) { this.maxRetries = maxRetries; this.backoffMillis = backoffMillis; + this.gitterMillis = gitterMillis; + this.random = new Random(); + } + + /** + * Create with maximum number of retries and linear backoff time and no gitter. + * + * @param maxRetries The maximum number of retry attempts + * @param backoffMillis The linear backoff between attempts in milliseconds + */ + public SimpleRetryHandler(int maxRetries, int backoffMillis) { + this(maxRetries, backoffMillis, 0); } @Override @@ -33,7 +49,8 @@ public boolean isRetry(int retryCount, HttpResponse response) { } log.debug("retry count:{} status:{} uri:{}", retryCount, response.statusCode(), response.uri()); try { - Thread.sleep(backoffMillis); + int gitter = gitterMillis < 1 ? 0 : random.nextInt(gitterMillis); + Thread.sleep(backoffMillis + gitter); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; diff --git a/client/src/test/java/io/avaje/http/client/RetryTest.java b/client/src/test/java/io/avaje/http/client/RetryTest.java index 7454160..0bde557 100644 --- a/client/src/test/java/io/avaje/http/client/RetryTest.java +++ b/client/src/test/java/io/avaje/http/client/RetryTest.java @@ -1,28 +1,38 @@ package io.avaje.http.client; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import java.net.http.HttpResponse; import static org.assertj.core.api.Assertions.assertThat; -public class RetryTest extends BaseWebTest { +class RetryTest extends BaseWebTest { - final MyIntercept myIntercept = new MyIntercept(); - final HttpClientContext clientContext = initClientWithRetry(); - HttpClientContext initClientWithRetry() { + HttpClientContext initClientWithRetry(MyIntercept myIntercept, RetryHandler retryHandler) { return HttpClientContext.newBuilder() .baseUrl("http://localhost:8887") .bodyAdapter(new JacksonBodyAdapter()) - .retryHandler(new SimpleRetryHandler(4, 1)) + .retryHandler(retryHandler) .requestIntercept(myIntercept) .build(); } @Test void retryTest() { + final MyIntercept myIntercept = new MyIntercept(); + final HttpClientContext clientContext = initClientWithRetry(myIntercept, new SimpleRetryHandler(4, 1)); + performGetRequestAndAssert(myIntercept, clientContext); + } + + @Test + void retryWithGitterTest() { + final MyIntercept myIntercept = new MyIntercept(); + final HttpClientContext clientContext = initClientWithRetry(myIntercept, new SimpleRetryHandler(4, 10, 20)); + performGetRequestAndAssert(myIntercept, clientContext); + } + + private void performGetRequestAndAssert(MyIntercept myIntercept, HttpClientContext clientContext) { HttpResponse res = clientContext.request() .label("http_client_hello_retry") .path("hello/retry") From a1e957c1273a729e97e695b585c042c3d811e888 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Fri, 25 Feb 2022 17:43:01 +1300 Subject: [PATCH 04/67] #44 - Migrate to use System.Logger rather than SLF4J-API (better support for jlink / module-path) --- client/pom.xml | 13 ++++---- .../java/io/avaje/http/client/DHttpApi.java | 9 +++-- .../io/avaje/http/client/RequestLogger.java | 33 +++++++++---------- .../avaje/http/client/SimpleRetryHandler.java | 10 +++--- client/src/main/java/module-info.java | 1 - 5 files changed, 32 insertions(+), 34 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index b075525..69eee94 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -18,12 +18,6 @@ - - org.slf4j - slf4j-api - 1.7.36 - - com.fasterxml.jackson.core jackson-databind @@ -40,6 +34,13 @@ + + io.avaje + avaje-slf4j-jpl + 1.0 + test + + io.avaje junit diff --git a/client/src/main/java/io/avaje/http/client/DHttpApi.java b/client/src/main/java/io/avaje/http/client/DHttpApi.java index 25376f4..86ba99e 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpApi.java +++ b/client/src/main/java/io/avaje/http/client/DHttpApi.java @@ -1,18 +1,17 @@ package io.avaje.http.client; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.HashMap; import java.util.Map; import java.util.ServiceLoader; +import static java.lang.System.Logger.Level.*; + /** * Service loads the HttpApiProvider for HttpApi. */ final class DHttpApi { - private static final Logger log = LoggerFactory.getLogger(DHttpApi.class); + private static final System.Logger log = System.getLogger("io.avaje.http.client"); private static final DHttpApi INSTANCE = new DHttpApi(); @@ -27,7 +26,7 @@ void init() { for (HttpApiProvider apiProvider : ServiceLoader.load(HttpApiProvider.class)) { addProvider(apiProvider); } - log.debug("providers for {}", providerMap.keySet()); + log.log(DEBUG, "providers for %s", providerMap.keySet()); } void addProvider(HttpApiProvider apiProvider) { diff --git a/client/src/main/java/io/avaje/http/client/RequestLogger.java b/client/src/main/java/io/avaje/http/client/RequestLogger.java index 48b8a53..b673b2d 100644 --- a/client/src/main/java/io/avaje/http/client/RequestLogger.java +++ b/client/src/main/java/io/avaje/http/client/RequestLogger.java @@ -1,8 +1,6 @@ package io.avaje.http.client; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - +import java.lang.System.Logger.Level; import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpResponse; @@ -11,23 +9,24 @@ import java.util.Set; /** - * Logs request and response details for debug logging purposes. + * Logs request and response details for debug logging purposes using System.Logger. + *

+ * This implementation logs the request and response with the same single logging entry + * rather than separate logging of the request and response. *

- * This implementation logs the request and response with the same - * single logging entry rather than separate logging of the request - * and response. + * With logging level set to {@code DEBUG} for {@code io.avaje.http.client.RequestLogger} the + * request and response are logged as a summary with response status and time. *

- * With logging level set to {@code DEBUG} for - * {@code io.avaje.http.client.RequestLogger} the request and response - * are logged as a summary with response status and time. + * Set the logging level to {@code TRACE} to include the request and response headers and body + * payloads with truncation for large bodies. *

- * Set the logging level to {@code TRACE} to include the request - * and response headers and body payloads with truncation for large - * bodies. + * Using System.Logger, messages by default go to JUL (Java Util Logging) unless a provider + * is registered. We can use io.avaje:avaje-slf4j-jpl to have System.Logger + * messages go to slf4j-api. */ public class RequestLogger implements RequestListener { - private static final Logger log = LoggerFactory.getLogger(RequestLogger.class); + private static final System.Logger log = System.getLogger("io.avaje.http.client.RequestLogger"); private final String delimiter; @@ -47,7 +46,7 @@ public RequestLogger(String delimiter) { @Override public void response(Event event) { - if (log.isDebugEnabled()) { + if (log.isLoggable(Level.DEBUG)) { final HttpResponse response = event.response(); final HttpRequest request = response.request(); long micros = event.responseTimeMicros(); @@ -58,13 +57,13 @@ public void response(Event event) { .append(" uri:").append(event.uri()) .append(" timeMicros:").append(micros); - if (log.isTraceEnabled()) { + if (log.isLoggable(Level.TRACE)) { headers(sb, "req-head: ", request.headers()); body(sb, "req-body: ", event.requestBody()); headers(sb, "res-head: ", response.headers()); body(sb, "res-body: ", event.responseBody()); } - log.debug(sb.toString()); + log.log(Level.DEBUG, sb.toString()); } } diff --git a/client/src/main/java/io/avaje/http/client/SimpleRetryHandler.java b/client/src/main/java/io/avaje/http/client/SimpleRetryHandler.java index 9089046..6ec75aa 100644 --- a/client/src/main/java/io/avaje/http/client/SimpleRetryHandler.java +++ b/client/src/main/java/io/avaje/http/client/SimpleRetryHandler.java @@ -1,8 +1,6 @@ package io.avaje.http.client; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - +import java.lang.System.Logger.Level; import java.net.http.HttpResponse; import java.util.Random; @@ -11,7 +9,7 @@ */ public class SimpleRetryHandler implements RetryHandler { - private static final Logger log = LoggerFactory.getLogger(SimpleRetryHandler.class); + private static final System.Logger log = System.getLogger("io.avaje.http.client"); private final int maxRetries; private final long backoffMillis; @@ -47,7 +45,9 @@ public boolean isRetry(int retryCount, HttpResponse response) { if (response.statusCode() < 500 || retryCount >= maxRetries) { return false; } - log.debug("retry count:{} status:{} uri:{}", retryCount, response.statusCode(), response.uri()); + if (log.isLoggable(Level.DEBUG)) { + log.log(Level.DEBUG, "retry count:%s status:%s uri:%s", retryCount, response.statusCode(), response.uri()); + } try { int gitter = gitterMillis < 1 ? 0 : random.nextInt(gitterMillis); Thread.sleep(backoffMillis + gitter); diff --git a/client/src/main/java/module-info.java b/client/src/main/java/module-info.java index a7b6fcb..dbd1559 100644 --- a/client/src/main/java/module-info.java +++ b/client/src/main/java/module-info.java @@ -3,7 +3,6 @@ uses io.avaje.http.client.HttpApiProvider; requires transitive java.net.http; - requires transitive org.slf4j; requires static com.fasterxml.jackson.databind; requires static com.fasterxml.jackson.annotation; requires static com.fasterxml.jackson.core; From c6359ddbeff67ac9dd81bd409aae863b9641adcf Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Fri, 25 Feb 2022 17:44:10 +1300 Subject: [PATCH 05/67] Update README --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 96e3d82..2c086cc 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ A lightweight wrapper to the [JDK 11+ Java Http Client](http://openjdk.java.net/ io.avaje avaje-http-client - 1.12 + 1.14 ``` @@ -37,7 +37,6 @@ Create a HttpClientContext with a baseUrl, Jackson or Gson based JSON public HttpClientContext client() { return HttpClientContext.newBuilder() .withBaseUrl(baseUrl) - .withRequestListener(new RequestLogger()) .withBodyAdapter(new JacksonBodyAdapter(new ObjectMapper())) // .withBodyAdapter(new GsonBodyAdapter(new Gson())) .build(); From 3b08b8c2529b56db39ea68e66135867a2237efa9 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Wed, 4 May 2022 13:05:29 +1200 Subject: [PATCH 06/67] Bump optional dependency avaje-jsonb to 0.15 --- client/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pom.xml b/client/pom.xml index 69eee94..d401f69 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -28,7 +28,7 @@ io.avaje avaje-jsonb - 0.12 + 0.15 true From 7e33fe55c49f5d50a6c5db9990008610a9a2ac46 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 5 May 2022 08:51:11 +1200 Subject: [PATCH 07/67] #46 - Change method .newBuilder() -> .builder() with deprecation --- README.md | 21 ++++++++++--------- .../client/DHttpClientContextBuilder.java | 2 +- .../avaje/http/client/HttpClientContext.java | 18 +++++++++++----- .../avaje/http/client/JacksonBodyAdapter.java | 2 +- .../avaje/http/client/JsonbBodyAdapter.java | 2 +- .../io/avaje/http/client/package-info.java | 2 +- .../io/avaje/http/client/AuthTokenTest.java | 2 +- .../io/avaje/http/client/BaseWebTest.java | 2 +- .../http/client/BasicAuthInterceptTest.java | 2 +- .../io/avaje/http/client/DHttpApiTest.java | 4 ++-- .../http/client/DHttpClientContextTest.java | 6 +++--- .../avaje/http/client/HelloBasicAuthTest.java | 2 +- .../http/client/RequestListenerTest.java | 3 +-- .../java/io/avaje/http/client/RetryTest.java | 2 +- .../java/org/example/github/GithubTest.java | 2 +- .../http/client/gson/GsonBodyAdapter.java | 10 ++++----- .../test/java/example/github/GithubTest.java | 2 +- 17 files changed, 46 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 2c086cc..97e8944 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,11 @@ Create a HttpClientContext with a baseUrl, Jackson or Gson based JSON ```java public HttpClientContext client() { - return HttpClientContext.newBuilder() - .withBaseUrl(baseUrl) - .withBodyAdapter(new JacksonBodyAdapter(new ObjectMapper())) -// .withBodyAdapter(new GsonBodyAdapter(new Gson())) + return HttpClientContext.builder() + .baseUrl(baseUrl) + .bodyAdapter(new JsonbBodyAdapter()) + //.bodyAdapter(new JacksonBodyAdapter(new ObjectMapper())) + //.bodyAdapter(new GsonBodyAdapter(new Gson())) .build(); } @@ -443,10 +444,10 @@ header ("Basic Auth"). ```java HttpClientContext clientContext = - HttpClientContext.newBuilder() - .withBaseUrl(baseUrl) + HttpClientContext.builder() + .baseUrl(baseUrl) ... - .withRequestIntercept(new BasicAuthIntercept("myUsername", "myPassword")) - - - io.avaje - avaje-inject-generator - 8.3 - test - - - - + + @@ -104,21 +100,43 @@ + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.1 + + + default-testCompile + + + + io.avaje + avaje-inject-generator + 8.6 + + + + + + + + maven-surefire-plugin - 3.0.0-M4 - - - --add-modules com.fasterxml.jackson.databind - --add-modules io.avaje.jsonb - --add-opens io.avaje.http.client/io.avaje.http.client=ALL-UNNAMED - --add-opens io.avaje.http.client/org.example.webserver=ALL-UNNAMED - --add-opens io.avaje.http.client/org.example.github=ALL-UNNAMED - --add-opens io.avaje.http.client/org.example.webserver=com.fasterxml.jackson.databind - --add-opens io.avaje.http.client/org.example.github=com.fasterxml.jackson.databind - --add-opens io.avaje.http.client/org.example.github=io.avaje.jsonb - - + 3.0.0-M6 + + + + + + + + + + + + + diff --git a/client/src/main/java/io/avaje/http/client/DHttpClientContextBuilder.java b/client/src/main/java/io/avaje/http/client/DHttpClientContextBuilder.java index 9db3bb0..75f068c 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientContextBuilder.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientContextBuilder.java @@ -1,5 +1,9 @@ package io.avaje.http.client; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.avaje.inject.BeanScope; +import io.avaje.jsonb.Jsonb; + import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; import java.net.Authenticator; @@ -10,11 +14,12 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.Executor; import static java.util.Objects.requireNonNull; -final class DHttpClientContextBuilder implements HttpClientContext.Builder { +final class DHttpClientContextBuilder implements HttpClientContext.Builder.State { private HttpClient client; private String baseUrl; @@ -148,6 +153,39 @@ public HttpClientContext.Builder priority(int priority) { return this; } + @Override + public State state() { + return this; + } + + @Override + public HttpClientContext.Builder configureWith(BeanScope beanScope) { + if (bodyAdapter == null) { + configureBodyAdapter(beanScope); + } + if (retryHandler == null) { + configureRetryHandler(beanScope); + } + return this; + } + + private void configureRetryHandler(BeanScope beanScope) { + beanScope.getOptional(RetryHandler.class) + .ifPresent(this::retryHandler); + } + + private void configureBodyAdapter(BeanScope beanScope) { + Optional body = beanScope.getOptional(BodyAdapter.class); + if (body.isPresent()) { + bodyAdapter = body.get(); + } else if (beanScope.contains("io.avaje.jsonb.Jsonb")) { + bodyAdapter = new JsonbBodyAdapter(beanScope.get(Jsonb.class)); + } else if (beanScope.contains("com.fasterxml.jackson.databind.ObjectMapper")) { + ObjectMapper objectMapper = beanScope.get(ObjectMapper.class); + bodyAdapter = new JacksonBodyAdapter(objectMapper); + } + } + @Override public HttpClientContext build() { requireNonNull(baseUrl, "baseUrl is not specified"); @@ -165,6 +203,36 @@ public HttpClientContext build() { return new DHttpClientContext(client, baseUrl, requestTimeout, bodyAdapter, retryHandler, buildListener(), authTokenProvider, buildIntercept()); } + @Override + public String baseUrl() { + return baseUrl; + } + + @Override + public BodyAdapter bodyAdapter() { + return bodyAdapter; + } + + @Override + public HttpClient client() { + return client; + } + + @Override + public boolean requestLogging() { + return requestLogging; + } + + @Override + public Duration requestTimeout() { + return requestTimeout; + } + + @Override + public RetryHandler retryHandler() { + return retryHandler; + } + private RequestListener buildListener() { if (listeners.isEmpty()) { return null; diff --git a/client/src/main/java/io/avaje/http/client/HttpClientContext.java b/client/src/main/java/io/avaje/http/client/HttpClientContext.java index ed7e8e4..3fc7b73 100644 --- a/client/src/main/java/io/avaje/http/client/HttpClientContext.java +++ b/client/src/main/java/io/avaje/http/client/HttpClientContext.java @@ -1,5 +1,7 @@ package io.avaje.http.client; +import io.avaje.inject.BeanScope; + import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; import java.net.Authenticator; @@ -311,6 +313,16 @@ interface Builder { */ Builder priority(int priority); + /** + * Configure BodyAdapter and RetryHandler using dependency injection BeanScope. + */ + Builder configureWith(BeanScope beanScope); + + /** + * Return the state of the builder. + */ + State state(); + /** * Build and return the context. * @@ -330,6 +342,42 @@ interface Builder { * } */ HttpClientContext build(); + + /** + * The state of the builder with methods to read the set state. + */ + interface State extends Builder { + + /** + * Return the base URL. + */ + String baseUrl(); + + /** + * Return the body adapter. + */ + BodyAdapter bodyAdapter(); + + /** + * Return the HttpClient. + */ + HttpClient client(); + + /** + * Return true if requestLogging is on. + */ + boolean requestLogging(); + + /** + * Return the request timeout. + */ + Duration requestTimeout(); + + /** + * Return the retry handler. + */ + RetryHandler retryHandler(); + } } /** diff --git a/client/src/main/java/module-info.java b/client/src/main/java/module-info.java index dbd1559..3f0117a 100644 --- a/client/src/main/java/module-info.java +++ b/client/src/main/java/module-info.java @@ -7,6 +7,7 @@ requires static com.fasterxml.jackson.annotation; requires static com.fasterxml.jackson.core; requires static io.avaje.jsonb; + requires static io.avaje.inject; exports io.avaje.http.client; } diff --git a/client/src/test/java/org/example/dinject/ConfigureWithDITest.java b/client/src/test/java/org/example/dinject/ConfigureWithDITest.java new file mode 100644 index 0000000..74f1496 --- /dev/null +++ b/client/src/test/java/org/example/dinject/ConfigureWithDITest.java @@ -0,0 +1,35 @@ +package org.example.dinject; + +import io.avaje.http.client.BodyAdapter; +import io.avaje.http.client.HttpClientContext; +import io.avaje.http.client.JsonbBodyAdapter; +import io.avaje.inject.BeanScope; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; + +class ConfigureWithDITest { + + @Test + void configureWith() { + try (BeanScope beanScope = BeanScope.builder().build()) { + + assertThat(beanScope.contains("io.avaje.jsonb.Jsonb")).isTrue(); + + HttpClientContext.Builder builder = HttpClientContext.builder(); + HttpClientContext.Builder.State state = builder.state(); + assertThat(state.baseUrl()).isNull(); + assertThat(state.bodyAdapter()).isNull(); + assertThat(state.client()).isNull(); + assertThat(state.requestLogging()).isTrue(); + assertThat(state.requestTimeout()).isEqualByComparingTo(Duration.ofSeconds(20)); + assertThat(state.retryHandler()).isNull(); + + builder.configureWith(beanScope); + BodyAdapter bodyAdapter = state.bodyAdapter(); + assertThat(bodyAdapter).isInstanceOf(JsonbBodyAdapter.class); + } + } +} diff --git a/client/src/test/java/org/example/dinject/MyDIConfig.java b/client/src/test/java/org/example/dinject/MyDIConfig.java new file mode 100644 index 0000000..35d27dc --- /dev/null +++ b/client/src/test/java/org/example/dinject/MyDIConfig.java @@ -0,0 +1,15 @@ +package org.example.dinject; + +import io.avaje.inject.Bean; +import io.avaje.inject.Factory; +import io.avaje.jsonb.Jsonb; + +@Factory +class MyDIConfig { + + @Bean + Jsonb jsonb() { + return Jsonb.builder().build(); + } + +} From b857fd269411bcfee74be8016541df3c539f3dbf Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 9 Jun 2022 22:13:13 +1200 Subject: [PATCH 26/67] [maven-release-plugin] prepare release avaje-http-client-1.18 --- client/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index 901501e..e79c82f 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -10,11 +10,11 @@ io.avaje avaje-http-client - 1.18-SNAPSHOT + 1.18 scm:git:git@github.com:avaje/avaje-http-client.git - HEAD + avaje-http-client-1.18 From 300bb42186e89be01fca0ab4a3eef5f3c731883b Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 9 Jun 2022 22:13:19 +1200 Subject: [PATCH 27/67] [maven-release-plugin] prepare for next development iteration --- client/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index e79c82f..a747183 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -10,11 +10,11 @@ io.avaje avaje-http-client - 1.18 + 1.19-SNAPSHOT scm:git:git@github.com:avaje/avaje-http-client.git - avaje-http-client-1.18 + HEAD From 9f1f86b164fe9f5bce8978408bc0240c782e833c Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 9 Jun 2022 22:18:33 +1200 Subject: [PATCH 28/67] Bump to 19-SNAPSHOT after release --- gson-adapter/pom.xml | 4 ++-- test/pom.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gson-adapter/pom.xml b/gson-adapter/pom.xml index 0fc1ae7..fe14a1a 100644 --- a/gson-adapter/pom.xml +++ b/gson-adapter/pom.xml @@ -10,7 +10,7 @@ io.avaje avaje-http-client-gson - 1.18-SNAPSHOT + 1.19-SNAPSHOT scm:git:git@github.com:avaje/avaje-http-client.git @@ -28,7 +28,7 @@ io.avaje avaje-http-client - 1.18-SNAPSHOT + 1.19-SNAPSHOT provided diff --git a/test/pom.xml b/test/pom.xml index 02a6d0f..2f40210 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -16,13 +16,13 @@ io.avaje avaje-http-client - 1.18-SNAPSHOT + 1.19-SNAPSHOT io.avaje avaje-http-client-gson - 1.18-SNAPSHOT + 1.19-SNAPSHOT From eb4312ec196e4bbe1ca6f1584b4367fe460aa379 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 26 Jul 2022 16:18:57 +1200 Subject: [PATCH 29/67] #52 - Change to use AppLog.getLogger() rather than directly using System.getLogger() --- client/pom.xml | 17 +++++++++++++++-- .../java/io/avaje/http/client/DHttpApi.java | 4 +++- .../io/avaje/http/client/RequestLogger.java | 4 +++- .../avaje/http/client/SimpleRetryHandler.java | 4 +++- client/src/main/java/module-info.java | 1 + 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index a747183..bb36e54 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -23,6 +23,12 @@ + + io.avaje + avaje-applog + 1.0 + + com.fasterxml.jackson.core jackson-databind @@ -46,10 +52,17 @@ + + + + + + + io.avaje - avaje-slf4j-jpl - 1.1 + avaje-applog-slf4j + 1.0 test diff --git a/client/src/main/java/io/avaje/http/client/DHttpApi.java b/client/src/main/java/io/avaje/http/client/DHttpApi.java index 783563e..643f484 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpApi.java +++ b/client/src/main/java/io/avaje/http/client/DHttpApi.java @@ -1,5 +1,7 @@ package io.avaje.http.client; +import io.avaje.applog.AppLog; + import java.util.HashMap; import java.util.Map; import java.util.ServiceLoader; @@ -11,7 +13,7 @@ */ final class DHttpApi { - private static final System.Logger log = System.getLogger("io.avaje.http.client"); + private static final System.Logger log = AppLog.getLogger("io.avaje.http.client"); private static final DHttpApi INSTANCE = new DHttpApi(); diff --git a/client/src/main/java/io/avaje/http/client/RequestLogger.java b/client/src/main/java/io/avaje/http/client/RequestLogger.java index b673b2d..391d04b 100644 --- a/client/src/main/java/io/avaje/http/client/RequestLogger.java +++ b/client/src/main/java/io/avaje/http/client/RequestLogger.java @@ -1,5 +1,7 @@ package io.avaje.http.client; +import io.avaje.applog.AppLog; + import java.lang.System.Logger.Level; import java.net.http.HttpHeaders; import java.net.http.HttpRequest; @@ -26,7 +28,7 @@ */ public class RequestLogger implements RequestListener { - private static final System.Logger log = System.getLogger("io.avaje.http.client.RequestLogger"); + private static final System.Logger log = AppLog.getLogger("io.avaje.http.client.RequestLogger"); private final String delimiter; diff --git a/client/src/main/java/io/avaje/http/client/SimpleRetryHandler.java b/client/src/main/java/io/avaje/http/client/SimpleRetryHandler.java index 0eb6821..a7575e3 100644 --- a/client/src/main/java/io/avaje/http/client/SimpleRetryHandler.java +++ b/client/src/main/java/io/avaje/http/client/SimpleRetryHandler.java @@ -1,5 +1,7 @@ package io.avaje.http.client; +import io.avaje.applog.AppLog; + import java.lang.System.Logger.Level; import java.net.http.HttpResponse; import java.util.Random; @@ -9,7 +11,7 @@ */ public class SimpleRetryHandler implements RetryHandler { - private static final System.Logger log = System.getLogger("io.avaje.http.client"); + private static final System.Logger log = AppLog.getLogger("io.avaje.http.client"); private final int maxRetries; private final long backoffMillis; diff --git a/client/src/main/java/module-info.java b/client/src/main/java/module-info.java index 3f0117a..56ad106 100644 --- a/client/src/main/java/module-info.java +++ b/client/src/main/java/module-info.java @@ -3,6 +3,7 @@ uses io.avaje.http.client.HttpApiProvider; requires transitive java.net.http; + requires transitive io.avaje.applog; requires static com.fasterxml.jackson.databind; requires static com.fasterxml.jackson.annotation; requires static com.fasterxml.jackson.core; From 1a2f76bb652c44dc89d9b532c1a4b82d0251abb6 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 26 Jul 2022 16:20:57 +1200 Subject: [PATCH 30/67] Bump parent pom to exclude maven descriptor from final artifact --- client/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pom.xml b/client/pom.xml index bb36e54..4d4d1e1 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -4,7 +4,7 @@ org.avaje java11-oss - 3.8 + 3.9 From fffc78d54424ee51ca694e88a8e1e7fcab977965 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 26 Jul 2022 16:21:22 +1200 Subject: [PATCH 31/67] [maven-release-plugin] prepare release avaje-http-client-1.19 --- client/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index 4d4d1e1..39ef321 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -10,11 +10,11 @@ io.avaje avaje-http-client - 1.19-SNAPSHOT + 1.19 scm:git:git@github.com:avaje/avaje-http-client.git - HEAD + avaje-http-client-1.19 From 7efeea6b0e12ab09c795970bb91b54edc22a7f03 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 26 Jul 2022 16:21:28 +1200 Subject: [PATCH 32/67] [maven-release-plugin] prepare for next development iteration --- client/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index 39ef321..b29cd98 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -10,11 +10,11 @@ io.avaje avaje-http-client - 1.19 + 1.20-SNAPSHOT scm:git:git@github.com:avaje/avaje-http-client.git - avaje-http-client-1.19 + HEAD From 3fdce8354abce376b052aff7064ca49531be5cfb Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 26 Jul 2022 16:23:21 +1200 Subject: [PATCH 33/67] Prepare gson release --- gson-adapter/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gson-adapter/pom.xml b/gson-adapter/pom.xml index fe14a1a..a7e56cc 100644 --- a/gson-adapter/pom.xml +++ b/gson-adapter/pom.xml @@ -4,7 +4,7 @@ org.avaje java11-oss - 3.8 + 3.9 @@ -28,7 +28,7 @@ io.avaje avaje-http-client - 1.19-SNAPSHOT + 1.19 provided From b53834336542caa8a4a87c83d00f4b2cd7809e61 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 26 Jul 2022 16:23:54 +1200 Subject: [PATCH 34/67] [maven-release-plugin] prepare release avaje-http-client-gson-1.19 --- gson-adapter/pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gson-adapter/pom.xml b/gson-adapter/pom.xml index a7e56cc..baca8d7 100644 --- a/gson-adapter/pom.xml +++ b/gson-adapter/pom.xml @@ -5,16 +5,16 @@ org.avaje java11-oss 3.9 - + io.avaje avaje-http-client-gson - 1.19-SNAPSHOT + 1.19 scm:git:git@github.com:avaje/avaje-http-client.git - HEAD + avaje-http-client-gson-1.19 From f205d5f2a7ff7b9832dc7261599ebf78cb74223d Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 26 Jul 2022 16:24:00 +1200 Subject: [PATCH 35/67] [maven-release-plugin] prepare for next development iteration --- gson-adapter/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gson-adapter/pom.xml b/gson-adapter/pom.xml index baca8d7..990a67b 100644 --- a/gson-adapter/pom.xml +++ b/gson-adapter/pom.xml @@ -10,11 +10,11 @@ io.avaje avaje-http-client-gson - 1.19 + 1.20-SNAPSHOT scm:git:git@github.com:avaje/avaje-http-client.git - avaje-http-client-gson-1.19 + HEAD From b95d48c415a0f9a1ba2d76a6521ac67356129963 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 26 Jul 2022 16:44:06 +1200 Subject: [PATCH 36/67] Post release bump versions --- gson-adapter/pom.xml | 2 +- test/pom.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gson-adapter/pom.xml b/gson-adapter/pom.xml index 990a67b..0f7f591 100644 --- a/gson-adapter/pom.xml +++ b/gson-adapter/pom.xml @@ -28,7 +28,7 @@ io.avaje avaje-http-client - 1.19 + 1.20-SNAPSHOT provided diff --git a/test/pom.xml b/test/pom.xml index 2f40210..fa21a8b 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -16,13 +16,13 @@ io.avaje avaje-http-client - 1.19-SNAPSHOT + 1.20-SNAPSHOT io.avaje avaje-http-client-gson - 1.19-SNAPSHOT + 1.20-SNAPSHOT From 7ecf3276b82da8b4bd1a1f8f2d429d103b066076 Mon Sep 17 00:00:00 2001 From: Josiah Noel <32279667+SentryMan@users.noreply.github.com> Date: Tue, 25 Oct 2022 21:45:45 -0400 Subject: [PATCH 37/67] add retry exception handler --- .../client/DHttpClientRequestWithRetry.java | 32 +++++++++++++++---- .../io/avaje/http/client/RetryHandler.java | 12 ++++++- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/client/src/main/java/io/avaje/http/client/DHttpClientRequestWithRetry.java b/client/src/main/java/io/avaje/http/client/DHttpClientRequestWithRetry.java index 6d5d266..eb7cdbf 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientRequestWithRetry.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientRequestWithRetry.java @@ -22,14 +22,34 @@ final class DHttpClientRequestWithRetry extends DHttpClientRequest { @Override protected HttpResponse performSend(HttpResponse.BodyHandler responseHandler) { HttpResponse res; - res = super.performSend(responseHandler); - if (res.statusCode() < 300) { - return res; - } - while (retryHandler.isRetry(retryCount++, res)) { - res = super.performSend(responseHandler); + HttpException ex; + + do { + try { + res = super.performSend(responseHandler); + ex = null; + } catch (final HttpException e) { + ex = e; + res = null; + } + if (res != null && res.statusCode() < 300) { + return res; + } + retryCount++; + } while (retry(res, ex)); + + if (res == null && ex != null) { + throw ex; } + return res; } + protected boolean retry(HttpResponse res, HttpException ex) { + + if (res != null) { + return retryHandler.isRetry(retryCount, res); + } + return retryHandler.isExceptionRetry(retryCount, ex); + } } diff --git a/client/src/main/java/io/avaje/http/client/RetryHandler.java b/client/src/main/java/io/avaje/http/client/RetryHandler.java index 67ed83d..e9d16ba 100644 --- a/client/src/main/java/io/avaje/http/client/RetryHandler.java +++ b/client/src/main/java/io/avaje/http/client/RetryHandler.java @@ -11,9 +11,19 @@ public interface RetryHandler { * Return true if the request should be retried. * * @param retryCount The number of retry attempts already executed - * @param response The HTTP response + * @param response The HTTP response * @return True if the request should be retried or false if not */ boolean isRetry(int retryCount, HttpResponse response); + /** + * Return true if the request should be retried. + * + * @param retryCount The number of retry attempts already executed + * @param exception The Wrapped Error thrown by the underlying Http Client + * @return True if the request should be retried or false if not + */ + default boolean isExceptionRetry(int retryCount, HttpException exception) { + throw exception; + } } From 3f9a3abb93ed0822d2fac8f2c9315c5c0eb76249 Mon Sep 17 00:00:00 2001 From: Josiah Noel <32279667+SentryMan@users.noreply.github.com> Date: Wed, 26 Oct 2022 18:50:48 -0400 Subject: [PATCH 38/67] now httpclient exception doesn't thro nullpointer on missing body --- .../io/avaje/http/client/HttpException.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 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 fe0fbdb..0c83020 100644 --- a/client/src/main/java/io/avaje/http/client/HttpException.java +++ b/client/src/main/java/io/avaje/http/client/HttpException.java @@ -91,28 +91,37 @@ public HttpException(int statusCode, Throwable cause) { } /** - * Return the response body content as a bean + * Return the response body content as a bean, or else null if body content doesn't exist. * * @param cls The type of bean to convert the response to * @return The response as a bean */ public T bean(Class cls) { + if (httpResponse == null) { + return null; + } final BodyContent body = context.readErrorContent(responseAsBytes, httpResponse); return context.readBean(cls, body); } /** - * Return the response body content as a UTF8 string. + * Return the response body content as a UTF8 string, or else null if body content doesn't exist. */ public String bodyAsString() { + if (httpResponse == null) { + return null; + } final BodyContent body = context.readErrorContent(responseAsBytes, httpResponse); return new String(body.content(), StandardCharsets.UTF_8); } - /** - * Return the response body content as raw bytes. + /** + * Return the response body content as raw bytes, or else null if body content doesn't exist. */ public byte[] bodyAsBytes() { + if (httpResponse == null) { + return null; + } final BodyContent body = context.readErrorContent(responseAsBytes, httpResponse); return body.content(); } From 65560c3025ddc328f22a89ad385031a670037d72 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Mon, 31 Oct 2022 08:35:25 +1300 Subject: [PATCH 39/67] [maven-release-plugin] prepare release avaje-http-client-1.20 --- client/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index b29cd98..f68464e 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -10,11 +10,11 @@ io.avaje avaje-http-client - 1.20-SNAPSHOT + 1.20 scm:git:git@github.com:avaje/avaje-http-client.git - HEAD + avaje-http-client-1.20 From 3ddff14b296ebd1d8c1009fe84ae51a67508b780 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Mon, 31 Oct 2022 08:35:31 +1300 Subject: [PATCH 40/67] [maven-release-plugin] prepare for next development iteration --- client/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index f68464e..bf5c54f 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -10,11 +10,11 @@ io.avaje avaje-http-client - 1.20 + 1.21-SNAPSHOT scm:git:git@github.com:avaje/avaje-http-client.git - avaje-http-client-1.20 + HEAD From e6376c5d9f7b1b0f6ccfc38ef9627ded58cc8bac Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Mon, 31 Oct 2022 08:37:57 +1300 Subject: [PATCH 41/67] release gson --- gson-adapter/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson-adapter/pom.xml b/gson-adapter/pom.xml index 0f7f591..7e58771 100644 --- a/gson-adapter/pom.xml +++ b/gson-adapter/pom.xml @@ -28,7 +28,7 @@ io.avaje avaje-http-client - 1.20-SNAPSHOT + 1.20 provided From d23abe80ec5a132ee025b1f284d0b2138f26f537 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Mon, 31 Oct 2022 08:38:28 +1300 Subject: [PATCH 42/67] [maven-release-plugin] prepare release avaje-http-client-gson-1.20 --- gson-adapter/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gson-adapter/pom.xml b/gson-adapter/pom.xml index 7e58771..ae44d3e 100644 --- a/gson-adapter/pom.xml +++ b/gson-adapter/pom.xml @@ -10,11 +10,11 @@ io.avaje avaje-http-client-gson - 1.20-SNAPSHOT + 1.20 scm:git:git@github.com:avaje/avaje-http-client.git - HEAD + avaje-http-client-gson-1.20 From 8eb29b0b5f1c64c981aebab354b6b784e6efa0be Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Mon, 31 Oct 2022 08:38:34 +1300 Subject: [PATCH 43/67] [maven-release-plugin] prepare for next development iteration --- gson-adapter/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gson-adapter/pom.xml b/gson-adapter/pom.xml index ae44d3e..87ec415 100644 --- a/gson-adapter/pom.xml +++ b/gson-adapter/pom.xml @@ -10,11 +10,11 @@ io.avaje avaje-http-client-gson - 1.20 + 1.21-SNAPSHOT scm:git:git@github.com:avaje/avaje-http-client.git - avaje-http-client-gson-1.20 + HEAD From 8c0527fbe50d24040a4ca054c200f96e94b6523f Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Mon, 31 Oct 2022 08:41:05 +1300 Subject: [PATCH 44/67] Bump SNAPSHOT after release --- gson-adapter/pom.xml | 2 +- test/pom.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gson-adapter/pom.xml b/gson-adapter/pom.xml index 87ec415..b33824e 100644 --- a/gson-adapter/pom.xml +++ b/gson-adapter/pom.xml @@ -28,7 +28,7 @@ io.avaje avaje-http-client - 1.20 + 1.21-SNAPSHOT provided diff --git a/test/pom.xml b/test/pom.xml index fa21a8b..3b80a17 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -16,13 +16,13 @@ io.avaje avaje-http-client - 1.20-SNAPSHOT + 1.21-SNAPSHOT io.avaje avaje-http-client-gson - 1.20-SNAPSHOT + 1.21-SNAPSHOT From 1568b3a11ede6fa09cf36c5867c0a8393f717f21 Mon Sep 17 00:00:00 2001 From: Josiah Noel <32279667+SentryMan@users.noreply.github.com> Date: Sun, 30 Oct 2022 15:28:25 -0500 Subject: [PATCH 45/67] add Retry example to README --- README.md | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1b4d6e4..f87cec7 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ A lightweight wrapper to the [JDK 11+ Java Http Client](http://openjdk.java.net/ io.avaje avaje-http-client - 1.17 + ${avaje.client.version} ``` @@ -63,7 +63,7 @@ From HttpClientContext: ## Limitations: -- NO support for POSTing multipart-form currently +- No support for POSTing multipart-form currently - Retry (when specified) does not apply to `async` response processing` @@ -299,7 +299,39 @@ HttpResponse res = clientContext.request() assertThat(res.statusCode()).isEqualTo(201); ``` +## Retry (Sync Requests Only) +To add Retry funtionality, use `.retryHandler(yourhandler)` on the builder to provide your retry handler. The `RetryHandler` interface provides two methods, one for status exceptions (e.g. you get a 4xx/5xx from the server) and another for exceptions thrown by the underlying client (e.g. server times out or client couldn't send request). Here is example implementation of `RetryHandler`. +``` +public final class ExampleRetry implements RetryHandler { + private static final int MAX_RETRIES = 2; + @Override + public boolean isRetry(int retryCount, HttpResponse response) { + + final var code = response.statusCode(); + + if (retryCount >= MAX_RETRIES || code >= 400) { + + return false; + } + + return true; + } + + @Override + public boolean isExceptionRetry(int retryCount, HttpException response) { + //unwrap the exception + final var cause = response.getCause(); + if (retryCount >= MAX_RETRIES) { + return false; + } + if (cause instanceof ConnectException) { + return true; + } + + return false; + } +``` ## Async processing From d9d87df2218a9e0c110bda4e0b686557ccb699a4 Mon Sep 17 00:00:00 2001 From: Josiah Noel <32279667+SentryMan@users.noreply.github.com> Date: Wed, 28 Dec 2022 15:45:07 -0500 Subject: [PATCH 46/67] add support for Jsonb generics --- client/pom.xml | 10 ++-- .../io/avaje/http/client/BodyAdapter.java | 19 ++++++++ .../avaje/http/client/DHttpClientContext.java | 14 ++++++ .../avaje/http/client/DHttpClientRequest.java | 28 ++++++++++- .../avaje/http/client/HttpClientResponse.java | 47 +++++++++++++++++++ .../avaje/http/client/JsonbBodyAdapter.java | 30 ++++++++---- .../http/client/HelloControllerTest.java | 2 +- 7 files changed, 135 insertions(+), 15 deletions(-) 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/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..5fc3e6c 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); 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);

From 27f9c81a618707726483a5cb319557e2623d8749 Mon Sep 17 00:00:00 2001
From: Josiah Noel <32279667+SentryMan@users.noreply.github.com>
Date: Wed, 28 Dec 2022 19:53:09 -0500
Subject: [PATCH 47/67] Update BodyAdapter.java

---
 client/src/main/java/io/avaje/http/client/BodyAdapter.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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 012e503..33e82f8 100644
--- a/client/src/main/java/io/avaje/http/client/BodyAdapter.java
+++ b/client/src/main/java/io/avaje/http/client/BodyAdapter.java
@@ -47,6 +47,6 @@ default  BodyReader beanReader(ParameterizedType type) {
    * @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");
+    throw new UnsupportedOperationException("Parameterized types not supported for this Body Adapter");
   }
 }

From eaf229559dbfd6c37920e9ec7be2eca39ff941d8 Mon Sep 17 00:00:00 2001
From: Josiah Noel <32279667+SentryMan@users.noreply.github.com>
Date: Wed, 28 Dec 2022 20:01:57 -0500
Subject: [PATCH 48/67] Revert "Update BodyAdapter.java"

This reverts commit 27f9c81a618707726483a5cb319557e2623d8749.
---
 client/src/main/java/io/avaje/http/client/BodyAdapter.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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 33e82f8..012e503 100644
--- a/client/src/main/java/io/avaje/http/client/BodyAdapter.java
+++ b/client/src/main/java/io/avaje/http/client/BodyAdapter.java
@@ -47,6 +47,6 @@ default  BodyReader beanReader(ParameterizedType type) {
    * @param type The bean type to convert the content to.
    */
   default  BodyReader> listReader(ParameterizedType type) {
-    throw new UnsupportedOperationException("Parameterized types not supported for this Body Adapter");
+    throw new UnsupportedOperationException("Parameterized types not supported for this adapter");
   }
 }

From 7c283af8195801392148c86835f2fc8b5c8b71e0 Mon Sep 17 00:00:00 2001
From: Josiah Noel <32279667+SentryMan@users.noreply.github.com>
Date: Wed, 28 Dec 2022 21:52:15 -0500
Subject: [PATCH 49/67] heh, forgot about the async and httpcall

---
 .../java/io/avaje/http/client/DHttpAsync.java | 22 ++++++
 .../java/io/avaje/http/client/DHttpCall.java  | 67 +++++++++++++++++--
 .../avaje/http/client/DHttpClientRequest.java | 22 ++++++
 .../avaje/http/client/HttpAsyncResponse.java  | 26 +++++++
 .../avaje/http/client/HttpCallResponse.java   | 25 +++++++
 5 files changed, 156 insertions(+), 6 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 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/DHttpClientRequest.java b/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java
index 5fc3e6c..22d885f 100644
--- a/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java
+++ b/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java
@@ -532,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);
+
 }

From 61619e55eb14de11611b8358b8bb2ac00bdf57e0 Mon Sep 17 00:00:00 2001
From: Josiah Noel <32279667+SentryMan@users.noreply.github.com>
Date: Sat, 31 Dec 2022 13:42:43 -0600
Subject: [PATCH 50/67] Update README.md

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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;
     }

From e3457935530b66b0ed1d432b55c66b0928a6797f Mon Sep 17 00:00:00 2001
From: Rob Bygrave 
Date: Tue, 3 Jan 2023 10:17:59 +1300
Subject: [PATCH 51/67] No effective change, format only

---
 .../main/java/io/avaje/http/client/DHttpClientRequest.java | 7 +++----
 1 file changed, 3 insertions(+), 4 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 22d885f..fd69887 100644
--- a/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java
+++ b/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java
@@ -434,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());
@@ -459,7 +459,7 @@ public  List list(ParameterizedType cls) {
     return context.readList(cls, encodedResponseBody);
   }
 
-  
+
   @Override
   public  Stream stream(ParameterizedType cls) {
     final HttpResponse> res = handler(HttpResponse.BodyHandlers.ofLines());
@@ -542,8 +542,7 @@ protected  List asyncList(ParameterizedType type, HttpResponse res
     return context.readList(type, encodedResponseBody);
   }
 
-  protected  Stream asyncStream(
-      ParameterizedType type, HttpResponse> response) {
+  protected  Stream asyncStream(ParameterizedType type, HttpResponse> response) {
     responseTimeNanos = System.nanoTime() - startAsyncNanos;
     httpResponse = response;
     context.afterResponse(this);

From 01540837f0877353d5a3577248a4b3edce0dc629 Mon Sep 17 00:00:00 2001
From: Rob Bygrave 
Date: Tue, 3 Jan 2023 10:22:42 +1300
Subject: [PATCH 52/67] Bump test dependencies

---
 client/pom.xml       | 4 ++--
 gson-adapter/pom.xml | 2 +-
 test/pom.xml         | 4 ++--
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/client/pom.xml b/client/pom.xml
index cb42865..4c96a3b 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -32,14 +32,14 @@
     
       com.fasterxml.jackson.core
       jackson-databind
-      2.13.3
+      2.14.1
       true
     
 
     
       io.avaje
       avaje-jsonb
-      1.1-RC2
+      1.1-RC3
       true
     
 
diff --git a/gson-adapter/pom.xml b/gson-adapter/pom.xml
index b33824e..4bb9f33 100644
--- a/gson-adapter/pom.xml
+++ b/gson-adapter/pom.xml
@@ -22,7 +22,7 @@
     
       com.google.code.gson
       gson
-      2.9.0
+      2.10
     
 
     
diff --git a/test/pom.xml b/test/pom.xml
index 3b80a17..b5cfc94 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -28,13 +28,13 @@
     
       io.avaje
       avaje-http-api
-      1.16
+      1.20
     
 
     
       com.fasterxml.jackson.core
       jackson-databind
-      2.13.3
+      2.14.1
     
 
     

From 38c8021e7f209663cc9efc98bad0fbc0272530b3 Mon Sep 17 00:00:00 2001
From: Rob Bygrave 
Date: Fri, 6 Jan 2023 11:43:13 +1300
Subject: [PATCH 53/67] [maven-release-plugin] prepare release
 avaje-http-client-1.21

---
 client/pom.xml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/client/pom.xml b/client/pom.xml
index 4c96a3b..3d7b2ed 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -10,11 +10,11 @@
 
   io.avaje
   avaje-http-client
-  1.21-SNAPSHOT
+  1.21
 
   
     scm:git:git@github.com:avaje/avaje-http-client.git
-    HEAD
+    avaje-http-client-1.21
   
 
   

From c3d93a185d36433c4657539ac5081bbc6a032f53 Mon Sep 17 00:00:00 2001
From: Rob Bygrave 
Date: Fri, 6 Jan 2023 11:43:21 +1300
Subject: [PATCH 54/67] [maven-release-plugin] prepare for next development
 iteration

---
 client/pom.xml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/client/pom.xml b/client/pom.xml
index 3d7b2ed..70bdae8 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -10,11 +10,11 @@
 
   io.avaje
   avaje-http-client
-  1.21
+  1.22-SNAPSHOT
 
   
     scm:git:git@github.com:avaje/avaje-http-client.git
-    avaje-http-client-1.21
+    HEAD
   
 
   

From 2c8f1cb484bf5bd3101e1b8f4c9856cb47b480bf Mon Sep 17 00:00:00 2001
From: Rob Bygrave 
Date: Fri, 6 Jan 2023 11:49:04 +1300
Subject: [PATCH 55/67] Bump versions after release

---
 gson-adapter/pom.xml | 4 ++--
 test/pom.xml         | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/gson-adapter/pom.xml b/gson-adapter/pom.xml
index 4bb9f33..9bd29fc 100644
--- a/gson-adapter/pom.xml
+++ b/gson-adapter/pom.xml
@@ -10,7 +10,7 @@
 
   io.avaje
   avaje-http-client-gson
-  1.21-SNAPSHOT
+  1.22-SNAPSHOT
 
   
     scm:git:git@github.com:avaje/avaje-http-client.git
@@ -28,7 +28,7 @@
     
       io.avaje
       avaje-http-client
-      1.21-SNAPSHOT
+      1.22-SNAPSHOT
       provided
     
 
diff --git a/test/pom.xml b/test/pom.xml
index b5cfc94..a6d110a 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -16,13 +16,13 @@
     
       io.avaje
       avaje-http-client
-      1.21-SNAPSHOT
+      1.22-SNAPSHOT
     
 
     
       io.avaje
       avaje-http-client-gson
-      1.21-SNAPSHOT
+      1.22-SNAPSHOT
     
 
     

From dfbcb620cc6b3e38a0e944bcc5fd2ce20da16545 Mon Sep 17 00:00:00 2001
From: Rob Bygrave 
Date: Mon, 16 Jan 2023 22:18:54 +1300
Subject: [PATCH 56/67] Introduce HttpClient.Metrics and
 HttpClient.Builder.State

---
 .../client/DHttpClientContextBuilder.java     |   2 +-
 .../java/io/avaje/http/client/HttpClient.java | 100 ++++++++++++++++++
 .../avaje/http/client/HttpClientContext.java  |  64 +----------
 3 files changed, 104 insertions(+), 62 deletions(-)
 create mode 100644 client/src/main/java/io/avaje/http/client/HttpClient.java

diff --git a/client/src/main/java/io/avaje/http/client/DHttpClientContextBuilder.java b/client/src/main/java/io/avaje/http/client/DHttpClientContextBuilder.java
index 75f068c..2e1bb15 100644
--- a/client/src/main/java/io/avaje/http/client/DHttpClientContextBuilder.java
+++ b/client/src/main/java/io/avaje/http/client/DHttpClientContextBuilder.java
@@ -19,7 +19,7 @@
 
 import static java.util.Objects.requireNonNull;
 
-final class DHttpClientContextBuilder implements HttpClientContext.Builder.State {
+final class DHttpClientContextBuilder implements HttpClientContext.Builder, HttpClientContext.Builder.State {
 
   private HttpClient client;
   private String baseUrl;
diff --git a/client/src/main/java/io/avaje/http/client/HttpClient.java b/client/src/main/java/io/avaje/http/client/HttpClient.java
new file mode 100644
index 0000000..643a43c
--- /dev/null
+++ b/client/src/main/java/io/avaje/http/client/HttpClient.java
@@ -0,0 +1,100 @@
+package io.avaje.http.client;
+
+import java.time.Duration;
+
+/**
+ * The HTTP client context that we use to build and process requests.
+ *
+ * 
{@code
+ *
+ *   HttpClient client = HttpClient.builder()
+ *       .baseUrl("http://localhost:8080")
+ *       .bodyAdapter(new JacksonBodyAdapter())
+ *       .build();
+ *
+ *  HelloDto dto = client.request()
+ *       .path("hello")
+ *       .queryParam("name", "Rob")
+ *       .queryParam("say", "Whats up")
+ *       .GET()
+ *       .bean(HelloDto.class);
+ *
+ * }
+ */ +public interface HttpClient { + + interface Builder { + + /** + * The state of the builder with methods to read the set state. + */ + interface State { + + /** + * Return the base URL. + */ + String baseUrl(); + + /** + * Return the body adapter. + */ + BodyAdapter bodyAdapter(); + + /** + * Return the HttpClient. + */ + java.net.http.HttpClient client(); + + /** + * Return true if requestLogging is on. + */ + boolean requestLogging(); + + /** + * Return the request timeout. + */ + Duration requestTimeout(); + + /** + * Return the retry handler. + */ + RetryHandler retryHandler(); + } + } + + /** + * Statistic metrics collected to provide an overview of activity of this client. + */ + interface Metrics { + + /** + * Return the total number of responses. + */ + long totalCount(); + + /** + * Return the total number of error responses (status code >= 300). + */ + long errorCount(); + + /** + * Return the total response bytes (excludes streaming responses). + */ + long responseBytes(); + + /** + * Return the total response time in microseconds. + */ + long totalMicros(); + + /** + * Return the max response time in microseconds (since the last reset). + */ + long maxMicros(); + + /** + * Return the average response time in microseconds. + */ + long avgMicros(); + } +} diff --git a/client/src/main/java/io/avaje/http/client/HttpClientContext.java b/client/src/main/java/io/avaje/http/client/HttpClientContext.java index 3fc7b73..33ab6f7 100644 --- a/client/src/main/java/io/avaje/http/client/HttpClientContext.java +++ b/client/src/main/java/io/avaje/http/client/HttpClientContext.java @@ -346,73 +346,15 @@ interface Builder { /** * The state of the builder with methods to read the set state. */ - interface State extends Builder { - - /** - * Return the base URL. - */ - String baseUrl(); - - /** - * Return the body adapter. - */ - BodyAdapter bodyAdapter(); - - /** - * Return the HttpClient. - */ - HttpClient client(); - - /** - * Return true if requestLogging is on. - */ - boolean requestLogging(); - - /** - * Return the request timeout. - */ - Duration requestTimeout(); - - /** - * Return the retry handler. - */ - RetryHandler retryHandler(); + interface State extends io.avaje.http.client.HttpClient.Builder.State { + } } /** * Statistic metrics collected to provide an overview of activity of this client. */ - interface Metrics { - - /** - * Return the total number of responses. - */ - long totalCount(); - - /** - * Return the total number of error responses (status code >= 300). - */ - long errorCount(); - - /** - * Return the total response bytes (excludes streaming responses). - */ - long responseBytes(); + interface Metrics extends io.avaje.http.client.HttpClient.Metrics { - /** - * Return the total response time in microseconds. - */ - long totalMicros(); - - /** - * Return the max response time in microseconds (since the last reset). - */ - long maxMicros(); - - /** - * Return the average response time in microseconds. - */ - long avgMicros(); } } From 5a1c2b9d378f623d30ea7b7e1e3ef920631946e8 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 17 Jan 2023 00:04:17 +1300 Subject: [PATCH 57/67] Introduce HttpClient and HttpClient.Builder --- .../io/avaje/http/client/DBaseBuilder.java | 172 +++++++++++ .../avaje/http/client/DHttpClientBuilder.java | 172 +++++++++++ .../avaje/http/client/DHttpClientContext.java | 2 +- .../client/DHttpClientContextBuilder.java | 151 +--------- .../java/io/avaje/http/client/HttpClient.java | 266 ++++++++++++++++++ .../avaje/http/client/HttpClientContext.java | 81 +----- .../io/avaje/http/client/SpiHttpClient.java | 53 ++++ .../http/client/DHttpClientContextTest.java | 26 +- .../http/client/HelloControllerTest.java | 37 ++- .../github/httpclient/Simple$HttpClient.java | 2 +- 10 files changed, 718 insertions(+), 244 deletions(-) create mode 100644 client/src/main/java/io/avaje/http/client/DBaseBuilder.java create mode 100644 client/src/main/java/io/avaje/http/client/DHttpClientBuilder.java create mode 100644 client/src/main/java/io/avaje/http/client/SpiHttpClient.java diff --git a/client/src/main/java/io/avaje/http/client/DBaseBuilder.java b/client/src/main/java/io/avaje/http/client/DBaseBuilder.java new file mode 100644 index 0000000..789316b --- /dev/null +++ b/client/src/main/java/io/avaje/http/client/DBaseBuilder.java @@ -0,0 +1,172 @@ +package io.avaje.http.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.avaje.inject.BeanScope; +import io.avaje.jsonb.Jsonb; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.net.Authenticator; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.ProxySelector; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Executor; + +import static java.util.Objects.requireNonNull; + +abstract class DBaseBuilder { + + java.net.http.HttpClient client; + String baseUrl; + boolean requestLogging = true; + Duration requestTimeout = Duration.ofSeconds(20); + BodyAdapter bodyAdapter; + RetryHandler retryHandler; + AuthTokenProvider authTokenProvider; + + CookieHandler cookieHandler = new CookieManager(); + java.net.http.HttpClient.Redirect redirect = java.net.http.HttpClient.Redirect.NORMAL; + java.net.http.HttpClient.Version version; + Executor executor; + ProxySelector proxy; + SSLContext sslContext; + SSLParameters sslParameters; + Authenticator authenticator; + int priority; + + final List interceptors = new ArrayList<>(); + final List listeners = new ArrayList<>(); + + void configureFromScope(BeanScope beanScope) { + if (bodyAdapter == null) { + configureBodyAdapter(beanScope); + } + if (retryHandler == null) { + configureRetryHandler(beanScope); + } + } + + private void configureRetryHandler(BeanScope beanScope) { + beanScope.getOptional(RetryHandler.class) + .ifPresent(this::setRetryHandler); + } + + private void setRetryHandler(RetryHandler retryHandler) { + this.retryHandler = retryHandler; + } + + private void configureBodyAdapter(BeanScope beanScope) { + Optional body = beanScope.getOptional(BodyAdapter.class); + if (body.isPresent()) { + bodyAdapter = body.get(); + } else if (beanScope.contains("io.avaje.jsonb.Jsonb")) { + bodyAdapter = new JsonbBodyAdapter(beanScope.get(Jsonb.class)); + } else if (beanScope.contains("com.fasterxml.jackson.databind.ObjectMapper")) { + ObjectMapper objectMapper = beanScope.get(ObjectMapper.class); + bodyAdapter = new JacksonBodyAdapter(objectMapper); + } + } + + private RequestListener buildListener() { + if (listeners.isEmpty()) { + return null; + } else if (listeners.size() == 1) { + return listeners.get(0); + } else { + return new DRequestListeners(listeners); + } + } + + private RequestIntercept buildIntercept() { + if (interceptors.isEmpty()) { + return null; + } else if (interceptors.size() == 1) { + return interceptors.get(0); + } else { + return new DRequestInterceptors(interceptors); + } + } + + private java.net.http.HttpClient defaultClient() { + final java.net.http.HttpClient.Builder builder = java.net.http.HttpClient.newBuilder() + .followRedirects(redirect) + .connectTimeout(Duration.ofSeconds(20)); + if (cookieHandler != null) { + builder.cookieHandler(cookieHandler); + } + if (version != null) { + builder.version(version); + } + if (executor != null) { + builder.executor(executor); + } + if (proxy != null) { + builder.proxy(proxy); + } + if (sslContext != null) { + builder.sslContext(sslContext); + } + if (sslParameters != null) { + builder.sslParameters(sslParameters); + } + if (authenticator != null) { + builder.authenticator(authenticator); + } + if (priority > 0) { + builder.priority(priority); + } + return builder.build(); + } + + /** + * Create a reasonable default BodyAdapter if avaje-jsonb or Jackson are present. + */ + private BodyAdapter defaultBodyAdapter() { + try { + return detectJsonb() ? new JsonbBodyAdapter() + : detectJackson() ? new JacksonBodyAdapter() + : null; + } catch (IllegalAccessError e) { + // not in module path + return null; + } + } + + private boolean detectJsonb() { + return detectTypeExists("io.avaje.jsonb.Jsonb"); + } + + private boolean detectJackson() { + return detectTypeExists("com.fasterxml.jackson.databind.ObjectMapper"); + } + + private boolean detectTypeExists(String className) { + try { + Class.forName(className); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + DHttpClientContext buildClient() { + requireNonNull(baseUrl, "baseUrl is not specified"); + requireNonNull(requestTimeout, "requestTimeout is not specified"); + if (client == null) { + client = defaultClient(); + } + if (requestLogging) { + // register the builtin request/response logging + this.listeners.add(new RequestLogger()); + } + if (bodyAdapter == null) { + bodyAdapter = defaultBodyAdapter(); + } + return new DHttpClientContext(client, baseUrl, requestTimeout, bodyAdapter, retryHandler, buildListener(), authTokenProvider, buildIntercept()); + } + +} diff --git a/client/src/main/java/io/avaje/http/client/DHttpClientBuilder.java b/client/src/main/java/io/avaje/http/client/DHttpClientBuilder.java new file mode 100644 index 0000000..18b3d1f --- /dev/null +++ b/client/src/main/java/io/avaje/http/client/DHttpClientBuilder.java @@ -0,0 +1,172 @@ +package io.avaje.http.client; + +import io.avaje.inject.BeanScope; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.net.Authenticator; +import java.net.CookieHandler; +import java.net.ProxySelector; +import java.time.Duration; +import java.util.concurrent.Executor; + +final class DHttpClientBuilder extends DBaseBuilder implements HttpClient.Builder, HttpClient.Builder.State { + + DHttpClientBuilder() { + } + + @Override + public HttpClient.Builder client(java.net.http.HttpClient client) { + this.client = client; + return this; + } + + @Override + public HttpClient.Builder baseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + @Override + public HttpClient.Builder requestTimeout(Duration requestTimeout) { + this.requestTimeout = requestTimeout; + return this; + } + + @Override + public HttpClient.Builder bodyAdapter(BodyAdapter adapter) { + this.bodyAdapter = adapter; + return this; + } + + @Override + public HttpClient.Builder retryHandler(RetryHandler retryHandler) { + this.retryHandler = retryHandler; + return this; + } + + @Override + public HttpClient.Builder requestLogging(boolean requestLogging) { + this.requestLogging = requestLogging; + return this; + } + + @Override + public HttpClient.Builder requestListener(RequestListener requestListener) { + this.listeners.add(requestListener); + return this; + } + + @Override + public HttpClient.Builder requestIntercept(RequestIntercept requestIntercept) { + this.interceptors.add(requestIntercept); + return this; + } + + @Override + public HttpClient.Builder authTokenProvider(AuthTokenProvider authTokenProvider) { + this.authTokenProvider = authTokenProvider; + return this; + } + + @Override + public HttpClient.Builder cookieHandler(CookieHandler cookieHandler) { + this.cookieHandler = cookieHandler; + return this; + } + + @Override + public HttpClient.Builder redirect(java.net.http.HttpClient.Redirect redirect) { + this.redirect = redirect; + return this; + } + + @Override + public HttpClient.Builder version(java.net.http.HttpClient.Version version) { + this.version = version; + return this; + } + + @Override + public HttpClient.Builder executor(Executor executor) { + this.executor = executor; + return this; + } + + @Override + public HttpClient.Builder proxy(ProxySelector proxySelector) { + this.proxy = proxySelector; + return this; + } + + @Override + public HttpClient.Builder sslContext(SSLContext sslContext) { + this.sslContext = sslContext; + return this; + } + + @Override + public HttpClient.Builder sslParameters(SSLParameters sslParameters) { + this.sslParameters = sslParameters; + return this; + } + + @Override + public HttpClient.Builder authenticator(Authenticator authenticator) { + this.authenticator = authenticator; + return this; + } + + @Override + public HttpClient.Builder priority(int priority) { + this.priority = priority; + return this; + } + + @Override + public HttpClient.Builder.State state() { + return this; + } + + @Override + public HttpClient.Builder configureWith(BeanScope beanScope) { + super.configureFromScope(beanScope); + return this; + } + + @Override + public HttpClient build() { + return buildClient(); + } + + @Override + public String baseUrl() { + return baseUrl; + } + + @Override + public BodyAdapter bodyAdapter() { + return bodyAdapter; + } + + @Override + public java.net.http.HttpClient client() { + return client; + } + + @Override + public boolean requestLogging() { + return requestLogging; + } + + @Override + public Duration requestTimeout() { + return requestTimeout; + } + + @Override + public RetryHandler retryHandler() { + return retryHandler; + } + +} 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 d2c1064..ea88054 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientContext.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientContext.java @@ -16,7 +16,7 @@ import java.util.concurrent.atomic.LongAccumulator; import java.util.concurrent.atomic.LongAdder; -final class DHttpClientContext implements HttpClientContext { +final class DHttpClientContext implements HttpClientContext, SpiHttpClient { /** * HTTP Authorization header. diff --git a/client/src/main/java/io/avaje/http/client/DHttpClientContextBuilder.java b/client/src/main/java/io/avaje/http/client/DHttpClientContextBuilder.java index 2e1bb15..08675e4 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientContextBuilder.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientContextBuilder.java @@ -1,46 +1,17 @@ package io.avaje.http.client; -import com.fasterxml.jackson.databind.ObjectMapper; import io.avaje.inject.BeanScope; -import io.avaje.jsonb.Jsonb; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; import java.net.Authenticator; import java.net.CookieHandler; -import java.net.CookieManager; import java.net.ProxySelector; import java.net.http.HttpClient; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; import java.util.concurrent.Executor; -import static java.util.Objects.requireNonNull; - -final class DHttpClientContextBuilder implements HttpClientContext.Builder, HttpClientContext.Builder.State { - - private HttpClient client; - private String baseUrl; - private boolean requestLogging = true; - private Duration requestTimeout = Duration.ofSeconds(20); - private BodyAdapter bodyAdapter; - private RetryHandler retryHandler; - private AuthTokenProvider authTokenProvider; - - private CookieHandler cookieHandler = new CookieManager(); - private HttpClient.Redirect redirect = HttpClient.Redirect.NORMAL; - private HttpClient.Version version; - private Executor executor; - private ProxySelector proxy; - private SSLContext sslContext; - private SSLParameters sslParameters; - private Authenticator authenticator; - private int priority; - - private final List interceptors = new ArrayList<>(); - private final List listeners = new ArrayList<>(); +final class DHttpClientContextBuilder extends DBaseBuilder implements HttpClientContext.Builder, HttpClientContext.Builder.State { DHttpClientContextBuilder() { } @@ -160,47 +131,13 @@ public State state() { @Override public HttpClientContext.Builder configureWith(BeanScope beanScope) { - if (bodyAdapter == null) { - configureBodyAdapter(beanScope); - } - if (retryHandler == null) { - configureRetryHandler(beanScope); - } + super.configureFromScope(beanScope); return this; } - private void configureRetryHandler(BeanScope beanScope) { - beanScope.getOptional(RetryHandler.class) - .ifPresent(this::retryHandler); - } - - private void configureBodyAdapter(BeanScope beanScope) { - Optional body = beanScope.getOptional(BodyAdapter.class); - if (body.isPresent()) { - bodyAdapter = body.get(); - } else if (beanScope.contains("io.avaje.jsonb.Jsonb")) { - bodyAdapter = new JsonbBodyAdapter(beanScope.get(Jsonb.class)); - } else if (beanScope.contains("com.fasterxml.jackson.databind.ObjectMapper")) { - ObjectMapper objectMapper = beanScope.get(ObjectMapper.class); - bodyAdapter = new JacksonBodyAdapter(objectMapper); - } - } - @Override public HttpClientContext build() { - requireNonNull(baseUrl, "baseUrl is not specified"); - requireNonNull(requestTimeout, "requestTimeout is not specified"); - if (client == null) { - client = defaultClient(); - } - if (requestLogging) { - // register the builtin request/response logging - requestListener(new RequestLogger()); - } - if (bodyAdapter == null) { - bodyAdapter = defaultBodyAdapter(); - } - return new DHttpClientContext(client, baseUrl, requestTimeout, bodyAdapter, retryHandler, buildListener(), authTokenProvider, buildIntercept()); + return super.buildClient(); } @Override @@ -233,86 +170,4 @@ public RetryHandler retryHandler() { return retryHandler; } - private RequestListener buildListener() { - if (listeners.isEmpty()) { - return null; - } else if (listeners.size() == 1) { - return listeners.get(0); - } else { - return new DRequestListeners(listeners); - } - } - - private RequestIntercept buildIntercept() { - if (interceptors.isEmpty()) { - return null; - } else if (interceptors.size() == 1) { - return interceptors.get(0); - } else { - return new DRequestInterceptors(interceptors); - } - } - - private HttpClient defaultClient() { - final HttpClient.Builder builder = HttpClient.newBuilder() - .followRedirects(redirect) - .connectTimeout(Duration.ofSeconds(20)); - if (cookieHandler != null) { - builder.cookieHandler(cookieHandler); - } - if (version != null) { - builder.version(version); - } - if (executor != null) { - builder.executor(executor); - } - if (proxy != null) { - builder.proxy(proxy); - } - if (sslContext != null) { - builder.sslContext(sslContext); - } - if (sslParameters != null) { - builder.sslParameters(sslParameters); - } - if (authenticator != null) { - builder.authenticator(authenticator); - } - if (priority > 0) { - builder.priority(priority); - } - return builder.build(); - } - - /** - * Create a reasonable default BodyAdapter if avaje-jsonb or Jackson are present. - */ - BodyAdapter defaultBodyAdapter() { - try { - return detectJsonb() ? new JsonbBodyAdapter() - : detectJackson() ? new JacksonBodyAdapter() - : null; - } catch (IllegalAccessError e) { - // not in module path - return null; - } - } - - boolean detectJsonb() { - return detectTypeExists("io.avaje.jsonb.Jsonb"); - } - - boolean detectJackson() { - return detectTypeExists("com.fasterxml.jackson.databind.ObjectMapper"); - } - - private boolean detectTypeExists(String className) { - try { - Class.forName(className); - return true; - } catch (ClassNotFoundException e) { - return false; - } - } - } diff --git a/client/src/main/java/io/avaje/http/client/HttpClient.java b/client/src/main/java/io/avaje/http/client/HttpClient.java index 643a43c..99c5f9a 100644 --- a/client/src/main/java/io/avaje/http/client/HttpClient.java +++ b/client/src/main/java/io/avaje/http/client/HttpClient.java @@ -1,6 +1,14 @@ package io.avaje.http.client; +import io.avaje.inject.BeanScope; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.net.Authenticator; +import java.net.CookieHandler; +import java.net.ProxySelector; import java.time.Duration; +import java.util.concurrent.Executor; /** * The HTTP client context that we use to build and process requests. @@ -23,8 +31,266 @@ */ public interface HttpClient { + /** + * Return the builder to config and build the client context. + * + *
{@code
+   *
+   *   HttpClient client = HttpClient.builder()
+   *       .baseUrl("http://localhost:8080")
+   *       .bodyAdapter(new JacksonBodyAdapter())
+   *       .build();
+   *
+   *  HttpResponse res = client.request()
+   *       .path("hello")
+   *       .GET().asString();
+   *
+   * }
+ */ + static Builder builder() { + return new DHttpClientBuilder(); + } + + /** + * Return the http client API implementation. + * + * @param clientInterface A @Client interface with annotated API methods. + * @param The service type. + * @return The http client API implementation. + */ + T create(Class clientInterface); + + /** + * Create a new request. + */ + HttpClientRequest request(); + + /** + * Return the body adapter used by the client context. + *

+ * This is the body adapter used to convert request and response + * bodies to java types. For example using Jackson with JSON payloads. + */ + BodyAdapter converters(); + + /** + * Return the current aggregate metrics. + *

+ * These metrics are collected for all requests sent via this context. + */ + HttpClientContext.Metrics metrics(); + + /** + * Return the current metrics with the option of resetting the underlying counters. + *

+ * These metrics are collected for all requests sent via this context. + */ + HttpClientContext.Metrics metrics(boolean reset); + + /** + * Builds the HttpClient. + * + *

{@code
+   *
+   *   HttpClient client = HttpClient.builder()
+   *       .baseUrl("http://localhost:8080")
+   *       .bodyAdapter(new JacksonBodyAdapter())
+   *       .build();
+   *
+   *  HelloDto dto = client.request()
+   *       .path("hello")
+   *       .queryParam("name", "Rob")
+   *       .queryParam("say", "Whats up")
+   *       .GET()
+   *       .bean(HelloDto.class);
+   *
+   * }
+ */ interface Builder { + /** + * Set the base URL to use for requests created from the context. + *

+ * Note that the base url can be replaced via {@link HttpClientRequest#url(String)}. + */ + Builder baseUrl(String baseUrl); + + /** + * Set the default request timeout. + * + * @see java.net.http.HttpRequest.Builder#timeout(Duration) + */ + Builder requestTimeout(Duration requestTimeout); + + /** + * Set the body adapter to use to convert beans to body content + * and response content back to beans. + */ + Builder bodyAdapter(BodyAdapter adapter); + + /** + * Set a RetryHandler to use to retry requests. + */ + Builder retryHandler(RetryHandler retryHandler); + + /** + * Disable or enable built in request and response logging. + *

+ * By default request logging is enabled. Set this to false to stop + * the default {@link RequestLogger} being registered to log request + * and response headers and bodies etc. + *

+ * With logging level set to {@code DEBUG} for + * {@code io.avaje.http.client.RequestLogger} the request and response + * are logged as a summary with response status and time. + *

+ * Set the logging level to {@code TRACE} to include the request + * and response headers and body payloads with truncation for large + * bodies. + * + *

Suppression

+ *

+ * We can also use {@link HttpClientRequest#suppressLogging()} to suppress + * logging on specific requests. + *

+ * Logging of Authorization headers is suppressed. + * {@link AuthTokenProvider} requests are suppressed. + * + * @param requestLogging Disable/enable the registration of the default logger + * @see RequestLogger + */ + Builder requestLogging(boolean requestLogging); + + /** + * Add a request listener. Multiple listeners may be added, when + * do so they will process events in the order they were added. + *

+ * Note that {@link RequestLogger} is an implementation for debug + * logging request/response headers and content which is registered + * by default depending on {@link #requestLogging(boolean)}. + * + * @see RequestLogger + */ + Builder requestListener(RequestListener requestListener); + + /** + * Add a request interceptor. Multiple interceptors may be added. + */ + Builder requestIntercept(RequestIntercept requestIntercept); + + /** + * Add a Authorization token provider. + *

+ * When set all requests are expected to use a Authorization Bearer token + * unless they are marked via {@link HttpClientRequest#skipAuthToken()}. + *

+ * The AuthTokenProvider obtains a new token typically with an expiry. This + * is automatically called as needed and the Authorization Bearer header set + * on all requests (not marked with skipAuthToken()). + */ + Builder authTokenProvider(AuthTokenProvider authTokenProvider); + + /** + * Set the underlying HttpClient to use. + *

+ * Used when we wish to control all options of the HttpClient. + */ + Builder client(java.net.http.HttpClient client); + + /** + * Specify a cookie handler to use on the HttpClient. This would override the default cookie handler. + * + * @see java.net.http.HttpClient.Builder#cookieHandler(CookieHandler) + */ + Builder cookieHandler(CookieHandler cookieHandler); + + /** + * Specify the redirect policy. Defaults to HttpClient.Redirect.NORMAL. + * + * @see java.net.http.HttpClient.Builder#followRedirects(java.net.http.HttpClient.Redirect) + */ + Builder redirect(java.net.http.HttpClient.Redirect redirect); + + /** + * Specify the HTTP version. Defaults to not set. + * + * @see java.net.http.HttpClient.Builder#version(java.net.http.HttpClient.Version) + */ + Builder version(java.net.http.HttpClient.Version version); + + /** + * Specify the Executor to use for asynchronous tasks. + * If not specified a default executor will be used. + * + * @see java.net.http.HttpClient.Builder#executor(Executor) + */ + Builder executor(Executor executor); + + /** + * Set the proxy to the underlying {@link java.net.http.HttpClient}. + * + * @see java.net.http.HttpClient.Builder#proxy(ProxySelector) + */ + Builder proxy(ProxySelector proxySelector); + + /** + * Set the sslContext to the underlying {@link java.net.http.HttpClient}. + * + * @see java.net.http.HttpClient.Builder#sslContext(SSLContext) + */ + Builder sslContext(SSLContext sslContext); + + /** + * Set the sslParameters to the underlying {@link java.net.http.HttpClient}. + * + * @see java.net.http.HttpClient.Builder#sslParameters(SSLParameters) + */ + Builder sslParameters(SSLParameters sslParameters); + + /** + * Set a HttpClient authenticator to the underlying {@link java.net.http.HttpClient}. + * + * @see java.net.http.HttpClient.Builder#authenticator(Authenticator) + */ + Builder authenticator(Authenticator authenticator); + + /** + * Set the priority for HTTP/2 requests to the underlying {@link java.net.http.HttpClient}. + * + * @see java.net.http.HttpClient.Builder#priority(int) + */ + Builder priority(int priority); + + /** + * Configure BodyAdapter and RetryHandler using dependency injection BeanScope. + */ + Builder configureWith(BeanScope beanScope); + + /** + * Return the state of the builder. + */ + Builder.State state(); + + /** + * Build and return the context. + * + *

{@code
+     *
+     *   HttpClient client = HttpClient.builder()
+     *       .baseUrl("http://localhost:8080")
+     *       .bodyAdapter(new JacksonBodyAdapter())
+     *       .build();
+     *
+     *  HelloDto dto = client.request()
+     *       .path("hello")
+     *       .queryParam("say", "Whats up")
+     *       .GET()
+     *       .bean(HelloDto.class);
+     *
+     * }
+ */ + HttpClient build(); + /** * The state of the builder with methods to read the set state. */ diff --git a/client/src/main/java/io/avaje/http/client/HttpClientContext.java b/client/src/main/java/io/avaje/http/client/HttpClientContext.java index 33ab6f7..5b45a0d 100644 --- a/client/src/main/java/io/avaje/http/client/HttpClientContext.java +++ b/client/src/main/java/io/avaje/http/client/HttpClientContext.java @@ -8,7 +8,6 @@ import java.net.CookieHandler; import java.net.ProxySelector; import java.net.http.HttpClient; -import java.net.http.HttpResponse; import java.time.Duration; import java.util.concurrent.Executor; @@ -31,7 +30,7 @@ * * }
*/ -public interface HttpClientContext { +public interface HttpClientContext extends io.avaje.http.client.HttpClient { /** * Return the builder to config and build the client context. @@ -61,84 +60,6 @@ static HttpClientContext.Builder newBuilder() { return builder(); } - /** - * Return the http client API implementation. - * - * @param clientInterface A @Client interface with annotated API methods. - * @param The service type. - * @return The http client API implementation. - */ - T create(Class clientInterface); - - /** - * Create a new request. - */ - HttpClientRequest request(); - - /** - * Return a UrlBuilder to use to build an URL taking into - * account the base URL. - */ - UrlBuilder url(); - - /** - * Return the body adapter used by the client context. - *

- * This is the body adapter used to convert request and response - * bodies to java types. For example using Jackson with JSON payloads. - */ - BodyAdapter converters(); - - /** - * Return the underlying http client. - */ - HttpClient httpClient(); - - /** - * Return the current aggregate metrics. - *

- * These metrics are collected for all requests sent via this context. - */ - Metrics metrics(); - - /** - * Return the current metrics with the option of resetting the underlying counters. - *

- * These metrics are collected for all requests sent via this context. - */ - Metrics metrics(boolean reset); - - /** - * Check the response status code and throw HttpException if the status - * code is in the error range. - */ - void checkResponse(HttpResponse response); - - /** - * Return the response content taking into account content encoding. - * - * @param httpResponse The HTTP response to decode the content from - * @return The decoded content - */ - BodyContent readContent(HttpResponse httpResponse); - - /** - * Decode the response content given the Content-Encoding http header. - * - * @param httpResponse The HTTP response - * @return The decoded content - */ - byte[] decodeContent(HttpResponse httpResponse); - - /** - * Decode the body using the given encoding. - * - * @param encoding The encoding used to decode the content - * @param content The raw content being decoded - * @return The decoded content - */ - byte[] decodeContent(String encoding, byte[] content); - /** * Builds the HttpClientContext. * diff --git a/client/src/main/java/io/avaje/http/client/SpiHttpClient.java b/client/src/main/java/io/avaje/http/client/SpiHttpClient.java new file mode 100644 index 0000000..b7a1c1f --- /dev/null +++ b/client/src/main/java/io/avaje/http/client/SpiHttpClient.java @@ -0,0 +1,53 @@ +package io.avaje.http.client; + +import java.net.http.HttpClient; +import java.net.http.HttpResponse; + +/** + * Internal Http Client interface. + */ +interface SpiHttpClient { + + /** + * Return a UrlBuilder to use to build an URL taking into + * account the base URL. + */ + UrlBuilder url(); + + /** + * Return the underlying http client. + */ + HttpClient httpClient(); + + /** + * Return the response content taking into account content encoding. + * + * @param httpResponse The HTTP response to decode the content from + * @return The decoded content + */ + BodyContent readContent(HttpResponse httpResponse); + + /** + * Decode the response content given the Content-Encoding http header. + * + * @param httpResponse The HTTP response + * @return The decoded content + */ + byte[] decodeContent(HttpResponse httpResponse); + + /** + * Decode the body using the given encoding. + * + * @param encoding The encoding used to decode the content + * @param content The raw content being decoded + * @return The decoded content + */ + byte[] decodeContent(String encoding, byte[] content); + + /** + * Check the response status code and throw HttpException if the status + * code is in the error range. + */ + void checkResponse(HttpResponse response); + +} diff --git a/client/src/test/java/io/avaje/http/client/DHttpClientContextTest.java b/client/src/test/java/io/avaje/http/client/DHttpClientContextTest.java index 405af5b..0811224 100644 --- a/client/src/test/java/io/avaje/http/client/DHttpClientContextTest.java +++ b/client/src/test/java/io/avaje/http/client/DHttpClientContextTest.java @@ -45,15 +45,16 @@ void build_basic() { .baseUrl("http://localhost") .build(); + SpiHttpClient spi = (SpiHttpClient)context; // has default client created - assertThat(context.httpClient()).isNotNull(); - assertThat(context.httpClient().version()).isEqualTo(HttpClient.Version.HTTP_2); - assertThat(context.httpClient().cookieHandler()).isPresent(); + assertThat(spi.httpClient()).isNotNull(); + assertThat(spi.httpClient().version()).isEqualTo(HttpClient.Version.HTTP_2); + assertThat(spi.httpClient().cookieHandler()).isPresent(); // has expected url building - assertThat(context.url().build()).isEqualTo("http://localhost"); - assertThat(context.url().path("hello").build()).isEqualTo("http://localhost/hello"); - assertThat(context.url().queryParam("hello","there").build()).isEqualTo("http://localhost?hello=there"); + assertThat(spi.url().build()).isEqualTo("http://localhost"); + assertThat(spi.url().path("hello").build()).isEqualTo("http://localhost/hello"); + assertThat(spi.url().queryParam("hello","there").build()).isEqualTo("http://localhost?hello=there"); } @Test @@ -66,15 +67,16 @@ void build_noCookieHandler() { .redirect(HttpClient.Redirect.ALWAYS) .build(); + SpiHttpClient spi = (SpiHttpClient)context; // has default client created - assertThat(context.httpClient()).isNotNull(); - assertThat(context.httpClient().version()).isEqualTo(HttpClient.Version.HTTP_2); - assertThat(context.httpClient().cookieHandler()).isEmpty(); + assertThat(spi.httpClient()).isNotNull(); + assertThat(spi.httpClient().version()).isEqualTo(HttpClient.Version.HTTP_2); + assertThat(spi.httpClient().cookieHandler()).isEmpty(); // has expected url building - assertThat(context.url().build()).isEqualTo("http://localhost"); - assertThat(context.url().path("hello").build()).isEqualTo("http://localhost/hello"); - assertThat(context.url().queryParam("hello","there").build()).isEqualTo("http://localhost?hello=there"); + assertThat(spi.url().build()).isEqualTo("http://localhost"); + assertThat(spi.url().path("hello").build()).isEqualTo("http://localhost/hello"); + assertThat(spi.url().queryParam("hello","there").build()).isEqualTo("http://localhost?hello=there"); } @Test 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 6b308ad..b77ca72 100644 --- a/client/src/test/java/io/avaje/http/client/HelloControllerTest.java +++ b/client/src/test/java/io/avaje/http/client/HelloControllerTest.java @@ -1,11 +1,12 @@ package io.avaje.http.client; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import org.example.webserver.ErrorResponse; import org.example.webserver.HelloDto; import org.junit.jupiter.api.Test; import java.io.*; -import java.net.http.HttpClient; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.nio.file.Path; @@ -22,13 +23,45 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static java.net.http.HttpClient.Version.*; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; class HelloControllerTest extends BaseWebTest { + private static final ObjectMapper objectMapper = new ObjectMapper(); + final HttpClientContext clientContext = client(); + @Test + void newClientTest() { + HttpClient client = HttpClient.builder() + .baseUrl("http://localhost:8887") + .bodyAdapter(new JacksonBodyAdapter()) + .build(); + + client.metrics(true); + Map params = new LinkedHashMap<>(); + params.put("A", "a"); + params.put("B", "b"); + + final HttpResponse hres = client.request() + .path("hello").path("message") + .queryParam(params) + .GET().asString(); + + assertThat(hres.statusCode()).isEqualTo(200); + assertThat(hres.uri().toString()).isEqualTo("http://localhost:8887/hello/message?A=a&B=b"); + + HttpClient.Metrics metrics = client.metrics(); + assertThat(metrics.totalCount()).isEqualTo(1); + assertThat(metrics.errorCount()).isEqualTo(0); + assertThat(metrics.responseBytes()).isGreaterThan(0); + assertThat(metrics.totalMicros()).isGreaterThan(0); + assertThat(metrics.maxMicros()).isEqualTo(metrics.totalMicros()); + assertThat(metrics.avgMicros()).isEqualTo(metrics.totalMicros()); + } + @Test void queryParamMap() { clientContext.metrics(true); @@ -945,7 +978,7 @@ void postForm_asVoid_validResponse() { assertThat(res.request()).isNotNull(); assertThat(res.previousResponse()).isEmpty(); assertThat(res.sslSession()).isEmpty(); - assertThat(res.version()).isEqualTo(HttpClient.Version.HTTP_1_1); + assertThat(res.version()).isEqualTo(HTTP_1_1); assertThat(res.uri().toString()).isEqualTo("http://localhost:8887/hello/saveform"); } diff --git a/client/src/test/java/org/example/github/httpclient/Simple$HttpClient.java b/client/src/test/java/org/example/github/httpclient/Simple$HttpClient.java index 797d0cb..73a16d0 100644 --- a/client/src/test/java/org/example/github/httpclient/Simple$HttpClient.java +++ b/client/src/test/java/org/example/github/httpclient/Simple$HttpClient.java @@ -58,7 +58,7 @@ public InputStream getById2(String id, InputStream is) { .body(() -> is) .GET().handler(HttpResponse.BodyHandlers.ofInputStream()); - context.checkResponse(response); + //context.checkResponse(response); return response.body(); } From de95cd29208e5333e4b844de402c7f047db18e84 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 17 Jan 2023 00:15:03 +1300 Subject: [PATCH 58/67] Flip HttpClient.Metrics to extend HttpClientContext.Metrics instead of the other way around --- .../avaje/http/client/DHttpClientContext.java | 13 ++++--- .../java/io/avaje/http/client/HttpClient.java | 35 ++----------------- .../avaje/http/client/HttpClientContext.java | 30 +++++++++++++++- .../http/client/HelloControllerTest.java | 3 +- 4 files changed, 39 insertions(+), 42 deletions(-) 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 ea88054..56eabbd 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientContext.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientContext.java @@ -3,7 +3,6 @@ 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; import java.net.http.HttpResponse; @@ -24,7 +23,7 @@ final class DHttpClientContext implements HttpClientContext, SpiHttpClient { static final String AUTHORIZATION = "Authorization"; private static final String BEARER = "Bearer "; - private final HttpClient httpClient; + private final java.net.http.HttpClient httpClient; private final String baseUrl; private final Duration requestTimeout; private final BodyAdapter bodyAdapter; @@ -41,7 +40,7 @@ final class DHttpClientContext implements HttpClientContext, SpiHttpClient { private final LongAdder metricResMicros = new LongAdder(); private final LongAccumulator metricResMaxMicros = new LongAccumulator(Math::max, 0); - DHttpClientContext(HttpClient httpClient, String baseUrl, Duration requestTimeout, BodyAdapter bodyAdapter, RetryHandler retryHandler, RequestListener requestListener, AuthTokenProvider authTokenProvider, RequestIntercept intercept) { + DHttpClientContext(java.net.http.HttpClient httpClient, String baseUrl, Duration requestTimeout, BodyAdapter bodyAdapter, RetryHandler retryHandler, RequestListener requestListener, AuthTokenProvider authTokenProvider, RequestIntercept intercept) { this.httpClient = httpClient; this.baseUrl = baseUrl; this.requestTimeout = requestTimeout; @@ -106,17 +105,17 @@ public UrlBuilder url() { } @Override - public HttpClient httpClient() { + public java.net.http.HttpClient httpClient() { return httpClient; } @Override - public Metrics metrics() { + public HttpClient.Metrics metrics() { return metrics(false); } @Override - public Metrics metrics(boolean reset) { + public HttpClient.Metrics metrics(boolean reset) { if (reset) { return new DMetrics(metricResTotal.sumThenReset(), metricResError.sumThenReset(), metricResBytes.sumThenReset(), metricResMicros.sumThenReset(), metricResMaxMicros.getThenReset()); } else { @@ -128,7 +127,7 @@ void metricsString(int stringBody) { metricResBytes.add(stringBody); } - static final class DMetrics implements Metrics { + static final class DMetrics implements HttpClient.Metrics { private final long totalCount; private final long errorCount; diff --git a/client/src/main/java/io/avaje/http/client/HttpClient.java b/client/src/main/java/io/avaje/http/client/HttpClient.java index 99c5f9a..cb4298e 100644 --- a/client/src/main/java/io/avaje/http/client/HttpClient.java +++ b/client/src/main/java/io/avaje/http/client/HttpClient.java @@ -78,14 +78,14 @@ static Builder builder() { *

* These metrics are collected for all requests sent via this context. */ - HttpClientContext.Metrics metrics(); + HttpClient.Metrics metrics(); /** * Return the current metrics with the option of resetting the underlying counters. *

* These metrics are collected for all requests sent via this context. */ - HttpClientContext.Metrics metrics(boolean reset); + HttpClient.Metrics metrics(boolean reset); /** * Builds the HttpClient. @@ -331,36 +331,7 @@ interface State { /** * Statistic metrics collected to provide an overview of activity of this client. */ - interface Metrics { + interface Metrics extends HttpClientContext.Metrics { - /** - * Return the total number of responses. - */ - long totalCount(); - - /** - * Return the total number of error responses (status code >= 300). - */ - long errorCount(); - - /** - * Return the total response bytes (excludes streaming responses). - */ - long responseBytes(); - - /** - * Return the total response time in microseconds. - */ - long totalMicros(); - - /** - * Return the max response time in microseconds (since the last reset). - */ - long maxMicros(); - - /** - * Return the average response time in microseconds. - */ - long avgMicros(); } } diff --git a/client/src/main/java/io/avaje/http/client/HttpClientContext.java b/client/src/main/java/io/avaje/http/client/HttpClientContext.java index 5b45a0d..c628948 100644 --- a/client/src/main/java/io/avaje/http/client/HttpClientContext.java +++ b/client/src/main/java/io/avaje/http/client/HttpClientContext.java @@ -275,7 +275,35 @@ interface State extends io.avaje.http.client.HttpClient.Builder.State { /** * Statistic metrics collected to provide an overview of activity of this client. */ - interface Metrics extends io.avaje.http.client.HttpClient.Metrics { + interface Metrics { + /** + * Return the total number of responses. + */ + long totalCount(); + + /** + * Return the total number of error responses (status code >= 300). + */ + long errorCount(); + + /** + * Return the total response bytes (excludes streaming responses). + */ + long responseBytes(); + /** + * Return the total response time in microseconds. + */ + long totalMicros(); + + /** + * Return the max response time in microseconds (since the last reset). + */ + long maxMicros(); + + /** + * Return the average response time in microseconds. + */ + long avgMicros(); } } 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 b77ca72..c81792b 100644 --- a/client/src/test/java/io/avaje/http/client/HelloControllerTest.java +++ b/client/src/test/java/io/avaje/http/client/HelloControllerTest.java @@ -1,6 +1,5 @@ package io.avaje.http.client; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.example.webserver.ErrorResponse; import org.example.webserver.HelloDto; @@ -23,7 +22,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import static java.net.http.HttpClient.Version.*; +import static java.net.http.HttpClient.Version.HTTP_1_1; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; From 1d754d119159683492a1b4ac0c69cacdd6a5a89f Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 17 Jan 2023 00:18:35 +1300 Subject: [PATCH 59/67] Refactor rename HttpClient.converters() to HttpClient.bodyAdapter() with deprecation --- .../io/avaje/http/client/DHttpClientContext.java | 2 +- .../main/java/io/avaje/http/client/HttpClient.java | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) 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 56eabbd..cd44ab7 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientContext.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientContext.java @@ -95,7 +95,7 @@ public HttpClientRequest request() { } @Override - public BodyAdapter converters() { + public BodyAdapter bodyAdapter() { return bodyAdapter; } diff --git a/client/src/main/java/io/avaje/http/client/HttpClient.java b/client/src/main/java/io/avaje/http/client/HttpClient.java index cb4298e..d351c76 100644 --- a/client/src/main/java/io/avaje/http/client/HttpClient.java +++ b/client/src/main/java/io/avaje/http/client/HttpClient.java @@ -66,12 +66,22 @@ static Builder builder() { HttpClientRequest request(); /** + * Deprecated - migrate to {@link #bodyAdapter()}. + *

* Return the body adapter used by the client context. *

* This is the body adapter used to convert request and response * bodies to java types. For example using Jackson with JSON payloads. */ - BodyAdapter converters(); + @Deprecated + default BodyAdapter converters() { + return bodyAdapter(); + } + + /** + * Return the BodyAdapter that this client is using. + */ + BodyAdapter bodyAdapter(); /** * Return the current aggregate metrics. From 3826b47837fef8bac6e4503df9ad88ffd8af9a8b Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 17 Jan 2023 00:21:50 +1300 Subject: [PATCH 60/67] Mark HttpClientContext.builder() as deprecated, migrate to HttpClient.builder() --- .../src/main/java/io/avaje/http/client/HttpClientContext.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/src/main/java/io/avaje/http/client/HttpClientContext.java b/client/src/main/java/io/avaje/http/client/HttpClientContext.java index c628948..23d7506 100644 --- a/client/src/main/java/io/avaje/http/client/HttpClientContext.java +++ b/client/src/main/java/io/avaje/http/client/HttpClientContext.java @@ -33,6 +33,8 @@ public interface HttpClientContext extends io.avaje.http.client.HttpClient { /** + * Deprecated - migrate to {@link io.avaje.http.client.HttpClient#builder()}. + *

* Return the builder to config and build the client context. * *

{@code
@@ -48,6 +50,7 @@ public interface HttpClientContext extends io.avaje.http.client.HttpClient {
    *
    * }
*/ + @Deprecated static HttpClientContext.Builder builder() { return new DHttpClientContextBuilder(); } From 89af6d1987ca817651647b1f251ec20414c1408b Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 17 Jan 2023 00:31:41 +1300 Subject: [PATCH 61/67] Additionally add deprecation to HttpClientContext --- .../src/main/java/io/avaje/http/client/HttpClientContext.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/src/main/java/io/avaje/http/client/HttpClientContext.java b/client/src/main/java/io/avaje/http/client/HttpClientContext.java index 23d7506..0e2289b 100644 --- a/client/src/main/java/io/avaje/http/client/HttpClientContext.java +++ b/client/src/main/java/io/avaje/http/client/HttpClientContext.java @@ -12,6 +12,9 @@ import java.util.concurrent.Executor; /** + * Deprecated in favor of {@link io.avaje.http.client.HttpClient}. + * Migrate to using {@link io.avaje.http.client.HttpClient#builder()}. + *

* The HTTP client context that we use to build and process requests. * *

{@code
@@ -30,6 +33,7 @@
  *
  * }
*/ +@Deprecated public interface HttpClientContext extends io.avaje.http.client.HttpClient { /** From 76a006a095a055e845e951d3c721cc830fe00b16 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 17 Jan 2023 07:41:30 +1300 Subject: [PATCH 62/67] #59 - Add support for setting connection timeout on the Builder --- .../src/main/java/io/avaje/http/client/DBaseBuilder.java | 5 +++-- .../main/java/io/avaje/http/client/DHttpClientBuilder.java | 6 ++++++ .../io/avaje/http/client/DHttpClientContextBuilder.java | 6 ++++++ client/src/main/java/io/avaje/http/client/HttpClient.java | 7 +++++++ .../main/java/io/avaje/http/client/HttpClientContext.java | 7 +++++++ client/src/test/java/io/avaje/http/client/BaseWebTest.java | 4 ++++ .../java/io/avaje/http/client/HelloControllerTest.java | 1 + 7 files changed, 34 insertions(+), 2 deletions(-) diff --git a/client/src/main/java/io/avaje/http/client/DBaseBuilder.java b/client/src/main/java/io/avaje/http/client/DBaseBuilder.java index 789316b..57cb5a7 100644 --- a/client/src/main/java/io/avaje/http/client/DBaseBuilder.java +++ b/client/src/main/java/io/avaje/http/client/DBaseBuilder.java @@ -23,6 +23,7 @@ abstract class DBaseBuilder { java.net.http.HttpClient client; String baseUrl; boolean requestLogging = true; + Duration connectionTimeout = Duration.ofSeconds(20); Duration requestTimeout = Duration.ofSeconds(20); BodyAdapter bodyAdapter; RetryHandler retryHandler; @@ -92,9 +93,9 @@ private RequestIntercept buildIntercept() { } private java.net.http.HttpClient defaultClient() { - final java.net.http.HttpClient.Builder builder = java.net.http.HttpClient.newBuilder() + final var builder = java.net.http.HttpClient.newBuilder() .followRedirects(redirect) - .connectTimeout(Duration.ofSeconds(20)); + .connectTimeout(connectionTimeout); if (cookieHandler != null) { builder.cookieHandler(cookieHandler); } diff --git a/client/src/main/java/io/avaje/http/client/DHttpClientBuilder.java b/client/src/main/java/io/avaje/http/client/DHttpClientBuilder.java index 18b3d1f..1641c23 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientBuilder.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientBuilder.java @@ -27,6 +27,12 @@ public HttpClient.Builder baseUrl(String baseUrl) { return this; } + @Override + public HttpClient.Builder connectionTimeout(Duration connectionTimeout) { + this.connectionTimeout = connectionTimeout; + return this; + } + @Override public HttpClient.Builder requestTimeout(Duration requestTimeout) { this.requestTimeout = requestTimeout; diff --git a/client/src/main/java/io/avaje/http/client/DHttpClientContextBuilder.java b/client/src/main/java/io/avaje/http/client/DHttpClientContextBuilder.java index 08675e4..98ba0a8 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientContextBuilder.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientContextBuilder.java @@ -28,6 +28,12 @@ public HttpClientContext.Builder baseUrl(String baseUrl) { return this; } + @Override + public HttpClientContext.Builder connectionTimeout(Duration connectionTimeout) { + this.connectionTimeout = connectionTimeout; + return this; + } + @Override public HttpClientContext.Builder requestTimeout(Duration requestTimeout) { this.requestTimeout = requestTimeout; diff --git a/client/src/main/java/io/avaje/http/client/HttpClient.java b/client/src/main/java/io/avaje/http/client/HttpClient.java index d351c76..ca95012 100644 --- a/client/src/main/java/io/avaje/http/client/HttpClient.java +++ b/client/src/main/java/io/avaje/http/client/HttpClient.java @@ -125,6 +125,13 @@ interface Builder { */ Builder baseUrl(String baseUrl); + /** + * Set the connection timeout to use. + * + * @see java.net.http.HttpClient.Builder#connectTimeout(Duration) + */ + Builder connectionTimeout(Duration connectionTimeout); + /** * Set the default request timeout. * diff --git a/client/src/main/java/io/avaje/http/client/HttpClientContext.java b/client/src/main/java/io/avaje/http/client/HttpClientContext.java index 0e2289b..12ce33d 100644 --- a/client/src/main/java/io/avaje/http/client/HttpClientContext.java +++ b/client/src/main/java/io/avaje/http/client/HttpClientContext.java @@ -102,6 +102,13 @@ interface Builder { */ Builder baseUrl(String baseUrl); + /** + * Set the connection timeout to use. + * + * @see java.net.http.HttpClient.Builder#connectTimeout(Duration) + */ + Builder connectionTimeout(Duration connectionTimeout); + /** * Set the default request timeout. * diff --git a/client/src/test/java/io/avaje/http/client/BaseWebTest.java b/client/src/test/java/io/avaje/http/client/BaseWebTest.java index 6003b70..7b38ff6 100644 --- a/client/src/test/java/io/avaje/http/client/BaseWebTest.java +++ b/client/src/test/java/io/avaje/http/client/BaseWebTest.java @@ -6,6 +6,8 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import java.time.Duration; + public class BaseWebTest { static Javalin webServer; @@ -26,6 +28,8 @@ public static void shutdown() { public static HttpClientContext client() { return HttpClientContext.builder() .baseUrl(baseUrl) + .connectionTimeout(Duration.ofSeconds(1)) + .requestTimeout(Duration.ofSeconds(1)) .bodyAdapter(new JacksonBodyAdapter(new ObjectMapper())) .build(); } 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 c81792b..c2b1aab 100644 --- a/client/src/test/java/io/avaje/http/client/HelloControllerTest.java +++ b/client/src/test/java/io/avaje/http/client/HelloControllerTest.java @@ -36,6 +36,7 @@ class HelloControllerTest extends BaseWebTest { void newClientTest() { HttpClient client = HttpClient.builder() .baseUrl("http://localhost:8887") + .connectionTimeout(Duration.ofSeconds(1)) .bodyAdapter(new JacksonBodyAdapter()) .build(); From 141cbc9ede39fd151a5fb43abad82b0385e860bb Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 17 Jan 2023 07:45:47 +1300 Subject: [PATCH 63/67] [maven-release-plugin] prepare release avaje-http-client-1.22 --- client/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index 70bdae8..408247c 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -10,11 +10,11 @@ io.avaje avaje-http-client - 1.22-SNAPSHOT + 1.22 scm:git:git@github.com:avaje/avaje-http-client.git - HEAD + avaje-http-client-1.22 From 7495a4c3a3a02b83695e5c31579a730185b6305f Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 17 Jan 2023 07:46:09 +1300 Subject: [PATCH 64/67] [maven-release-plugin] prepare for next development iteration --- client/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index 408247c..6f7c1bf 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -10,11 +10,11 @@ io.avaje avaje-http-client - 1.22 + 1.23-SNAPSHOT scm:git:git@github.com:avaje/avaje-http-client.git - avaje-http-client-1.22 + HEAD From 0806379ac127886a873513528a6a6368d92f8be4 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 17 Jan 2023 08:05:33 +1300 Subject: [PATCH 65/67] Bump to 1.23-SNAPSHOT after release --- gson-adapter/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gson-adapter/pom.xml b/gson-adapter/pom.xml index 9bd29fc..c487768 100644 --- a/gson-adapter/pom.xml +++ b/gson-adapter/pom.xml @@ -10,7 +10,7 @@ io.avaje avaje-http-client-gson - 1.22-SNAPSHOT + 1.23-SNAPSHOT scm:git:git@github.com:avaje/avaje-http-client.git @@ -28,7 +28,7 @@ io.avaje avaje-http-client - 1.22-SNAPSHOT + 1.23-SNAPSHOT provided From 09f562506e539b23b674cb2a472980a73a852a44 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 17 Jan 2023 08:36:56 +1300 Subject: [PATCH 66/67] Bump to 1.23-SNAPSHOT after release --- test/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/pom.xml b/test/pom.xml index a6d110a..4f16915 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -16,13 +16,13 @@ io.avaje avaje-http-client - 1.22-SNAPSHOT + 1.23-SNAPSHOT io.avaje avaje-http-client-gson - 1.22-SNAPSHOT + 1.23-SNAPSHOT From b6a638af75c25652b415615316967af37734c14f Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 17 Jan 2023 10:43:40 +1300 Subject: [PATCH 67/67] Update README specify that this has moved to avaje-http --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 5513b53..e40a5b0 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,14 @@ +# MOVED !!! + +This project has been moved to be a sub-module of avaje-http. + +It is now at: https://github.com/avaje/avaje-http/tree/master/http-client + + +----------------- + + + [![Build](https://github.com/avaje/avaje-http-client/actions/workflows/build.yml/badge.svg)](https://github.com/avaje/avaje-http-client/actions/workflows/build.yml) [![Maven Central](https://img.shields.io/maven-central/v/io.avaje/avaje-http-client.svg?label=Maven%20Central)](https://mvnrepository.com/artifact/io.avaje/avaje-http-client) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/avaje/avaje-http-client/blob/master/LICENSE)