From 6c97adea93f11d48658557c504182675a0c91126 Mon Sep 17 00:00:00 2001 From: "Lars K. Johansen" Date: Mon, 8 Feb 2016 15:25:53 -0800 Subject: [PATCH] Including Advanced User Search API client bacon: test resolves OKTA-78337 --- .gitignore | 3 +- README.md | 56 +- pom.xml | 7 + .../okta/sdk/clients/AppGroupApiClient.java | 12 +- .../sdk/clients/AppInstanceApiClient.java | 12 +- .../com/okta/sdk/clients/AuthApiClient.java | 10 +- .../com/okta/sdk/clients/EventApiClient.java | 10 +- .../sdk/clients/FactorsAdminApiClient.java | 14 +- .../okta/sdk/clients/FactorsApiClient.java | 11 +- .../okta/sdk/clients/SessionApiClient.java | 20 +- .../com/okta/sdk/clients/UserApiClient.java | 557 +++++++++++++++--- .../okta/sdk/clients/UserGroupApiClient.java | 11 +- .../RateLimitExceededException.java | 2 + .../com/okta/sdk/framework/ApiClient.java | 1 + .../com/okta/sdk/framework/ErrorResponse.java | 1 + .../com/okta/sdk/framework/FilterBuilder.java | 168 +++--- .../java/com/okta/sdk/framework/Filters.java | 2 + .../okta/sdk/framework/RateLimitContext.java | 47 +- .../java/com/okta/sdk/framework/Utils.java | 1 + .../java/com/okta/sdk/models/links/Link.java | 2 + .../java/com/okta/sdk/models/links/Links.java | 1 + .../models/links/LinksUnionDeserializer.java | 4 +- src/test/java/FilterBuilderTest.java | 150 ++++- 23 files changed, 822 insertions(+), 280 deletions(-) diff --git a/.gitignore b/.gitignore index 51bd908f9c6..cd137959ea6 100644 --- a/.gitignore +++ b/.gitignore @@ -32,8 +32,7 @@ hs_err_pid* .idea/uiDesigner.xml # Maven: -target/maven-archiver -target/surefire-reports +target # Gradle: .idea/gradle.xml diff --git a/README.md b/README.md index b09a43b4d26..4d86edf3d0f 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,7 @@ To build and install: 1. Clone the repo 2. Navigate to the repo directory. It should contain pom.xml -2. `mvn -Dmaven.test.skip=true install` - -To run the build with tests: - -1. Set the following environment variables: - * OKTA_TEST_URL - * OKTA_TEST_KEY - * OKTA_TEST_ADMIN_NAME - * OKTA_TEST_ADMIN_PASSWORD -2. `mvn build` - +3. Build with tests `mvn install` or without tests `mvn -Dmaven.test.skip=true install` ###Client configuration ```java @@ -44,13 +34,13 @@ String status = result.getStatus(); This client is used to perform CRUD operations on user objects (http://developer.okta.com/docs/api/resources/users.html). ```java -UsersApiClient usersClient = new UsersApiClient(oktaSettings); +UserApiClient userApiClient = new UserApiClient(oktaSettings); // Create a new user // First Name, Last Name, Email and Login are required. Password is optional. // The boolean variable is for activate which means that activate the user as soon as // it is created. -User newUser = usersClient.createUser( +User newUser = userApiClient.createUser( "First", "Last", "login@example.com", @@ -75,44 +65,52 @@ user.setProfile(userProfile); user.setCredentials(loginCredentials); // true is for activate user as soon as it is created -userClient.createUser(user, true); +userApiClient.createUser(user, true); // Read/Search // There are plenty of methods for reading users. // 1. Search user when user ID/loginName/loginShortName is known -User user = usersClient.getUser("ID/loginName/loginShortName"); +User user = userApiClient.getUser("ID/loginName/loginShortName"); // 2. Search user using filters. You can query the API for searching a user // with the help of filters mentioned at - http://developer.okta.com/docs/api/resources/users.html#filters // Example - search for first name. Returns a list of users matching that query FilterBuilder filterBuilder = new FilterBuilder("profile.firstName eq \"" + firstName + "\""); -List users = userClient.getUsersWithFilter(filterBuilder); +List users = userApiClient.getUsersWithFilter(filterBuilder); + +// 3. Advanced search provides the option to filter on any user profile attribute, any custom defined +// profile attribute, as well as the following top-level attributes: id, status, created, activated, +// statusChanged and lastUpdated. The advanced search performs a case insensitive filter against all fields +// specified in the search parameter. Note that the results might not yet be up to date, as the most up to date +// data can be delayed up to a few seconds, so use for convenience. +FilterBuilder filterBuilder = new FilterBuilder("profile.flightNumber eq \"A415\""); +List users = userApiClient.getUsersWithSearch(filterBuilder); -// 3. Search users only on firstName, lastName or email +// 4. Search users only on firstName, lastName or email // The parameter passed is searched in the attributes - firstName, lastName and email of all Users. -List users = userClient.getUsersWithQuery("firstName/lastName/email"); +List users = userApiClient.getUsersWithQuery("firstName/lastName/email"); // Update newUser.getProfile().setLastName("NewLast"); -usersClient.updateUser(newUser); +userApiClient.updateUser(newUser); // Delete (for Users this is the same as deactivate) -usersClient.deleteUser(newUser.getId()); +userApiClient.deleteUser(newUser.getId()); ``` ###Paging ```java -PagedResults pagedResults = usersClient.getUsersPagedResultsWithLimit(10); +PagedResults pagedResults = userApiClient.getUsersPagedResultsWithLimit(10); -int counter = 0; -do { - if(!pagedResults.isFirstPage()) { - pagedResults = usersClient.getUsersPagedResultsByUrl(pagedResults.getNextUrl()); - } - - for(User user : pagedResults.getResult()) { +while (true) { + for (User user : pagedResults.getResult()) { // Do something with user } + + if (!pagedResults.isLastPage()) { + pagedResults = userApiClient.getUsersPagedResultsByUrl(pagedResults.getNextUrl()); + } else { + break; + } } -while(!pagedResults.isLastPage()); ``` diff --git a/pom.xml b/pom.xml index f07c4f48045..40747bd3572 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,7 @@ 2.18.1 1.7.10 6.8.17 + 1.3 @@ -57,6 +58,12 @@ ${testng.version} test + + org.hamcrest + hamcrest-core + ${org.hamcrest.version} + test + diff --git a/src/main/java/com/okta/sdk/clients/AppGroupApiClient.java b/src/main/java/com/okta/sdk/clients/AppGroupApiClient.java index 6f2b47f2b1d..97842c4703b 100644 --- a/src/main/java/com/okta/sdk/clients/AppGroupApiClient.java +++ b/src/main/java/com/okta/sdk/clients/AppGroupApiClient.java @@ -15,15 +15,11 @@ import java.util.Map; public class AppGroupApiClient extends JsonApiClient { + public AppGroupApiClient(ApiClientConfiguration config) { super(config); } - @Override - protected String getFullPath(String relativePath) { - return String.format("/api/v%d/apps%s", this.apiVersion, relativePath); - } - //////////////////////////////////////////// // COMMON METHODS //////////////////////////////////////////// @@ -113,4 +109,10 @@ public PagedResults getAppGroupsPagedResultsAfterCursorWithLimit(Strin public PagedResults getAppGroupsPagedResultsWithUrl(String url) throws IOException { return new PagedResults(getAppGroupsApiResponseWithUrl(url)); } + + @Override + protected String getFullPath(String relativePath) { + return String.format("/api/v%d/apps%s", this.apiVersion, relativePath); + } + } diff --git a/src/main/java/com/okta/sdk/clients/AppInstanceApiClient.java b/src/main/java/com/okta/sdk/clients/AppInstanceApiClient.java index 32899d48dc3..276b0b90704 100644 --- a/src/main/java/com/okta/sdk/clients/AppInstanceApiClient.java +++ b/src/main/java/com/okta/sdk/clients/AppInstanceApiClient.java @@ -17,15 +17,11 @@ import java.util.Map; public class AppInstanceApiClient extends JsonApiClient { + public AppInstanceApiClient(ApiClientConfiguration config) { super(config); } - @Override - protected String getFullPath(String relativePath) { - return String.format("/api/v%d/apps%s", this.apiVersion, relativePath); - } - //////////////////////////////////////////// // COMMON METHODS //////////////////////////////////////////// @@ -196,4 +192,10 @@ private FilterBuilder getGroupFilter(String... groups) { private FilterBuilder getUserFilter(String... users) { return Utils.getFilter(Filters.AppInstance.USER_ID, users); } + + @Override + protected String getFullPath(String relativePath) { + return String.format("/api/v%d/apps%s", this.apiVersion, relativePath); + } + } diff --git a/src/main/java/com/okta/sdk/clients/AuthApiClient.java b/src/main/java/com/okta/sdk/clients/AuthApiClient.java index ec34871716f..8504ea04cb1 100644 --- a/src/main/java/com/okta/sdk/clients/AuthApiClient.java +++ b/src/main/java/com/okta/sdk/clients/AuthApiClient.java @@ -31,11 +31,6 @@ public AuthApiClient(ApiClientConfiguration config) { super(config); } - @Override - protected String getFullPath(String relativePath) { - return String.format("/api/v1/authn%s", relativePath); - } - //////////////////////////////////////////// // COMMON METHODS //////////////////////////////////////////// @@ -206,4 +201,9 @@ public AuthResult cancelTransaction(String stateToken, String relayState) throws return post(getEncodedPath("/cancel"), params, new TypeReference() { }); } + @Override + protected String getFullPath(String relativePath) { + return String.format("/api/v1/authn%s", relativePath); + } + } diff --git a/src/main/java/com/okta/sdk/clients/EventApiClient.java b/src/main/java/com/okta/sdk/clients/EventApiClient.java index b0239533102..257984f14fd 100644 --- a/src/main/java/com/okta/sdk/clients/EventApiClient.java +++ b/src/main/java/com/okta/sdk/clients/EventApiClient.java @@ -24,11 +24,6 @@ public EventApiClient(ApiClientConfiguration config) { super(config); } - @Override - protected String getFullPath(String relativePath) { - return String.format("/api/v%d/events%s", this.apiVersion, relativePath); - } - //////////////////////////////////////////// // COMMON METHODS //////////////////////////////////////////// @@ -148,4 +143,9 @@ public PagedResults getEventsPagedResultsAfterCursorWithLimit(String afte public PagedResults getEventsPagedResultsWithUrl(String url) throws IOException { return new PagedResults(getEventsApiResponseWithUrl(url)); } + + @Override + protected String getFullPath(String relativePath) { + return String.format("/api/v%d/events%s", this.apiVersion, relativePath); + } } diff --git a/src/main/java/com/okta/sdk/clients/FactorsAdminApiClient.java b/src/main/java/com/okta/sdk/clients/FactorsAdminApiClient.java index 772c12f3fc6..feede0f9da1 100644 --- a/src/main/java/com/okta/sdk/clients/FactorsAdminApiClient.java +++ b/src/main/java/com/okta/sdk/clients/FactorsAdminApiClient.java @@ -15,11 +15,6 @@ public FactorsAdminApiClient(ApiClientConfiguration config) { super(config); } - @Override - protected String getFullPath(String relativePath) { - return String.format("/api/v%d/org%s", this.apiVersion, relativePath); - } - //////////////////////////////////////////// // COMMON METHODS //////////////////////////////////////////// @@ -39,10 +34,17 @@ public OrgAuthFactor activateOrgFactor(String orgAuthFactorId) throws IOExceptio } public OrgAuthFactor activateOrgFactor(String orgAuthFactorId, OrgAuthFactor orgAuthFactor) throws IOException { - return post(getEncodedPath("/factors/%s/lifecycle/activate", orgAuthFactorId), orgAuthFactor, new TypeReference() { }); + return post(getEncodedPath("/factors/%s/lifecycle/activate", orgAuthFactorId), orgAuthFactor, new TypeReference() { + }); } public OrgAuthFactor deActivateOrgFactor(String orgAuthFactorId) throws IOException { return post(getEncodedPath("/factors/%s/lifecycle/deactivate", orgAuthFactorId), null, new TypeReference() { }); } + + @Override + protected String getFullPath(String relativePath) { + return String.format("/api/v%d/org%s", this.apiVersion, relativePath); + } + } diff --git a/src/main/java/com/okta/sdk/clients/FactorsApiClient.java b/src/main/java/com/okta/sdk/clients/FactorsApiClient.java index ea00b9bfd38..dfeee50775e 100644 --- a/src/main/java/com/okta/sdk/clients/FactorsApiClient.java +++ b/src/main/java/com/okta/sdk/clients/FactorsApiClient.java @@ -30,11 +30,6 @@ public FactorsApiClient(ApiClientConfiguration config) { super(config); } - @Override - protected String getFullPath(String relativePath) { - return String.format("/api/v%d/users%s", this.apiVersion, relativePath); - } - //////////////////////////////////////////// // COMMON METHODS //////////////////////////////////////////// @@ -127,4 +122,10 @@ public Factor activateFactorDevice(String userId, String userFactorId, String de verification.setPassCode(passCode); return post(getEncodedPath("/%s/factors/%s/devices/%s/lifecycle/activate", userId, userFactorId, deviceId), verification, new TypeReference() { }); } + + @Override + protected String getFullPath(String relativePath) { + return String.format("/api/v%d/users%s", this.apiVersion, relativePath); + } + } diff --git a/src/main/java/com/okta/sdk/clients/SessionApiClient.java b/src/main/java/com/okta/sdk/clients/SessionApiClient.java index 0cbd5144f69..13edb8c91f7 100644 --- a/src/main/java/com/okta/sdk/clients/SessionApiClient.java +++ b/src/main/java/com/okta/sdk/clients/SessionApiClient.java @@ -16,11 +16,6 @@ public SessionApiClient(ApiClientConfiguration config) { super(config); } - @Override - protected String getFullPath(String relativePath) { - return String.format("/api/v%d/sessions%s", this.apiVersion, relativePath); - } - //////////////////////////////////////////// // COMMON METHODS //////////////////////////////////////////// @@ -58,7 +53,8 @@ public Session createSessionWithCredentialsAndCookieTokenUrl(String username, St public Session createSessionWithSessionToken(String sessionToken) throws IOException { Map params = new HashMap(); params.put("sessionToken", sessionToken); - return post(getEncodedPath("/"), params, new TypeReference() { }); + return post(getEncodedPath("/"), params, new TypeReference() { + }); } public Session createSessionWithSessionTokenAndAdditionalFields(String sessionToken, String additionalFields) throws IOException { @@ -68,14 +64,22 @@ public Session createSessionWithSessionTokenAndAdditionalFields(String sessionTo } public Session validateSession(String sessionId) throws IOException { - return get(getEncodedPath("/%s", sessionId), new TypeReference() { }); + return get(getEncodedPath("/%s", sessionId), new TypeReference() { + }); } public Session extendSession(String sessionId) throws IOException { - return put(getEncodedPath("/%s", sessionId), new TypeReference() { }); + return put(getEncodedPath("/%s", sessionId), new TypeReference() { + }); } public void clearSession(String sessionId) throws IOException { delete(getEncodedPath("/%s", sessionId)); } + + @Override + protected String getFullPath(String relativePath) { + return String.format("/api/v%d/sessions%s", this.apiVersion, relativePath); + } + } \ No newline at end of file diff --git a/src/main/java/com/okta/sdk/clients/UserApiClient.java b/src/main/java/com/okta/sdk/clients/UserApiClient.java index d395d040ee1..c8f34b887fb 100644 --- a/src/main/java/com/okta/sdk/clients/UserApiClient.java +++ b/src/main/java/com/okta/sdk/clients/UserApiClient.java @@ -26,76 +26,184 @@ public class UserApiClient extends JsonApiClient { + /** + * Class constructor specifying which configuration to use. + * + * @param config The Api client configuration to be used. + */ public UserApiClient(ApiClientConfiguration config) { super(config); } - @Override - protected String getFullPath(String relativePath) { - return String.format("/api/v%d/users%s", this.apiVersion, relativePath); - } - - //////////////////////////////////////////// - // COMMON METHODS - //////////////////////////////////////////// + // List users. + /** + * Get all users in Okta. + * + * @return List Users containing matching results. + * @throws IOException If an input or output exception occurred. + */ public List getUsers() throws IOException { return getUsersWithLimit(Utils.getDefaultResultsLimit()); } /** - * Search Okta User attributes - firstName, lastName and email for the query passed and return - * matching results - * @param query - firstName/lastName/email to be searched for - * @return List containing matching results - * @throws IOException + * Search for Okta users that have firstName, lastName or email starting with {@code query}. + * + * @param query The to be searched for + * @return List Users containing matching results. + * @throws IOException If an input or output exception occurred. */ public List getUsersWithQuery(String query) throws IOException { - return get(getEncodedPath("?" + SEARCH_QUERY + "=%s", query), new TypeReference>() { }); + return get(getEncodedPath("?" + SEARCH_QUERY + "=%s", query), new TypeReference>() { + }); } /** - * Return all users in Okta within a limit - * @return List - * @throws IOException + * Return all users in Okta with an upper limit of the number of results. + * + * @return List Users containing matching results. + * @throws IOException If an input or output exception occurred. */ public List getUsersWithLimit(int limit) throws IOException { - return get(getEncodedPath("?" + LIMIT + "=%s", Integer.toString(limit)), new TypeReference>() { }); + return get(getEncodedPath("?" + LIMIT + "=%s", Integer.toString(limit)), new TypeReference>() { + }); } /** - * Return all users in Okta that match the given filter - * Filters can be found at http://developer.okta.com/docs/api/resources/users.html#filters - * @param filterBuilder - * @return List that match the given filter - * @throws IOException + * Return all users in Okta that match the given SCIM filter. It is possible to filter on + * id, status, activated, lastUpdated, profile.firstName, profile.lastName, profile.login and profile.email. + * + * @param filterBuilder The filter that's being used to match users. + * @return List Users that match the given {@code filter}. + * @throws IOException If an input or output exception occurred. + * @see Filters */ public List getUsersWithFilter(FilterBuilder filterBuilder) throws IOException { - return get(getEncodedPath("?" + FILTER + "=%s", filterBuilder.toString()), new TypeReference>() { }); + return get(getEncodedPath("?" + FILTER + "=%s", filterBuilder.toString()), new TypeReference>() { + }); } + /** + * Return all users in Okta that match the given SCIM filter. It is possible to filter on all user profile attributes, + * all custom user profile attributes as well as the following attributes: id, status, created, activated, + * statusChanged and lastUpdated. Note that the results might not yet be up to date, as the most up to date data + * can be delayed up to a few seconds, so use for convenience. + * + * @param filterBuilder The filter that's being used to match users. + * @return List Users that match the given {@code filter}. + * @throws IOException If an input or output exception occurred. + * @see Filters + */ + public List getUsersWithAdvancedSearch(FilterBuilder filterBuilder) throws IOException { + return get(getEncodedPath("?" + SEARCH + "=%s", filterBuilder.toString()), new TypeReference>() { + }); + } + + /** + * Get users by URL. + * + * @param url The URL to get users from. + * @return List Users that is fetched from the {@code url}. + * @throws IOException If an input or output exception occurred. + */ public List getUsersByUrl(String url) throws IOException { - return get(url, new TypeReference>() { }); + return get(url, new TypeReference>() { + }); } - // CRUD + /** + * Get the current user. + * + * @return User My user. + * @throws IOException If an input or output exception occurred. + */ + public User getMyUser() throws IOException { + return get(getEncodedPath("/me"), new TypeReference() { + }); + } + /** + * Get a user based on the {@code userId}. + * + * @param userId The id of the returned user. + * @return User The user with id equal to {@code userId}. + * @throws IOException If an input or output exception occurred. + */ + public User getUser(String userId) throws IOException { + return get(getEncodedPath("/%s", userId), new TypeReference() { + }); + } + + // Create users. + + /** + * Create user based on the {@code user}. + * + * @param user The user to be created. + * @return User The user that's created. + * @throws IOException If an input or output exception occurred. + */ public User createUser(User user) throws IOException { - return post(getEncodedPath("/"), user, new TypeReference() { }); + return post(getEncodedPath("/"), user, new TypeReference() { + }); } + /** + * Create and activate user based on the {@code user} and {@code activate}. + * + * @param user The user to be created. + * @param activate Determines whether to activate the user. + * @return User The user that's created. + * @throws IOException If an input or output exception occurred. + */ public User createUser(User user, boolean activate) throws IOException { - return post(getEncodedPath("?activate=%s", String.valueOf(activate)), user, new TypeReference() { }); + return post(getEncodedPath("?activate=%s", String.valueOf(activate)), user, new TypeReference() { + }); } + /** + * Create a user based on {@code firstName}, {@code lastName}, {@code login}, and {@code email}. + * + * @param firstName The first name of the user. + * @param lastName The last name of the user. + * @param login The login of the user. + * @param email The email of the user. + * @return User The user that's created. + * @throws IOException If an input or output exception occurred. + */ public User createUser(String firstName, String lastName, String login, String email) throws IOException { return createUser(firstName, lastName, login, email, null); } + /** + * Create a user based on {@code firstName}, {@code lastName}, {@code login}, {@code email} and {@code secondEmail}. + * + * @param firstName The first name of the user. + * @param lastName The last name of the user. + * @param login The login of the user. + * @param email The email of the user. + * @param secondEmail The secondary email of the user. + * @return User The user that's created. + * @throws IOException If an input or output exception occurred. + */ public User createUser(String firstName, String lastName, String login, String email, String secondEmail) throws IOException { return createUser(firstName, lastName, login, email, secondEmail, null); } + /** + * Create a user based on {@code firstName}, {@code lastName}, {@code login}, {@code email}, {@code secondEmail} + * and {@code mobilePhone}. + * + * @param firstName The first name of the user. + * @param lastName The last name of the user. + * @param login The login of the user. + * @param email The email of the user. + * @param secondEmail The secondary email of the user. + * @param mobilePhone The mobile phone number of the user. + * @return User The user that's created. + * @throws IOException If an input or output exception occurred. + */ public User createUser(String firstName, String lastName, String login, String email, String secondEmail, String mobilePhone) throws IOException { UserProfile userProfile = new UserProfile(); userProfile.setFirstName(firstName); @@ -111,14 +219,53 @@ public User createUser(String firstName, String lastName, String login, String e return createUser(user); } + /** + * Create and activate a user based on {@code firstName}, {@code lastName}, {@code login}, {@code email} + * and {@code activate}. + * + * @param firstName The first name of the user. + * @param lastName The last name of the user. + * @param login The login of the user. + * @param email The email of the user. + * @param activate Determines whether to activate the user. + * @return User The user that's created. + * @throws IOException If an input or output exception occurred. + */ public User createUser(String firstName, String lastName, String login, String email, boolean activate) throws IOException { return createUser(firstName, lastName, login, email, null, null, activate); } + /** + * Create and activate a user based on {@code firstName}, {@code lastName}, {@code login}, {@code email}, + * {@code secondEmail} and {@code activate}. + * + * @param firstName The first name of the user. + * @param lastName The last name of the user. + * @param login The login of the user. + * @param email The email of the user. + * @param secondEmail The secondary email of the user. + * @param activate Determines whether to activate the user. + * @return User The user that's created. + * @throws IOException If an input or output exception occurred. + */ public User createUser(String firstName, String lastName, String login, String email, String secondEmail, boolean activate) throws IOException { return createUser(firstName, lastName, login, email, secondEmail, null, activate); } + /** + * Create and activate a user based on {@code firstName}, {@code lastName}, {@code login}, {@code email}, + * {@code secondEmail}, {@code mobilePhone} and {@code activate}. + * + * @param firstName The first name of the user. + * @param lastName The last name of the user. + * @param login The login of the user. + * @param email The email of the user. + * @param secondEmail The secondary email of the user. + * @param mobilePhone The mobile phone number of the user. + * @param activate Determines whether to activate the user. + * @return User The user that's created. + * @throws IOException If an input or output exception occurred. + */ public User createUser(String firstName, String lastName, String login, String email, String secondEmail, String mobilePhone, boolean activate) throws IOException { UserProfile userProfile = new UserProfile(); userProfile.setFirstName(firstName); @@ -134,53 +281,78 @@ public User createUser(String firstName, String lastName, String login, String e return createUser(user, activate); } - /** - * Get the current user - * @return - * @throws IOException - */ - public User getMyUser() throws IOException { - return get(getEncodedPath("/me"), new TypeReference() { }); - } + // Get user groups. /** - * Get a user, given the userId - * @param userId - * @return - * @throws IOException + * Get all groups for a user. + * + * @param userId The id of the user to get groups for. + * @return List The groups the user with id equal to {@code userId} is member of. + * @throws IOException If an input or output exception occurred. */ - public User getUser(String userId) throws IOException { - return get(getEncodedPath("/%s", userId), new TypeReference() { }); + public List getUserGroups(String userId) throws IOException { + return get(getEncodedPath("/%s/groups", userId), new TypeReference>() { + }); } + // Update users. + /** - * Get all groups for a user - * @param userId - * @return - * @throws IOException + * Update a user with parameters. + * + * @param params The user parameters to be updated. + * @return User The updated user. + * @throws IOException If an input or output exception occurred. */ - public List getUserGroups(String userId) throws IOException { - return get(getEncodedPath("/%s/groups", userId), new TypeReference>() { }); - } - public User updateUser(User params) throws IOException { return put(getEncodedPath("/%s", params.getId()), params, new TypeReference() { }); } + /** + * Update a user with parameters. + * + * @param userId The id of the user to be updated. + * @param params The user parameters to be updated. + * @return User The updated user. + * @throws IOException If an input or output exception occurred. + */ public User updateUser(String userId, User params) throws IOException { return put(getEncodedPath("/%s", userId), params, new TypeReference() { }); } + // Delete users. + + /** + * Delete a user. + * + * @param userId The id of the user to be deleted. + * @throws IOException If an input or output exception occurred. + */ public void deleteUser(String userId) throws IOException { delete(getEncodedPath("/%s", userId)); } - // LIFECYCLE MANAGEMENT + // Activate users. + /** + * Activate user. + * + * @param userId The id of the user to be activated. + * @return User The activated user. + * @throws IOException If an input or output exception occurred. + */ public Map activateUser(String userId) throws IOException { return activateUser(userId, null); } + /** + * Activate user. + * + * @param userId The id of the user to be activated. + * @param sendEmail Determines whether to send an activate email. + * @return User The activated user. + * @throws IOException If an input or output exception occurred. + */ public Map activateUser(String userId, Boolean sendEmail) throws IOException { if (sendEmail != null && sendEmail) { return post(getEncodedPath("/%s/lifecycle/activate", userId), null, new TypeReference() { }); @@ -190,19 +362,51 @@ public Map activateUser(String userId, Boolean sendEmail) throws IOException { } } + // Users lifecycle. + + /** + * Deactivate user. + * + * @param userId The id of the user to be deactivated. + * @return User The deactivated user. + * @throws IOException If an input or output exception occurred. + */ public Map deactivateUser(String userId) throws IOException { return post(getEncodedPath("/%s/lifecycle/deactivate", userId), null, new TypeReference() { }); } + /** + * Unlock user. + * + * @param userId The id of the user to be unlocked. + * @return User The unlocked user. + * @throws IOException If an input or output exception occurred. + */ public Map unlockUser(String userId) throws IOException { - return post(getEncodedPath("/%s/lifecycle/unlock", userId), null, new TypeReference() { }); + return post(getEncodedPath("/%s/lifecycle/unlock", userId), null, new TypeReference() { + }); } + /** + * Expire password for a user. + * + * @param userId The id of the user to expire password for. + * @return User The user that has gotten it's password expired. + * @throws IOException If an input or output exception occurred. + */ public User expirePassword(String userId) throws IOException { return post(getEncodedPath("/%s/lifecycle/expire_password", userId), null, new TypeReference() { }); } + /** + * Expire password for a user. + * + * @param userId The id of the user to expire password for. + * @param tempPassword The new temporary password for the user. + * @return User The user that has gotten it's password expired. + * @throws IOException If an input or output exception occurred. + */ public TempPassword expirePassword(String userId, boolean tempPassword) throws IOException { Map params = new HashMap(); params.put("tempPassword", String.valueOf(tempPassword)); @@ -210,11 +414,26 @@ public TempPassword expirePassword(String userId, boolean tempPassword) throws I new TypeReference() { }); } + /** + * Generates a reset password link for a user. + * + * @param userId The id of the user to get a reset password link for. + * @return User The user that has gotten a password reset request. + * @throws IOException If an input or output exception occurred. + */ public ResetPasswordToken forgotPassword(String userId) throws IOException { return post(getEncodedPath("/%s/lifecycle/forgot_password", userId), null, new TypeReference() { }); } + /** + * Generates a reset password link for a user. + * + * @param userId The id of the user to get a reset password link for. + * @param sendEmail Determines whether to notify user with an email. + * @return User The user that has gotten a password reset request. + * @throws IOException If an input or output exception occurred. + */ public ResetPasswordToken forgotPassword(String userId, boolean sendEmail) throws IOException { Map params = new HashMap(); params.put("sendEmail", String.valueOf(sendEmail)); @@ -222,11 +441,26 @@ public ResetPasswordToken forgotPassword(String userId, boolean sendEmail) throw new TypeReference() { }); } + /** + * Reset password for a user, the user can no longer log in with the old password. + * + * @param userId The id of the user to reset password for. + * @return User The user that has gotten its password reset. + * @throws IOException If an input or output exception occurred. + */ public ResetPasswordToken resetPassword(String userId) throws IOException { return post(getEncodedPath("/%s/lifecycle/reset_password", userId), null, new TypeReference() { }); } + /** + * Reset password for a user, the user can no longer log in with the old password. + * + * @param userId The id of the user to reset password for. + * @param sendEmail Determines whether to notify user with an email. + * @return User The user that has gotten its password reset. + * @throws IOException If an input or output exception occurred. + */ public ResetPasswordToken resetPassword(String userId, boolean sendEmail) throws IOException { Map params = new HashMap(); params.put("sendEmail", String.valueOf(sendEmail)); @@ -234,18 +468,42 @@ public ResetPasswordToken resetPassword(String userId, boolean sendEmail) throws new TypeReference() { }); } + /** + * Reset factors for a user. + * + * @param userId The id of the user to reset factors for. + * @return User The user that has gotten its factors reset. + * @throws IOException If an input or output exception occurred. + */ public Map resetFactors(String userId) throws Exception { return post(getEncodedPath("/%s/lifecycle/reset_factors", userId), null, new TypeReference() { }); } - // CREDENTIAL MANAGEMENT + // User credentials + /** + * Set credentials for a user. + * + * @param userId The id of the user to set credentials for. + * @param loginCredentials The login credentials for the user. + * @return User The user that has gotten its credentials set. + * @throws IOException If an input or output exception occurred. + */ public User setCredentials(String userId, LoginCredentials loginCredentials) throws IOException { User dummyUser = new User(); dummyUser.setCredentials(loginCredentials); - return put(getEncodedPath("/%s", userId), dummyUser, new TypeReference() { }); + return put(getEncodedPath("/%s", userId), dummyUser, new TypeReference() { + }); } + /** + * Set password for a user. + * + * @param userId The id of the user to set password for. + * @param userPassword The password for the user. + * @return User The user that has gotten its password set. + * @throws IOException If an input or output exception occurred. + */ public User setPassword(String userId, String userPassword) throws IOException { LoginCredentials loginCredentials = new LoginCredentials(); Password password = new Password(); @@ -254,6 +512,15 @@ public User setPassword(String userId, String userPassword) throws IOException { return setCredentials(userId, loginCredentials); } + /** + * Set recovery question for a user. + * + * @param userId The id of the user to set recovery question for. + * @param question The recovery question for the user. + * @param answer The recovery answer for the user. + * @return User The user that has gotten its recovery question set. + * @throws IOException If an input or output exception occurred. + */ public User setRecoveryQuestion(String userId, String question, String answer) throws IOException { RecoveryQuestion recoveryQuestion = new RecoveryQuestion(); recoveryQuestion.setQuestion(question); @@ -265,6 +532,15 @@ public User setRecoveryQuestion(String userId, String question, String answer) t return setCredentials(userId, loginCredentials); } + /** + * Change password for a user. + * + * @param userId The id of the user to change password for. + * @param oldPassword The old password for the user. + * @param newPassword The new password for the user. + * @return LoginCredentials The login credentials for the user that has gotten its password changed. + * @throws IOException If an input or output exception occurred. + */ public LoginCredentials changePassword(String userId, String oldPassword, String newPassword) throws IOException { ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(); Password oldPasswordObj = new Password(); @@ -275,9 +551,19 @@ public LoginCredentials changePassword(String userId, String oldPassword, String changePasswordRequest.setNewPassword(newPasswordObj); return post(getEncodedPath("/%s/credentials/change_password", userId), changePasswordRequest, - new TypeReference() { }); + new TypeReference() { + }); } + /** + * Change recovery question for a user. + * + * @param userId The id of the user to change recovery question for. + * @param currentPassword The current password for the user. + * @param recoveryQuestion The recovery question for the user. + * @return LoginCredentials The login credentials for the user that has gotten its recovery question changed. + * @throws IOException If an input or output exception occurred. + */ public LoginCredentials changeRecoveryQuestion(String userId, String currentPassword, RecoveryQuestion recoveryQuestion) throws IOException { ChangeRecoveryQuestionRequest changeRecoveryQuestionRequest = new ChangeRecoveryQuestionRequest(); Password currentPasswordObj = new Password(); @@ -286,9 +572,20 @@ public LoginCredentials changeRecoveryQuestion(String userId, String currentPass changeRecoveryQuestionRequest.setRecoveryQuestion(recoveryQuestion); return post(getEncodedPath("/%s/credentials/change_password", userId), changeRecoveryQuestionRequest, - new TypeReference() { }); + new TypeReference() { + }); } + /** + * Change recovery question for a user. + * + * @param userId The id of the user to change recovery question for. + * @param currentPassword The current password for the user. + * @param question The recovery question for the user. + * @param answer The recovery answer for the user. + * @return LoginCredentials The login credentials for the user that has gotten its recovery question changed. + * @throws IOException If an input or output exception occurred. + */ public LoginCredentials changeRecoveryQuestion(String userId, String currentPassword, String question, String answer) throws IOException { RecoveryQuestion recoveryQuestion = new RecoveryQuestion(); recoveryQuestion.setQuestion(question); @@ -296,6 +593,109 @@ public LoginCredentials changeRecoveryQuestion(String userId, String currentPass return changeRecoveryQuestion(userId, currentPassword, recoveryQuestion); } + /** + * Get all users with paging info. + * + * @return PagedResults The users with paging info. + * @throws IOException If an input or output exception occurred. + */ + public PagedResults getUsersPagedResults() throws IOException { + return new PagedResults(getUsersApiResponse()); + } + + /** + * Get all users up to a limit with paging info. + * + * @param limit The max number of results returned. + * @return PagedResults The users with paging info. + * @throws IOException If an input or output exception occurred. + */ + public PagedResults getUsersPagedResultsWithLimit(int limit) throws IOException { + return new PagedResults(getUsersApiResponseWithLimit(limit)); + } + + /** + * Get all users with paging info that matches filter. + * + * @param filterBuilder The filter that's being used to match users. + * @return PagedResults The users with paging info. + * @throws IOException If an input or output exception occurred. + */ + public PagedResults getUsersPagedResultsWithFilter(FilterBuilder filterBuilder) throws IOException { + return new PagedResults(getUsersApiResponseWithFilter(filterBuilder)); + } + + /** + * Get all users up to a limit with paging info that matches filter. + * + * @param filterBuilder The filter that's being used to match users. + * @param limit The max number of results returned. + * @return PagedResults The users with paging info. + * @throws IOException If an input or output exception occurred. + */ + public PagedResults getUsersPagedResultsWithFilterAndLimit(FilterBuilder filterBuilder, int limit) throws IOException { + return new PagedResults(getUsersApiResponseWithFilterAndLimit(filterBuilder, limit)); + } + + /** + * Get all users up to a limit using cursor with paging info. + * + * @param after The cursor that determines which users to return after. + * @param limit The max number of results returned. + * @return PagedResults The users with paging info. + * @throws IOException If an input or output exception occurred. + */ + public PagedResults getUsersPagedResultsAfterCursorWithLimit(String after, int limit) throws IOException { + return new PagedResults(getUsersApiResponseAfterCursorWithLimit(after, limit)); + } + + /** + * Get all users from url with paging info. + * + * @param url The URL to get users from. + * @return PagedResults The users with paging info. + * @throws IOException If an input or output exception occurred. + */ + public PagedResults getUsersPagedResultsByUrl(String url) throws IOException { + return new PagedResults(getUsersApiResponseByUrl(url)); + } + + /** + * Get all users using advanced search with paging info. + * + * @param filterBuilder The advanced search filter thats being used to match users. + * @return PagedResults The users with paging info. + * @throws IOException If an input or output exception occurred. + */ + public PagedResults getUsersPagedResultsWithAdvancedSearch(FilterBuilder filterBuilder) throws IOException { + return new PagedResults(getUsersApiResponseWithAdvancedSearch(filterBuilder)); + } + + /** + * Get all users up to a limit using advanced search with paging info. + * + * @param filterBuilder The advanced search filter thats being used to match users. + * @param limit The max number of results returned. + * @return PagedResults The users with paging info. + * @throws IOException If an input or output exception occurred. + */ + public PagedResults getUsersPagedResultsWithAdvancedSearchAndLimit(FilterBuilder filterBuilder, int limit) throws IOException { + return new PagedResults(getUsersApiResponseWithAdvancedSearchAndLimit(filterBuilder, limit)); + } + + /** + * Get all users up to a limit using advanced search and cursor with paging info. + * + * @param filterBuilder The advanced search filter thats being used to match users. + * @param limit The max number of results returned. + * @param after The cursor that determines which users to return after. + * @return PagedResults The users with paging info. + * @throws IOException If an input or output exception occurred. + */ + public PagedResults getUsersPagedResultsWithAdvancedSearchAndLimitAndAfterCursor(FilterBuilder filterBuilder, int limit, String after) throws IOException { + return new PagedResults(getUsersApiResponseWithAdvancedSearchAndLimitAndAfterCursor(filterBuilder, limit, after)); + } + //////////////////////////////////////////// // API RESPONSE METHODS //////////////////////////////////////////// @@ -334,37 +734,40 @@ protected ApiResponse> getUsersApiResponseAfterCursorWithLimit(String return new ApiResponse>(resp, users); } - protected ApiResponse> getUsersApiResponseByUrl(String url) throws IOException { - HttpResponse resp = getHttpResponse(url); + protected ApiResponse> getUsersApiResponseWithAdvancedSearch(FilterBuilder filterBuilder) throws IOException { + HttpResponse resp = getHttpResponse(getEncodedPath("?" + SEARCH + "=%s", filterBuilder.toString())); List users = unmarshallResponse(new TypeReference>() { }, resp); return new ApiResponse>(resp, users); } - //////////////////////////////////////////// - // PAGED RESULTS METHODS - //////////////////////////////////////////// - - public PagedResults getUsersPagedResults() throws IOException { - return new PagedResults(getUsersApiResponse()); - } - - public PagedResults getUsersPagedResultsWithLimit(int limit) throws IOException { - return new PagedResults(getUsersApiResponseWithLimit(limit)); + protected ApiResponse> getUsersApiResponseWithAdvancedSearchAndLimit(FilterBuilder filterBuilder, int limit) throws IOException { + Map params = new HashMap(); + params.put(SEARCH, filterBuilder.toString()); + params.put(LIMIT, Integer.toString(limit)); + HttpResponse resp = getHttpResponse(getEncodedPathWithQueryParams("/", params)); + List users = unmarshallResponse(new TypeReference>() { }, resp); + return new ApiResponse>(resp, users); } - public PagedResults getUsersPagedResultsWithFilter(FilterBuilder filterBuilder) throws IOException { - return new PagedResults(getUsersApiResponseWithFilter(filterBuilder)); + protected ApiResponse> getUsersApiResponseWithAdvancedSearchAndLimitAndAfterCursor(FilterBuilder filterBuilder, int limit, String after) throws IOException { + Map params = new HashMap(); + params.put(SEARCH, filterBuilder.toString()); + params.put(AFTER_CURSOR, after); + params.put(LIMIT, Integer.toString(limit)); + HttpResponse resp = getHttpResponse(getEncodedPathWithQueryParams("/", params)); + List users = unmarshallResponse(new TypeReference>() { }, resp); + return new ApiResponse>(resp, users); } - public PagedResults getUsersPagedResultsWithFilterAndLimit(FilterBuilder filterBuilder, int limit) throws IOException { - return new PagedResults(getUsersApiResponseWithFilterAndLimit(filterBuilder, limit)); + protected ApiResponse> getUsersApiResponseByUrl(String url) throws IOException { + HttpResponse resp = getHttpResponse(url); + List users = unmarshallResponse(new TypeReference>() { }, resp); + return new ApiResponse>(resp, users); } - public PagedResults getUsersPagedResultsAfterCursorWithLimit(String after, int limit) throws IOException { - return new PagedResults(getUsersApiResponseAfterCursorWithLimit(after, limit)); + @Override + protected String getFullPath(String relativePath) { + return String.format("/api/v%d/users%s", this.apiVersion, relativePath); } - public PagedResults getUsersPagedResultsByUrl(String url) throws IOException { - return new PagedResults(getUsersApiResponseByUrl(url)); - } } \ No newline at end of file diff --git a/src/main/java/com/okta/sdk/clients/UserGroupApiClient.java b/src/main/java/com/okta/sdk/clients/UserGroupApiClient.java index 7f48a207f70..3957855c72b 100644 --- a/src/main/java/com/okta/sdk/clients/UserGroupApiClient.java +++ b/src/main/java/com/okta/sdk/clients/UserGroupApiClient.java @@ -21,11 +21,6 @@ public UserGroupApiClient(ApiClientConfiguration config) { super(config); } - @Override - protected String getFullPath(String relativePath) { - return String.format("/api/v%d/groups%s", this.apiVersion, relativePath); - } - //////////////////////////////////////////// // COMMON METHODS //////////////////////////////////////////// @@ -186,4 +181,10 @@ public PagedResults getUsersPagedResultsWithUrl(String url) throws IOExcep public PagedResults getUsersPagedResultsAfterCursorWithLimit(String groupId, String after, int limit) throws IOException { return new PagedResults(getUsersApiResponseAfterCursorWithLimit(groupId, after, limit)); } + + @Override + protected String getFullPath(String relativePath) { + return String.format("/api/v%d/groups%s", this.apiVersion, relativePath); + } + } diff --git a/src/main/java/com/okta/sdk/exceptions/RateLimitExceededException.java b/src/main/java/com/okta/sdk/exceptions/RateLimitExceededException.java index 27eddbbf9e7..16c89a63328 100644 --- a/src/main/java/com/okta/sdk/exceptions/RateLimitExceededException.java +++ b/src/main/java/com/okta/sdk/exceptions/RateLimitExceededException.java @@ -3,7 +3,9 @@ import com.okta.sdk.framework.ErrorResponse; public class RateLimitExceededException extends ApiException { + public RateLimitExceededException(ErrorResponse errorResponse) { super(429, errorResponse); } + } diff --git a/src/main/java/com/okta/sdk/framework/ApiClient.java b/src/main/java/com/okta/sdk/framework/ApiClient.java index 7eb081974fd..3a00a278ab6 100644 --- a/src/main/java/com/okta/sdk/framework/ApiClient.java +++ b/src/main/java/com/okta/sdk/framework/ApiClient.java @@ -33,6 +33,7 @@ public abstract class ApiClient { public static final String AFTER_CURSOR = "after"; public static final String LIMIT = "limit"; public static final String FILTER = "filter"; + public static final String SEARCH = "search"; public static final String SEARCH_QUERY = "q"; protected HttpClient httpClient; diff --git a/src/main/java/com/okta/sdk/framework/ErrorResponse.java b/src/main/java/com/okta/sdk/framework/ErrorResponse.java index b1dc01f8c8c..a3cc03f8624 100644 --- a/src/main/java/com/okta/sdk/framework/ErrorResponse.java +++ b/src/main/java/com/okta/sdk/framework/ErrorResponse.java @@ -60,4 +60,5 @@ public void addCause(String cause) { errorCause.setErrorSummary(cause); this.errorCauses.add(errorCause); } + } diff --git a/src/main/java/com/okta/sdk/framework/FilterBuilder.java b/src/main/java/com/okta/sdk/framework/FilterBuilder.java index 761a240f3f6..19aa25e3c68 100644 --- a/src/main/java/com/okta/sdk/framework/FilterBuilder.java +++ b/src/main/java/com/okta/sdk/framework/FilterBuilder.java @@ -3,84 +3,73 @@ import com.okta.sdk.exceptions.SdkException; import org.joda.time.DateTime; -import java.util.HashSet; - public class FilterBuilder { - public FilterBuilder() { } - public FilterBuilder(String StringFilter) { - STRING_BUILDER.append(StringFilter); - } + // Boolean operators + private static final String EQUAL_SIGN = " eq "; + private static final String CONTAIN_SIGN = " co "; + private static final String STARTS_WITH_SIGN = " sw "; + private static final String PRESENT_SIGN = " pr"; + private static final String GREATER_THAN_SIGN = " gt "; + private static final String GREATER_THAN_OR_EQUAL_SIGN = " ge "; + private static final String LESS_THAN_SIGN = " lt "; + private static final String LESS_THAN_OR_EQUAL_SIGN = " le "; - private final StringBuilder STRING_BUILDER = new StringBuilder(); - private final String EQUAL_SIGN = " eq "; - private final String CONTAIN_SIGN = " co "; - private final String STARTS_WITH_SIGN = " sw "; - private final String PRESENT_SIGN = " pr"; - private final String GREATER_THAN_SIGN = " gt "; - private final String GREATER_THAN_OR_EQUAL_SIGN = " ge "; - private final String LESS_THAN_SIGN = " lt "; - private final String LESS_THAN_OR_EQUAL_SIGN = " le "; - private final String AND_SIGN = " and "; - private final String OR_SIGN = " or "; + // Logical operators + private static final String AND_SIGN = " and "; + private static final String OR_SIGN = " or "; - private final HashSet ATTRIBUTES = new HashSet(); - private int orCount = 0; + private static final int MAX_ALLOWED_OPERATORS = 20; - @Override - public String toString() { - return this.STRING_BUILDER.toString(); + private final StringBuilder filterBuilder = new StringBuilder(); + private int totalNumberOfOperators = 0; + + public FilterBuilder() { } + + public FilterBuilder(String filter) { + filterBuilder.append(filter); + totalNumberOfOperators = getNumberOfLogicalOperators(filterBuilder.toString()); + checkComplexity(); } - private boolean hasTooManyAttributesWithOr() { - if (ATTRIBUTES.size() > 1 && orCount > 0) { - SdkException sdkException = new SdkException("Cannot create a filter with two different attributes combined by \"or\""); - throw new RuntimeException(sdkException); - } - return false; + @Override + public String toString() { + return this.filterBuilder.toString(); } public FilterBuilder where(String attr) { return this.attr(attr); } - public FilterBuilder where(FilterBuilder filterBuilderBuilder) { - ATTRIBUTES.addAll(filterBuilderBuilder.ATTRIBUTES); - orCount += filterBuilderBuilder.orCount; - hasTooManyAttributesWithOr(); // The nested filter could contain extra attributes - STRING_BUILDER.append("(" + filterBuilderBuilder.toString() + ")"); + public FilterBuilder where(FilterBuilder filterBuilder) { + totalNumberOfOperators += getNumberOfLogicalOperators(filterBuilder.toString()); + checkComplexity(); + this.filterBuilder.append("(" + filterBuilder.toString() + ")"); return this; } public FilterBuilder attr(String attr) { - ATTRIBUTES.add(attr); - hasTooManyAttributesWithOr(); // We could have added too many attributes - STRING_BUILDER.append(attr); + filterBuilder.append(attr); return this; } public FilterBuilder value(String value) { - STRING_BUILDER.append('"' + value + '"'); + filterBuilder.append('"' + value + '"'); return this; } public FilterBuilder value(boolean value) { - STRING_BUILDER.append(String.valueOf(value).toLowerCase()); + filterBuilder.append(String.valueOf(value).toLowerCase()); return this; } public FilterBuilder value(int value) { - STRING_BUILDER.append(value); + filterBuilder.append(value); return this; } public FilterBuilder value(DateTime value) { - STRING_BUILDER.append('"' + Utils.convertDateTimeToString(value) + '"'); - return this; - } - - private FilterBuilder equalTo() { - STRING_BUILDER.append(EQUAL_SIGN); + filterBuilder.append('"' + Utils.convertDateTimeToString(value) + '"'); return this; } @@ -100,11 +89,6 @@ public FilterBuilder equalTo(DateTime value) { return equalTo().value(value); } - private FilterBuilder contains() { - STRING_BUILDER.append(CONTAIN_SIGN); - return this; - } - public FilterBuilder contains(String value) { return contains().value(value); } @@ -113,11 +97,6 @@ public FilterBuilder contains(int value) { return contains().value(value); } - private FilterBuilder startsWith() { - STRING_BUILDER.append(STARTS_WITH_SIGN); - return this; - } - public FilterBuilder startsWith(String value) { return startsWith().value(value); } @@ -127,7 +106,7 @@ public FilterBuilder startsWith(int value) { } public FilterBuilder present() { - STRING_BUILDER.append(PRESENT_SIGN); + filterBuilder.append(PRESENT_SIGN); return this; } @@ -135,11 +114,6 @@ public FilterBuilder present(String value) { return value(value).present(); } - private FilterBuilder greaterThan() { - STRING_BUILDER.append(GREATER_THAN_SIGN); - return this; - } - public FilterBuilder greaterThan(String value) { return greaterThan().value(value); } @@ -152,11 +126,6 @@ public FilterBuilder greaterThan(DateTime value) { return greaterThan().value(value); } - private FilterBuilder greaterThanOrEqual() { - STRING_BUILDER.append(GREATER_THAN_OR_EQUAL_SIGN); - return this; - } - public FilterBuilder greaterThanOrEqual(String value) { return greaterThanOrEqual().value(value); } @@ -169,11 +138,6 @@ public FilterBuilder greaterThanOrEqual(DateTime value) { return greaterThanOrEqual().value(value); } - private FilterBuilder lessThan() { - STRING_BUILDER.append(LESS_THAN_SIGN); - return this; - } - public FilterBuilder lessThan(String value) { return lessThan().value(value); } @@ -187,7 +151,7 @@ public FilterBuilder lessThan(DateTime value) { } private FilterBuilder lessThanOrEqual() { - STRING_BUILDER.append(LESS_THAN_OR_EQUAL_SIGN); + filterBuilder.append(LESS_THAN_OR_EQUAL_SIGN); return this; } @@ -204,24 +168,72 @@ public FilterBuilder lessThanOrEqual(DateTime value) { } public FilterBuilder and() { - STRING_BUILDER.append(AND_SIGN); + filterBuilder.append(AND_SIGN); + totalNumberOfOperators += 1; + checkComplexity(); return this; } public FilterBuilder and(FilterBuilder filterBuilder) { - STRING_BUILDER.append(AND_SIGN); + this.filterBuilder.append(AND_SIGN); return where(filterBuilder); } public FilterBuilder or() { - orCount++; - STRING_BUILDER.append(OR_SIGN); + filterBuilder.append(OR_SIGN); + totalNumberOfOperators += 1; + checkComplexity(); return this; } public FilterBuilder or(FilterBuilder filterBuilder) { - orCount++; - STRING_BUILDER.append(OR_SIGN); + this.filterBuilder.append(OR_SIGN); return where(filterBuilder); } + + private FilterBuilder equalTo() { + filterBuilder.append(EQUAL_SIGN); + return this; + } + + private FilterBuilder contains() { + filterBuilder.append(CONTAIN_SIGN); + return this; + } + + private FilterBuilder startsWith() { + filterBuilder.append(STARTS_WITH_SIGN); + return this; + } + + private FilterBuilder greaterThan() { + filterBuilder.append(GREATER_THAN_SIGN); + return this; + } + + private FilterBuilder greaterThanOrEqual() { + filterBuilder.append(GREATER_THAN_OR_EQUAL_SIGN); + return this; + } + + private FilterBuilder lessThan() { + filterBuilder.append(LESS_THAN_SIGN); + return this; + } + + private boolean checkComplexity() { + if (totalNumberOfOperators > MAX_ALLOWED_OPERATORS) { + SdkException sdkException = new SdkException(String.format("Cannot create a filter with more than %s logical totalNumberOfOperators.", MAX_ALLOWED_OPERATORS)); + throw new RuntimeException(sdkException); + } + return false; + } + + private int getNumberOfLogicalOperators(String filter) { + if (filter == null) { + return 0; + } + return (filter.split(OR_SIGN, -1).length - 1) + (filter.split(AND_SIGN, -1).length - 1); + } + } diff --git a/src/main/java/com/okta/sdk/framework/Filters.java b/src/main/java/com/okta/sdk/framework/Filters.java index 73e3fcfca2b..1f6c1323741 100644 --- a/src/main/java/com/okta/sdk/framework/Filters.java +++ b/src/main/java/com/okta/sdk/framework/Filters.java @@ -1,6 +1,7 @@ package com.okta.sdk.framework; public class Filters { + public static class User { public final static String ID = "id"; public final static String STATUS = "status"; @@ -20,4 +21,5 @@ public static class AppInstance { public static class OrgAuthFactor { public final static String STATUS = "status"; } + } diff --git a/src/main/java/com/okta/sdk/framework/RateLimitContext.java b/src/main/java/com/okta/sdk/framework/RateLimitContext.java index 0c50d9c749e..f893c097fd2 100644 --- a/src/main/java/com/okta/sdk/framework/RateLimitContext.java +++ b/src/main/java/com/okta/sdk/framework/RateLimitContext.java @@ -6,35 +6,13 @@ import org.joda.time.DateTime; public class RateLimitContext { + private HttpResponse httpResponse; public RateLimitContext(HttpResponse httpResponse) { this.httpResponse = httpResponse; } - private String getHeaderValueString(String headerName) throws Exception { - if (httpResponse == null) { - throw new SdkException("No http response"); - } - - Header[] headers = httpResponse.getHeaders(headerName); - if (headers.length > 0) { - Header header = headers[0]; - return header.getValue(); - } else { - throw new SdkException("No " + headerName + " header"); - } - } - - private long getHeaderValueLong(String headerName) throws Exception { - String headerString = getHeaderValueString(headerName); - try { - return Long.parseLong(headerString); - } catch (Exception e){ - throw new SdkException("Error parsing " + headerName + " header"); - } - } - /** * @return The number of requests remaining in the current window * @throws Exception @@ -73,4 +51,27 @@ public DateTime getNextWindowDateTime() throws Exception { public Long getRequestLimit() throws Exception { return getHeaderValueLong("X-Rate-Limit-Limit"); } + + private String getHeaderValueString(String headerName) throws Exception { + if (httpResponse == null) { + throw new SdkException("No http response"); + } + + Header[] headers = httpResponse.getHeaders(headerName); + if (headers.length > 0) { + Header header = headers[0]; + return header.getValue(); + } else { + throw new SdkException("No " + headerName + " header"); + } + } + + private long getHeaderValueLong(String headerName) throws Exception { + String headerString = getHeaderValueString(headerName); + try { + return Long.parseLong(headerString); + } catch (Exception e){ + throw new SdkException("Error parsing " + headerName + " header"); + } + } } \ No newline at end of file diff --git a/src/main/java/com/okta/sdk/framework/Utils.java b/src/main/java/com/okta/sdk/framework/Utils.java index 410aedc29ed..d5676711f12 100644 --- a/src/main/java/com/okta/sdk/framework/Utils.java +++ b/src/main/java/com/okta/sdk/framework/Utils.java @@ -5,6 +5,7 @@ import org.joda.time.format.ISODateTimeFormat; public class Utils { + protected static DateTimeFormatter DATE_TIME_FORMATTER = ISODateTimeFormat.dateTime(); protected static int DEFAULT_RESULTS_LIMIT = 200; diff --git a/src/main/java/com/okta/sdk/models/links/Link.java b/src/main/java/com/okta/sdk/models/links/Link.java index 4d68e03714b..0f2ce92c2a3 100644 --- a/src/main/java/com/okta/sdk/models/links/Link.java +++ b/src/main/java/com/okta/sdk/models/links/Link.java @@ -21,6 +21,7 @@ public class Link implements LinksUnion { private String title; private String type; private Map> hints; + public String getRel() { return rel; } @@ -28,6 +29,7 @@ public String getRel() { public void setRel(String rel) { this.rel = rel; } + public String getHref() { return href; } diff --git a/src/main/java/com/okta/sdk/models/links/Links.java b/src/main/java/com/okta/sdk/models/links/Links.java index 2c3d4c41b9a..a12663b3575 100644 --- a/src/main/java/com/okta/sdk/models/links/Links.java +++ b/src/main/java/com/okta/sdk/models/links/Links.java @@ -8,6 +8,7 @@ @JsonDeserialize(using = JsonDeserializer.None.class) // use default public class Links extends LinkedList implements LinksUnion { + public Links() { super(); } diff --git a/src/main/java/com/okta/sdk/models/links/LinksUnionDeserializer.java b/src/main/java/com/okta/sdk/models/links/LinksUnionDeserializer.java index ca4d63e6c24..d2eb35de4ff 100644 --- a/src/main/java/com/okta/sdk/models/links/LinksUnionDeserializer.java +++ b/src/main/java/com/okta/sdk/models/links/LinksUnionDeserializer.java @@ -9,13 +9,13 @@ import java.io.IOException; public class LinksUnionDeserializer extends StdDeserializer { + public LinksUnionDeserializer() { super(LinksUnion.class); } @Override - public LinksUnion deserialize(JsonParser parser, DeserializationContext context) throws IOException - { + public LinksUnion deserialize(JsonParser parser, DeserializationContext context) throws IOException { ObjectMapper mapper = (ObjectMapper) parser.getCodec(); JsonNode root = mapper.readTree(parser); if (root.isArray()) { diff --git a/src/test/java/FilterBuilderTest.java b/src/test/java/FilterBuilderTest.java index 1a8a947fa8e..ee34d9bb825 100644 --- a/src/test/java/FilterBuilderTest.java +++ b/src/test/java/FilterBuilderTest.java @@ -1,14 +1,16 @@ import com.okta.sdk.framework.FilterBuilder; import com.okta.sdk.framework.Filters; -import com.okta.sdk.framework.Utils; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; -import org.testng.Assert; import org.testng.annotations.Test; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + public class FilterBuilderTest { + @Test - public void TestFilter() throws Exception { + public void testFilterWithAndOperatorAndMultipleAttributes() { FilterBuilder filterBuilder = new FilterBuilder() .where(Filters.User.STATUS) .equalTo("ACTIVE") @@ -16,41 +18,139 @@ public void TestFilter() throws Exception { .where(Filters.User.LAST_UPDATED) .greaterThan(new DateTime(2014, 1, 1, 1, 1, DateTimeZone.forOffsetHours(-8))); - Assert.assertEquals(filterBuilder.toString(), "status eq \"ACTIVE\" and lastUpdated gt \"2014-01-01T01:01:00.000-08:00\""); + assertThat(filterBuilder.toString(), is("status eq \"ACTIVE\" and lastUpdated gt \"2014-01-01T01:01:00.000-08:00\"")); } - @Test( expectedExceptions = { RuntimeException.class}) - public void TestFilterException() throws Exception { + @Test + public void testFilterWithOrOperatorAndMultipleAttributes() { FilterBuilder filterBuilder = new FilterBuilder() .where(Filters.User.STATUS) - .equalTo("ACTIVE") + .equalTo("LOCKED_OUT") .or() .where(Filters.User.LAST_UPDATED) - .greaterThan(new DateTime(2014, 1, 1, 1, 1, DateTimeZone.forOffsetHours(-8))); - filterBuilder.toString(); + .lessThan(new DateTime(2014, 1, 1, 1, 1, DateTimeZone.forOffsetHours(-8))); + + assertThat(filterBuilder.toString(), is("status eq \"LOCKED_OUT\" or lastUpdated lt \"2014-01-01T01:01:00.000-08:00\"")); } - @Test(expectedExceptions = { RuntimeException.class }) - public void TestTooManyNestedFieldsFilterException() throws Exception { - FilterBuilder nestedFilterBuilder = new FilterBuilder() - .where(Filters.User.STATUS) - .equalTo("ACTIVE"); + @Test(expectedExceptions = {RuntimeException.class}) + public void testFilterFailsWithTooComplexFilter() throws Exception { + new FilterBuilder() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE"); // Max 20 logical operators allowed. + } + @Test(expectedExceptions = {RuntimeException.class}) + public void testFilterFailsWithTooComplexFilterUsingNestedFields() throws Exception { FilterBuilder filterBuilder = new FilterBuilder() - .where(Filters.User.LAST_UPDATED) - .greaterThan(new DateTime(2014, 1, 1, 1, 1, DateTimeZone.forOffsetHours(-8))) - .or(nestedFilterBuilder); // Should Throw an error here + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE"); + new FilterBuilder(filterBuilder.toString()).and() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE"); // Max 20 logical operators allowed. } - @Test() - public void TestMultipleFieldsWithOr() throws Exception { + @Test + public void testFilterWithComplexFilter() throws Exception { FilterBuilder filterBuilder = new FilterBuilder() - .where(Filters.User.STATUS) - .equalTo("LOCKED_OUT") - .or() - .where(Filters.User.STATUS) // Should be fine - .equalTo("PASSWORD_EXPIRED"); + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE"); + assertThat(filterBuilder.toString(), is("status eq \"ACTIVE\" and status eq \"ACTIVE\"" + + " and status eq \"ACTIVE\" or status eq \"ACTIVE\" or status eq \"ACTIVE\" or " + + "status eq \"ACTIVE\" and status eq \"ACTIVE\" or status eq \"ACTIVE\" or status" + + " eq \"ACTIVE\" or status eq \"ACTIVE\" and status eq \"ACTIVE\" and status eq" + + " \"ACTIVE\" and status eq \"ACTIVE\" or status eq \"ACTIVE\" or status eq \"ACTIVE\"" + + " or status eq \"ACTIVE\" and status eq \"ACTIVE\" and status eq \"ACTIVE\" and " + + "status eq \"ACTIVE\" or status eq \"ACTIVE\" or status eq \"ACTIVE\"")); } - FilterBuilder filterBuilderWithUtil = Utils.getFilter(Filters.User.STATUS, "LOCKED_OUT", "PASSWORD_EXPIRED"); + @Test + public void testFilterWithComplexFilterUsingNestedFields() throws Exception { + FilterBuilder filterBuilder = new FilterBuilder() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE"); + filterBuilder = new FilterBuilder(filterBuilder.toString()).or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").and() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE").or() + .where(Filters.User.STATUS).equalTo("ACTIVE"); + assertThat(filterBuilder.toString(), is("status eq \"ACTIVE\" or status eq \"ACTIVE\"" + + " or status eq \"ACTIVE\" and status eq \"ACTIVE\" and status eq \"ACTIVE\" or " + + "status eq \"ACTIVE\" or status eq \"ACTIVE\" or status eq \"ACTIVE\" or status" + + " eq \"ACTIVE\" or status eq \"ACTIVE\" or status eq \"ACTIVE\" and status eq" + + " \"ACTIVE\" or status eq \"ACTIVE\" or status eq \"ACTIVE\" or status eq \"ACTIVE\"" + + " and status eq \"ACTIVE\" and status eq \"ACTIVE\" and status eq \"ACTIVE\" or status " + + "eq \"ACTIVE\" or status eq \"ACTIVE\" or status eq \"ACTIVE\"")); } }