From 20dea96bf28273a41853aa37f8f97c72431b53ff Mon Sep 17 00:00:00 2001 From: Richard Marmorstein Date: Tue, 11 May 2021 23:49:00 -0400 Subject: [PATCH 1/4] binary streaming --- src/main/java/com/stripe/net/ApiResource.java | 17 +++ src/main/java/com/stripe/net/HttpClient.java | 75 +++++++++++-- .../stripe/net/HttpURLConnectionClient.java | 31 +++++- .../stripe/net/LiveStripeResponseGetter.java | 36 ++++++ .../java/com/stripe/net/RequestTelemetry.java | 2 +- .../java/com/stripe/net/StripeResponse.java | 14 ++- .../com/stripe/net/StripeResponseGetter.java | 11 ++ .../stripe/net/StripeResponseInterface.java | 37 +++++++ .../com/stripe/net/StripeResponseStream.java | 103 ++++++++++++++++++ .../functional/StripeResponseStreamTest.java | 97 +++++++++++++++++ 10 files changed, 400 insertions(+), 23 deletions(-) create mode 100644 src/main/java/com/stripe/net/StripeResponseInterface.java create mode 100644 src/main/java/com/stripe/net/StripeResponseStream.java create mode 100644 src/test/java/com/stripe/functional/StripeResponseStreamTest.java diff --git a/src/main/java/com/stripe/net/ApiResource.java b/src/main/java/com/stripe/net/ApiResource.java index e0e17307a4c..c23e7a5beed 100644 --- a/src/main/java/com/stripe/net/ApiResource.java +++ b/src/main/java/com/stripe/net/ApiResource.java @@ -22,6 +22,7 @@ import com.stripe.model.StripeRawJsonObject; import com.stripe.model.StripeRawJsonObjectDeserializer; import com.stripe.util.StringUtils; +import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.Charset; @@ -179,6 +180,22 @@ public static T request( return ApiResource.stripeResponseGetter.request(method, url, params, clazz, options); } + public static InputStream requestStream( + ApiResource.RequestMethod method, String url, ApiRequestParams params, RequestOptions options) + throws StripeException { + checkNullTypedParams(url, params); + return requestStream(method, url, params.toMap(), options); + } + + public static InputStream requestStream( + ApiResource.RequestMethod method, + String url, + Map params, + RequestOptions options) + throws StripeException { + return ApiResource.stripeResponseGetter.requestStream(method, url, params, options); + } + public static > T requestCollection( String url, ApiRequestParams params, Class clazz, RequestOptions options) throws StripeException { diff --git a/src/main/java/com/stripe/net/HttpClient.java b/src/main/java/com/stripe/net/HttpClient.java index 40edb282f90..84618660afb 100644 --- a/src/main/java/com/stripe/net/HttpClient.java +++ b/src/main/java/com/stripe/net/HttpClient.java @@ -29,7 +29,7 @@ public abstract class HttpClient { protected HttpClient() {} /** - * Sends the given request to Stripe's API. + * Sends the given request to Stripe's API, buffering the response body into memory. * * @param request the request * @return the response @@ -38,13 +38,24 @@ protected HttpClient() {} public abstract StripeResponse request(StripeRequest request) throws StripeException; /** - * Sends the given request to Stripe's API, handling telemetry if not disabled. + * Sends the given request to Stripe's API, streaming the response body. * * @param request the request * @return the response * @throws StripeException If the request fails for any reason */ - public StripeResponse requestWithTelemetry(StripeRequest request) throws StripeException { + public StripeResponseStream requestStream(StripeRequest request) throws StripeException { + throw new UnsupportedOperationException( + "streamingRequest is unimplemented for this HttpClient"); + } + + @FunctionalInterface + private interface RequestSendFunction { + R apply(StripeRequest request) throws StripeException; + } + + private T sendWithTelemetry( + StripeRequest request, RequestSendFunction send) throws StripeException { Optional telemetryHeaderValue = requestTelemetry.getHeaderValue(request.headers()); if (telemetryHeaderValue.isPresent()) { request = @@ -53,7 +64,7 @@ public StripeResponse requestWithTelemetry(StripeRequest request) throws StripeE Stopwatch stopwatch = Stopwatch.startNew(); - StripeResponse response = this.request(request); + T response = send.apply(request); stopwatch.stop(); @@ -63,23 +74,40 @@ public StripeResponse requestWithTelemetry(StripeRequest request) throws StripeE } /** - * Sends the given request to Stripe's API, retrying the request in cases of intermittent - * problems. + * Sends the given request to Stripe's API, handling telemetry if not disabled. * * @param request the request * @return the response * @throws StripeException If the request fails for any reason */ - public StripeResponse requestWithRetries(StripeRequest request) throws StripeException { + public StripeResponse requestWithTelemetry(StripeRequest request) throws StripeException { + return sendWithTelemetry(request, (r) -> this.request(r)); + } + + /** + * Sends the given request to Stripe's API, streaming the response, and handling telemetry if not + * disabled. + * + * @param request the request + * @return the response + * @throws StripeException If the request fails for any reason + */ + public StripeResponseStream requestStreamWithTelemetry(StripeRequest request) + throws StripeException { + return sendWithTelemetry(request, (r) -> this.requestStream(r)); + } + + public T sendWithRetries( + StripeRequest request, RequestSendFunction send) throws StripeException { ApiConnectionException requestException = null; - StripeResponse response = null; + T response = null; int retry = 0; while (true) { requestException = null; try { - response = this.requestWithTelemetry(request); + response = send.apply(request); } catch (ApiConnectionException e) { requestException = e; } @@ -106,6 +134,31 @@ public StripeResponse requestWithRetries(StripeRequest request) throws StripeExc return response; } + /** + * Sends the given request to Stripe's API, retrying the request in cases of intermittent + * problems. + * + * @param request the request + * @return the response + * @throws StripeException If the request fails for any reason + */ + public StripeResponse requestWithRetries(StripeRequest request) throws StripeException { + return sendWithRetries(request, (r) -> this.requestWithTelemetry(r)); + } + + /** + * Sends the given request to Stripe's API, streaming the response, retrying the request in cases + * of intermittent problems. + * + * @param request the request + * @return the response + * @throws StripeException If the request fails for any reason + */ + public StripeResponseStream requestStreamWithRetries(StripeRequest request) + throws StripeException { + return sendWithRetries(request, (r) -> this.requestStreamWithTelemetry(r)); + } + /** * Builds the value of the {@code User-Agent} header. * @@ -165,8 +218,8 @@ private static String formatAppInfo(Map info) { return str; } - private boolean shouldRetry( - int numRetries, StripeException exception, StripeRequest request, StripeResponse response) { + private boolean shouldRetry( + int numRetries, StripeException exception, StripeRequest request, T response) { // Do not retry if we are out of retries. if (numRetries >= request.options().getMaxNetworkRetries()) { return false; diff --git a/src/main/java/com/stripe/net/HttpURLConnectionClient.java b/src/main/java/com/stripe/net/HttpURLConnectionClient.java index afd3ca77474..6b3120ae21b 100644 --- a/src/main/java/com/stripe/net/HttpURLConnectionClient.java +++ b/src/main/java/com/stripe/net/HttpURLConnectionClient.java @@ -2,7 +2,6 @@ import com.stripe.Stripe; import com.stripe.exception.ApiConnectionException; -import com.stripe.util.StreamUtils; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -29,7 +28,7 @@ public HttpURLConnectionClient() { * @throws ApiConnectionException if an error occurs when sending or receiving */ @Override - public StripeResponse request(StripeRequest request) throws ApiConnectionException { + public StripeResponseStream requestStream(StripeRequest request) throws ApiConnectionException { try { final HttpURLConnection conn = createStripeConnection(request); @@ -43,12 +42,32 @@ public StripeResponse request(StripeRequest request) throws ApiConnectionExcepti ? conn.getInputStream() : conn.getErrorStream(); - final String responseBody = StreamUtils.readToEnd(responseStream, ApiResource.CHARSET); - - responseStream.close(); + return new StripeResponseStream(responseCode, headers, responseStream); - return new StripeResponse(responseCode, headers, responseBody); + } catch (IOException e) { + throw new ApiConnectionException( + String.format( + "IOException during API request to Stripe (%s): %s " + + "Please check your internet connection and try again. If this problem persists," + + "you should check Stripe's service status at https://twitter.com/stripestatus," + + " or let us know at support@stripe.com.", + Stripe.getApiBase(), e.getMessage()), + e); + } + } + /** + * Sends the given request to Stripe's API, and returns a buffered response. + * + * @param request the request + * @return the response + * @throws ApiConnectionException if an error occurs when sending or receiving + */ + @Override + public StripeResponse request(StripeRequest request) throws ApiConnectionException { + final StripeResponseStream responseStream = requestStream(request); + try { + return responseStream.unstream(); } catch (IOException e) { throw new ApiConnectionException( String.format( diff --git a/src/main/java/com/stripe/net/LiveStripeResponseGetter.java b/src/main/java/com/stripe/net/LiveStripeResponseGetter.java index b6b67b27f2c..6f098e83a25 100644 --- a/src/main/java/com/stripe/net/LiveStripeResponseGetter.java +++ b/src/main/java/com/stripe/net/LiveStripeResponseGetter.java @@ -2,6 +2,8 @@ import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; +import com.stripe.Stripe; +import com.stripe.exception.ApiConnectionException; import com.stripe.exception.ApiException; import com.stripe.exception.AuthenticationException; import com.stripe.exception.CardException; @@ -20,6 +22,8 @@ import com.stripe.model.StripeObject; import com.stripe.model.StripeObjectInterface; import com.stripe.model.oauth.OAuthError; +import java.io.IOException; +import java.io.InputStream; import java.util.Map; public class LiveStripeResponseGetter implements StripeResponseGetter { @@ -73,6 +77,38 @@ public T request( return resource; } + @Override + public InputStream requestStream( + ApiResource.RequestMethod method, + String url, + Map params, + RequestOptions options) + throws StripeException { + StripeRequest request = new StripeRequest(method, url, params, options); + StripeResponseStream responseStream = httpClient.requestStreamWithRetries(request); + + int responseCode = responseStream.code(); + + if (responseCode < 200 || responseCode >= 300) { + StripeResponse response; + try { + response = responseStream.unstream(); + } catch (IOException e) { + throw new ApiConnectionException( + String.format( + "IOException during API request to Stripe (%s): %s " + + "Please check your internet connection and try again. If this problem persists," + + "you should check Stripe's service status at https://twitter.com/stripestatus," + + " or let us know at support@stripe.com.", + Stripe.getApiBase(), e.getMessage()), + e); + } + handleApiError(response); + } + + return responseStream.bodyStream(); + } + @Override public T oauthRequest( ApiResource.RequestMethod method, diff --git a/src/main/java/com/stripe/net/RequestTelemetry.java b/src/main/java/com/stripe/net/RequestTelemetry.java index 26ce17174e7..679adc9c3cb 100644 --- a/src/main/java/com/stripe/net/RequestTelemetry.java +++ b/src/main/java/com/stripe/net/RequestTelemetry.java @@ -52,7 +52,7 @@ public Optional getHeaderValue(HttpHeaders headers) { * @param response the Stripe response * @param duration the request duration */ - public void maybeEnqueueMetrics(StripeResponse response, Duration duration) { + public void maybeEnqueueMetrics(StripeResponseInterface response, Duration duration) { if (!Stripe.enableTelemetry) { return; } diff --git a/src/main/java/com/stripe/net/StripeResponse.java b/src/main/java/com/stripe/net/StripeResponse.java index e354454145c..27b73452658 100644 --- a/src/main/java/com/stripe/net/StripeResponse.java +++ b/src/main/java/com/stripe/net/StripeResponse.java @@ -6,21 +6,22 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Optional; -import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import lombok.Value; import lombok.experimental.Accessors; import lombok.experimental.NonFinal; -/** A response from Stripe's API. */ +/** A response from Stripe's API, with body represented as a String. */ @Value @Accessors(fluent = true) -public class StripeResponse { +public class StripeResponse implements StripeResponseInterface { /** The HTTP status code of the response. */ + @Getter(onMethod_ = {@Override}) int code; /** The HTTP headers of the response. */ + @Getter(onMethod_ = {@Override}) HttpHeaders headers; /** The body of the response. */ @@ -28,8 +29,8 @@ public class StripeResponse { /** Number of times the request was retried. Used for internal tests only. */ @NonFinal - @Getter(AccessLevel.PACKAGE) - @Setter(AccessLevel.PACKAGE) + @Getter(onMethod_ = {@Override}) + @Setter(onMethod_ = {@Override}) int numRetries; /** @@ -54,6 +55,7 @@ public StripeResponse(int code, HttpHeaders headers, String body) { * * @return the date of the request, as returned by Stripe */ + @Override public Instant date() { Optional dateStr = this.headers.firstValue("Date"); if (!dateStr.isPresent()) { @@ -67,6 +69,7 @@ public Instant date() { * * @return the idempotency key of the request, as returned by Stripe */ + @Override public String idempotencyKey() { return this.headers.firstValue("Idempotency-Key").orElse(null); } @@ -76,6 +79,7 @@ public String idempotencyKey() { * * @return the ID of the request, as returned by Stripe */ + @Override public String requestId() { return this.headers.firstValue("Request-Id").orElse(null); } diff --git a/src/main/java/com/stripe/net/StripeResponseGetter.java b/src/main/java/com/stripe/net/StripeResponseGetter.java index 9e353102b0d..d70b79b4eca 100644 --- a/src/main/java/com/stripe/net/StripeResponseGetter.java +++ b/src/main/java/com/stripe/net/StripeResponseGetter.java @@ -2,6 +2,7 @@ import com.stripe.exception.StripeException; import com.stripe.model.StripeObjectInterface; +import java.io.InputStream; import java.util.Map; public interface StripeResponseGetter { @@ -20,4 +21,14 @@ T oauthRequest( Class clazz, RequestOptions options) throws StripeException; + + default InputStream requestStream( + ApiResource.RequestMethod method, + String url, + Map params, + RequestOptions options) + throws StripeException { + throw new UnsupportedOperationException( + "requestStream is unimplemented for this StripeResponseGetter"); + } } diff --git a/src/main/java/com/stripe/net/StripeResponseInterface.java b/src/main/java/com/stripe/net/StripeResponseInterface.java new file mode 100644 index 00000000000..c5247054573 --- /dev/null +++ b/src/main/java/com/stripe/net/StripeResponseInterface.java @@ -0,0 +1,37 @@ +package com.stripe.net; + +import java.time.Instant; + +/** Common interface representing an HTTP response from Stripe. */ +interface StripeResponseInterface { + /** The HTTP status code of the response. */ + public int code(); + + /** The HTTP headers of the response. */ + public HttpHeaders headers(); + + /** + * Gets the date of the request, as returned by Stripe. + * + * @return the date of the request, as returned by Stripe + */ + public Instant date(); + /** + * Gets the idempotency key of the request, as returned by Stripe. + * + * @return the idempotency key of the request, as returned by Stripe + */ + public String idempotencyKey(); + /** + * Gets the ID of the request, as returned by Stripe. + * + * @return the ID of the request, as returned by Stripe + */ + public String requestId(); + + /** Get number of times the request was retried. Used for internal tests only. */ + public int numRetries(); + + /** Set number of times the request was retried. Used for internal tests only. */ + public StripeResponseInterface numRetries(int numRetries); +} diff --git a/src/main/java/com/stripe/net/StripeResponseStream.java b/src/main/java/com/stripe/net/StripeResponseStream.java new file mode 100644 index 00000000000..decaee222d2 --- /dev/null +++ b/src/main/java/com/stripe/net/StripeResponseStream.java @@ -0,0 +1,103 @@ +package com.stripe.net; + +import static java.util.Objects.requireNonNull; + +import com.stripe.util.StreamUtils; +import java.io.IOException; +import java.io.InputStream; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.Value; +import lombok.experimental.Accessors; +import lombok.experimental.NonFinal; + +/** A response from Stripe's API, with a body represented as an InputStream. */ +@Value +@Accessors(fluent = true) +public class StripeResponseStream implements StripeResponseInterface { + /** The HTTP status code of the response. */ + @Getter(onMethod_ = {@Override}) + int code; + + /** The HTTP headers of the response. */ + @Getter(onMethod_ = {@Override}) + HttpHeaders headers; + + /** The body of the response. */ + @NonFinal + @Getter(AccessLevel.PACKAGE) + InputStream bodyStream; + + /** Number of times the request was retried. Used for internal tests only. */ + @NonFinal + @Getter(onMethod_ = {@Override}) + @Setter(onMethod_ = {@Override}) + int numRetries; + + /** + * Initializes a new instance of the {@link StripeResponseStream} class. + * + * @param code the HTTP status code of the response + * @param headers the HTTP headers of the response + * @param bodyStream streaming body response + * @throws NullPointerException if {@code headers} or {@code body} is {@code null} + */ + public StripeResponseStream(int code, HttpHeaders headers, InputStream bodyStream) { + requireNonNull(headers); + requireNonNull(bodyStream); + + this.code = code; + this.headers = headers; + this.bodyStream = bodyStream; + } + + /** + * Buffers the entire response body into a string, constructing the appropriate StripeResponse + * + * @return the StripeResponse + */ + public StripeResponse unstream() throws IOException { + final String bodyString = StreamUtils.readToEnd(this.bodyStream, ApiResource.CHARSET); + this.bodyStream.close(); + return new StripeResponse(this.code, this.headers, bodyString); + } + + /** + * Gets the date of the request, as returned by Stripe. + * + * @return the date of the request, as returned by Stripe + */ + @Override + public Instant date() { + Optional dateStr = this.headers.firstValue("Date"); + if (!dateStr.isPresent()) { + return null; + } + return ZonedDateTime.parse(dateStr.get(), DateTimeFormatter.RFC_1123_DATE_TIME).toInstant(); + } + + /** + * Gets the idempotency key of the request, as returned by Stripe. + * + * @return the idempotency key of the request, as returned by Stripe + */ + @Override + public String idempotencyKey() { + return this.headers.firstValue("Idempotency-Key").orElse(null); + } + + /** + * Gets the ID of the request, as returned by Stripe. + * + * @return the ID of the request, as returned by Stripe + */ + @Override + public String requestId() { + return this.headers.firstValue("Request-Id").orElse(null); + } +} diff --git a/src/test/java/com/stripe/functional/StripeResponseStreamTest.java b/src/test/java/com/stripe/functional/StripeResponseStreamTest.java new file mode 100644 index 00000000000..e23ab73c3ca --- /dev/null +++ b/src/test/java/com/stripe/functional/StripeResponseStreamTest.java @@ -0,0 +1,97 @@ +package com.stripe.functional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.google.gson.annotations.SerializedName; +import com.stripe.BaseStripeTest; +import com.stripe.Stripe; +import com.stripe.exception.InvalidRequestException; +import com.stripe.exception.StripeException; +import com.stripe.model.HasId; +import com.stripe.net.ApiResource; +import com.stripe.net.RequestOptions; +import com.stripe.util.StreamUtils; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import lombok.Cleanup; +import lombok.Getter; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.Test; + +public class StripeResponseStreamTest extends BaseStripeTest { + + private static class TestResource extends ApiResource implements HasId { + @Getter(onMethod_ = {@Override}) + @SerializedName("id") + String id; + + public static TestResource retrieve(String id) throws StripeException { + return ApiResource.request( + ApiResource.RequestMethod.GET, + String.format("%s/v1/foos/%s", Stripe.getApiBase(), ApiResource.urlEncodeId(id)), + (Map) null, + TestResource.class, + (RequestOptions) null); + } + + public InputStream pdf() throws StripeException { + String url = + String.format( + "%s%s", + Stripe.getApiBase(), + String.format("/v1/foobars/%s/pdf", ApiResource.urlEncodeId(this.getId()))); + return ApiResource.requestStream( + ApiResource.RequestMethod.POST, url, (Map) null, (RequestOptions) null); + } + } + + @Test + public void testStreamedResponseSuccess() + throws StripeException, IOException, InterruptedException { + @Cleanup MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody("{\"id\": \"foo_123\"}")); + server.enqueue(new MockResponse().setBody("}this is a pdf, not valid json{")); + server.start(); + + Stripe.overrideApiBase(server.url("").toString()); + + TestResource t = TestResource.retrieve("foo_123"); + server.takeRequest(); + assertEquals("foo_123", t.id); + + InputStream stream = t.pdf(); + + final String body = StreamUtils.readToEnd(stream, ApiResource.CHARSET); + stream.close(); + assertEquals("}this is a pdf, not valid json{", body); + server.shutdown(); + } + + @Test + public void testStreamedResponseFailure() + throws StripeException, IOException, InterruptedException { + @Cleanup MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody("{\"id\": \"foo_123\"}")); + server.enqueue( + new MockResponse() + .setResponseCode(400) + .setBody("{\"error\": {\"message\": \"bad bad bad\"}}")); + server.start(); + + Stripe.overrideApiBase(server.url("").toString()); + + TestResource r = TestResource.retrieve("foo_123"); + server.takeRequest(); + assertEquals("foo_123", r.id); + + assertThrows( + InvalidRequestException.class, + () -> { + r.pdf(); + }); + server.shutdown(); + } +} From 6d2b0dc6caa0154b37c9757a5cb265742396ed9d Mon Sep 17 00:00:00 2001 From: Richard Marmorstein Date: Fri, 14 May 2021 14:24:09 -0400 Subject: [PATCH 2/4] Use abstract class --- .../stripe/net/AbstractStripeResponse.java | 84 +++++++++++++++++++ src/main/java/com/stripe/net/HttpClient.java | 13 ++- .../stripe/net/LiveStripeResponseGetter.java | 2 +- .../java/com/stripe/net/RequestTelemetry.java | 2 +- .../java/com/stripe/net/StripeResponse.java | 74 +--------------- .../stripe/net/StripeResponseInterface.java | 37 -------- .../com/stripe/net/StripeResponseStream.java | 83 ++---------------- 7 files changed, 99 insertions(+), 196 deletions(-) create mode 100644 src/main/java/com/stripe/net/AbstractStripeResponse.java delete mode 100644 src/main/java/com/stripe/net/StripeResponseInterface.java diff --git a/src/main/java/com/stripe/net/AbstractStripeResponse.java b/src/main/java/com/stripe/net/AbstractStripeResponse.java new file mode 100644 index 00000000000..c375bded107 --- /dev/null +++ b/src/main/java/com/stripe/net/AbstractStripeResponse.java @@ -0,0 +1,84 @@ +package com.stripe.net; + +import static java.util.Objects.requireNonNull; + +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.NonFinal; + +/** Common interface representing an HTTP response from Stripe. */ +@Accessors(fluent = true) +abstract class AbstractStripeResponse { + /** The HTTP status code of the response. */ + int code; + + /** The HTTP headers of the response. */ + HttpHeaders headers; + + /** The body of the response. */ + T body; + + public final int code() { + return this.code; + } + + public final HttpHeaders headers() { + return this.headers; + } + + public final T body() { + return this.body; + } + + /** Number of times the request was retried. Used for internal tests only. */ + @NonFinal + @Getter(AccessLevel.PACKAGE) + @Setter(AccessLevel.PACKAGE) + int numRetries; + + /** + * Gets the date of the request, as returned by Stripe. + * + * @return the date of the request, as returned by Stripe + */ + public Instant date() { + Optional dateStr = this.headers.firstValue("Date"); + if (!dateStr.isPresent()) { + return null; + } + return ZonedDateTime.parse(dateStr.get(), DateTimeFormatter.RFC_1123_DATE_TIME).toInstant(); + } + + /** + * Gets the idempotency key of the request, as returned by Stripe. + * + * @return the idempotency key of the request, as returned by Stripe + */ + public String idempotencyKey() { + return this.headers.firstValue("Idempotency-Key").orElse(null); + } + + /** + * Gets the ID of the request, as returned by Stripe. + * + * @return the ID of the request, as returned by Stripe + */ + public String requestId() { + return this.headers.firstValue("Request-Id").orElse(null); + } + + protected AbstractStripeResponse(int code, HttpHeaders headers, T body) { + requireNonNull(headers); + requireNonNull(body); + + this.code = code; + this.headers = headers; + this.body = body; + } +} diff --git a/src/main/java/com/stripe/net/HttpClient.java b/src/main/java/com/stripe/net/HttpClient.java index 84618660afb..0b907ae569d 100644 --- a/src/main/java/com/stripe/net/HttpClient.java +++ b/src/main/java/com/stripe/net/HttpClient.java @@ -45,8 +45,7 @@ protected HttpClient() {} * @throws StripeException If the request fails for any reason */ public StripeResponseStream requestStream(StripeRequest request) throws StripeException { - throw new UnsupportedOperationException( - "streamingRequest is unimplemented for this HttpClient"); + throw new UnsupportedOperationException("requestStream is unimplemented for this HttpClient"); } @FunctionalInterface @@ -54,7 +53,7 @@ private interface RequestSendFunction { R apply(StripeRequest request) throws StripeException; } - private T sendWithTelemetry( + private > T sendWithTelemetry( StripeRequest request, RequestSendFunction send) throws StripeException { Optional telemetryHeaderValue = requestTelemetry.getHeaderValue(request.headers()); if (telemetryHeaderValue.isPresent()) { @@ -81,7 +80,7 @@ private T sendWithTelemetry( * @throws StripeException If the request fails for any reason */ public StripeResponse requestWithTelemetry(StripeRequest request) throws StripeException { - return sendWithTelemetry(request, (r) -> this.request(r)); + return sendWithTelemetry(request, this::request); } /** @@ -94,10 +93,10 @@ public StripeResponse requestWithTelemetry(StripeRequest request) throws StripeE */ public StripeResponseStream requestStreamWithTelemetry(StripeRequest request) throws StripeException { - return sendWithTelemetry(request, (r) -> this.requestStream(r)); + return sendWithTelemetry(request, this::requestStream); } - public T sendWithRetries( + public > T sendWithRetries( StripeRequest request, RequestSendFunction send) throws StripeException { ApiConnectionException requestException = null; T response = null; @@ -218,7 +217,7 @@ private static String formatAppInfo(Map info) { return str; } - private boolean shouldRetry( + private > boolean shouldRetry( int numRetries, StripeException exception, StripeRequest request, T response) { // Do not retry if we are out of retries. if (numRetries >= request.options().getMaxNetworkRetries()) { diff --git a/src/main/java/com/stripe/net/LiveStripeResponseGetter.java b/src/main/java/com/stripe/net/LiveStripeResponseGetter.java index 6f098e83a25..25b2876fa5d 100644 --- a/src/main/java/com/stripe/net/LiveStripeResponseGetter.java +++ b/src/main/java/com/stripe/net/LiveStripeResponseGetter.java @@ -106,7 +106,7 @@ public InputStream requestStream( handleApiError(response); } - return responseStream.bodyStream(); + return responseStream.body(); } @Override diff --git a/src/main/java/com/stripe/net/RequestTelemetry.java b/src/main/java/com/stripe/net/RequestTelemetry.java index 679adc9c3cb..34a128c77d7 100644 --- a/src/main/java/com/stripe/net/RequestTelemetry.java +++ b/src/main/java/com/stripe/net/RequestTelemetry.java @@ -52,7 +52,7 @@ public Optional getHeaderValue(HttpHeaders headers) { * @param response the Stripe response * @param duration the request duration */ - public void maybeEnqueueMetrics(StripeResponseInterface response, Duration duration) { + public void maybeEnqueueMetrics(AbstractStripeResponse response, Duration duration) { if (!Stripe.enableTelemetry) { return; } diff --git a/src/main/java/com/stripe/net/StripeResponse.java b/src/main/java/com/stripe/net/StripeResponse.java index 27b73452658..41b88f084b5 100644 --- a/src/main/java/com/stripe/net/StripeResponse.java +++ b/src/main/java/com/stripe/net/StripeResponse.java @@ -1,38 +1,7 @@ package com.stripe.net; -import static java.util.Objects.requireNonNull; - -import java.time.Instant; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Optional; -import lombok.Getter; -import lombok.Setter; -import lombok.Value; -import lombok.experimental.Accessors; -import lombok.experimental.NonFinal; - /** A response from Stripe's API, with body represented as a String. */ -@Value -@Accessors(fluent = true) -public class StripeResponse implements StripeResponseInterface { - /** The HTTP status code of the response. */ - @Getter(onMethod_ = {@Override}) - int code; - - /** The HTTP headers of the response. */ - @Getter(onMethod_ = {@Override}) - HttpHeaders headers; - - /** The body of the response. */ - String body; - - /** Number of times the request was retried. Used for internal tests only. */ - @NonFinal - @Getter(onMethod_ = {@Override}) - @Setter(onMethod_ = {@Override}) - int numRetries; - +public class StripeResponse extends AbstractStripeResponse { /** * Initializes a new instance of the {@link StripeResponse} class. * @@ -42,45 +11,6 @@ public class StripeResponse implements StripeResponseInterface { * @throws NullPointerException if {@code headers} or {@code body} is {@code null} */ public StripeResponse(int code, HttpHeaders headers, String body) { - requireNonNull(headers); - requireNonNull(body); - - this.code = code; - this.headers = headers; - this.body = body; - } - - /** - * Gets the date of the request, as returned by Stripe. - * - * @return the date of the request, as returned by Stripe - */ - @Override - public Instant date() { - Optional dateStr = this.headers.firstValue("Date"); - if (!dateStr.isPresent()) { - return null; - } - return ZonedDateTime.parse(dateStr.get(), DateTimeFormatter.RFC_1123_DATE_TIME).toInstant(); - } - - /** - * Gets the idempotency key of the request, as returned by Stripe. - * - * @return the idempotency key of the request, as returned by Stripe - */ - @Override - public String idempotencyKey() { - return this.headers.firstValue("Idempotency-Key").orElse(null); - } - - /** - * Gets the ID of the request, as returned by Stripe. - * - * @return the ID of the request, as returned by Stripe - */ - @Override - public String requestId() { - return this.headers.firstValue("Request-Id").orElse(null); + super(code, headers, body); } } diff --git a/src/main/java/com/stripe/net/StripeResponseInterface.java b/src/main/java/com/stripe/net/StripeResponseInterface.java deleted file mode 100644 index c5247054573..00000000000 --- a/src/main/java/com/stripe/net/StripeResponseInterface.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.stripe.net; - -import java.time.Instant; - -/** Common interface representing an HTTP response from Stripe. */ -interface StripeResponseInterface { - /** The HTTP status code of the response. */ - public int code(); - - /** The HTTP headers of the response. */ - public HttpHeaders headers(); - - /** - * Gets the date of the request, as returned by Stripe. - * - * @return the date of the request, as returned by Stripe - */ - public Instant date(); - /** - * Gets the idempotency key of the request, as returned by Stripe. - * - * @return the idempotency key of the request, as returned by Stripe - */ - public String idempotencyKey(); - /** - * Gets the ID of the request, as returned by Stripe. - * - * @return the ID of the request, as returned by Stripe - */ - public String requestId(); - - /** Get number of times the request was retried. Used for internal tests only. */ - public int numRetries(); - - /** Set number of times the request was retried. Used for internal tests only. */ - public StripeResponseInterface numRetries(int numRetries); -} diff --git a/src/main/java/com/stripe/net/StripeResponseStream.java b/src/main/java/com/stripe/net/StripeResponseStream.java index decaee222d2..256aae476ae 100644 --- a/src/main/java/com/stripe/net/StripeResponseStream.java +++ b/src/main/java/com/stripe/net/StripeResponseStream.java @@ -1,44 +1,10 @@ package com.stripe.net; -import static java.util.Objects.requireNonNull; - import com.stripe.util.StreamUtils; import java.io.IOException; import java.io.InputStream; -import java.time.Instant; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Optional; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.Setter; -import lombok.Value; -import lombok.experimental.Accessors; -import lombok.experimental.NonFinal; - -/** A response from Stripe's API, with a body represented as an InputStream. */ -@Value -@Accessors(fluent = true) -public class StripeResponseStream implements StripeResponseInterface { - /** The HTTP status code of the response. */ - @Getter(onMethod_ = {@Override}) - int code; - - /** The HTTP headers of the response. */ - @Getter(onMethod_ = {@Override}) - HttpHeaders headers; - - /** The body of the response. */ - @NonFinal - @Getter(AccessLevel.PACKAGE) - InputStream bodyStream; - - /** Number of times the request was retried. Used for internal tests only. */ - @NonFinal - @Getter(onMethod_ = {@Override}) - @Setter(onMethod_ = {@Override}) - int numRetries; +public class StripeResponseStream extends AbstractStripeResponse { /** * Initializes a new instance of the {@link StripeResponseStream} class. * @@ -47,13 +13,8 @@ public class StripeResponseStream implements StripeResponseInterface { * @param bodyStream streaming body response * @throws NullPointerException if {@code headers} or {@code body} is {@code null} */ - public StripeResponseStream(int code, HttpHeaders headers, InputStream bodyStream) { - requireNonNull(headers); - requireNonNull(bodyStream); - - this.code = code; - this.headers = headers; - this.bodyStream = bodyStream; + public StripeResponseStream(int code, HttpHeaders headers, InputStream body) { + super(code, headers, body); } /** @@ -62,42 +23,8 @@ public StripeResponseStream(int code, HttpHeaders headers, InputStream bodyStrea * @return the StripeResponse */ public StripeResponse unstream() throws IOException { - final String bodyString = StreamUtils.readToEnd(this.bodyStream, ApiResource.CHARSET); - this.bodyStream.close(); + final String bodyString = StreamUtils.readToEnd(this.body, ApiResource.CHARSET); + this.body.close(); return new StripeResponse(this.code, this.headers, bodyString); } - - /** - * Gets the date of the request, as returned by Stripe. - * - * @return the date of the request, as returned by Stripe - */ - @Override - public Instant date() { - Optional dateStr = this.headers.firstValue("Date"); - if (!dateStr.isPresent()) { - return null; - } - return ZonedDateTime.parse(dateStr.get(), DateTimeFormatter.RFC_1123_DATE_TIME).toInstant(); - } - - /** - * Gets the idempotency key of the request, as returned by Stripe. - * - * @return the idempotency key of the request, as returned by Stripe - */ - @Override - public String idempotencyKey() { - return this.headers.firstValue("Idempotency-Key").orElse(null); - } - - /** - * Gets the ID of the request, as returned by Stripe. - * - * @return the ID of the request, as returned by Stripe - */ - @Override - public String requestId() { - return this.headers.firstValue("Request-Id").orElse(null); - } } From 4d7f8d114bb76dcd24ebec4154e2bc1db298c231 Mon Sep 17 00:00:00 2001 From: Richard Marmorstein Date: Mon, 21 Jun 2021 15:45:17 -0400 Subject: [PATCH 3/4] Fix javadoc --- src/main/java/com/stripe/net/StripeResponseStream.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/stripe/net/StripeResponseStream.java b/src/main/java/com/stripe/net/StripeResponseStream.java index 256aae476ae..6781b092600 100644 --- a/src/main/java/com/stripe/net/StripeResponseStream.java +++ b/src/main/java/com/stripe/net/StripeResponseStream.java @@ -10,7 +10,7 @@ public class StripeResponseStream extends AbstractStripeResponse { * * @param code the HTTP status code of the response * @param headers the HTTP headers of the response - * @param bodyStream streaming body response + * @param body streaming body response * @throws NullPointerException if {@code headers} or {@code body} is {@code null} */ public StripeResponseStream(int code, HttpHeaders headers, InputStream body) { From e05c66225f1963f2d824a4018f913335593b16fa Mon Sep 17 00:00:00 2001 From: Richard Marmorstein Date: Thu, 24 Jun 2021 12:13:41 -0400 Subject: [PATCH 4/4] Make unstream package private --- src/main/java/com/stripe/net/StripeResponseStream.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/stripe/net/StripeResponseStream.java b/src/main/java/com/stripe/net/StripeResponseStream.java index 6781b092600..11a99b55dc8 100644 --- a/src/main/java/com/stripe/net/StripeResponseStream.java +++ b/src/main/java/com/stripe/net/StripeResponseStream.java @@ -22,7 +22,7 @@ public StripeResponseStream(int code, HttpHeaders headers, InputStream body) { * * @return the StripeResponse */ - public StripeResponse unstream() throws IOException { + StripeResponse unstream() throws IOException { final String bodyString = StreamUtils.readToEnd(this.body, ApiResource.CHARSET); this.body.close(); return new StripeResponse(this.code, this.headers, bodyString);