* Noting that afterResponse interceptors are processed in reverse order.
*/
-class DRequestInterceptors implements RequestIntercept {
+final class DRequestInterceptors implements RequestIntercept {
private final List
+ * Note that when testing with async requests we frequently use {@code .join()}
+ * on the {@code CompletableFuture} such that the main thread waits for the async
+ * processing to complete. After that various asserts can run knowing that the
+ * async callback code has been executed.
+ *
+ *
+ * In this example POST async that will return a bean converted from json response.
+ *
+ * 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.
+ */
+ @Deprecated
+ default BodyAdapter converters() {
+ return bodyAdapter();
+ }
+
+ /**
+ * Return the BodyAdapter that this client is using.
+ */
+ BodyAdapter bodyAdapter();
+
+ /**
+ * Return the current aggregate metrics.
+ *
+ * These metrics are collected for all requests sent via this context.
+ */
+ 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.
+ */
+ HttpClient.Metrics metrics(boolean reset);
+
+ /**
+ * Builds the HttpClient.
+ *
+ *
+ * Note that the base url can be replaced via {@link HttpClientRequest#url(String)}.
+ */
+ 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.
+ *
+ * @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.
+ *
+ *
+ * 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.
+ *
+ *
* The HTTP client context that we use to build and process requests.
*
*
* Return the builder to config and build 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.
+ * Deprecated - migrate to builder().
*/
- BodyAdapter converters();
-
- /**
- * Return the underlying http client.
- */
- HttpClient httpClient();
-
- /**
- * 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
+ * In this example POST async that will return a bean converted from json response.
+ *
@@ -80,6 +108,51 @@ public interface HttpClientResponse {
*/
+ * 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
+ */
+
+ * 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
+ */
+
+ * 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
+ */
+
@@ -141,6 +214,14 @@ public interface HttpClientResponse {
/**
* Return the response using the given response body handler.
*/
-
- * 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 Testing and .join()
+ * Example using .join() for testing purposes
+ * {@code
+ *
+ * clientContext.request()
+ * ...
+ * .POST().async()
+ * .bean(HelloDto.class)
+ * .whenComplete((helloDto, throwable) -> {
+ * ...
+ * }).join(); // wait for async processing to complete
+ *
+ * // can assert now ...
+ * assertThat(...)
+ *
+ * }
+ *
+ * Example async().bean()
+ * {@code
+ *
+ * clientContext.request()
+ * ...
+ * .POST().async()
+ * .bean(HelloDto.class)
+ * .whenComplete((helloDto, throwable) -> {
+ *
+ * if (throwable != null) {
+ * HttpException httpException = (HttpException) throwable.getCause();
+ * int statusCode = httpException.statusCode();
+ *
+ * // maybe convert json error response body to a bean (using Jackson/Gson)
+ * MyErrorBean errorResponse = httpException.bean(MyErrorBean.class);
+ * ..
+ *
+ * } else {
+ * // process helloDto
+ * ...
+ * }
+ * });
+ * }
*/
public interface HttpAsyncResponse {
@@ -144,7 +194,7 @@ public interface HttpAsyncResponse {
* 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
+ */
+
> 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
+ */
+
{@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 {
+
+ /**
+ * Return the builder to config and build the client context.
+ *
+ * {@code
+ *
+ * HttpClient client = HttpClient.builder()
+ * .baseUrl("http://localhost:8080")
+ * .bodyAdapter(new JacksonBodyAdapter())
+ * .build();
+ *
+ * HttpResponse
+ */
+ static Builder builder() {
+ return new DHttpClientBuilder();
+ }
+
+ /**
+ * Return the http client API implementation.
+ *
+ * @param clientInterface A @Client
interface with annotated API methods.
+ * @param {@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.
+ * Suppression
+ * {@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.
+ */
+ 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 extends HttpClientContext.Metrics {
+
+ }
+}
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 139c63b..12ce33d 100644
--- a/client/src/main/java/io/avaje/http/client/HttpClientContext.java
+++ b/client/src/main/java/io/avaje/http/client/HttpClientContext.java
@@ -1,21 +1,25 @@
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.net.http.HttpClient;
-import java.net.http.HttpResponse;
import java.time.Duration;
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()}.
+ * {@code
*
- * HttpClientContext ctx = HttpClientContext.newBuilder()
+ * HttpClientContext ctx = HttpClientContext.builder()
* .baseUrl("http://localhost:8080")
* .bodyAdapter(new JacksonBodyAdapter())
* .build();
@@ -29,14 +33,17 @@
*
* }
*/
-public interface HttpClientContext {
+@Deprecated
+public interface HttpClientContext extends io.avaje.http.client.HttpClient {
/**
+ * Deprecated - migrate to {@link io.avaje.http.client.HttpClient#builder()}.
+ * {@code
*
- * HttpClientContext ctx = HttpClientContext.newBuilder()
+ * HttpClientContext ctx = HttpClientContext.builder()
* .baseUrl("http://localhost:8080")
* .bodyAdapter(new JacksonBodyAdapter())
* .build();
@@ -47,80 +54,25 @@ public interface HttpClientContext {
*
* }
*/
- static HttpClientContext.Builder newBuilder() {
+ @Deprecated
+ static HttpClientContext.Builder builder() {
return new DHttpClientContextBuilder();
}
/**
- * Return the http client API implementation.
- *
- * @param clientInterface A @Client
interface with annotated API methods.
- * @param Content-Encoding
http header.
- *
- * @param httpResponse The HTTP response
- * @return The decoded content
- */
- byte[] decodeContent(HttpResponse{@code
*
- * HttpClientContext ctx = HttpClientContext.newBuilder()
+ * HttpClientContext ctx = HttpClientContext.builder()
* .baseUrl("http://localhost:8080")
* .bodyAdapter(new JacksonBodyAdapter())
* .build();
@@ -150,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.
*
@@ -289,12 +248,22 @@ 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.
*
*
{@code
*
- * HttpClientContext ctx = HttpClientContext.newBuilder()
+ * HttpClientContext ctx = HttpClientContext.builder()
* .baseUrl("http://localhost:8080")
* .bodyAdapter(new JacksonBodyAdapter())
* .build();
@@ -308,5 +277,47 @@ interface Builder {
* }
*/
HttpClientContext build();
+
+ /**
+ * The state of the builder with methods to read the set state.
+ */
+ 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();
+
+ /**
+ * 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/HttpClientResponse.java b/client/src/main/java/io/avaje/http/client/HttpClientResponse.java
index 67e1ee8..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;
@@ -14,6 +15,32 @@ public interface HttpClientResponse {
/**
* Send the request async using CompletableFuture.
+ *
+ * Example async().bean()
+ * {@code
+ *
+ * clientContext.request()
+ * ...
+ * .POST().async()
+ * .bean(HelloDto.class)
+ * .whenComplete((helloDto, throwable) -> {
+ *
+ * if (throwable != null) {
+ * HttpException httpException = (HttpException) throwable.getCause();
+ * int statusCode = httpException.statusCode();
+ *
+ * // maybe convert json error response body to a bean (using Jackson/Gson)
+ * MyErrorBean errorResponse = httpException.bean(MyErrorBean.class);
+ * ..
+ *
+ * } else {
+ * // process helloDto
+ * ...
+ * }
+ * });
+ * }
*/
HttpAsyncResponse async();
@@ -59,6 +86,7 @@ public interface HttpClientResponse {
*/
{@code
*
- * HttpClientContext.newBuilder()
+ * HttpClientContext.builder()
* .baseUrl(baseUrl)
* .bodyAdapter(new JacksonBodyAdapter())
* .build();
*
* }
*/
-public class JacksonBodyAdapter implements BodyAdapter {
+public final class JacksonBodyAdapter implements BodyAdapter {
private final ObjectMapper mapper;
@@ -98,7 +99,7 @@ public T readBody(String content) {
try {
return reader.readValue(content);
} catch (IOException e) {
- throw new RuntimeException(e);
+ throw new UncheckedIOException(e);
}
}
@@ -107,7 +108,7 @@ public T read(BodyContent bodyContent) {
try {
return reader.readValue(bodyContent.content());
} catch (IOException e) {
- throw new RuntimeException(e);
+ throw new UncheckedIOException(e);
}
}
}
@@ -132,7 +133,7 @@ public BodyContent write(T bean) {
try {
return BodyContent.asJson(writer.writeValueAsBytes(bean));
} catch (JsonProcessingException e) {
- throw new RuntimeException(e);
+ throw new UncheckedIOException(e);
}
}
}
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 e04a0dc..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,29 +1,31 @@
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
*
- * HttpClientContext.newBuilder()
+ * HttpClientContext.builder()
* .baseUrl(baseUrl)
* .bodyAdapter(new JsonbBodyAdapter())
* .build();
*
* }
*/
-public class JsonbBodyAdapter implements BodyAdapter {
+public final class JsonbBodyAdapter implements BodyAdapter {
private final Jsonb jsonb;
- private final ConcurrentHashMap> listReader(ParameterizedType cls) {
+ return (BodyReader
>) listReaderCache.computeIfAbsent(cls, aClass -> new JReader<>(jsonb.type(cls).list()));
+ }
+
@SuppressWarnings("unchecked")
@Override
public
> listReader(Class
System.Logger
.
*