From 3c0bdb38951aca691c433e4a68f5cbd24505b8ed Mon Sep 17 00:00:00 2001 From: Steviep Date: Mon, 21 Sep 2020 10:52:35 +0900 Subject: [PATCH] Sp/better handling of errors (#60) * POC for how to handle error responses better * refactor * fix test case, start putting together new test cases * cleanup * wrap up refactoring * Update documentation * update docs --- 3_0_0_migration_notes.MD | 97 ++++- CHANGELOG.md | 7 +- .../pardot/api/InvalidRequestException.java | 5 + .../com/darksci/pardot/api/PardotClient.java | 389 ++++++++++++------ .../api/parser/DeleteResponseParser.java | 31 ++ .../pardot/api/response/ErrorCode.java | 7 +- .../darksci/pardot/api/response/Result.java | 227 ++++++++++ .../AbstractPardotClientIntegrationTest.java | 95 +++-- .../api/PardotClientSsoIntegrationTest.java | 6 + .../pardot/api/PardotClient_SsoAuthTest.java | 73 +++- ...dotClient_UsernameAndPasswordAuthTest.java | 87 +++- .../mockResponses/userRead_invalidUserId.xml | 4 + 12 files changed, 842 insertions(+), 186 deletions(-) create mode 100644 src/main/java/com/darksci/pardot/api/parser/DeleteResponseParser.java create mode 100644 src/main/java/com/darksci/pardot/api/response/Result.java create mode 100644 src/test/resources/mockResponses/userRead_invalidUserId.xml diff --git a/3_0_0_migration_notes.MD b/3_0_0_migration_notes.MD index ee8e14d..edeca21 100644 --- a/3_0_0_migration_notes.MD +++ b/3_0_0_migration_notes.MD @@ -1,6 +1,6 @@ # Pardot Java API Client 2.x.x to 3.0.0 Migration Guide -## Authentication & Configuration Breaking Changes +## Breaking Change : Authentication & Configuration In order to properly support the transition of the Pardot API from authenticating using Pardot username, password, and user keys, to Salesforce Single Signon, several breaking changes were made to how you configure the Pardot API Client. @@ -48,4 +48,97 @@ final ConfigurationBuilder configuration = Configuration.newBuilder() final PardotClient client = new PardotClient(configuration); ``` -See [Offical Pardot Developer Documentation](https://developer.pardot.com/kb/authentication/) and [Salesforce OAuth Setup](https://help.salesforce.com/articleView?id=remoteaccess_oauth_flows.htm) for details on how to obtain the above required properties. \ No newline at end of file +See [Offical Pardot Developer Documentation](https://developer.pardot.com/kb/authentication/) and [Salesforce OAuth Setup](https://help.salesforce.com/articleView?id=remoteaccess_oauth_flows.htm) for details on how to obtain the above required properties. + +## Breaking Change : Read request methods now return Optionals + +Previously methods on `PardotClient` for reading objects (such as `PardotClient.readProspect()`, `PardotClient.readCampaign()`, etc...) +either returned the object you wished to retrieve, or threw an `InvalidRequestException` if the API returned a response stating that the record +could not be found. + +This often led to code like: +```java +// Try to lookup prospect by Email. +Prospect myProspect = null; +try { + myProspect = client.prospectRead(new ProspectReadRequest() + .selectByEmail("test@prospect.com") + ); +} catch (final InvalidRequestException exception) { + // Prospect could not be found. Swallow exception. +} + +// Handle prospect if found +if (myProspect != null) { + ... +} +``` + +These methods now return `Optional`,which means you no longer need to catch and handle `InvalidRequestException` +when an object is unable to be found in the API. The above example code can be simplified to: + +```java +// Check if the Optional is present +final Optional prospectResponse = client.prospectRead(new ProspectReadRequest() + .selectByEmail("test@prospect.com") +); +if (prospectResponse.isPresent()) { + final Prospect myProspect = prospectResponse.get() +} + +// Or even better by using the methods available on Optional +client.prospectRead(new ProspectReadRequest() + .selectByEmail('test@prospect.com') +).ifPresent((prospect) -> { + // Make use of prospect if found + logger.info("Found prospect: {}", prospect.getEmail()); +}); +``` + +## Breaking Change : Delete request methods no longer return boolean + +Previously methods on `PardotClient` for deleting objects (such as `PardotClient.deleteProspect()`, `PardotClient.deleteCampaign()`, etc...) +always returned a value of `true` regardless if the delete request was successful or not. Version 3.0.0 changes the return type for these methods +to a type of `Result`. The [Result](src/main/java/com/darksci/pardot/api/response/Result.java) object +allows for returning a successful response (value of boolean `true`) as well as an [ErrorResponse](src/main/java/com/darksci/pardot/api/response/ErrorResponse.java) +instance if the request was not successful. + +The quick way to simply migrate your code: + +```java +// Code that looked like the following: +final boolean result = client.userDelete(deleteRequest); +// or +if (client.userDelete(deleteRequest)) { + +// Now should look like: +final boolean result = client.userDelete(deleteRequest).isSuccess(); +// or +if (client.userDelete(deleteRequest.isSuccess())) { +``` + +But this new return type also gives you additional flexibility to handle errors more gracefully: +```java +// Handle success conditions: +client + .userDelete(deleteRequest) + .ifSuccess((success) -> logger.info("Successfully deleted user!")); + +// Handle error conditions: +client + .userDelete(deleteRequest) + .ifError((error -> logger.info("Failed to delete user: {}", error.getMessage()))); + +// Or handle both cases: +client + .userDelete(deleteRequest) + .handle( + (success) -> { + logger.info("Successfully deleted user!"); + return true; + }, + (error) -> { + logger.info("Error deleting user: {}", error.getMessage()); + return false; + }); +``` \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ae3272c..4f9235b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## 3.0.0 (UNRELEASED) + +### Breaking Changes +This release has several breaking changes. See [3.0.0 Migration Notes](3_0_0_migration_notes.MD) for details on breaking changes and how to upgrade. + - [ISSUE-58](https://github.com/Crim/pardot-java-client/issues/58) Add support for Salesforce SSO authentication. +- [PR-60](https://github.com/Crim/pardot-java-client/pull/60) Alters return values from `PardotClient.readXYZ()` and `PardotClient.deleteXYZ()` to allow for better error handling. -### Breaking Change -See [3.0.0 Migration Notes](3_0_0_migration_notes.MD) for details on breaking changes and how to upgrade. ## 2.1.0 (07/30/2020) - [ISSUE-56](https://github.com/Crim/pardot-java-client/issues/56) Adds support for Dynamic Content. diff --git a/src/main/java/com/darksci/pardot/api/InvalidRequestException.java b/src/main/java/com/darksci/pardot/api/InvalidRequestException.java index 6194b9b..d78e0bb 100644 --- a/src/main/java/com/darksci/pardot/api/InvalidRequestException.java +++ b/src/main/java/com/darksci/pardot/api/InvalidRequestException.java @@ -23,6 +23,11 @@ public class InvalidRequestException extends RuntimeException { private final int errorCode; + /** + * Constructor. + * @param message Error message returned by Pardot API. + * @param errorCode Error code returned by Pardot API. + */ public InvalidRequestException(final String message, final int errorCode) { super(message); this.errorCode = errorCode; diff --git a/src/main/java/com/darksci/pardot/api/PardotClient.java b/src/main/java/com/darksci/pardot/api/PardotClient.java index f8e4b45..59050cc 100644 --- a/src/main/java/com/darksci/pardot/api/PardotClient.java +++ b/src/main/java/com/darksci/pardot/api/PardotClient.java @@ -21,9 +21,9 @@ import com.darksci.pardot.api.auth.SessionRefreshHandler; import com.darksci.pardot.api.auth.SsoSessionRefreshHandler; import com.darksci.pardot.api.config.Configuration; +import com.darksci.pardot.api.parser.DeleteResponseParser; import com.darksci.pardot.api.parser.ErrorResponseParser; import com.darksci.pardot.api.parser.ResponseParser; -import com.darksci.pardot.api.parser.StringResponseParser; import com.darksci.pardot.api.parser.account.AccountReadResponseParser; import com.darksci.pardot.api.parser.campaign.CampaignQueryResponseParser; import com.darksci.pardot.api.parser.campaign.CampaignReadResponseParser; @@ -135,6 +135,7 @@ import com.darksci.pardot.api.request.visitoractivity.VisitorActivityReadRequest; import com.darksci.pardot.api.response.ErrorCode; import com.darksci.pardot.api.response.ErrorResponse; +import com.darksci.pardot.api.response.Result; import com.darksci.pardot.api.response.account.Account; import com.darksci.pardot.api.response.campaign.Campaign; import com.darksci.pardot.api.response.campaign.CampaignQueryResponse; @@ -181,7 +182,9 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.Arrays; import java.util.Objects; +import java.util.Optional; /** * Interface for Pardot's API. @@ -253,9 +256,8 @@ public PardotClient(final ConfigurationBuilder configurationBuilder, final RestC } } - private T submitRequest(final Request request, ResponseParser responseParser) { - // Ugly hack, - // avoid doing login check if we're doing a login request. + private Result submitRequest(final Request request, final ResponseParser responseParser) { + // Avoid doing login check if we're doing a login request. if (!(request instanceof LoginRequestMarker)) { // Check for auth token checkLogin(); @@ -266,7 +268,7 @@ private T submitRequest(final Request request, ResponseParser responsePar final int responseCode = restResponse.getHttpCode(); String responseStr = restResponse.getResponseStr(); - // If we have a valid response + // Don't log out responses from Login requests to avoid leaking sensitive details. if (!(request instanceof LoginRequestMarker)) { logger.info("Response: {}", restResponse); } @@ -284,7 +286,7 @@ private T submitRequest(final Request request, ResponseParser responsePar if (responseStr.contains(" T submitRequest(final Request request, ResponseParser responsePar // Replay original request return submitRequest(request, responseParser); } else if (ErrorCode.WRONG_API_VERSION.getCode() == error.getCode() && ! "4".equals(configuration.getPardotApiVersion())) { - // This means we requested api version 3, but the account requires api version 4 + // This means we execute a request against api version 3, but the account requires api version 4. // Lets transparently switch to version 4 and re-send the request. logger.info("Detected API version 4 should be used, retrying request with API Version 4."); - // Upgrade to version 4 + // Upgrade to version 4. configuration.setPardotApiVersion("4"); // Replay original request return submitRequest(request, responseParser); } - // throw exception - throw new InvalidRequestException(error.getMessage(), error.getCode()); + // Return error response. + return Result.newFailure(error); } catch (final IOException exception) { throw new ParserException(exception.getMessage(), exception); } @@ -327,16 +329,17 @@ private T submitRequest(final Request request, ResponseParser responsePar throw new ParserException(exception.getMessage(), exception); } } - - // throw an exception. + // We got an error http response code, but the API didn't return an error response.... + // Not sure this scenario exists, but lets throw an exception. throw new InvalidRequestException("Invalid http response code from server: " + restResponse.getHttpCode(), restResponse.getHttpCode()); } - // Attempt to parse successful response. + // Attempt to parse and return a Success result. try { - return responseParser.parseResponse(restResponse.getResponseStr()); + return Result.newSuccess( + responseParser.parseResponse(restResponse.getResponseStr()) + ); } catch (final IOException exception) { - // Unparsable response value? throw new ParserException(exception.getMessage(), exception); } } @@ -346,7 +349,7 @@ private T submitRequest(final Request request, ResponseParser responsePar * * @return Return Pardot API Configuration. */ - public Configuration getConfiguration() { + Configuration getConfiguration() { return configuration; } @@ -372,6 +375,7 @@ RestClient getRestClient() { * Check to see if we're already logged in and have an API key. * If no existing API key is found, this will attempt to authenticate and * get a new API key. + * @throws LoginFailedException if credentials are invalid. */ private void checkLogin() { if (sessionRefreshHandler.isValid()) { @@ -390,26 +394,29 @@ private void checkLogin() { * with SSO authentication. {@link PardotClient#login(SsoLoginRequest)} */ public LoginResponse login(final LoginRequest request) { - try { - final LoginResponse loginResponse = submitRequest(request, new LoginResponseParser()); - - // If we have a version mis-match. - if (!loginResponse.getApiVersion().equals(getConfiguration().getPardotApiVersion())) { - // Log what we're doing - logger.info( - "Upgrading API version from {} to {}", - getConfiguration().getPardotApiVersion(), - loginResponse.getApiVersion()); - - // Update configuration - getConfiguration().setPardotApiVersion(loginResponse.getApiVersion()); - } - - return loginResponse; - } catch (final InvalidRequestException exception) { - // Throw more specific exception - throw new LoginFailedException(exception.getMessage(), exception.getErrorCode(), exception); + final LoginResponse loginResponse = submitRequest(request, new LoginResponseParser()) + .handleError((errorResponse) -> { + // If authentication error response + if (ErrorCode.LOGIN_FAILED.getCode() == errorResponse.getCode()) { + // Throw specific login failed exception. + throw new LoginFailedException(errorResponse.getMessage(), errorResponse.getCode()); + } + // Otherwise throw generic exception. + throw new InvalidRequestException(errorResponse.getMessage(), errorResponse.getCode()); + }); + + // If we have a version mis-match. + if (!loginResponse.getApiVersion().equals(getConfiguration().getPardotApiVersion())) { + // Log what we're doing + logger.info( + "Upgrading API version from {} to {}", + getConfiguration().getPardotApiVersion(), + loginResponse.getApiVersion()); + + // Update configuration + getConfiguration().setPardotApiVersion(loginResponse.getApiVersion()); } + return loginResponse; } /** @@ -421,7 +428,7 @@ public LoginResponse login(final LoginRequest request) { */ public SsoLoginResponse login(final SsoLoginRequest request) { try { - return submitRequest(request, new SsoLoginResponseParser()); + return submitRequest(request, new SsoLoginResponseParser()).get(); } catch (final InvalidRequestException exception) { // Rethrow login failed exceptions as-is. if (exception instanceof LoginFailedException) { @@ -438,7 +445,8 @@ public SsoLoginResponse login(final SsoLoginRequest request) { * @return Parsed api response. */ public Account accountRead(final AccountReadRequest request) { - return submitRequest(request, new AccountReadResponseParser()); + return submitRequest(request, new AccountReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -447,7 +455,8 @@ public Account accountRead(final AccountReadRequest request) { * @return Parsed user query response. */ public UserQueryResponse.Result userQuery(final UserQueryRequest request) { - return submitRequest(request, new UserQueryResponseParser()); + return submitRequest(request, new UserQueryResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -456,7 +465,8 @@ public UserQueryResponse.Result userQuery(final UserQueryRequest request) { * @return Parsed api response. */ public UserAbilitiesResponse.Result userAbilities(final UserAbilitiesRequest request) { - return submitRequest(request, new UserAbilitiesParser()); + return submitRequest(request, new UserAbilitiesParser()) + .orElseThrowInvalidRequestException(); } /** @@ -465,7 +475,8 @@ public UserAbilitiesResponse.Result userAbilities(final UserAbilitiesRequest req * @return Parsed api response. */ public Cookie userCookie(final UserCookieRequest request) { - return submitRequest(request, new UserCookieParser()); + return submitRequest(request, new UserCookieParser()) + .orElseThrowInvalidRequestException(); } /** @@ -473,18 +484,20 @@ public Cookie userCookie(final UserCookieRequest request) { * @param request Request definition. * @return Parsed api response. */ - public User userRead(final UserReadRequest request) { - return submitRequest(request, new UserReadResponseParser()); + public Optional userRead(final UserReadRequest request) { + return optionalUnlessErrorCode( + submitRequest(request, new UserReadResponseParser()), + ErrorCode.INVALID_USER_ID + ); } /** * Make API request to delete a specific user. * @param request Request definition. - * @return Parsed api response. + * @return Result instance containing boolean true if a success, or an ErrorResponse if an error occurred. */ - public boolean userDelete(final UserDeleteRequest request) { - submitRequest(request, new StringResponseParser()); - return true; + public Result userDelete(final UserDeleteRequest request) { + return submitRequest(request, new DeleteResponseParser()); } /** @@ -493,7 +506,9 @@ public boolean userDelete(final UserDeleteRequest request) { * @return Parsed api response. */ public User userCreate(final UserCreateRequest request) { - return submitRequest(request, new UserCreateResponseParser()).getUser(); + return submitRequest(request, new UserCreateResponseParser()) + .orElseThrowInvalidRequestException() + .getUser(); } /** @@ -502,7 +517,8 @@ public User userCreate(final UserCreateRequest request) { * @return Parsed api response containing the updated user record. */ public User userUpdateRole(final UserUpdateRoleRequest request) { - return submitRequest(request, new UserReadResponseParser()); + return submitRequest(request, new UserReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -511,16 +527,20 @@ public User userUpdateRole(final UserUpdateRoleRequest request) { * @return Parsed api response. */ public CampaignQueryResponse.Result campaignQuery(final CampaignQueryRequest request) { - return submitRequest(request, new CampaignQueryResponseParser()); + return submitRequest(request, new CampaignQueryResponseParser()) + .orElseThrowInvalidRequestException(); } /** * Make API request to read a specific campaign. * @param request Request definition. - * @return Parsed api response. + * @return Optional of Campaign that was selected. */ - public Campaign campaignRead(final CampaignReadRequest request) { - return submitRequest(request, new CampaignReadResponseParser()); + public Optional campaignRead(final CampaignReadRequest request) { + return optionalUnlessErrorCode( + submitRequest(request, new CampaignReadResponseParser()), + ErrorCode.INVALID_ID + ); } /** @@ -529,7 +549,8 @@ public Campaign campaignRead(final CampaignReadRequest request) { * @return Parsed api response. */ public Campaign campaignCreate(final CampaignCreateRequest request) { - return submitRequest(request, new CampaignReadResponseParser()); + return submitRequest(request, new CampaignReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -538,7 +559,8 @@ public Campaign campaignCreate(final CampaignCreateRequest request) { * @return Parsed api response. */ public Campaign campaignUpdate(final CampaignUpdateRequest request) { - return submitRequest(request, new CampaignReadResponseParser()); + return submitRequest(request, new CampaignReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -547,7 +569,8 @@ public Campaign campaignUpdate(final CampaignUpdateRequest request) { * @return Parsed api response. */ public CustomFieldQueryResponse.Result customFieldQuery(final CustomFieldQueryRequest request) { - return submitRequest(request, new CustomFieldQueryResponseParser()); + return submitRequest(request, new CustomFieldQueryResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -555,8 +578,11 @@ public CustomFieldQueryResponse.Result customFieldQuery(final CustomFieldQueryRe * @param request Request definition. * @return Parsed api response. */ - public CustomField customFieldRead(final CustomFieldReadRequest request) { - return submitRequest(request, new CustomFieldReadResponseParser()); + public Optional customFieldRead(final CustomFieldReadRequest request) { + return optionalUnlessErrorCode( + submitRequest(request, new CustomFieldReadResponseParser()), + ErrorCode.INVALID_ID + ); } /** @@ -565,7 +591,8 @@ public CustomField customFieldRead(final CustomFieldReadRequest request) { * @return Parsed api response. */ public CustomField customFieldCreate(final CustomFieldCreateRequest request) { - return submitRequest(request, new CustomFieldReadResponseParser()); + return submitRequest(request, new CustomFieldReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -574,17 +601,17 @@ public CustomField customFieldCreate(final CustomFieldCreateRequest request) { * @return Parsed api response. */ public CustomField customFieldUpdate(final CustomFieldUpdateRequest request) { - return submitRequest(request, new CustomFieldReadResponseParser()); + return submitRequest(request, new CustomFieldReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** * Make API request to delete a custom field. * @param request Request definition. - * @return true if success, false if error. + * @return Result instance containing boolean true if a success, or an ErrorResponse if an error occurred. */ - public boolean customFieldDelete(final CustomFieldDeleteRequest request) { - submitRequest(request, new StringResponseParser()); - return true; + public Result customFieldDelete(final CustomFieldDeleteRequest request) { + return submitRequest(request, new DeleteResponseParser()); } /** @@ -593,7 +620,8 @@ public boolean customFieldDelete(final CustomFieldDeleteRequest request) { * @return Parsed api response. */ public CustomRedirectQueryResponse.Result customRedirectQuery(final CustomRedirectQueryRequest request) { - return submitRequest(request, new CustomRedirectQueryResponseParser()); + return submitRequest(request, new CustomRedirectQueryResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -601,8 +629,11 @@ public CustomRedirectQueryResponse.Result customRedirectQuery(final CustomRedire * @param request Request definition. * @return Parsed api response. */ - public CustomRedirect customRedirectRead(final CustomRedirectReadRequest request) { - return submitRequest(request, new CustomRedirectReadResponseParser()); + public Optional customRedirectRead(final CustomRedirectReadRequest request) { + return optionalUnlessErrorCode( + submitRequest(request, new CustomRedirectReadResponseParser()), + ErrorCode.INVALID_ID + ); } /** @@ -611,7 +642,8 @@ public CustomRedirect customRedirectRead(final CustomRedirectReadRequest request * @return Parsed api response. */ public DynamicContentQueryResponse.Result dynamicContentQuery(final DynamicContentQueryRequest request) { - return submitRequest(request, new DynamicContentQueryResponseParser()); + return submitRequest(request, new DynamicContentQueryResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -619,8 +651,11 @@ public DynamicContentQueryResponse.Result dynamicContentQuery(final DynamicConte * @param request Request definition. * @return Parsed api response. */ - public DynamicContent dynamicContentRead(final DynamicContentReadRequest request) { - return submitRequest(request, new DynamicContentReadResponseParser()); + public Optional dynamicContentRead(final DynamicContentReadRequest request) { + return optionalUnlessErrorCode( + submitRequest(request, new DynamicContentReadResponseParser()), + ErrorCode.INVALID_ID + ); } /** @@ -628,8 +663,11 @@ public DynamicContent dynamicContentRead(final DynamicContentReadRequest request * @param request Request definition. * @return Parsed api response. */ - public Email emailRead(final EmailReadRequest request) { - return submitRequest(request, new EmailReadResponseParser()); + public Optional emailRead(final EmailReadRequest request) { + return optionalUnlessErrorCode( + submitRequest(request, new EmailReadResponseParser()), + ErrorCode.INVALID_ID + ); } /** @@ -637,8 +675,11 @@ public Email emailRead(final EmailReadRequest request) { * @param request Request definition. * @return Parsed api response. */ - public EmailStatsResponse.Stats emailStats(final EmailStatsRequest request) { - return submitRequest(request, new EmailStatsResponseParser()); + public Optional emailStats(final EmailStatsRequest request) { + return optionalUnlessErrorCode( + submitRequest(request, new EmailStatsResponseParser()), + ErrorCode.INVALID_ID + ); } /** @@ -647,7 +688,8 @@ public EmailStatsResponse.Stats emailStats(final EmailStatsRequest request) { * @return Parsed api response. */ public Email emailSendOneToOne(final EmailSendOneToOneRequest request) { - return submitRequest(request, new EmailReadResponseParser()); + return submitRequest(request, new EmailReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -656,7 +698,8 @@ public Email emailSendOneToOne(final EmailSendOneToOneRequest request) { * @return Parsed api response. */ public Email emailSendList(final EmailSendListRequest request) { - return submitRequest(request, new EmailReadResponseParser()); + return submitRequest(request, new EmailReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -665,7 +708,8 @@ public Email emailSendList(final EmailSendListRequest request) { * @return Parsed api response. */ public EmailClickQueryResponse.Result emailClickQuery(final EmailClickQueryRequest request) { - return submitRequest(request, new EmailClickQueryResponseParser()); + return submitRequest(request, new EmailClickQueryResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -673,8 +717,11 @@ public EmailClickQueryResponse.Result emailClickQuery(final EmailClickQueryReque * @param request Request definition. * @return Parsed api response. */ - public EmailTemplate emailTemplateRead(final EmailTemplateReadRequest request) { - return submitRequest(request, new EmailTemplateReadResponseParser()); + public Optional emailTemplateRead(final EmailTemplateReadRequest request) { + return optionalUnlessErrorCode( + submitRequest(request, new EmailTemplateReadResponseParser()), + ErrorCode.INVALID_ID, ErrorCode.INVALID_EMAIL_TEMPLATE + ); } /** @@ -682,7 +729,8 @@ public EmailTemplate emailTemplateRead(final EmailTemplateReadRequest request) { * @return Parsed api response. */ public EmailTemplateListOneToOneResponse.Result emailTemplateListOneToOne() { - return submitRequest(new EmailTemplateListOneToOneRequest(), new EmailTemplateListOneToOneResponseParser()); + return submitRequest(new EmailTemplateListOneToOneRequest(), new EmailTemplateListOneToOneResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -691,17 +739,17 @@ public EmailTemplateListOneToOneResponse.Result emailTemplateListOneToOne() { * @return Parsed api response. */ public Form formCreate(final FormCreateRequest request) { - return submitRequest(request, new FormReadResponseParser()); + return submitRequest(request, new FormReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** * Make API request to delete a form. * @param request Request definition. - * @return Parsed api response. + * @return Result instance containing boolean true if a success, or an ErrorResponse if an error occurred. */ - public boolean formDelete(final FormDeleteRequest request) { - submitRequest(request, new StringResponseParser()); - return true; + public Result formDelete(final FormDeleteRequest request) { + return submitRequest(request, new DeleteResponseParser()); } /** @@ -710,7 +758,8 @@ public boolean formDelete(final FormDeleteRequest request) { * @return Parsed api response. */ public FormQueryResponse.Result formQuery(final FormQueryRequest request) { - return submitRequest(request, new FormQueryResponseParser()); + return submitRequest(request, new FormQueryResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -718,8 +767,11 @@ public FormQueryResponse.Result formQuery(final FormQueryRequest request) { * @param request Request definition. * @return Parsed api response. */ - public Form formRead(final FormReadRequest request) { - return submitRequest(request, new FormReadResponseParser()); + public Optional
formRead(final FormReadRequest request) { + return optionalUnlessErrorCode( + submitRequest(request, new FormReadResponseParser()), + ErrorCode.INVALID_ID + ); } /** @@ -728,7 +780,8 @@ public Form formRead(final FormReadRequest request) { * @return Parsed api response. */ public Form formUpdate(final FormUpdateRequest request) { - return submitRequest(request, new FormReadResponseParser()); + return submitRequest(request, new FormReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -737,7 +790,8 @@ public Form formUpdate(final FormUpdateRequest request) { * @return Parsed api response. */ public ListQueryResponse.Result listQuery(final ListQueryRequest request) { - return submitRequest(request, new ListQueryResponseParser()); + return submitRequest(request, new ListQueryResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -745,8 +799,11 @@ public ListQueryResponse.Result listQuery(final ListQueryRequest request) { * @param request Request definition. * @return Parsed api response. */ - public List listRead(final ListReadRequest request) { - return submitRequest(request, new ListReadResponseParser()); + public Optional listRead(final ListReadRequest request) { + return optionalUnlessErrorCode( + submitRequest(request, new ListReadResponseParser()), + ErrorCode.INVALID_ID + ); } /** @@ -755,7 +812,8 @@ public List listRead(final ListReadRequest request) { * @return Parsed api response. */ public List listCreate(final ListCreateRequest request) { - return submitRequest(request, new ListReadResponseParser()); + return submitRequest(request, new ListReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -764,7 +822,8 @@ public List listCreate(final ListCreateRequest request) { * @return Parsed api response. */ public List listUpdate(final ListUpdateRequest request) { - return submitRequest(request, new ListReadResponseParser()); + return submitRequest(request, new ListReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -773,7 +832,8 @@ public List listUpdate(final ListUpdateRequest request) { * @return Parsed api response. */ public ListMembershipQueryResponse.Result listMembershipQuery(final ListMembershipQueryRequest request) { - return submitRequest(request, new ListMembershipQueryResponseParser()); + return submitRequest(request, new ListMembershipQueryResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -781,8 +841,11 @@ public ListMembershipQueryResponse.Result listMembershipQuery(final ListMembersh * @param request Request definition. * @return Parsed api response. */ - public ListMembership listMembershipRead(final ListMembershipReadRequest request) { - return submitRequest(request, new ListMembershipReadResponseParser()); + public Optional listMembershipRead(final ListMembershipReadRequest request) { + return optionalUnlessErrorCode( + submitRequest(request, new ListMembershipReadResponseParser()), + ErrorCode.INVALID_LIST_ID, ErrorCode.INVALID_ID + ); } /** @@ -791,7 +854,8 @@ public ListMembership listMembershipRead(final ListMembershipReadRequest request * @return Parsed api response. */ public ListMembership listMembershipCreate(final ListMembershipCreateRequest request) { - return submitRequest(request, new ListMembershipReadResponseParser()); + return submitRequest(request, new ListMembershipReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -800,7 +864,8 @@ public ListMembership listMembershipCreate(final ListMembershipCreateRequest req * @return Parsed api response. */ public ListMembership listMembershipUpdate(final ListMembershipUpdateRequest request) { - return submitRequest(request, new ListMembershipReadResponseParser()); + return submitRequest(request, new ListMembershipReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -809,7 +874,8 @@ public ListMembership listMembershipUpdate(final ListMembershipUpdateRequest req * @return Parsed api response. */ public OpportunityQueryResponse.Result opportunityQuery(final OpportunityQueryRequest request) { - return submitRequest(request, new OpportunityQueryResponseParser()); + return submitRequest(request, new OpportunityQueryResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -817,8 +883,11 @@ public OpportunityQueryResponse.Result opportunityQuery(final OpportunityQueryRe * @param request Request definition. * @return Parsed api response. */ - public Opportunity opportunityRead(final OpportunityReadRequest request) { - return submitRequest(request, new OpportunityReadResponseParser()); + public Optional opportunityRead(final OpportunityReadRequest request) { + return optionalUnlessErrorCode( + submitRequest(request, new OpportunityReadResponseParser()), + ErrorCode.INVALID_ID, ErrorCode.INVALID_OPPORTUNITY_ID + ); } /** @@ -827,7 +896,8 @@ public Opportunity opportunityRead(final OpportunityReadRequest request) { * @return Parsed api response. */ public Opportunity opportunityCreate(final OpportunityCreateRequest request) { - return submitRequest(request, new OpportunityReadResponseParser()); + return submitRequest(request, new OpportunityReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -836,27 +906,26 @@ public Opportunity opportunityCreate(final OpportunityCreateRequest request) { * @return Parsed api response. */ public Opportunity opportunityUpdate(final OpportunityUpdateRequest request) { - return submitRequest(request, new OpportunityReadResponseParser()); + return submitRequest(request, new OpportunityReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** * Make API request to delete an opportunity. * @param request Request definition. - * @return Parsed api response. + * @return Result instance containing boolean true if a success, or an ErrorResponse if an error occurred. */ - public boolean opportunityDelete(final OpportunityDeleteRequest request) { - submitRequest(request, new StringResponseParser()); - return true; + public Result opportunityDelete(final OpportunityDeleteRequest request) { + return submitRequest(request, new DeleteResponseParser()); } /** * Make API request to un-delete an opportunity. * @param request Request definition. - * @return Parsed api response. + * @return Result instance containing boolean true if a success, or an ErrorResponse if an error occurred. */ - public boolean opportunityUndelete(final OpportunityUndeleteRequest request) { - submitRequest(request, new StringResponseParser()); - return true; + public Result opportunityUndelete(final OpportunityUndeleteRequest request) { + return submitRequest(request, new DeleteResponseParser()); } /** @@ -864,8 +933,11 @@ public boolean opportunityUndelete(final OpportunityUndeleteRequest request) { * @param request Request definition. * @return Parsed api response. */ - public Prospect prospectRead(final ProspectReadRequest request) { - return submitRequest(request, new ProspectReadResponseParser()); + public Optional prospectRead(final ProspectReadRequest request) { + return optionalUnlessErrorCode( + submitRequest(request, new ProspectReadResponseParser()), + ErrorCode.INVALID_ID, ErrorCode.INVALID_PROSPECT_ID, ErrorCode.INVALID_PROSPECT_EMAIL_ADDRESS + ); } /** @@ -874,7 +946,8 @@ public Prospect prospectRead(final ProspectReadRequest request) { * @return Parsed api response. */ public Prospect prospectCreate(final ProspectCreateRequest request) { - return submitRequest(request, new ProspectReadResponseParser()); + return submitRequest(request, new ProspectReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -883,7 +956,8 @@ public Prospect prospectCreate(final ProspectCreateRequest request) { * @return Parsed api response. */ public Prospect prospectUpdate(final ProspectUpdateRequest request) { - return submitRequest(request, new ProspectReadResponseParser()); + return submitRequest(request, new ProspectReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -892,7 +966,8 @@ public Prospect prospectUpdate(final ProspectUpdateRequest request) { * @return Parsed api response. */ public Prospect prospectUpsert(final ProspectUpsertRequest request) { - return submitRequest(request, new ProspectReadResponseParser()); + return submitRequest(request, new ProspectReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -901,17 +976,17 @@ public Prospect prospectUpsert(final ProspectUpsertRequest request) { * @return Parsed api response. */ public ProspectQueryResponse.Result prospectQuery(final ProspectQueryRequest request) { - return submitRequest(request, new ProspectQueryResponseParser()); + return submitRequest(request, new ProspectQueryResponseParser()) + .orElseThrowInvalidRequestException(); } /** * Make API request to delete prospects. * @param request Request definition. - * @return true if success, false if error. + * @return Result instance containing boolean true if a success, or an ErrorResponse if an error occurred. */ - public boolean prospectDelete(final ProspectDeleteRequest request) { - submitRequest(request, new StringResponseParser()); - return true; + public Result prospectDelete(final ProspectDeleteRequest request) { + return submitRequest(request, new DeleteResponseParser()); } /** @@ -920,7 +995,8 @@ public boolean prospectDelete(final ProspectDeleteRequest request) { * @return Parsed api response. */ public Prospect prospectAssign(final ProspectAssignRequest request) { - return submitRequest(request, new ProspectReadResponseParser()); + return submitRequest(request, new ProspectReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -929,7 +1005,8 @@ public Prospect prospectAssign(final ProspectAssignRequest request) { * @return Parsed api response. */ public Prospect prospectUnassign(final ProspectUnassignRequest request) { - return submitRequest(request, new ProspectReadResponseParser()); + return submitRequest(request, new ProspectReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -938,7 +1015,8 @@ public Prospect prospectUnassign(final ProspectUnassignRequest request) { * @return Parsed api response. */ public TagQueryResponse.Result tagQuery(final TagQueryRequest request) { - return submitRequest(request, new TagQueryResponseParser()); + return submitRequest(request, new TagQueryResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -946,8 +1024,11 @@ public TagQueryResponse.Result tagQuery(final TagQueryRequest request) { * @param request Request definition. * @return Parsed api response. */ - public Tag tagRead(final TagReadRequest request) { - return submitRequest(request, new TagReadResponseParser()); + public Optional tagRead(final TagReadRequest request) { + return optionalUnlessErrorCode( + submitRequest(request, new TagReadResponseParser()), + ErrorCode.INVALID_ID + ); } /** @@ -956,7 +1037,8 @@ public Tag tagRead(final TagReadRequest request) { * @return Parsed api response. */ public TagObjectQueryResponse.Result tagObjectQuery(final TagObjectQueryRequest request) { - return submitRequest(request, new TagObjectQueryResponseParser()); + return submitRequest(request, new TagObjectQueryResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -964,8 +1046,11 @@ public TagObjectQueryResponse.Result tagObjectQuery(final TagObjectQueryRequest * @param request Request definition. * @return Parsed api response. */ - public TagObject tagObjectRead(final TagObjectReadRequest request) { - return submitRequest(request, new TagObjectReadResponseParser()); + public Optional tagObjectRead(final TagObjectReadRequest request) { + return optionalUnlessErrorCode( + submitRequest(request, new TagObjectReadResponseParser()), + ErrorCode.INVALID_ID + ); } /** @@ -974,7 +1059,8 @@ public TagObject tagObjectRead(final TagObjectReadRequest request) { * @return Parsed api response. */ public Visitor visitorAssign(final VisitorAssignRequest request) { - return submitRequest(request, new VisitorReadResponseParser()); + return submitRequest(request, new VisitorReadResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -983,7 +1069,8 @@ public Visitor visitorAssign(final VisitorAssignRequest request) { * @return Parsed api response. */ public VisitorQueryResponse.Result visitorQuery(final VisitorQueryRequest request) { - return submitRequest(request, new VisitorQueryResponseParser()); + return submitRequest(request, new VisitorQueryResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -991,8 +1078,11 @@ public VisitorQueryResponse.Result visitorQuery(final VisitorQueryRequest reques * @param request Request definition. * @return Parsed api response */ - public Visitor visitorRead(final VisitorReadRequest request) { - return submitRequest(request, new VisitorReadResponseParser()); + public Optional visitorRead(final VisitorReadRequest request) { + return optionalUnlessErrorCode( + submitRequest(request, new VisitorReadResponseParser()), + ErrorCode.INVALID_ID, ErrorCode.INVALID_VISITOR_ID + ); } /** @@ -1001,7 +1091,8 @@ public Visitor visitorRead(final VisitorReadRequest request) { * @return Parsed api response. */ public VisitorActivityQueryResponse.Result visitorActivityQuery(final VisitorActivityQueryRequest request) { - return submitRequest(request, new VisitorActivityQueryResponseParser()); + return submitRequest(request, new VisitorActivityQueryResponseParser()) + .orElseThrowInvalidRequestException(); } /** @@ -1009,8 +1100,11 @@ public VisitorActivityQueryResponse.Result visitorActivityQuery(final VisitorAct * @param request Request definition. * @return Parsed api response */ - public VisitorActivity visitorActivityRead(final VisitorActivityReadRequest request) { - return submitRequest(request, new VisitorActivityReadResponseParser()); + public Optional visitorActivityRead(final VisitorActivityReadRequest request) { + return optionalUnlessErrorCode( + submitRequest(request, new VisitorActivityReadResponseParser()), + ErrorCode.INVALID_ID + ); } /** @@ -1021,7 +1115,7 @@ public VisitorActivity visitorActivityRead(final VisitorActivityReadRequest requ * @param Parsed return type. * @return parsed response. */ - public ResponseObject userDefinedRequest(final UserDefinedRequest request) { + public Result userDefinedRequest(final UserDefinedRequest request) { return submitRequest(request, request.getResponseParser()); } @@ -1031,4 +1125,25 @@ public ResponseObject userDefinedRequest(final UserDefine public void close() { getRestClient().close(); } + + /** + * Helper method. + * @param result API result. + * @param errorCodes Error codes to allow returning an Optional.empty() for. + * @param Underlying result object. + * @return Optional unless an error code not passed is given. + */ + private Optional optionalUnlessErrorCode(final Result result, final ErrorCode ... errorCodes) { + return Optional.ofNullable( + result.handleError((errorResponse) -> { + final boolean matchedErrorCode = Arrays.stream(errorCodes) + .anyMatch((errorCode) -> errorCode.getCode() == result.getFailure().getCode()); + + if (matchedErrorCode) { + return null; + } + throw new InvalidRequestException(result.getFailure().getMessage(), result.getFailure().getCode()); + }) + ); + } } \ No newline at end of file diff --git a/src/main/java/com/darksci/pardot/api/parser/DeleteResponseParser.java b/src/main/java/com/darksci/pardot/api/parser/DeleteResponseParser.java new file mode 100644 index 0000000..7f934bd --- /dev/null +++ b/src/main/java/com/darksci/pardot/api/parser/DeleteResponseParser.java @@ -0,0 +1,31 @@ +/** + * Copyright 2017, 2018, 2019, 2020 Stephen Powis https://github.com/Crim/pardot-java-client + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.darksci.pardot.api.parser; + +import java.io.IOException; + +/** + * Delete requests are expected to return http code 204 with a null response. + * If this gets a null responseStr, return true. + */ +public class DeleteResponseParser implements ResponseParser { + @Override + public Boolean parseResponse(final String responseStr) throws IOException { + return (responseStr == null || responseStr.isEmpty()); + } +} diff --git a/src/main/java/com/darksci/pardot/api/response/ErrorCode.java b/src/main/java/com/darksci/pardot/api/response/ErrorCode.java index d778108..f83f948 100644 --- a/src/main/java/com/darksci/pardot/api/response/ErrorCode.java +++ b/src/main/java/com/darksci/pardot/api/response/ErrorCode.java @@ -19,6 +19,7 @@ /** * List of Pardot API Response Error Codes. + * Source: http://developer.pardot.com/kb/error-codes-messages/ * * Incomplete List. */ @@ -29,13 +30,17 @@ public enum ErrorCode { INVALID_PROSPECT_ID(3), INVALID_PROSPECT_EMAIL_ADDRESS(4), INVALID_USER_ID(10), - INVALID_ID(11), + INVALID_USER_EMAIL(11), + INVALID_ID(16), // Returned if authentication credentials are invalid. LOGIN_FAILED(15), + INVALID_VISITOR_ID(24), + INVALID_OPPORTUNITY_ID(35), INVALID_CAMPAIGN_ID(38), EMAIL_ADDRESS_IS_ALREADY_IN_USE(54), INVALID_LIST_ID(55), INVALID_EMAIL_FORMAT(65), + INVALID_EMAIL_TEMPLATE(68), // Returned if requested with API Version 3, but required to use Version 4. WRONG_API_VERSION(88), MISSING_REQUIRED_HEADER(181), diff --git a/src/main/java/com/darksci/pardot/api/response/Result.java b/src/main/java/com/darksci/pardot/api/response/Result.java new file mode 100644 index 0000000..c352ca5 --- /dev/null +++ b/src/main/java/com/darksci/pardot/api/response/Result.java @@ -0,0 +1,227 @@ +/** + * Copyright 2017, 2018, 2019, 2020 Stephen Powis https://github.com/Crim/pardot-java-client + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.darksci.pardot.api.response; + +import com.darksci.pardot.api.InvalidRequestException; + +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Represents either a successful response from the API, or an error response. + * @param The return type for successful responses. + */ +public class Result { + private final T value; + private final ErrorResponse errorResponse; + + /** + * Private constructor. Use factory methods. {@link Result#newSuccess} {@link Result#newFailure} + * @param response Success result object. + * @param errorResponse error result. + */ + private Result(final T response, final ErrorResponse errorResponse) { + if (response != null && errorResponse != null) { + throw new IllegalArgumentException("You may not pass both parameters as non-null"); + } else if (response == null && errorResponse == null) { + throw new IllegalArgumentException("You may not pass both parameters as null"); + } + + this.value = response; + this.errorResponse = errorResponse; + } + + /** + * Factory method for when there is a successful response. + * @param response Response object. + * @param Type of the response object. + * @return new ResultOrError instance. + */ + public static Result newSuccess(final T response) { + return new Result<>(Objects.requireNonNull(response), null); + } + + /** + * Factory method for when there is an error returned from the API. + * @param errorResponse ErrorResponse object. + * @param Type of the response object. + * @return new ResultOrError instance. + */ + public static Result newFailure(final ErrorResponse errorResponse) { + return new Result<>(null, Objects.requireNonNull(errorResponse)); + } + + /** + * If a response from the API is present, returns the value, otherwise throws + * {@code NoSuchElementException}. + * + * @return the non-{@code null} value described by this {@code Optional} + * @throws NoSuchElementException if no success response is present + * @see Result#isSuccess() + */ + public T get() { + if (!isSuccess()) { + throw new NoSuchElementException("Cannot access response as there was an error"); + } + return value; + } + + /** + * If an error response from the API is present, return the error response. + * Otherwise throws {@code NoSuchElementException}. + * @return ErrorResponse from API. + * @throws NoSuchElementException if no error response is present. + * @see Result#isFailure() () + */ + public ErrorResponse getFailure() { + if (isSuccess()) { + throw new NoSuchElementException("Cannot access response as there was an error"); + } + return errorResponse; + } + + /** + * Return {@code true} if there is a successful result from the API present, otherwise {@code false}. + * + * @return {@code true} if there is a value present, otherwise {@code false} + */ + public boolean isSuccess() { + return value != null; + } + + /** + * Return {@code true} if there is a failure result from the API present, otherwise {@code false}. + * + * @return {@code true} if there is a value present, otherwise {@code false} + */ + public boolean isFailure() { + return !isSuccess(); + } + + /** + * Returns the success value if present, otherwise returns the default value. + * @param defaultValue value to be returned if no success value is present. + * @return Success value if present, otherwise returns the default value. + */ + public T getOrDefault(final T defaultValue) { + if (isSuccess()) { + return get(); + } + return defaultValue; + } + + /** + * For handling response values. + * + * @param successConsumer called if there is a successful response. + * @param errorResponseConsumer called if there is an error response. + * @return On success, call and return value from success handler. On error, call and return value from error handler. + */ + public T handle(final Function successConsumer, final Function errorResponseConsumer) { + if (isSuccess()) { + return successConsumer.apply(value); + } else { + return errorResponseConsumer.apply(errorResponse); + } + } + + /** + * For handling response values. + * + * @param errorResponseConsumer called if there is an error response. + * @return On success, return the success result, otherwise calls the error handler and returns + * the value it generates. + */ + public T handleError(final Function errorResponseConsumer) { + if (isSuccess()) { + return value; + } else { + return errorResponseConsumer.apply(errorResponse); + } + } + + /** + * If a success value is present, invoke the specified consumer with the value, + * otherwise do nothing. + * + * @param consumer block to be executed if a value is present + * @throws NullPointerException if value is present and {@code consumer} is null + */ + public void ifSuccess(final Consumer consumer) { + if (isSuccess()) { + consumer.accept(value); + } + } + + /** + * If an error value is present, invoke the specified consumer with the error value, + * otherwise do nothing. + * + * @param consumer block to be executed if a value is present + * @throws NullPointerException if value is present and {@code consumer} is null + */ + public void ifError(final Consumer consumer) { + if (isFailure()) { + consumer.accept(getFailure()); + } + } + + /** + * If a value is present, returns the value, otherwise throws an exception + * produced by the exception supplying function. + * + * A method reference to the exception constructor with an empty argument + * list can be used as the supplier. For example, + * {@code IllegalStateException::new} + * + * @param Type of the exception to be thrown + * @param exceptionSupplier the supplying function that produces an + * exception to be thrown + * @return the value, if present + * @throws X if no value is present + * @throws NullPointerException if no value is present and the exception + * supplying function is {@code null} + */ + public T orElseThrow(final Supplier exceptionSupplier) throws X { + if (value != null) { + return value; + } else { + throw exceptionSupplier.get(); + } + } + + /** + * If a value is present, returns the value, otherwise throws an exception + * produced by the exception supplying function. + * + * A method reference to the exception constructor with an empty argument + * list can be used as the supplier. For example {@code IllegalStateException::new} + * + * @param Type of the exception to be thrown + * @return the value, if present + * @throws X if no value is present + * @throws NullPointerException if no value is present and the exception + * supplying function is {@code null} + */ + public T orElseThrowInvalidRequestException() throws X { + return orElseThrow(() -> new InvalidRequestException(errorResponse.getMessage(), errorResponse.getCode())); + } +} diff --git a/src/test/java/com/darksci/pardot/api/AbstractPardotClientIntegrationTest.java b/src/test/java/com/darksci/pardot/api/AbstractPardotClientIntegrationTest.java index 422d584..bce6a04 100644 --- a/src/test/java/com/darksci/pardot/api/AbstractPardotClientIntegrationTest.java +++ b/src/test/java/com/darksci/pardot/api/AbstractPardotClientIntegrationTest.java @@ -82,6 +82,7 @@ import com.darksci.pardot.api.request.visitor.VisitorReadRequest; import com.darksci.pardot.api.request.visitoractivity.VisitorActivityQueryRequest; import com.darksci.pardot.api.request.visitoractivity.VisitorActivityReadRequest; +import com.darksci.pardot.api.response.Result; import com.darksci.pardot.api.response.account.Account; import com.darksci.pardot.api.response.campaign.Campaign; import com.darksci.pardot.api.response.campaign.CampaignQueryResponse; @@ -129,6 +130,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Optional; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -200,7 +202,6 @@ public void userAbilitiesTest() { /** * Attempt to retrieve current user's cookie. */ - public void userCookieTest() { final Cookie response = client.userCookie(new UserCookieRequest()); assertNotNull("Should not be null", response); @@ -210,12 +211,11 @@ public void userCookieTest() { /** * Attempt to retrieve a user. */ - public void userReadTest() { UserReadRequest readRequest = new UserReadRequest() .selectById(1L); - final User response = client.userRead(readRequest); + final Optional response = client.userRead(readRequest); assertNotNull("Should not be null", response); logger.info("Response: {}", response); } @@ -266,7 +266,7 @@ public void userDeleteByEmailTest() { final UserDeleteRequest deleteRequest = new UserDeleteRequest() .deleteByEmail(email); - final boolean result = client.userDelete(deleteRequest); + final boolean result = client.userDelete(deleteRequest).isSuccess(); assertTrue("Response should be true", result); } @@ -293,7 +293,7 @@ public void userDeleteByIdTest() { final UserDeleteRequest deleteRequest = new UserDeleteRequest() .deleteById(response.getId()); - final boolean result = client.userDelete(deleteRequest); + final boolean result = client.userDelete(deleteRequest).isSuccess(); assertTrue("Response should be true", result); } @@ -356,7 +356,7 @@ public void campaignReadTest() { CampaignReadRequest request = new CampaignReadRequest() .selectById(14885L); - final Campaign response = client.campaignRead(request); + final Optional response = client.campaignRead(request); assertNotNull("Should not be null", response); logger.info("Response: {}", response); } @@ -431,7 +431,7 @@ public void customFieldReadTest() { CustomFieldReadRequest request = new CustomFieldReadRequest() .selectById(customFieldId); - final CustomField response = client.customFieldRead(request); + final Optional response = client.customFieldRead(request); assertNotNull("Should not be null", response); logger.info("Response: {}", response); } @@ -498,7 +498,7 @@ public void customFieldDeleteTest() { .withCustomFieldId(customFieldId); // Send Request - final Boolean response = client.customFieldDelete(request); + final Boolean response = client.customFieldDelete(request).isSuccess(); assertNotNull("Should not be null", response); assertTrue("Is true", response); logger.info("Response: {}", response); @@ -525,7 +525,7 @@ public void customRedirectReadTest() { CustomRedirectReadRequest request = new CustomRedirectReadRequest() .selectById(customRedirectId); - final CustomRedirect response = client.customRedirectRead(request); + final Optional response = client.customRedirectRead(request); assertNotNull("Should not be null", response); logger.info("Response: {}", response); } @@ -551,7 +551,7 @@ public void dynamicContentReadTest() { DynamicContentReadRequest request = new DynamicContentReadRequest() .selectById(id); - final DynamicContent response = client.dynamicContentRead(request); + final Optional response = client.dynamicContentRead(request); assertNotNull("Should not be null", response); logger.info("Response: {}", response); } @@ -566,7 +566,7 @@ public void emailReadTest() { EmailReadRequest request = new EmailReadRequest() .selectById(emailId); - final Email response = client.emailRead(request); + final Optional response = client.emailRead(request); assertNotNull("Should not be null", response); logger.info("Response: {}", response); } @@ -581,7 +581,7 @@ public void emailStatsTest() { EmailStatsRequest request = new EmailStatsRequest() .selectByListEmailId(listEmailId); - final EmailStatsResponse.Stats response = client.emailStats(request); + final Optional response = client.emailStats(request); assertNotNull("Should not be null", response); logger.info("Response: {}", response); } @@ -661,7 +661,7 @@ public void emailTemplateReadTest() { final EmailTemplateReadRequest request = new EmailTemplateReadRequest() .selectById(emailTemplateId); - final EmailTemplate response = client.emailTemplateRead(request); + final Optional response = client.emailTemplateRead(request); assertNotNull("Should not be null", response); logger.info("Response: {}", response); } @@ -713,7 +713,7 @@ public void formDeleteTest() { final FormDeleteRequest request = new FormDeleteRequest() .withFormId(formId); - final boolean response = client.formDelete(request); + final boolean response = client.formDelete(request).isSuccess(); assertTrue("Should be true", response); } @@ -737,7 +737,7 @@ public void formReadTest() { final FormReadRequest request = new FormReadRequest() .selectById(1L); - final Form response = client.formRead(request); + final Optional response = client.formRead(request); assertNotNull("Should not be null", response); logger.info("Response: {}", response); } @@ -791,7 +791,7 @@ public void listReadTest() { final ListReadRequest request = new ListReadRequest() .selectById(33173L); - final List response = client.listRead(request); + final Optional response = client.listRead(request); assertNotNull("Should not be null", response); logger.info("Response: {}", response); } @@ -862,7 +862,7 @@ public void listMembershipByListIdAndProspectIdReadTest() { final ListMembershipReadRequest request = new ListMembershipReadRequest() .selectByListIdAndProspectId(33173L, 59156811L); - final ListMembership response = client.listMembershipRead(request); + final Optional response = client.listMembershipRead(request); assertNotNull("Should not be null", response); logger.info("Response: {}", response); } @@ -875,7 +875,7 @@ public void listMembershipByIdReadTest() { final ListMembershipReadRequest request = new ListMembershipReadRequest() .selectById(170293539L); - final ListMembership response = client.listMembershipRead(request); + final Optional response = client.listMembershipRead(request); assertNotNull("Should not be null", response); logger.info("Response: {}", response); } @@ -934,7 +934,7 @@ public void opportunityQueryTest() { public void opportunityReadTest() { final long opportunityId = 1; - final Opportunity response = client.opportunityRead(new OpportunityReadRequest() + final Optional response = client.opportunityRead(new OpportunityReadRequest() .selectById(opportunityId) ); assertNotNull("Should not be null", response); @@ -1013,7 +1013,7 @@ public void opportunityUndeleteTest() { public void prospectReadTest() { final long prospectId = 59164595L; - final Prospect response = client.prospectRead(new ProspectReadRequest() + final Optional response = client.prospectRead(new ProspectReadRequest() .selectById(prospectId) ); assertNotNull("Should not be null", response); @@ -1153,9 +1153,8 @@ public void prospectUpdateTest() { } /** - * Test creating prospect. + * Test deleting prospect. */ - public void prospectDeleteTest() { final long prospectId = 59138429L; @@ -1163,10 +1162,40 @@ public void prospectDeleteTest() { .withProspectId(prospectId); // Issue request - final boolean response = client.prospectDelete(request); + final Result response = client.prospectDelete(request); logger.info("Response: {}", response); } + /** + * Test creating and then deleting a prospect. + */ + public void prospectCreateAndDeleteTest() { + final Prospect prospect = new Prospect(); + prospect.setEmail("random-email" + System.currentTimeMillis() + "@example.com"); + prospect.setFirstName("Test"); + prospect.setLastName("User"); + prospect.setCity("Some City"); + + final ProspectCreateRequest createRequest = new ProspectCreateRequest() + .withProspect(prospect); + + // Issue request + final Prospect createdProspect = client.prospectCreate(createRequest); + + assertNotNull("Should not be null", createdProspect); + logger.info("Response: {}", createdProspect); + + final ProspectDeleteRequest deleteRequest = new ProspectDeleteRequest() + .withProspectId(createdProspect.getId()); + + // Issue request + final boolean result = client + .prospectDelete(deleteRequest) + .isSuccess(); + + assertTrue(result); + } + /** * Test assigning prospect. */ @@ -1245,14 +1274,14 @@ public void tagReadTest() { final TagReadRequest request = new TagReadRequest() .selectById(tagId); - final Tag response = client.tagRead(request); + final Optional response = client.tagRead(request); logger.info("Response: {}", response); assertNotNull("Should not be null", response); - assertNotNull("Should have a name", response.getName()); - assertNotNull("Should have an id", response.getId()); - assertNotNull("Should have a create at", response.getCreatedAt()); - assertNotNull("Should have an updated at", response.getUpdatedAt()); + assertNotNull("Should have a name", response.get().getName()); + assertNotNull("Should have an id", response.get().getId()); + assertNotNull("Should have a create at", response.get().getCreatedAt()); + assertNotNull("Should have an updated at", response.get().getUpdatedAt()); } /** @@ -1287,10 +1316,10 @@ public void tagObjectReadQuery() { final TagObjectReadRequest request = new TagObjectReadRequest() .selectById(tagObjectId); - final TagObject response = client.tagObjectRead(request); + final Optional response = client.tagObjectRead(request); assertNotNull("Should not be null", response); - assertNotNull(response.getType()); - assertNotNull(response.getTypeName()); + assertNotNull(response.get().getType()); + assertNotNull(response.get().getTypeName()); logger.info("Response: {}", response); } @@ -1314,7 +1343,7 @@ public void visitorReadTest() { final VisitorReadRequest request = new VisitorReadRequest() .selectById(visitorId); - final Visitor response = client.visitorRead(request); + final Optional response = client.visitorRead(request); assertNotNull("Should not be null", response); logger.info("Response: {}", response); } @@ -1354,7 +1383,7 @@ public void visitorActivityReadTest() { final VisitorActivityReadRequest request = new VisitorActivityReadRequest() .selectById(visitorActivityId); - final VisitorActivity response = client.visitorActivityRead(request); + final Optional response = client.visitorActivityRead(request); assertNotNull("Should not be null", response); logger.info("Response: {}", response); } diff --git a/src/test/java/com/darksci/pardot/api/PardotClientSsoIntegrationTest.java b/src/test/java/com/darksci/pardot/api/PardotClientSsoIntegrationTest.java index a6d052b..8e1a8d2 100644 --- a/src/test/java/com/darksci/pardot/api/PardotClientSsoIntegrationTest.java +++ b/src/test/java/com/darksci/pardot/api/PardotClientSsoIntegrationTest.java @@ -447,6 +447,12 @@ public void prospectDeleteTest() { super.prospectDeleteTest(); } + @Test + @Override + public void prospectCreateAndDeleteTest() { + super.prospectCreateAndDeleteTest(); + } + @Test @Override public void prospectAssignTest() { diff --git a/src/test/java/com/darksci/pardot/api/PardotClient_SsoAuthTest.java b/src/test/java/com/darksci/pardot/api/PardotClient_SsoAuthTest.java index fde71b3..f061779 100644 --- a/src/test/java/com/darksci/pardot/api/PardotClient_SsoAuthTest.java +++ b/src/test/java/com/darksci/pardot/api/PardotClient_SsoAuthTest.java @@ -20,8 +20,10 @@ import com.darksci.pardot.api.config.Configuration; import com.darksci.pardot.api.request.login.SsoLoginRequest; import com.darksci.pardot.api.request.tag.TagReadRequest; +import com.darksci.pardot.api.request.user.UserReadRequest; import com.darksci.pardot.api.response.login.SsoLoginResponse; import com.darksci.pardot.api.response.tag.Tag; +import com.darksci.pardot.api.response.user.User; import com.darksci.pardot.api.rest.RestClient; import com.darksci.pardot.api.rest.RestResponse; import org.junit.Before; @@ -29,11 +31,14 @@ import util.TestHelper; import java.io.IOException; +import java.util.Optional; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -133,10 +138,13 @@ public void testIndirectLogin() { .thenReturn(createRestResponseFromFile("tagRead.xml", 200)); // Call method under test - final Tag response = pardotClient.tagRead(tagReadRequest); + final Optional responseOptional = pardotClient.tagRead(tagReadRequest); // Validate response for tag - assertNotNull(response); + assertNotNull(responseOptional); + assertTrue(responseOptional.isPresent()); + + final Tag response = responseOptional.get(); assertEquals(1L, (long) response.getId()); assertEquals("Standard Tag", response.getName()); @@ -186,10 +194,13 @@ public void testReAuthenticationOnSessionTimeout() { ); // Call method under test - final Tag response = pardotClient.tagRead(tagReadRequest); + final Optional responseOptional = pardotClient.tagRead(tagReadRequest); // Validate response for tag - assertNotNull(response); + assertNotNull(responseOptional); + assertTrue(responseOptional.isPresent()); + + final Tag response = responseOptional.get(); assertEquals(1L, (long) response.getId()); assertEquals("Standard Tag", response.getName()); @@ -245,6 +256,54 @@ public void testReAuthenticationOnSessionTimeout_triggersInvalidCredentials() { }); } + /** + * Verify behavior when attempting to retrieve a user by id, but the API returns an 'invalid user id' error. + * The result should be an empty optional. + */ + @Test + public void userRead_invalidUserId_returnsEmptyOptional() { + // All login to succeed. + mockSuccessfulLogin(); + + // Mock responses from RestClient/Api Server. + // When we request for a user read, we should get a does not exist API error. + // This should force an empty optional to be returned. + when(mockRestClient.submitRequest(isA(UserReadRequest.class))) + .thenReturn( + // First call should return an invalid API key response. + createRestResponseFromFile("userRead_invalidUserId.xml", 200) + ); + + final Optional response = pardotClient.userRead(new UserReadRequest()); + + assertNotNull("Should not be null", response); + assertFalse("Should not be present", response.isPresent()); + } + + /** + * Verify behavior when attempting to retrieve a user by id, but the API returns an 'invalid user id' error. + * The result should be an empty optional. + */ + @Test + public void userRead_validUserId_returnsPopulatedOptional() { + // All login to succeed. + mockSuccessfulLogin(); + + // Mock responses from RestClient/Api Server. + // When we request for a user read, we should get a does not exist API error. + // This should force an empty optional to be returned. + when(mockRestClient.submitRequest(isA(UserReadRequest.class))) + .thenReturn( + // First call should return a valid user response. + createRestResponseFromFile("userRead.xml", 200) + ); + + final Optional response = pardotClient.userRead(new UserReadRequest()); + + assertNotNull("Should not be null", response); + assertTrue("Should be present", response.isPresent()); + } + private RestResponse createRestResponseFromFile(final String filename, int httpCode) { try { return new RestResponse( @@ -261,4 +320,10 @@ private void verifyNoMoreRestClientInteractions() { .init(apiConfig); verifyNoMoreInteractions(mockRestClient); } + + private void mockSuccessfulLogin() { + // Mock successful login response from RestClient/Api Server. + when(mockRestClient.submitRequest(isA(SsoLoginRequest.class))) + .thenReturn(createRestResponseFromFile("ssoLoginSuccess.json", 200)); + } } diff --git a/src/test/java/com/darksci/pardot/api/PardotClient_UsernameAndPasswordAuthTest.java b/src/test/java/com/darksci/pardot/api/PardotClient_UsernameAndPasswordAuthTest.java index 8babc87..b602829 100644 --- a/src/test/java/com/darksci/pardot/api/PardotClient_UsernameAndPasswordAuthTest.java +++ b/src/test/java/com/darksci/pardot/api/PardotClient_UsernameAndPasswordAuthTest.java @@ -20,8 +20,10 @@ import com.darksci.pardot.api.config.Configuration; import com.darksci.pardot.api.request.login.LoginRequest; import com.darksci.pardot.api.request.tag.TagReadRequest; +import com.darksci.pardot.api.request.user.UserReadRequest; import com.darksci.pardot.api.response.login.LoginResponse; import com.darksci.pardot.api.response.tag.Tag; +import com.darksci.pardot.api.response.user.User; import com.darksci.pardot.api.rest.RestClient; import com.darksci.pardot.api.rest.RestResponse; import org.junit.Before; @@ -29,11 +31,14 @@ import util.TestHelper; import java.io.IOException; +import java.util.Optional; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.mock; @@ -125,12 +130,13 @@ public void testIndirectLogin() { .thenReturn(createRestResponseFromFile("tagRead.xml", 200)); // Call method under test - final Tag response = pardotClient.tagRead(tagReadRequest); + final Optional response = pardotClient.tagRead(tagReadRequest); // Validate response for tag assertNotNull(response); - assertEquals(1L, (long) response.getId()); - assertEquals("Standard Tag", response.getName()); + assertTrue(response.isPresent()); + assertEquals(1L, (long) response.get().getId()); + assertEquals("Standard Tag", response.get().getName()); // Validate we updated our ApiConfig based on the login. assertNotNull("ApiKey should no longer be null", apiConfig.getPasswordLoginCredentials().getApiKey()); @@ -178,12 +184,13 @@ public void testReAuthenticationOnSessionTimeout() { ); // Call method under test - final Tag response = pardotClient.tagRead(tagReadRequest); + final Optional response = pardotClient.tagRead(tagReadRequest); // Validate response for tag assertNotNull(response); - assertEquals(1L, (long) response.getId()); - assertEquals("Standard Tag", response.getName()); + assertTrue(response.isPresent()); + assertEquals(1L, (long) response.get().getId()); + assertEquals("Standard Tag", response.get().getName()); // Validate we updated our ApiConfig based on the login. assertNotNull("ApiKey should no longer be null", apiConfig.getPasswordLoginCredentials().getApiKey()); @@ -237,7 +244,61 @@ public void testReAuthenticationOnSessionTimeout_triggersInvalidCredentials() { }); } - private RestResponse createRestResponseFromFile(final String filename, int httpCode) { + /** + * Verify behavior when attempting to retrieve a user by id, but the API returns an 'invalid user id' error. + * The result should be an empty optional. + */ + @Test + public void userRead_invalidUserId_returnsEmptyOptional() { + // All login to succeed. + mockSuccessfulLogin(); + + // Mock responses from RestClient/Api Server. + // When we request for a user read, we should get a does not exist API error. + // This should force an empty optional to be returned. + when(mockRestClient.submitRequest(isA(UserReadRequest.class))) + .thenReturn( + // First call should return an invalid API key response. + createRestResponseFromFile("userRead_invalidUserId.xml", 200) + ); + + final Optional response = pardotClient.userRead(new UserReadRequest()); + + assertNotNull("Should not be null", response); + assertFalse("Should not be present", response.isPresent()); + } + + /** + * Verify behavior when attempting to retrieve a user by id, but the API returns an 'invalid user id' error. + * The result should be an empty optional. + */ + @Test + public void userRead_validUserId_returnsPopulatedOptional() { + // All login to succeed. + mockSuccessfulLogin(); + + // Mock responses from RestClient/Api Server. + // When we request for a user read, we should get a does not exist API error. + // This should force an empty optional to be returned. + when(mockRestClient.submitRequest(isA(UserReadRequest.class))) + .thenReturn( + // First call should return a valid user response. + createRestResponseFromFile("userRead.xml", 200) + ); + + final Optional response = pardotClient.userRead(new UserReadRequest()); + + assertNotNull("Should not be null", response); + assertTrue("Should be present", response.isPresent()); + } + + /** + * Helper method to generate a RestResponse as loaded from a mockResponse resource file. + * @param filename file containing the mock response. + * @param httpCode Http response code to mock. + * @return RestResponse instance. + */ + private RestResponse createRestResponseFromFile(final String filename, final int httpCode) { try { return new RestResponse( TestHelper.readFile("mockResponses/" + filename), @@ -248,9 +309,21 @@ private RestResponse createRestResponseFromFile(final String filename, int httpC } } + /** + * Helper method to verify no further interactions occurred. + */ private void verifyNoMoreRestClientInteractions() { + // init() should always be called. verify(mockRestClient, times(1)) .init(apiConfig); + + // Verify no others. verifyNoMoreInteractions(mockRestClient); } + + private void mockSuccessfulLogin() { + // Mock successful login response from RestClient/Api Server. + when(mockRestClient.submitRequest(isA(LoginRequest.class))) + .thenReturn(createRestResponseFromFile("login.xml", 200)); + } } diff --git a/src/test/resources/mockResponses/userRead_invalidUserId.xml b/src/test/resources/mockResponses/userRead_invalidUserId.xml new file mode 100644 index 0000000..ca56bbf --- /dev/null +++ b/src/test/resources/mockResponses/userRead_invalidUserId.xml @@ -0,0 +1,4 @@ + + + Invalid user ID + \ No newline at end of file