From dadb4786d107c77aa296e93b094c2e352cb37bd4 Mon Sep 17 00:00:00 2001 From: jchen293 Date: Fri, 30 Jun 2023 17:43:08 -0400 Subject: [PATCH] feat: adds Request and Response Hooks --- CHANGELOG.md | 4 + README.md | 21 +++ dependency-check-suppressions.xml | 1 + .../java/com/easypost/hooks/EventHook.java | 36 +++++ .../java/com/easypost/hooks/RequestHook.java | 4 + .../java/com/easypost/hooks/ResponseHook.java | 4 + .../java/com/easypost/hooks/package-info.java | 9 ++ .../java/com/easypost/http/Requestor.java | 39 +++++- .../com/easypost/service/AddressService.java | 3 +- .../com/easypost/service/EasyPostClient.java | 43 ++++++ src/test/cassettes/hook/create.json | 97 ++++++++++++++ src/test/java/com/easypost/HookTest.java | 126 ++++++++++++++++++ 12 files changed, 381 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/easypost/hooks/EventHook.java create mode 100644 src/main/java/com/easypost/hooks/RequestHook.java create mode 100644 src/main/java/com/easypost/hooks/ResponseHook.java create mode 100644 src/main/java/com/easypost/hooks/package-info.java create mode 100644 src/test/cassettes/hook/create.json create mode 100644 src/test/java/com/easypost/HookTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a1f402df..91a04d1b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## Next release + +- Adds new `RequestHook` and `ResponseHook` classes. (un)subscribe to them with the new `subscribeToRequestHook`, `subscribeToResponseHook`, `unsubscribeFromRequestHook`, or `unsubscribeFromResponseHook` methods of an `EasyPostClient` + ## v6.7.0 (2023-06-06) - Retrieving carrier metadata is now generally available via `client.carrierMetadata.retrieve` diff --git a/README.md b/README.md index 56eb38e84..2ff7d4e25 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,27 @@ public class CreateShipment { } ``` +## HTTP Hooks + +Users can subscribe to HTTP requests and responses via the `RequestHook` and `ResponseHook` objects. To do so, pass a function to the `subscribeToRequest_hook` or `subscribeToResponse_hook` methods of an `EasyPostClient` object: + +```java + public static Object customFunction(HashMap datas) { + // Pass your code here, the information about the request/response is available within the datas parameter. + for (Map.Entry entry : datas.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + System.out.println("Key: " + key + ", Value: " + value); + } + + return true; + } + + EasyPostClient client = new EasyPostClient(System.getenv("EASYPOST_API_KEY")); + + client.subscribeToRequestHook(customFunction); // subscribe to request hook by passing your custom function + client.unsubscribeToRequestHook(customFunction); // unsubscribe from request hook +``` ## Documentation API documentation can be found at: . diff --git a/dependency-check-suppressions.xml b/dependency-check-suppressions.xml index 6a42b6c8d..86f0d6eeb 100644 --- a/dependency-check-suppressions.xml +++ b/dependency-check-suppressions.xml @@ -8,5 +8,6 @@ CVE-2022-3171 CVE-2022-3509 CVE-2022-3510 + CVE-2023-2976 diff --git a/src/main/java/com/easypost/hooks/EventHook.java b/src/main/java/com/easypost/hooks/EventHook.java new file mode 100644 index 000000000..0de4e3624 --- /dev/null +++ b/src/main/java/com/easypost/hooks/EventHook.java @@ -0,0 +1,36 @@ +package com.easypost.hooks; + +import java.util.function.Function; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class EventHook { + private List, Object>> eventHandlers = new ArrayList<>(); + + /** + * Add a function to the list of event handlers. + * @param handler The event handler function to be added. + */ + public void addEventHandler(Function, Object> handler) { + eventHandlers.add(handler); + } + + /** + * Remove a function to the list of event handlers. + * @param handler The event handler function to be removed. + */ + public void removeEventHandler(Function, Object> handler) { + eventHandlers.remove(handler); + } + + /** + * Execute all the functions from the event handlers. + * @param datas The datas from the hooks. + */ + public void executeEventHandler(HashMap datas) { + for (Function, Object> eventHandler : eventHandlers) { + Object result = eventHandler.apply(datas); + } + } +} diff --git a/src/main/java/com/easypost/hooks/RequestHook.java b/src/main/java/com/easypost/hooks/RequestHook.java new file mode 100644 index 000000000..702395d69 --- /dev/null +++ b/src/main/java/com/easypost/hooks/RequestHook.java @@ -0,0 +1,4 @@ +package com.easypost.hooks; + +public class RequestHook extends EventHook { +} diff --git a/src/main/java/com/easypost/hooks/ResponseHook.java b/src/main/java/com/easypost/hooks/ResponseHook.java new file mode 100644 index 000000000..717e2ff88 --- /dev/null +++ b/src/main/java/com/easypost/hooks/ResponseHook.java @@ -0,0 +1,4 @@ +package com.easypost.hooks; + +public class ResponseHook extends EventHook { +} diff --git a/src/main/java/com/easypost/hooks/package-info.java b/src/main/java/com/easypost/hooks/package-info.java new file mode 100644 index 000000000..17ee3aca4 --- /dev/null +++ b/src/main/java/com/easypost/hooks/package-info.java @@ -0,0 +1,9 @@ +/** + * Custom hook classes for the EasyPost API. + * + * @author EasyPost developers + * @version 1.0 + * @see EasyPost API documentation + * @since 1.0 + */ +package com.easypost.hooks; diff --git a/src/main/java/com/easypost/http/Requestor.java b/src/main/java/com/easypost/http/Requestor.java index 534889f68..0eafcb6a2 100644 --- a/src/main/java/com/easypost/http/Requestor.java +++ b/src/main/java/com/easypost/http/Requestor.java @@ -47,10 +47,12 @@ import java.net.URL; import java.net.URLEncoder; import java.net.URLStreamHandler; +import java.time.Instant; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Scanner; +import java.util.UUID; public abstract class Requestor { public enum RequestMethod { @@ -417,6 +419,7 @@ private static EasyPostResponse makeURLConnectionRequest(final RequestMethod met * @throws PaymentError when the request requires payment. * @throws NotFoundError when the request endpoint is not found. * @throws MethodNotAllowedError when the request method is not allowed. + * @throws MissingParameterError when the request client doesn't have API key. * @throws TimeoutError when the request times out. * @throws InvalidRequestError when the request is invalid. * @throws RateLimitError when the request exceeds the rate limit. @@ -429,7 +432,7 @@ public static T request(final RequestMethod method, final String endpoint, f final Class clazz, final EasyPostClient client) throws GatewayTimeoutError, RateLimitError, InvalidRequestError, NotFoundError, TimeoutError, EncodingError, UnauthorizedError, MethodNotAllowedError, InternalServerError, UnknownApiError, ServiceUnavailableError, - ForbiddenError, JsonError, HttpError, RedirectError, PaymentError { + ForbiddenError, JsonError, HttpError, RedirectError, PaymentError, MissingParameterError { String apiVersion = client.getApiVersion(); return request(method, endpoint, params, clazz, client, apiVersion); } @@ -454,6 +457,7 @@ public static T request(final RequestMethod method, final String endpoint, f * @throws PaymentError when the request requires payment. * @throws NotFoundError when the request endpoint is not found. * @throws MethodNotAllowedError when the request method is not allowed. + * @throws MissingParameterError when the request client doesn't have API key. * @throws TimeoutError when the request times out. * @throws InvalidRequestError when the request is invalid. * @throws RateLimitError when the request exceeds the rate limit. @@ -466,7 +470,8 @@ public static T request(final RequestMethod method, final String endpoint, f final Class clazz, final EasyPostClient client, final String apiVersion) throws EncodingError, JsonError, RedirectError, UnauthorizedError, ForbiddenError, PaymentError, NotFoundError, MethodNotAllowedError, TimeoutError, InvalidRequestError, RateLimitError, - InternalServerError, ServiceUnavailableError, GatewayTimeoutError, UnknownApiError, HttpError { + InternalServerError, ServiceUnavailableError, GatewayTimeoutError, UnknownApiError, HttpError, + MissingParameterError { String originalDNSCacheTTL = null; boolean allowedToSetTTL = true; String url = client.getApiBase() + "/" + apiVersion + "/" + endpoint; @@ -511,6 +516,7 @@ public static T request(final RequestMethod method, final String endpoint, f * @throws PaymentError when the request requires payment. * @throws NotFoundError when the request endpoint is not found. * @throws MethodNotAllowedError when the request method is not allowed. + * @throws MissingParameterError when the request client doesn't have API key. * @throws TimeoutError when the request times out. * @throws InvalidRequestError when the request is invalid. * @throws RateLimitError when the request exceeds the rate limit. @@ -524,7 +530,8 @@ private static T httpRequest(final RequestMethod method, final String url, f final Class clazz, final EasyPostClient client) throws EncodingError, JsonError, RedirectError, UnauthorizedError, ForbiddenError, PaymentError, NotFoundError, MethodNotAllowedError, TimeoutError, InvalidRequestError, RateLimitError, - InternalServerError, ServiceUnavailableError, GatewayTimeoutError, UnknownApiError, HttpError { + InternalServerError, ServiceUnavailableError, GatewayTimeoutError, UnknownApiError, HttpError, + MissingParameterError { String query = null; JsonObject body = null; if (params != null) { @@ -553,7 +560,20 @@ private static T httpRequest(final RequestMethod method, final String url, f break; } } - + Instant requestTimestamp = Instant.now(); + UUID requestUuid = UUID.randomUUID(); + Map headers = new HashMap(); + headers = generateHeaders(client.getApiKey()); + + HashMap requestBodyForHook = new HashMap(); + requestBodyForHook.put("headers", headers); + requestBodyForHook.put("method", method.toString()); + requestBodyForHook.put("path", url); + requestBodyForHook.put("request_body", body); + requestBodyForHook.put("request_timestamp", requestTimestamp); + requestBodyForHook.put("request_uuid", requestUuid); + client.getRequestHooks().executeEventHandler(requestBodyForHook); + EasyPostResponse response; try { // HTTPSURLConnection verifies SSL cert by default @@ -573,6 +593,17 @@ private static T httpRequest(final RequestMethod method, final String url, f handleAPIError(rBody, rCode); } + HashMap responseBodyForHook = new HashMap(); + responseBodyForHook.put("http_status", rCode); + responseBodyForHook.put("headers", headers); + responseBodyForHook.put("method", method.toString()); + responseBodyForHook.put("path", url); + responseBodyForHook.put("response_body", rBody); + responseBodyForHook.put("response_timestamp", Instant.now()); + responseBodyForHook.put("request_timestamp", requestTimestamp); + responseBodyForHook.put("request_uuid", requestUuid); + client.getResponseHooks().executeEventHandler(responseBodyForHook); + return Constants.Http.GSON.fromJson(rBody, clazz); } diff --git a/src/main/java/com/easypost/service/AddressService.java b/src/main/java/com/easypost/service/AddressService.java index 0a2d89077..bc0e6b18a 100644 --- a/src/main/java/com/easypost/service/AddressService.java +++ b/src/main/java/com/easypost/service/AddressService.java @@ -1,6 +1,5 @@ package com.easypost.service; -import com.easypost.exception.APIException; import com.easypost.exception.EasyPostException; import com.easypost.exception.General.EndOfPaginationError; import com.easypost.http.Requestor; @@ -71,7 +70,7 @@ public Address retrieve(final String id) throws EasyPostException { * @return AddressCollection object. * @throws APIException when the request fails. */ - public AddressCollection all(final Map params) throws APIException { + public AddressCollection all(final Map params) throws EasyPostException { String endpoint = "addresses"; return Requestor.request(RequestMethod.GET, endpoint, params, AddressCollection.class, client); diff --git a/src/main/java/com/easypost/service/EasyPostClient.java b/src/main/java/com/easypost/service/EasyPostClient.java index a57032fd7..22112eeac 100644 --- a/src/main/java/com/easypost/service/EasyPostClient.java +++ b/src/main/java/com/easypost/service/EasyPostClient.java @@ -1,7 +1,14 @@ package com.easypost.service; +import lombok.Getter; + import com.easypost.Constants; import com.easypost.exception.General.MissingParameterError; +import com.easypost.hooks.ResponseHook; +import com.easypost.hooks.RequestHook; + +import java.util.HashMap; +import java.util.function.Function; public class EasyPostClient { private final int connectTimeoutMilliseconds; @@ -37,6 +44,10 @@ public class EasyPostClient { public final TrackerService tracker; public final UserService user; public final WebhookService webhook; + @Getter + private RequestHook requestHooks = new RequestHook(); + @Getter + private ResponseHook responseHooks = new ResponseHook(); /** * EasyPostClient constructor. @@ -145,6 +156,38 @@ public EasyPostClient(String apiKey, int connectTimeoutMilliseconds, int readTim this.webhook = new WebhookService(this); } + /** + * Subscribes to a request hook from the given function. + * @param function + */ + public void subscribeToRequestHook(Function, Object> function) { + this.requestHooks.addEventHandler(function); + } + + /** + * Unsubscribes to a request hook from the given function. + * @param function + */ + public void unsubscribeFromRequestHook(Function, Object> function) { + this.requestHooks.removeEventHandler(function); + } + + /** + * Subscribes to a response hook from the given function. + * @param function + */ + public void subscribeToResponseHook(Function, Object> function) { + this.responseHooks.addEventHandler(function); + } + + /** + * Unubscribes to a response hook from the given function. + * @param function + */ + public void unsubscribeFromResponseHook(Function, Object> function) { + this.responseHooks.removeEventHandler(function); + } + /** * Get connection timeout milliseconds for this EasyPostClient object. * diff --git a/src/test/cassettes/hook/create.json b/src/test/cassettes/hook/create.json new file mode 100644 index 000000000..ef00db564 --- /dev/null +++ b/src/test/cassettes/hook/create.json @@ -0,0 +1,97 @@ +[ + { + "recordedAt": 1688583359, + "request": { + "body": "{\n \"parcel\": {\n \"length\": 10.0,\n \"width\": 8.0,\n \"weight\": 15.4,\n \"height\": 4.0\n }\n}", + "method": "POST", + "headers": { + "Accept-Charset": [ + "UTF-8" + ], + "User-Agent": [ + "REDACTED" + ], + "Content-Type": [ + "application/json" + ] + }, + "uri": "https://api.easypost.com/v2/parcels" + }, + "response": { + "body": "{\n \"mode\": \"test\",\n \"updated_at\": \"2023-07-05T18:55:58Z\",\n \"predefined_package\": null,\n \"length\": 10.0,\n \"width\": 8.0,\n \"created_at\": \"2023-07-05T18:55:58Z\",\n \"weight\": 15.4,\n \"id\": \"prcl_a66081bdc80a46e0a6d515e6c9d222ef\",\n \"object\": \"Parcel\",\n \"height\": 4.0\n}", + "httpVersion": null, + "headers": { + "null": [ + "HTTP/1.1 201 Created" + ], + "content-length": [ + "229" + ], + "expires": [ + "0" + ], + "x-node": [ + "bigweb11nuq" + ], + "x-frame-options": [ + "SAMEORIGIN" + ], + "x-backend": [ + "easypost" + ], + "x-permitted-cross-domain-policies": [ + "none" + ], + "x-download-options": [ + "noopen" + ], + "strict-transport-security": [ + "max-age\u003d31536000; includeSubDomains; preload" + ], + "pragma": [ + "no-cache" + ], + "x-content-type-options": [ + "nosniff" + ], + "x-xss-protection": [ + "1; mode\u003dblock" + ], + "x-ep-request-uuid": [ + "f79bed8f64a5bcbee78738640003c5ae" + ], + "x-proxied": [ + "extlb2nuq 5ab12a3ed2", + "intlb2nuq d3d339cca1" + ], + "referrer-policy": [ + "strict-origin-when-cross-origin" + ], + "x-runtime": [ + "0.029785" + ], + "etag": [ + "W/\"364841687648ebdd0b2c3401080c1dfa\"" + ], + "content-type": [ + "application/json; charset\u003dutf-8" + ], + "location": [ + "/api/v2/parcels/prcl_a66081bdc80a46e0a6d515e6c9d222ef" + ], + "x-version-label": [ + "easypost-202307051633-3d7bd4ea33-master" + ], + "cache-control": [ + "private, no-cache, no-store" + ] + }, + "status": { + "code": 201, + "message": "Created" + }, + "uri": "https://api.easypost.com/v2/parcels" + }, + "duration": 486 + } +] \ No newline at end of file diff --git a/src/test/java/com/easypost/HookTest.java b/src/test/java/com/easypost/HookTest.java new file mode 100644 index 000000000..a3f22ec09 --- /dev/null +++ b/src/test/java/com/easypost/HookTest.java @@ -0,0 +1,126 @@ +package com.easypost; + +import com.easypost.exception.EasyPostException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.HashMap; +import java.util.function.Function; + +public class HookTest { + private static TestUtils.VCR vcr; + + /** + * Set up the testing environment for this file. + * + * @throws EasyPostException when the request fails. + */ + @BeforeAll + public static void setup() throws EasyPostException { + vcr = new TestUtils.VCR("hook", TestUtils.ApiKey.TEST); + } + + /** + * Test failing a hook if we subscribed. + * + * @param input The input HashMap representing the hook data. + * @return The result of the test. + * @throws EasyPostException when the request fails. + */ + public static Object failIfSubscribed(HashMap input) { + fail("Test failed"); + + return false; + } + + /** + * Test subscribing a request hook. + * + * @param datas The input HashMap representing the hook data. + * @return The result of the test. + * @throws EasyPostException when the request fails. + */ + public static Object testRequestHooks(HashMap datas) { + // Assertions + assertEquals("https://api.easypost.com/v2/parcels", datas.get("path")); + assertEquals("POST", datas.get("method")); + assertTrue(datas.containsKey("headers")); + assertTrue(datas.containsKey("request_body")); + assertTrue(datas.containsKey("request_timestamp")); + assertTrue(datas.containsKey("request_uuid")); + + return true; + } + + /** + * Test subscribing a response hook. + * + * @param datas The input HashMap representing the hook data. + * @return The result of the test. + * @throws EasyPostException when the request fails. + */ + public static Object testResponseHooks(HashMap datas) { + // Assertions + assertEquals("https://api.easypost.com/v2/parcels", datas.get("path")); + assertEquals("POST", datas.get("method")); + assertEquals(201, datas.get("http_status")); + assertTrue(datas.containsKey("headers")); + assertTrue(datas.containsKey("response_body")); + assertTrue(datas.containsKey("response_timestamp")); + assertTrue(datas.containsKey("request_timestamp")); + assertTrue(datas.containsKey("request_uuid")); + + return true; + } + + /** + * Test creating a Parcel with request hook subscribed. + * + * @throws EasyPostException when the request fails. + */ + @Test + public void testCreateParcelWithRequestHook() throws EasyPostException { + vcr.setUpTest("create"); + Function, Object> requestHook = HookTest::testRequestHooks; + vcr.client.subscribeToRequestHook(requestHook); + vcr.client.parcel.create(Fixtures.basicParcel()); + } + + /** + * Test creating a Parcel with response hook subscribed. + * + * @throws EasyPostException when the request fails. + */ + @Test + public void testCreateParcelWithResponseHook() throws EasyPostException { + vcr.setUpTest("create"); + Function, Object> requestHook = HookTest::testResponseHooks; + vcr.client.subscribeToResponseHook(requestHook); + vcr.client.parcel.create(Fixtures.basicParcel()); + } + + /** + * Test creating a Parcel with unsubscribed hooks. + * + * @throws EasyPostException when the request fails. + */ + @Test + public void testUnsubscribeHooks() throws EasyPostException { + vcr.setUpTest("create"); + + Function, Object> failedHook = HookTest::failIfSubscribed; + + vcr.client.subscribeToRequestHook(failedHook); + vcr.client.unsubscribeFromRequestHook(failedHook); + + vcr.client.subscribeToResponseHook(failedHook); + vcr.client.unsubscribeFromResponseHook(failedHook); + + vcr.client.parcel.create(Fixtures.basicParcel()); + + } +}