diff --git a/pom.xml b/pom.xml index caee2b2..b06eb84 100644 --- a/pom.xml +++ b/pom.xml @@ -43,8 +43,8 @@ - 1.11 - 1.11 + 1.8 + 1.8 UTF-8 @@ -55,11 +55,22 @@ 4.13.2 test + + org.mockito + mockito-core + 4.6.1 + test + com.fasterxml.jackson.core jackson-databind 2.13.3 + + com.squareup.okhttp3 + okhttp + 4.10.0 + @@ -148,8 +159,8 @@ org.apache.maven.plugins maven-compiler-plugin - 11 - 11 + 8 + 8 diff --git a/src/main/java/com/smartystreets/api/ClientBuilder.java b/src/main/java/com/smartystreets/api/ClientBuilder.java index 01bf4e3..27b1b84 100644 --- a/src/main/java/com/smartystreets/api/ClientBuilder.java +++ b/src/main/java/com/smartystreets/api/ClientBuilder.java @@ -2,7 +2,6 @@ import java.net.InetSocketAddress; import java.net.Proxy; -import java.net.ProxySelector; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -27,7 +26,7 @@ public class ClientBuilder { private int maxRetries; private int maxTimeout; private String urlPrefix; - private ProxySelector proxy; + private Proxy proxy; private Map customHeaders; private List licenses = new ArrayList<>(); @@ -112,7 +111,7 @@ public ClientBuilder withCustomHeaders(Map customHeaders) { * @return Returns this to accommodate method chaining. */ public ClientBuilder withProxy(Proxy.Type proxyType, String proxyHost, int proxyPort) { - this.proxy = ProxySelector.of(new InetSocketAddress(proxyHost, proxyPort)); + this.proxy = new Proxy(proxyType, new InetSocketAddress(proxyHost, proxyPort)); return this; } diff --git a/src/main/java/com/smartystreets/api/Request.java b/src/main/java/com/smartystreets/api/Request.java index 1331244..5ee6a19 100644 --- a/src/main/java/com/smartystreets/api/Request.java +++ b/src/main/java/com/smartystreets/api/Request.java @@ -1,7 +1,6 @@ package com.smartystreets.api; import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; @@ -36,7 +35,7 @@ public void putParameter(String name, String value) { private static String urlEncode(String value) { try { - return URLEncoder.encode(value, StandardCharsets.UTF_8); + return URLEncoder.encode(value, CHARSET); } catch (Exception ex) { return ""; } diff --git a/src/main/java/com/smartystreets/api/RetrySender.java b/src/main/java/com/smartystreets/api/RetrySender.java index 5425b09..bcbee3d 100644 --- a/src/main/java/com/smartystreets/api/RetrySender.java +++ b/src/main/java/com/smartystreets/api/RetrySender.java @@ -22,7 +22,11 @@ public Response send(Request request) throws SmartyException, IOException, Inter for (int i = 0; i <= this.maxRetries; i++) { Response response = this.trySend(request, i); if (response instanceof TooManyRequestsResponse) { - long wait = ((TooManyRequestsResponse) response).getHeaders().firstValueAsLong("Retry-After").orElse(10L); + long wait = 10L; + String retryAfter = ((TooManyRequestsResponse) response).getHeaders().get("Retry-After"); + if (retryAfter != null && retryAfter.length() > 0) { + wait = Long.parseLong(retryAfter); + } if (wait < 1) { wait = 1L; } diff --git a/src/main/java/com/smartystreets/api/SmartySender.java b/src/main/java/com/smartystreets/api/SmartySender.java index 6750f55..ce39706 100644 --- a/src/main/java/com/smartystreets/api/SmartySender.java +++ b/src/main/java/com/smartystreets/api/SmartySender.java @@ -1,15 +1,14 @@ package com.smartystreets.api; import com.smartystreets.api.exceptions.SmartyException; +import okhttp3.Headers; +import okhttp3.OkHttpClient; +import okhttp3.RequestBody; import java.io.IOException; -import java.net.ProxySelector; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; +import java.net.Proxy; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; @@ -17,11 +16,11 @@ public class SmartySender implements Sender { private int maxTimeOut; - private HttpClient client; + private OkHttpClient client; public SmartySender() { this.maxTimeOut = 10000; - this.client = HttpClient.newHttpClient(); + this.client = new OkHttpClient(); } public SmartySender(int maxTimeout) { @@ -29,44 +28,58 @@ public SmartySender(int maxTimeout) { this.maxTimeOut = maxTimeout; } - SmartySender(int maxTimeOut, ProxySelector proxy) { + SmartySender(int maxTimeOut, Proxy proxy) { this.maxTimeOut = maxTimeOut; - this.client = HttpClient.newBuilder().proxy(proxy).build(); + this.client = new OkHttpClient.Builder() + .proxy(proxy) + .writeTimeout(this.maxTimeOut, TimeUnit.MILLISECONDS) + .readTimeout(this.maxTimeOut, TimeUnit.MILLISECONDS) + .connectTimeout(this.maxTimeOut, TimeUnit.MILLISECONDS) + .build(); } - SmartySender(HttpClient client) { + SmartySender(OkHttpClient client) { this(); this.client = client; } public Response send(Request smartyRequest) throws SmartyException, IOException { - HttpRequest httpRequest = buildHttpRequest(smartyRequest); + okhttp3.Request httpRequest = buildHttpRequest(smartyRequest); - try { - return buildResponse(this.client.send(httpRequest, HttpResponse.BodyHandlers.ofString())); - } catch(InterruptedException ex) { + try (okhttp3.Response httpResponse = client.newCall(httpRequest).execute()){ + int statusCode = httpResponse.code(); + if (statusCode == 429){ + return new TooManyRequestsResponse(httpResponse.headers(), statusCode, httpResponse.body().bytes()); + } + return new Response(statusCode, httpResponse.body().bytes()); + } catch(IOException ex) { return new Response(ex.hashCode(), new byte[0]); } } - private HttpRequest buildHttpRequest(Request smartyRequest) throws IOException { - java.net.http.HttpRequest.Builder builder = java.net.http.HttpRequest - .newBuilder(URI.create(smartyRequest.getUrl())) - .timeout(Duration.ofSeconds(maxTimeOut)); + private okhttp3.Request buildHttpRequest(Request smartyRequest) throws IOException { + Map headers = smartyRequest.getHeaders(); - for (String headerName : headers.keySet()) - builder.setHeader(headerName, headers.get(headerName).toString()); + Headers.Builder headersBuilder = new Headers.Builder(); + for (String headerName : headers.keySet()) { + headersBuilder.add(headerName, headers.get(headerName).toString()); + } - if (smartyRequest.getMethod().equals("GET")) - return builder.GET().build(); + okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder() + .url(smartyRequest.getUrl()) + .headers(headersBuilder.build()); - return builder - .POST(HttpRequest.BodyPublishers.ofByteArray(smartyRequest.getPayload())) - .build(); + if (smartyRequest.getMethod().equals("GET")) { + return requestBuilder + .get() + .build(); + } + + return requestBuilder.post(RequestBody.create(smartyRequest.getPayload())).build(); } - private Response buildResponse(HttpResponse httpResponse) { - int statusCode = httpResponse.statusCode(); + private Response buildResponse(okhttp3.Response httpResponse) { + int statusCode = httpResponse.code(); if (statusCode == 429){ return new TooManyRequestsResponse(httpResponse.headers(), statusCode, httpResponse.body().toString().getBytes()); } @@ -74,7 +87,7 @@ private Response buildResponse(HttpResponse httpResponse) { } static void enableLogging() { - Logger logger = Logger.getLogger(HttpClient.class.getName()); + Logger logger = Logger.getLogger(OkHttpClient.class.getName()); logger.setLevel(Level.ALL); logger.addHandler(new Handler() { @Override diff --git a/src/main/java/com/smartystreets/api/TooManyRequestsResponse.java b/src/main/java/com/smartystreets/api/TooManyRequestsResponse.java index 0cc2d76..49960a9 100644 --- a/src/main/java/com/smartystreets/api/TooManyRequestsResponse.java +++ b/src/main/java/com/smartystreets/api/TooManyRequestsResponse.java @@ -1,17 +1,17 @@ package com.smartystreets.api; -import java.net.http.HttpHeaders; +import okhttp3.Headers; public class TooManyRequestsResponse extends Response { - private HttpHeaders headers; + private Headers headers; - public TooManyRequestsResponse(HttpHeaders headers, int statusCode, byte[] payload) { + public TooManyRequestsResponse(Headers headers, int statusCode, byte[] payload) { super(statusCode, payload); this.headers = headers; } - public HttpHeaders getHeaders() { + public Headers getHeaders() { return headers; } } diff --git a/src/test/java/com/smartystreets/api/SmartySenderTest.java b/src/test/java/com/smartystreets/api/SmartySenderTest.java index fbc4883..000f960 100644 --- a/src/test/java/com/smartystreets/api/SmartySenderTest.java +++ b/src/test/java/com/smartystreets/api/SmartySenderTest.java @@ -1,36 +1,22 @@ package com.smartystreets.api; +import okhttp3.*; import org.junit.Test; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLParameters; -import javax.net.ssl.SSLSession; import java.io.IOException; -import java.net.Authenticator; -import java.net.CookieHandler; -import java.net.ProxySelector; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpHeaders; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; import java.util.Map; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class SmartySenderTest { - private HttpRequest httpRequest; - - //region [ Request Building ] @Test public void testHttpRequestContainsCorrectHeaders() throws Exception { - SmartySender sender = new SmartySender(this.getMockClient(200)); + SmartySender sender = new SmartySender(mockHttpClient("{\"key\": \"value\"}", 200)); Request request = new Request(); request.setUrlPrefix("http://localhost"); request.putHeader("X-name1", "value1"); @@ -45,25 +31,27 @@ public void testHttpRequestContainsCorrectHeaders() throws Exception { @Test public void testHttpRequestContainsGetWhenAppropriate() throws Exception { - SmartySender sender = new SmartySender(this.getMockClient(200)); + String responseString = "This is a GET response."; + SmartySender sender = new SmartySender(mockHttpClient(responseString, 200)); Request request = new Request(); request.setUrlPrefix("http://localhost"); Response response = sender.send(request); - assertArrayEquals("This is a GET response.".getBytes(), response.getPayload()); + assertArrayEquals(responseString.getBytes(), response.getPayload()); } @Test public void testHttpRequestContainsPostWhenAppropriate() throws Exception { - SmartySender sender = new SmartySender(this.getMockClient(200)); + String responseString = "This is a POST response."; + SmartySender sender = new SmartySender(mockHttpClient(responseString, 200)); Request request = new Request(); request.setUrlPrefix("http://localhost"); request.setPayload(new byte[0]); Response response = sender.send(request); - assertArrayEquals("This is a POST response.".getBytes(), response.getPayload()); + assertArrayEquals(responseString.getBytes(), response.getPayload()); } // @Test @@ -84,18 +72,20 @@ public void testHttpRequestContainsPostWhenAppropriate() throws Exception { @Test public void testResponseContainsCorrectPayload() throws Exception { - SmartySender sender = new SmartySender(this.getMockClient(200)); + String responseBody = "{\"key\": \"value\"}"; + SmartySender sender = new SmartySender(mockHttpClient(responseBody, 200)); Request request = new Request(); request.setUrlPrefix("http://localhost"); Response response = sender.send(request); - assertArrayEquals("This is a GET response.".getBytes(), response.getPayload()); + assertArrayEquals(responseBody.getBytes(), response.getPayload()); } @Test public void testResponseContainsStatusCode200OnSuccess() throws Exception { - SmartySender sender = new SmartySender(this.getMockClient(200)); + String responseBody = "{\"key\": \"value\"}"; + SmartySender sender = new SmartySender(mockHttpClient(responseBody, 200)); Request request = new Request(); request.setUrlPrefix("http://localhost"); @@ -106,7 +96,8 @@ public void testResponseContainsStatusCode200OnSuccess() throws Exception { @Test public void testResponseContainsStatusCode400WhenA400IsThrown() throws Exception { - SmartySender sender = new SmartySender(this.getMockClient(400)); + String responseBody = "{\"key\": \"value\"}"; + SmartySender sender = new SmartySender(mockHttpClient(responseBody, 400)); Request request = new Request(); request.setUrlPrefix("http://localhost"); @@ -115,120 +106,24 @@ public void testResponseContainsStatusCode400WhenA400IsThrown() throws Exception assertEquals(400, response.getStatusCode()); } - //endregion + private static OkHttpClient mockHttpClient(final String serializedBody, final int code) throws IOException { + final OkHttpClient okHttpClient = mock(OkHttpClient.class); - //region [ Transports ] - - private HttpClient getMockClient(int statusCode) { - final int status = statusCode; - return new HttpClient() { - @Override - public Optional cookieHandler() { - return Optional.empty(); - } - - @Override - public Optional connectTimeout() { - return Optional.empty(); - } - - @Override - public Redirect followRedirects() { - return null; - } - - @Override - public Optional proxy() { - return Optional.empty(); - } - - @Override - public SSLContext sslContext() { - return null; - } - - @Override - public SSLParameters sslParameters() { - return null; - } - - @Override - public Optional authenticator() { - return Optional.empty(); - } - - @Override - public Version version() { - return null; - } - - @Override - public Optional executor() { - return Optional.empty(); - } - - @Override - public HttpResponse send(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) throws IOException, InterruptedException { - httpRequest = request; - return new HttpResponse() { - @Override - public int statusCode() { - return status; - } - - @Override - public HttpRequest request() { - return httpRequest; - } - - @Override - public Optional previousResponse() { - return Optional.empty(); - } - - @Override - public HttpHeaders headers() { - return null; - } - - @Override - public Object body() { - if (httpRequest.method().equals("GET")) - return "This is a GET response."; - else { - return "This is a POST response."; - - } - } - - @Override - public Optional sslSession() { - return Optional.empty(); - } - - @Override - public URI uri() { - return null; - } - - @Override - public Version version() { - return null; - } - }; - } - - @Override - public CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) { - return null; - } - - @Override - public CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler, HttpResponse.PushPromiseHandler pushPromiseHandler) { - return null; - } - }; - } + final Call remoteCall = mock(Call.class); - //endregion + final okhttp3.Response response = new okhttp3.Response.Builder() + .request(new okhttp3.Request.Builder().url("http://url.com").build()) + .protocol(Protocol.HTTP_2) + .code(code).message("").body( + ResponseBody.create( + serializedBody, + MediaType.parse("application/json") + )) + .build(); + + when(remoteCall.execute()).thenReturn(response); + when(okHttpClient.newCall(any())).thenReturn(remoteCall); + + return okHttpClient; + } } \ No newline at end of file diff --git a/src/test/java/com/smartystreets/api/mocks/MockCrashingSender.java b/src/test/java/com/smartystreets/api/mocks/MockCrashingSender.java index 0bb859d..eb982be 100644 --- a/src/test/java/com/smartystreets/api/mocks/MockCrashingSender.java +++ b/src/test/java/com/smartystreets/api/mocks/MockCrashingSender.java @@ -5,11 +5,9 @@ import com.smartystreets.api.Sender; import com.smartystreets.api.TooManyRequestsResponse; import com.smartystreets.api.exceptions.SmartyException; +import okhttp3.Headers; import java.io.IOException; -import java.net.http.HttpHeaders; -import java.util.List; -import java.util.Map; public class MockCrashingSender implements Sender { private int sendCount = 0; @@ -23,7 +21,7 @@ public Response send(Request request) throws SmartyException, IOException { if (request.getUrl().contains("TooManyRequests")) { if (this.sendCount <= 2) { - response = new TooManyRequestsResponse(HttpHeaders.of(Map.of("Retry-After", List.of("7")), (x, y) -> true), STATUS_CODE, new byte[]{}); + response = new TooManyRequestsResponse(Headers.of("Retry-After", "7"), STATUS_CODE, new byte[]{}); } }