diff --git a/src/main/java/com/github/bannmann/restflow/AbstractRequester.java b/src/main/java/com/github/bannmann/restflow/AbstractRequester.java new file mode 100644 index 0000000..e3f53a9 --- /dev/null +++ b/src/main/java/com/github/bannmann/restflow/AbstractRequester.java @@ -0,0 +1,118 @@ +package com.github.bannmann.restflow; + +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import lombok.RequiredArgsConstructor; + +import dev.failsafe.Failsafe; +import dev.failsafe.Policy; + +@RequiredArgsConstructor +abstract class AbstractRequester implements Requester +{ + protected final HttpRequest request; + protected final ClientConfig clientConfig; + protected final ConcurrentMap diagnosticsData = new ConcurrentHashMap<>(); + + @Override + public final CompletableFuture start() + { + var diagnosticsDataSupplier = clientConfig.getDiagnosticsDataSupplier(); + if (diagnosticsDataSupplier != null) + { + Map values = diagnosticsDataSupplier.get(); + diagnosticsData.putAll(values); + } + + return send().thenApply(this::extractValue); + } + + private CompletableFuture> send() + { + List>> policies = clientConfig.getPolicies(); + if (!policies.isEmpty()) + { + return Failsafe.with(policies) + .getStageAsync(context -> sendOnce()); + } + + return sendOnce(); + } + + private CompletableFuture> sendOnce() + { + return clientConfig.getHttpClient() + .sendAsync(request, getBodyHandler()) + .handle(this::addDetailsForLowLevelExceptions) + .thenApply(this::failOrPassThrough); + } + + protected abstract HttpResponse.BodyHandler getBodyHandler(); + + private T addDetailsForLowLevelExceptions(T result, Throwable throwable) + { + if (throwable != null) + { + String message = String.format("Request to URL %s failed", request.uri()); + throw new RequestFailureException(request, message, throwable, diagnosticsData); + } + return result; + } + + private HttpResponse failOrPassThrough(HttpResponse response) + { + verifyNoErrors(response); + return response; + } + + protected abstract void verifyNoErrors(HttpResponse response); + + protected abstract R extractValue(HttpResponse response); + + protected boolean isFailure(int responseStatus) + { + return !isSuccess(responseStatus); + } + + protected boolean isSuccess(int responseStatus) + { + return responseStatus >= 200 && responseStatus < 300; + } + + protected RequestStatusException createException(HttpResponse response) + { + int status = response.statusCode(); + String body = getStringBody(response); + String message = String.format("Got status %d with message '%s' for %s %s", + status, + body, + request.method(), + request.uri()); + + return RequestStatusException.builder() + .message(message) + .request(request) + .response(response) + .status(status) + .body(body) + .diagnosticsData(diagnosticsData) + .build(); + } + + private String getStringBody(HttpResponse response) + { + T body = response.body(); + if (body != null) + { + return body.toString() + .trim(); + } + return null; + } +} diff --git a/src/main/java/com/github/bannmann/restflow/ClientConfig.java b/src/main/java/com/github/bannmann/restflow/ClientConfig.java index 56948f1..17a0a3a 100644 --- a/src/main/java/com/github/bannmann/restflow/ClientConfig.java +++ b/src/main/java/com/github/bannmann/restflow/ClientConfig.java @@ -3,6 +3,8 @@ import java.net.http.HttpClient; import java.net.http.HttpResponse; import java.util.List; +import java.util.Map; +import java.util.function.Supplier; import javax.json.bind.Jsonb; @@ -20,6 +22,8 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public final class ClientConfig { + private final Supplier> diagnosticsDataSupplier; + private final @NonNull HttpClient httpClient; @Singular diff --git a/src/main/java/com/github/bannmann/restflow/ExecuteHandle.java b/src/main/java/com/github/bannmann/restflow/ExecuteHandle.java index b71ecf0..daa1f5f 100644 --- a/src/main/java/com/github/bannmann/restflow/ExecuteHandle.java +++ b/src/main/java/com/github/bannmann/restflow/ExecuteHandle.java @@ -16,7 +16,7 @@ public ExecuteHandle customizingRequest(RequestCustomizer requestCustomizer) public CompletableFuture execute() { - return Requesters.createRegular(requestSpecification) + return RegularRequester.forSpec(requestSpecification) .start(); } } diff --git a/src/main/java/com/github/bannmann/restflow/FetchHandle.java b/src/main/java/com/github/bannmann/restflow/FetchHandle.java index 91ef888..79d8aa2 100644 --- a/src/main/java/com/github/bannmann/restflow/FetchHandle.java +++ b/src/main/java/com/github/bannmann/restflow/FetchHandle.java @@ -17,13 +17,13 @@ public FetchHandle customizingRequest(RequestCustomizer requestCustomizer) public CompletableFuture fetch() { - return Requesters.createRegular(requestSpecification) + return RegularRequester.forSpec(requestSpecification) .start(); } public CompletableFuture> tryFetch() { - return Requesters.createOptional(requestSpecification) + return OptionalRequester.forSpec(requestSpecification) .start(); } } diff --git a/src/main/java/com/github/bannmann/restflow/OptionalRequester.java b/src/main/java/com/github/bannmann/restflow/OptionalRequester.java new file mode 100644 index 0000000..eb69597 --- /dev/null +++ b/src/main/java/com/github/bannmann/restflow/OptionalRequester.java @@ -0,0 +1,54 @@ +package com.github.bannmann.restflow; + +import java.net.http.HttpResponse; +import java.util.Optional; + +import com.github.mizool.core.rest.errorhandling.HttpStatus; + +final class OptionalRequester extends AbstractRequester> +{ + public static Requester> forSpec(RequestSpecification spec) + { + return new OptionalRequester<>(spec); + } + + private final RequestSpecification spec; + + private OptionalRequester(RequestSpecification spec) + { + super(spec.createFinalRequest(), spec.getClientConfig()); + this.spec = spec; + } + + @Override + protected HttpResponse.BodyHandler getBodyHandler() + { + return spec.getResponseBodyConfig() + .getBodyHandler(); + } + + @Override + protected void verifyNoErrors(HttpResponse response) + { + int responseStatus = response.statusCode(); + if (isFailure(responseStatus) && responseStatus != HttpStatus.NOT_FOUND) + { + throw createException(response); + } + } + + @Override + protected Optional extractValue(HttpResponse response) + { + if (response.statusCode() == HttpStatus.NOT_FOUND) + { + return Optional.empty(); + } + + B body = response.body(); + R result = spec.getResponseBodyConfig() + .getResponseConverter() + .apply(body); + return Optional.of(result); + } +} diff --git a/src/main/java/com/github/bannmann/restflow/RegularRequester.java b/src/main/java/com/github/bannmann/restflow/RegularRequester.java new file mode 100644 index 0000000..07b4349 --- /dev/null +++ b/src/main/java/com/github/bannmann/restflow/RegularRequester.java @@ -0,0 +1,44 @@ +package com.github.bannmann.restflow; + +import java.net.http.HttpResponse; + +final class RegularRequester extends AbstractRequester +{ + public static Requester forSpec(RequestSpecification spec) + { + return new RegularRequester<>(spec); + } + + private final RequestSpecification spec; + + private RegularRequester(RequestSpecification spec) + { + super(spec.createFinalRequest(), spec.getClientConfig()); + this.spec = spec; + } + + @Override + protected HttpResponse.BodyHandler getBodyHandler() + { + return spec.getResponseBodyConfig() + .getBodyHandler(); + } + + @Override + protected void verifyNoErrors(HttpResponse response) + { + int responseStatus = response.statusCode(); + if (isFailure(responseStatus)) + { + throw createException(response); + } + } + + @Override + protected R extractValue(HttpResponse response) + { + return spec.getResponseBodyConfig() + .getResponseConverter() + .apply(response.body()); + } +} diff --git a/src/main/java/com/github/bannmann/restflow/RequestException.java b/src/main/java/com/github/bannmann/restflow/RequestException.java index e7bfed4..5dd6a2f 100644 --- a/src/main/java/com/github/bannmann/restflow/RequestException.java +++ b/src/main/java/com/github/bannmann/restflow/RequestException.java @@ -1,6 +1,7 @@ package com.github.bannmann.restflow; import java.net.http.HttpRequest; +import java.util.Map; import lombok.Getter; @@ -12,10 +13,13 @@ public abstract class RequestException extends RuntimeException { private final transient HttpRequest request; + private final transient Map diagnosticsData; - protected RequestException(HttpRequest request, String message, Throwable cause) + protected RequestException( + HttpRequest request, String message, Throwable cause, Map diagnosticsData) { super(message, cause); this.request = request; + this.diagnosticsData = diagnosticsData; } } diff --git a/src/main/java/com/github/bannmann/restflow/RequestFailureException.java b/src/main/java/com/github/bannmann/restflow/RequestFailureException.java index 9295938..158724e 100644 --- a/src/main/java/com/github/bannmann/restflow/RequestFailureException.java +++ b/src/main/java/com/github/bannmann/restflow/RequestFailureException.java @@ -1,6 +1,7 @@ package com.github.bannmann.restflow; import java.net.http.HttpRequest; +import java.util.Map; /** * Thrown when the request failed without a response. @@ -9,8 +10,9 @@ */ public class RequestFailureException extends RequestException { - public RequestFailureException(HttpRequest request, String message, Throwable cause) + public RequestFailureException( + HttpRequest request, String message, Throwable cause, Map diagnosticsData) { - super(request, message, cause); + super(request, message, cause, diagnosticsData); } } diff --git a/src/main/java/com/github/bannmann/restflow/RequestHandle.java b/src/main/java/com/github/bannmann/restflow/RequestHandle.java index b8ec1d8..4c52464 100644 --- a/src/main/java/com/github/bannmann/restflow/RequestHandle.java +++ b/src/main/java/com/github/bannmann/restflow/RequestHandle.java @@ -25,7 +25,7 @@ public FetchHandle returning(Class responseClass) var responseBodyConfig = new ResponseBodyConfig<>(HttpResponse.BodyHandlers.ofString(), s -> clientConfig.getJsonb() .fromJson(s, responseClass)); - var spec = new RequestSpecification(request, responseBodyConfig, clientConfig); + var spec = new RequestSpecification<>(request, responseBodyConfig, clientConfig); return new FetchHandle<>(spec); } @@ -34,7 +34,7 @@ public FetchHandle returning(Type runtimeType) var responseBodyConfig = new ResponseBodyConfig(HttpResponse.BodyHandlers.ofString(), s -> clientConfig.getJsonb() .fromJson(s, runtimeType)); - var spec = new RequestSpecification(request, responseBodyConfig, clientConfig); + var spec = new RequestSpecification<>(request, responseBodyConfig, clientConfig); return new FetchHandle<>(spec); } @@ -56,7 +56,7 @@ public FetchHandle returningJsonArray() public FetchHandle returningString() { var responseBodyConfig = new ResponseBodyConfig<>(HttpResponse.BodyHandlers.ofString(), string -> string); - var spec = new RequestSpecification(request, responseBodyConfig, clientConfig); + var spec = new RequestSpecification<>(request, responseBodyConfig, clientConfig); return new FetchHandle<>(spec); } @@ -64,7 +64,7 @@ public FetchHandle returningInputStream() { var responseBodyConfig = new ResponseBodyConfig<>(HttpResponse.BodyHandlers.ofInputStream(), inputStream -> inputStream); - var spec = new RequestSpecification(request, responseBodyConfig, clientConfig); + var spec = new RequestSpecification<>(request, responseBodyConfig, clientConfig); return new FetchHandle<>(spec); } @@ -72,7 +72,7 @@ public ExecuteHandle returningNothing() { // Note: we use ofString() handler because discarding() would also discard the body of an error response. var responseBodyConfig = new ResponseBodyConfig(HttpResponse.BodyHandlers.ofString(), v -> null); - var spec = new RequestSpecification(request, responseBodyConfig, clientConfig); + var spec = new RequestSpecification<>(request, responseBodyConfig, clientConfig); return new ExecuteHandle(spec); } } diff --git a/src/main/java/com/github/bannmann/restflow/RequestStatusException.java b/src/main/java/com/github/bannmann/restflow/RequestStatusException.java index 765cc77..c255530 100644 --- a/src/main/java/com/github/bannmann/restflow/RequestStatusException.java +++ b/src/main/java/com/github/bannmann/restflow/RequestStatusException.java @@ -2,6 +2,7 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.util.Map; import lombok.Builder; import lombok.Getter; @@ -20,9 +21,15 @@ public class RequestStatusException extends RequestException @Builder public RequestStatusException( - String message, Throwable cause, HttpRequest request, HttpResponse response, int status, String body) + String message, + Throwable cause, + HttpRequest request, + HttpResponse response, + int status, + String body, + Map diagnosticsData) { - super(request, message, cause); + super(request, message, cause, diagnosticsData); this.response = response; this.status = status; diff --git a/src/main/java/com/github/bannmann/restflow/Requesters.java b/src/main/java/com/github/bannmann/restflow/Requesters.java deleted file mode 100644 index 289736a..0000000 --- a/src/main/java/com/github/bannmann/restflow/Requesters.java +++ /dev/null @@ -1,214 +0,0 @@ -package com.github.bannmann.restflow; - -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.function.Function; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.experimental.UtilityClass; -import lombok.extern.slf4j.Slf4j; - -import com.github.mizool.core.rest.errorhandling.HttpStatus; -import dev.failsafe.Failsafe; -import dev.failsafe.Policy; - -@Slf4j -@UtilityClass -class Requesters -{ - @RequiredArgsConstructor - private abstract static class AbstractRequester implements Requester - { - protected final HttpRequest request; - protected final ClientConfig clientConfig; - - @Override - public final CompletableFuture start() - { - return send().thenApply(this::extractValue); - } - - private CompletableFuture> send() - { - List>> policies = clientConfig.getPolicies(); - if (!policies.isEmpty()) - { - return Failsafe.with(policies) - .getStageAsync(context -> sendOnce()); - } - - return sendOnce(); - } - - private CompletableFuture> sendOnce() - { - return clientConfig.getHttpClient() - .sendAsync(request, getBodyHandler()) - .handle(this::addDetailsForLowLevelExceptions) - .thenApply(this::failOrPassThrough); - } - - protected abstract HttpResponse.BodyHandler getBodyHandler(); - - private T addDetailsForLowLevelExceptions(T result, Throwable throwable) - { - if (throwable != null) - { - String message = String.format("Request to URL %s failed", request.uri()); - throw new RequestFailureException(request, message, throwable); - } - return result; - } - - private HttpResponse failOrPassThrough(HttpResponse response) - { - verifyNoErrors(response); - return response; - } - - protected abstract void verifyNoErrors(HttpResponse response); - - protected abstract R extractValue(HttpResponse response); - - protected boolean isFailure(int responseStatus) - { - return !isSuccess(responseStatus); - } - - protected boolean isSuccess(int responseStatus) - { - return responseStatus >= 200 && responseStatus < 300; - } - - protected RequestStatusException createException(HttpResponse response) - { - int status = response.statusCode(); - String body = getStringBody(response); - String message = String.format("Got status %d with message '%s' for %s %s", - status, - body, - request.method(), - request.uri()); - - return RequestStatusException.builder() - .message(message) - .request(request) - .response(response) - .status(status) - .body(body) - .build(); - } - - private String getStringBody(HttpResponse response) - { - T body = response.body(); - if (body != null) - { - return body.toString() - .trim(); - } - return null; - } - } - - private static final class RegularRequester extends AbstractRequester - { - @Getter - private final HttpResponse.BodyHandler bodyHandler; - - private final Function responseConverter; - - private RegularRequester( - HttpResponse.BodyHandler bodyHandler, - HttpRequest request, - Function responseConverter, - ClientConfig clientConfig) - { - super(request, clientConfig); - this.bodyHandler = bodyHandler; - this.responseConverter = responseConverter; - } - - @Override - protected void verifyNoErrors(HttpResponse response) - { - int responseStatus = response.statusCode(); - if (isFailure(responseStatus)) - { - throw createException(response); - } - } - - @Override - protected R extractValue(HttpResponse response) - { - return responseConverter.apply(response.body()); - } - } - - private static final class OptionalRequester extends AbstractRequester> - { - @Getter - private final HttpResponse.BodyHandler bodyHandler; - - private final Function responseConverter; - - private OptionalRequester( - HttpResponse.BodyHandler bodyHandler, - HttpRequest request, - Function responseConverter, - ClientConfig clientConfig) - { - super(request, clientConfig); - this.bodyHandler = bodyHandler; - this.responseConverter = responseConverter; - } - - @Override - protected void verifyNoErrors(HttpResponse response) - { - int responseStatus = response.statusCode(); - if (isFailure(responseStatus) && responseStatus != HttpStatus.NOT_FOUND) - { - throw createException(response); - } - } - - @Override - protected Optional extractValue(HttpResponse response) - { - if (response.statusCode() == HttpStatus.NOT_FOUND) - { - return Optional.empty(); - } - - B body = response.body(); - R result = responseConverter.apply(body); - return Optional.of(result); - } - } - - public static Requester createRegular(RequestSpecification spec) - { - return new RegularRequester<>(spec.getResponseBodyConfig() - .getBodyHandler(), - spec.createFinalRequest(), - spec.getResponseBodyConfig() - .getResponseConverter(), - spec.getClientConfig()); - } - - public static Requester> createOptional(RequestSpecification spec) - { - return new OptionalRequester<>(spec.getResponseBodyConfig() - .getBodyHandler(), - spec.createFinalRequest(), - spec.getResponseBodyConfig() - .getResponseConverter(), - spec.getClientConfig()); - } -}