From fe816b83650af48d98a23e200dcee75d27bcba57 Mon Sep 17 00:00:00 2001 From: BojithaPiyathilake Date: Thu, 28 Apr 2022 17:51:02 +0530 Subject: [PATCH] Cursor based pagination for scim resources. Cursor pagination changes - phase 1 formatting adjustments before PR more formatting adjustments and removing unneccesary comments Formatting the code Adjustments made to Cursor Pagination for SCIM resources Changes made as per the code review Changing the response type from a List to UsersGetResponse type object Changing the response type from a List to UsersGetResponse type object - changes based on the code review formatting changes Made changes to support POST/.Search cursor pagination Made changes so that the ServiceProviderConfig endpoint shows cursor pagination capability minor changes Fix for NullPointerException when running UserResourceManagerTest Updated wording for serviceProviderConfigEndpoint changes Formatting changes Removing unused parameters from processPagination method Updating tests broken due to changes and writing new tests for cursor pagination formatting --- .../core/config/CharonConfiguration.java | 11 ++ .../core/config/SCIMConfigConstants.java | 1 + .../charon3/core/encoder/JSONDecoder.java | 8 +- .../charon3/core/encoder/JSONEncoder.java | 6 + .../charon3/core/extensions/UserManager.java | 53 ++++-- .../charon3/core/objects/ListedResource.java | 36 ++++ .../core/objects/plainobjects/Cursor.java | 54 ++++++ .../plainobjects/UsersGetResponse.java | 34 ++++ .../charon3/core/protocol/SCIMResponse.java | 1 + .../protocol/endpoints/ResourceManager.java | 48 ++++-- .../endpoints/UserResourceManager.java | 162 ++++++++++++++++-- .../charon3/core/schema/SCIMConstants.java | 21 +++ .../core/schema/SCIMSchemaDefinitions.java | 20 ++- .../core/utils/ResourceManagerUtil.java | 46 +++++ .../core/utils/codeutils/SearchRequest.java | 30 ++++ .../endpoints/UserResourceManagerTest.java | 60 +++++++ .../core/utils/ResourceManagerUtilTest.java | 62 +++++++ .../usermanager/InMemoryUserManager.java | 5 +- 18 files changed, 612 insertions(+), 46 deletions(-) create mode 100644 modules/charon-core/src/main/java/org/wso2/charon3/core/objects/plainobjects/Cursor.java diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/config/CharonConfiguration.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/config/CharonConfiguration.java index 8e4be9094..002a41ff4 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/config/CharonConfiguration.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/config/CharonConfiguration.java @@ -36,6 +36,7 @@ public class CharonConfiguration implements Configuration { private int maxPayLoadSize; private int maxResults; private ArrayList authenticationSchemes = new ArrayList(); + private boolean cursorSupport; //default count value for pagination private int count; @@ -78,6 +79,15 @@ public void setFilterSupport(boolean supported, int maxResults) { this.maxResults = maxResults; } + /** + * Set cursor pagination support + * @param supported + */ + public void setCursorPaginationSupport(boolean supported) { + + this.cursorSupport = supported; + } + /* * set Change Password Support * @param supported @@ -145,6 +155,7 @@ public HashMap getConfig() { configMap.put(SCIMConfigConstants.PATCH, patchSupport); configMap.put(SCIMConfigConstants.AUTHENTICATION_SCHEMES, authenticationSchemes); configMap.put(SCIMConfigConstants.PAGINATION_DEFAULT_COUNT, count); + configMap.put(SCIMConfigConstants.CURSOR, cursorSupport); return configMap; } diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/config/SCIMConfigConstants.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/config/SCIMConfigConstants.java index e32c16cd0..d47456d88 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/config/SCIMConfigConstants.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/config/SCIMConfigConstants.java @@ -47,6 +47,7 @@ public class SCIMConfigConstants { public static final String AUTHENTICATION_SCHEMES = "authenticationSchemes"; public static final String SCIM_SCHEMA_EXTENSION_CONFIG = "scim2-schema-extension.config"; public static final String PAGINATION_DEFAULT_COUNT = "pagination-default-count"; + public static final String CURSOR = "cursor"; } diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONDecoder.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONDecoder.java index ca116281e..f8cc3c774 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONDecoder.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONDecoder.java @@ -830,7 +830,13 @@ public SearchRequest decodeSearchRequestBody(String scimResourceString, searchRequest.setExcludedAttributes(excludedAttributes); searchRequest.setSchema((String) schemas.get(0)); searchRequest.setCountStr(decodedJsonObj.optString(SCIMConstants.OperationalConstants.COUNT)); - searchRequest.setStartIndexStr(decodedJsonObj.optString(SCIMConstants.OperationalConstants.START_INDEX)); + if (!decodedJsonObj.optString(SCIMConstants.OperationalConstants.START_INDEX).equals("")) { + searchRequest.setStartIndexStr( + decodedJsonObj.optString(SCIMConstants.OperationalConstants.START_INDEX)); + } + if (decodedJsonObj.has(SCIMConstants.OperationalConstants.CURSOR)) { + searchRequest.setCursor(decodedJsonObj.optString(SCIMConstants.OperationalConstants.CURSOR)); + } searchRequest.setDomainName(decodedJsonObj.optString(SCIMConstants.OperationalConstants.DOMAIN)); searchRequest.setFilter(rootNode); if (!decodedJsonObj.optString(SCIMConstants.OperationalConstants.SORT_BY).equals("")) { diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONEncoder.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONEncoder.java index f06a9455c..008a5c61d 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONEncoder.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONEncoder.java @@ -418,6 +418,10 @@ public String buildServiceProviderConfigJsonBody(HashMap config) changePasswordObject.put(SCIMConstants.ServiceProviderConfigSchemaConstants.SUPPORTED, config.get(SCIMConfigConstants.CHNAGE_PASSWORD)); + JSONObject paginationObject = new JSONObject(); + paginationObject.put(SCIMConstants.ServiceProviderConfigSchemaConstants.CURSOR, + config.get(SCIMConfigConstants.CURSOR)); + JSONArray authenticationSchemesArray = new JSONArray(); ArrayList values = (ArrayList) config.get(SCIMConfigConstants.AUTHENTICATION_SCHEMES); @@ -457,6 +461,8 @@ public String buildServiceProviderConfigJsonBody(HashMap config) etagObject); rootObject.put(SCIMConstants.ServiceProviderConfigSchemaConstants.AUTHENTICATION_SCHEMAS, authenticationSchemesArray); + rootObject.put(SCIMConstants.ServiceProviderConfigSchemaConstants.PAGINATION, + paginationObject); return rootObject.toString(); diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/extensions/UserManager.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/extensions/UserManager.java index 90b9456c8..8e6dc4275 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/extensions/UserManager.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/extensions/UserManager.java @@ -24,6 +24,7 @@ import org.wso2.charon3.core.exceptions.NotImplementedException; import org.wso2.charon3.core.objects.Group; import org.wso2.charon3.core.objects.User; +import org.wso2.charon3.core.objects.plainobjects.Cursor; import org.wso2.charon3.core.objects.plainobjects.GroupsGetResponse; import org.wso2.charon3.core.objects.plainobjects.UsersGetResponse; import org.wso2.charon3.core.schema.AttributeSchema; @@ -53,22 +54,44 @@ public void deleteUser(String userId) throws NotFoundException, CharonException, NotImplementedException, BadRequestException; /** - * List users with Get. + * List users with Get using offset pagination. * - * @param node Node - * @param startIndex Start Index - * @param count Count - * @param sortBy Sort by - * @param sortOrder Sort order - * @param domainName Domain name - * @param requiredAttributes Required user attributes - * @return Users with requested attributes - * @throws CharonException Error while listing users - * @throws NotImplementedException Operation note implemented - * @throws BadRequestException Bad request + * @param node Tree node built based on the filtering conditions. + * @param startIndex Start Index. + * @param count Count. + * @param sortBy Sort by. + * @param sortOrder Sort order. + * @param domainName Domain name. + * @param requiredAttributes Required user attributes. + * @return Users with requested attributes. + * @throws CharonException Error while listing users. + * @throws NotImplementedException Operation note implemented. + * @throws BadRequestException Bad request. */ default UsersGetResponse listUsersWithGET(Node node, Integer startIndex, Integer count, String sortBy, - String sortOrder, String domainName, Map requiredAttributes) + String sortOrder, String domainName, Map requiredAttributes) + throws CharonException, NotImplementedException, BadRequestException { + + return null; + } + + /** + * List users with Get using cursor pagination. + * + * @param node Tree node built based on the filtering conditions. + * @param cursor Cursor value for pagination and the Pagination direction. + * @param count Count. + * @param sortBy Sort by. + * @param sortOrder Sort order. + * @param domainName Domain name. + * @param requiredAttributes Required user attributes. + * @return Users with requested attributes. + * @throws CharonException Error while listing users. + * @throws NotImplementedException Operation note implemented. + * @throws BadRequestException Bad request. + */ + default UsersGetResponse listUsersWithGET(Node node, Cursor cursor, Integer count, String sortBy, String sortOrder, + String domainName, Map requiredAttributes) throws CharonException, NotImplementedException, BadRequestException { return null; @@ -86,7 +109,7 @@ default UsersGetResponse listUsersWithGET(Node node, Integer startIndex, Integer */ @Deprecated default UsersGetResponse listUsersWithGET(Node node, int startIndex, int count, String sortBy, String sortOrder, - String domainName, Map requiredAttributes) + String domainName, Map requiredAttributes) throws CharonException, NotImplementedException, BadRequestException { return null; @@ -188,7 +211,7 @@ public Group updateGroup(Group oldGroup, Group newGroup, Map re * * @param oldGroup * @param newGroup - * + * * @throws NotImplementedException * @throws BadRequestException * @throws CharonException diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/objects/ListedResource.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/objects/ListedResource.java index 478c62206..f3abfef6f 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/objects/ListedResource.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/objects/ListedResource.java @@ -130,6 +130,42 @@ public void setStartIndex(int startIndex) { } } + /** + * Setting the next cursor as an SCIM attribute. + * + * @param nextCursor The base64 encoded JSON string, made up of the cursor value and the pagination direction. + */ + public void setNextCursor(String nextCursor) { + + if (!isAttributeExist(SCIMConstants.ListedResourceSchemaConstants.NEXT_CURSOR)) { + SimpleAttribute nextCursorAttribute = + new SimpleAttribute(SCIMConstants.ListedResourceSchemaConstants.NEXT_CURSOR, nextCursor); + attributeList.put(SCIMConstants.ListedResourceSchemaConstants.NEXT_CURSOR, nextCursorAttribute); + } else { + SimpleAttribute nextCursorAttribute = ((SimpleAttribute) attributeList + .get(SCIMConstants.ListedResourceSchemaConstants.NEXT_CURSOR)); + nextCursorAttribute.setValue(nextCursor); + } + } + + /** + * Setting the previous cursor as an SCIM attribute. + * + * @param previousCursor The base64 encoded JSON string, made up of the cursor value and the pagination direction. + */ + public void setPreviousCursor(String previousCursor) { + + if (!isAttributeExist(SCIMConstants.ListedResourceSchemaConstants.PREVIOUS_CURSOR)) { + SimpleAttribute prevCursorAttribute = + new SimpleAttribute(SCIMConstants.ListedResourceSchemaConstants.PREVIOUS_CURSOR, previousCursor); + attributeList.put(SCIMConstants.ListedResourceSchemaConstants.PREVIOUS_CURSOR, prevCursorAttribute); + } else { + SimpleAttribute previousCursorAttribute = ((SimpleAttribute) attributeList + .get(SCIMConstants.ListedResourceSchemaConstants.PREVIOUS_CURSOR)); + previousCursorAttribute.setValue(previousCursor); + } + } + /** * set the listed resources * diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/objects/plainobjects/Cursor.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/objects/plainobjects/Cursor.java new file mode 100644 index 000000000..099d939e9 --- /dev/null +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/objects/plainobjects/Cursor.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.com). + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.charon3.core.objects.plainobjects; + +/** + * This class representation can be used to create a cursor type object which carries direction and cursor value + * to be used in cursor-based pagination. + */ +public class Cursor { + + private String cursorValue; + private String direction; + + public Cursor (String cursorVal, String direction) { + this.cursorValue = cursorVal; + this.direction = direction; + } + + public String getCursorValue() { + + return cursorValue; + } + + public void setCursorValue(String cursorValue) { + + this.cursorValue = cursorValue; + } + + public String getDirection() { + + return direction; + } + + public void setDirection(String direction) { + + this.direction = direction; + } +} diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/objects/plainobjects/UsersGetResponse.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/objects/plainobjects/UsersGetResponse.java index c6045c91a..e5ac8386c 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/objects/plainobjects/UsersGetResponse.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/objects/plainobjects/UsersGetResponse.java @@ -22,12 +22,20 @@ import java.util.List; /** +<<<<<<< HEAD * This class representation can be used to create a UsersGetResponse type object which carries the total number of * users identified from the filter and the list of users returned for this request. +======= + * This class representation can be used to create a UserGetResponse type object which carries the list of users, + * total number of users and optionally the previous and next cursor value. + * +>>>>>>> Cursor based pagination for scim resources. */ public class UsersGetResponse { private int totalUsers; + private String nextCursor; + private String previousCursor; private List users; /** @@ -39,6 +47,17 @@ public UsersGetResponse(int totalUsers, List users) { this.users = users; } + /** + * Constructor used to build a response object when using cursor pagination. + */ + public UsersGetResponse(int totalUsers, String nextCursor, String previousCursor, List users) { + + this.totalUsers = totalUsers; + this.nextCursor = nextCursor; + this.previousCursor = previousCursor; + this.users = users; + } + public int getTotalUsers() { return totalUsers; @@ -48,6 +67,21 @@ public void setTotalUsers(int totalUsers) { this.totalUsers = totalUsers; } + public String getNextCursor() { + return nextCursor; + } + + public void setNextCursor(String nextCursor) { + this.nextCursor = nextCursor; + } + + public String getPrevCursor() { + return previousCursor; + } + + public void setPrevCursor(String prevCursor) { + this.previousCursor = prevCursor; + } public List getUsers() { diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/SCIMResponse.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/SCIMResponse.java index a3eedee37..d74f273b3 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/SCIMResponse.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/SCIMResponse.java @@ -28,6 +28,7 @@ public class SCIMResponse { //If there are any HTTP header parameters to be set in response other than response code, protected Map headerParamMap; + /* * Constructor with three params * diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/endpoints/ResourceManager.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/endpoints/ResourceManager.java index 507510641..6a00125f5 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/endpoints/ResourceManager.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/endpoints/ResourceManager.java @@ -78,24 +78,44 @@ public interface ResourceManager { */ @Deprecated SCIMResponse listWithGET(UserManager userManager, String filter, int startIndex, int count, String sortBy, - String sortOrder, String domainName, String attributes, String excludeAttributes); + String sortOrder, String domainName, String attributes, String excludeAttributes); /* - * Get resources - * - * @param userManager User manager - * @param filter Filter to be executed - * @param startIndexInt Starting index value of the filter - * @param countInt Number of required results - * @param sortBy SortBy - * @param sortOrder Sorting order - * @param domainName Domain name - * @param attributes Attributes in the request - * @param excludeAttributes Exclude attributes - * @return SCIM response + * Get resources. + * + * @param userManager User manager. + * @param filter Filter to be executed. + * @param startIndexInt Starting index value of the filter. + * @param countInt Number of required results. + * @param sortBy SortBy. + * @param sortOrder Sorting order. + * @param domainName Domain name. + * @param attributes Attributes in the request. + * @param excludeAttributes Exclude attributes. + * @return SCIM response. */ default SCIMResponse listWithGET(UserManager userManager, String filter, Integer startIndexInt, Integer countInt, - String sortBy, String sortOrder, String domainName, String attributes, String excludeAttributes) { + String sortBy, String sortOrder, String domainName, String attributes, String excludeAttributes) { + + return null; + } + + /* + * Get resources with cursor. + * + * @param userManager User manager. + * @param filter Filter to be executed. + * @param cursor Cursor value for pagination. + * @param countInt Number of required results. + * @param sortBy SortBy. + * @param sortOrder Sorting order. + * @param domainName Domain name. + * @param attributes Attributes in the request. + * @param excludeAttributes Exclude attributes. + * @return SCIM response. + */ + default SCIMResponse listWithGET(UserManager userManager, String filter, String cursor, Integer countInt, + String sortBy, String sortOrder, String domainName, String attributes, String excludeAttributes) { return null; } diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/endpoints/UserResourceManager.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/endpoints/UserResourceManager.java index e37a8c218..3774565bc 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/endpoints/UserResourceManager.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/endpoints/UserResourceManager.java @@ -18,6 +18,7 @@ import org.apache.commons.lang.StringUtils; +import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.wso2.charon3.core.attributes.Attribute; @@ -33,6 +34,7 @@ import org.wso2.charon3.core.extensions.UserManager; import org.wso2.charon3.core.objects.ListedResource; import org.wso2.charon3.core.objects.User; +import org.wso2.charon3.core.objects.plainobjects.Cursor; import org.wso2.charon3.core.objects.plainobjects.UsersGetResponse; import org.wso2.charon3.core.protocol.ResponseCodeConstants; import org.wso2.charon3.core.protocol.SCIMResponse; @@ -49,6 +51,8 @@ import org.wso2.charon3.core.utils.codeutils.SearchRequest; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -297,7 +301,8 @@ public SCIMResponse listWithGET(UserManager userManager, String filter, int star .listUsersWithGET(rootNode, startIndex, count, sortBy, sortOrder, domainName, requiredAttributes); - return processUserList(usersGetResponse, encoder, schema, attributes, excludeAttributes, startIndex); + return processUserList(usersGetResponse, encoder, schema, attributes, excludeAttributes, startIndex, + null, count); } else { String error = "Provided user manager handler is null."; // Log the error as well. @@ -361,7 +366,75 @@ public SCIMResponse listWithGET(UserManager userManager, String filter, Integer UsersGetResponse usersGetResponse = userManager .listUsersWithGET(rootNode, startIndex, count, sortBy, sortOrder, domainName, requiredAttributes); - return processUserList(usersGetResponse, encoder, schema, attributes, excludeAttributes, startIndex); + + return processUserList(usersGetResponse, encoder, schema, attributes, excludeAttributes, startIndex, + null, count); + } else { + String error = "Provided user manager handler is null."; + // Log the error as well. + // Throw internal server error. + throw new InternalErrorException(error); + } + } catch (CharonException | NotFoundException | InternalErrorException | BadRequestException | + NotImplementedException e) { + return AbstractResourceManager.encodeSCIMException(e); + } catch (IOException e) { + String error = "Error in tokenization of the input filter"; + CharonException charonException = new CharonException(error); + return AbstractResourceManager.encodeSCIMException(charonException); + } + } + + /** + * Method to list users at the Users endpoint - Using cursor instead of offset. + * In the method, when the count is zero, the response will get zero results. When the count value is not + * specified (null) a default number of values for response will be returned. Any negative value to the count + * will return all the users. + * + * @param userManager User manager. + * @param filter Filter to be executed. + * @param cursorString Cursor value for pagination. + * @param countInt Number of required results. + * @param sortBy SortBy. + * @param sortOrder Sorting order. + * @param domainName Domain name. + * @param attributes Attributes in the request. + * @param excludeAttributes Exclude attributes. + * @return SCIM response. + */ + @Override + public SCIMResponse listWithGET(UserManager userManager, String filter, String cursorString, Integer countInt, + String sortBy, String sortOrder, String domainName, String attributes, + String excludeAttributes) { + + try { + Integer count = ResourceManagerUtil.processCount(countInt); + //For handling null cursor and decoding the cursor to get the value and direction. + Cursor cursor = ResourceManagerUtil.processCursor(cursorString); + + // Resolve sorting order. + sortOrder = resolveSortOrder(sortOrder, sortBy); + + // Unless configured returns core-user schema or else returns extended user schema). + SCIMResourceTypeSchema schema = getSchema(userManager); + + // Build node for filtering. + Node rootNode = buildNode(filter, schema); + + // Obtain the json encoder. + JSONEncoder encoder = getEncoder(); + + // Get the URIs of required attributes which must be given a value + Map requiredAttributes = ResourceManagerUtil + .getOnlyRequiredAttributesURIs((SCIMResourceTypeSchema) CopyUtil.deepCopy(schema), attributes, + excludeAttributes); + + // API user should pass a user manager to UserResourceEndpoint. + if (userManager != null) { + UsersGetResponse usersGetResponse = userManager + .listUsersWithGET(rootNode, cursor, count, sortBy, sortOrder, domainName, requiredAttributes); + return processUserList(usersGetResponse, encoder, schema, attributes, excludeAttributes, null, + cursor, count); } else { String error = "Provided user manager handler is null."; // Log the error as well. @@ -424,20 +497,23 @@ private String resolveSortOrder(String sortOrder, String sortBy) throws BadReque /** * Method to process a user list and return a SCIM response. * - * @param usersGetResponse Filtered user list and total user count. + * @param usersGetResponse UsersGetResponse type object with the users, total count and cursor values. * @param encoder Json encoder * @param schema Schema * @param attributes Required attributes * @param excludeAttributes Exclude attributes * @param startIndex Starting index + * @param cursor Cursor for pagination and Pagination direction. + * @param limit Page size. * @return SCIM response * @throws NotFoundException * @throws CharonException * @throws BadRequestException */ private SCIMResponse processUserList(UsersGetResponse usersGetResponse, JSONEncoder encoder, - SCIMResourceTypeSchema schema, String attributes, String excludeAttributes, int startIndex) - throws NotFoundException, CharonException, BadRequestException { + SCIMResourceTypeSchema schema, + String attributes, String excludeAttributes, Integer startIndex, Cursor cursor, + Integer limit) throws NotFoundException, CharonException, BadRequestException { if (usersGetResponse == null) { usersGetResponse = new UsersGetResponse(0, Collections.emptyList()); @@ -450,7 +526,8 @@ private SCIMResponse processUserList(UsersGetResponse usersGetResponse, JSONEnco ServerSideValidator.validateRetrievedSCIMObjectInList(user, schema, attributes, excludeAttributes); } // Create a listed resource object out of the returned users list. - ListedResource listedResource = createListedResource(usersGetResponse, startIndex); + ListedResource listedResource = createListedResource(usersGetResponse, startIndex, + cursor != null ? cursor.getCursorValue() : null, limit); // Convert the listed resource into specific format. String encodedListedResource = encoder.encodeSCIMObject(listedResource); // If there are any http headers to be added in the response header. @@ -481,7 +558,9 @@ public SCIMResponse listWithPOST(String resourceString, UserManager userManager) SearchRequest searchRequest = decoder.decodeSearchRequestBody(resourceString, schema); searchRequest.setCount(ResourceManagerUtil.processCount(searchRequest.getCountStr())); - searchRequest.setStartIndex(ResourceManagerUtil.processStartIndex(searchRequest.getStartIndexStr())); + if (searchRequest.getCursor() == null) { + searchRequest.setStartIndex(ResourceManagerUtil.processStartIndex(searchRequest.getStartIndexStr())); + } //check whether provided sortOrder is valid or not if (searchRequest.getSortOder() != null) { @@ -514,7 +593,11 @@ public SCIMResponse listWithPOST(String resourceString, UserManager userManager) } //create a listed resource object out of the returned users list. ListedResource listedResource = createListedResource( - usersGetResponse, searchRequest.getStartIndex()); + // If no cursor and no startIndex are provided, the startIndex will be set to 1. + // If no count is provided, the count will be set to the server pagination limit. + usersGetResponse, searchRequest.getStartIndex() > 0 ? searchRequest.getStartIndex() : null, + searchRequest.getCursor() != null ? searchRequest.getCursor().getCursorValue() : null, + searchRequest.getCount()); //cursor != null ? cursor.getCursorValue() : null //convert the listed resource into specific format. String encodedListedResource = encoder.encodeSCIMObject(listedResource); //if there are any http headers to be added in the response header. @@ -753,26 +836,79 @@ public SCIMResponse updateWithPATCH(String existingId, String scimObjectString, /* * Creates the Listed Resource. * - * @param usersGetResponse - * @param startIndex + * @param users List of users retrieved from the user store. + * @param startIndex Offset. + * @param totalResults Total results retrieved. + * @param cursor Cursor value used for cursor pagination. + * @param direction Direction of cursor pagination. * @return * @throws CharonException * @throws NotFoundException */ - protected ListedResource createListedResource(UsersGetResponse usersGetResponse, int startIndex) + protected ListedResource createListedResource(UsersGetResponse usersGetResponse, Integer startIndex, String cursor, + Integer limit) throws CharonException, NotFoundException { + ListedResource listedResource = new ListedResource(); listedResource.setSchema(SCIMConstants.LISTED_RESOURCE_CORE_SCHEMA_URI); - listedResource.setTotalResults(usersGetResponse.getTotalUsers()); - listedResource.setStartIndex(startIndex); listedResource.setItemsPerPage(usersGetResponse.getUsers().size()); + listedResource.setTotalResults(usersGetResponse.getTotalUsers()); for (User user : usersGetResponse.getUsers()) { Map userAttributes = user.getAttributeList(); listedResource.setResources(userAttributes); } + //Set startIndex if offset pagination is requested. + if (cursor == null) { + listedResource.setStartIndex(startIndex); + } else { //Set nextCursor and previousCursor if cursor pagination is requested. + String prevCursor = StringUtils.EMPTY; + String nextCursor = StringUtils.EMPTY; + if (!(usersGetResponse.getUsers().isEmpty())) { + //Setting the previous cursor. + prevCursor = encodePreviousCursor(usersGetResponse.getPrevCursor()); + //Setting the next cursor. + int nextCursorLoc = usersGetResponse.getUsers().size() - 1; + if (nextCursorLoc != limit - 1) { + nextCursor = StringUtils.EMPTY; + } else { + nextCursor = encodeNextCursor(usersGetResponse.getNextCursor()); + } + } + //If it's the very first page, then don't show a previous cursor. + if (cursor.isEmpty()) { + listedResource.setNextCursor(nextCursor); + } else { + listedResource.setPreviousCursor(prevCursor); + listedResource.setNextCursor(nextCursor); + } + } return listedResource; } + private String encodePreviousCursor(String plainPreviousCursor) { + + JSONObject previousCursorJSONObject = new JSONObject(); + previousCursorJSONObject.put(SCIMConstants.ListedResourceSchemaConstants.DIRECTION, + SCIMConstants.ListedResourceSchemaConstants.PREVIOUS); + previousCursorJSONObject.put(SCIMConstants.ListedResourceSchemaConstants.VALUE, + plainPreviousCursor); + //Encoding the JSONObject (which has the cursor value and direction in Base64 as a String) + byte[] prevCursorBytes = previousCursorJSONObject.toString().getBytes(StandardCharsets.UTF_8); + return Base64.getEncoder().withoutPadding().encodeToString(prevCursorBytes); + } + + private String encodeNextCursor(String plainNextCursor) { + + JSONObject nextCursorJSONObject = new JSONObject(); + nextCursorJSONObject.put(SCIMConstants.ListedResourceSchemaConstants.DIRECTION, + SCIMConstants.ListedResourceSchemaConstants.NEXT); + nextCursorJSONObject.put(SCIMConstants.ListedResourceSchemaConstants.VALUE, + plainNextCursor); + //Encoding the JSONObject (which has the cursor value and direction in Base64 as a String) + byte[] nextCursorBytes = nextCursorJSONObject.toString().getBytes(StandardCharsets.UTF_8); + return Base64.getEncoder().withoutPadding().encodeToString(nextCursorBytes); + } + private SCIMResourceTypeSchema getSchema(UserManager userManager) throws BadRequestException, NotImplementedException, CharonException { diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMConstants.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMConstants.java index 78c225eb7..8eb69ea4b 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMConstants.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMConstants.java @@ -69,6 +69,12 @@ public class SCIMConstants { public static final String DEFAULT = "default"; + //Cursor Pagination types and other + public static final String OFFSET = "offset"; + public static final String CURSOR = "cursor"; + public static final String VALUE = "value"; + public static final String DIRECTION = "direction"; + public static final String NEXT = "NEXT"; /** * Constants found in core-common schema. @@ -130,6 +136,12 @@ public static class ListedResourceSchemaConstants { public static final String RESOURCES = "Resources"; public static final String ITEMS_PER_PAGE = "itemsPerPage"; public static final String START_INDEX = "startIndex"; + public static final String NEXT_CURSOR = "nextCursor"; + public static final String PREVIOUS_CURSOR = "previousCursor"; + public static final String PREVIOUS = "PREVIOUS"; + public static final String NEXT = "NEXT"; + public static final String DIRECTION = "direction"; + public static final String VALUE = "value"; } /** @@ -620,6 +632,12 @@ public static class ServiceProviderConfigSchemaConstants { "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig:etag.supported"; public static final String AUTHENTICATION_SCHEMAS_DOCUMENTATION_URI_URI = "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig:authenticationSchemes.documentationUri"; + public static final String PAGINATION = "pagination"; + public static final String PAGINATION_URI = + "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig:pagination"; + public static final String CURSOR = "cursor"; + public static final String CURSOR_URI = + "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig:pagination.cursor"; /*******Attributes descriptions of the attributes found in Service Provider Config Schema.***************/ @@ -627,6 +645,8 @@ public static class ServiceProviderConfigSchemaConstants { public static final String DOCUMENTATION_URI_DESC = "An HTTP-addressable URL pointing to the service " + "provider's human-consumable help documentation."; public static final String PATCH_DESC = "A complex type that specifies PATCH configuration options."; + public static final String PAGINATION_DESC = "A complex type which specifies the pagination types available."; + public static final String CURSOR_DESC = "A boolean which specifies if cursor pagination is supported"; public static final String BULK_DESC = "A complex type that specifies bulk configuration options."; public static final String FILTERS_DESC = "A complex type that specifies FILTER options."; public static final String CHANGE_PASSWORD_DESC = "A complex type that specifies configuration options " + @@ -755,6 +775,7 @@ public static class OperationalConstants { public static final String EXCLUDED_ATTRIBUTES = "excludedAttributes"; public static final String COUNT = "count"; public static final String START_INDEX = "startIndex"; + public static final String CURSOR = "cursor"; public static final String SORT_BY = "sortBy"; public static final String SORT_ORDER = "sortOrder"; public static final String FILTER = "filter"; diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMSchemaDefinitions.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMSchemaDefinitions.java index b6609655f..475bf9910 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMSchemaDefinitions.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMSchemaDefinitions.java @@ -1216,6 +1216,15 @@ public static class SCIMServiceProviderConfigSchemaDefinition { .EXTERNAL)), null); + public static final SCIMAttributeSchema CURSOR_PAGINATION = + SCIMAttributeSchema.createSCIMAttributeSchema( + SCIMConstants.ServiceProviderConfigSchemaConstants.CURSOR_URI, + SCIMConstants.ServiceProviderConfigSchemaConstants.CURSOR, + SCIMDefinitions.DataType.BOOLEAN, false, + SCIMConstants.ServiceProviderConfigSchemaConstants.CURSOR_DESC, true, false, + SCIMDefinitions.Mutability.READ_ONLY, SCIMDefinitions.Returned.DEFAULT, + SCIMDefinitions.Uniqueness.NONE, null, null, null); + public static final SCIMAttributeSchema TYPE = SCIMAttributeSchema.createSCIMAttributeSchema(SCIMConstants.ServiceProviderConfigSchemaConstants .TYPE_URL, @@ -1319,7 +1328,15 @@ public static class SCIMServiceProviderConfigSchemaDefinition { new ArrayList(Arrays.asList(NAME, DESCRIPTION, SPEC_URI, AUTHENTICATION_SCHEMES_DOCUMENTATION_URI, TYPE, PRIMARY))); - + public static final SCIMAttributeSchema PAGINATION = + SCIMAttributeSchema.createSCIMAttributeSchema( + SCIMConstants.ServiceProviderConfigSchemaConstants.PAGINATION_URI, + SCIMConstants.ServiceProviderConfigSchemaConstants.PAGINATION, + SCIMDefinitions.DataType.COMPLEX, false, + SCIMConstants.ServiceProviderConfigSchemaConstants.PAGINATION_DESC, true, false, + SCIMDefinitions.Mutability.READ_ONLY, SCIMDefinitions.Returned.DEFAULT, + SCIMDefinitions.Uniqueness.NONE, null, null, + new ArrayList(Arrays.asList(CURSOR_PAGINATION))); } /** @@ -1490,6 +1507,7 @@ public static class SCIMResourceTypeSchemaDefinition { SCIMServiceProviderConfigSchemaDefinition.SORT, SCIMServiceProviderConfigSchemaDefinition.FILTER, SCIMServiceProviderConfigSchemaDefinition.CHANGE_PASSWORD, + SCIMServiceProviderConfigSchemaDefinition.PAGINATION, SCIMServiceProviderConfigSchemaDefinition.ETAG, SCIMServiceProviderConfigSchemaDefinition.AUTHENTICATION_SCHEMES); diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/utils/ResourceManagerUtil.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/utils/ResourceManagerUtil.java index e09c2402d..10292a43d 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/utils/ResourceManagerUtil.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/utils/ResourceManagerUtil.java @@ -16,15 +16,21 @@ package org.wso2.charon3.core.utils; +import org.apache.commons.lang.StringUtils; +import org.json.JSONObject; import org.wso2.charon3.core.config.CharonConfiguration; import org.wso2.charon3.core.exceptions.BadRequestException; import org.wso2.charon3.core.exceptions.CharonException; +import org.wso2.charon3.core.objects.plainobjects.Cursor; import org.wso2.charon3.core.schema.AttributeSchema; +import org.wso2.charon3.core.schema.SCIMConstants; import org.wso2.charon3.core.schema.SCIMDefinitions; import org.wso2.charon3.core.schema.SCIMResourceTypeSchema; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -462,6 +468,46 @@ public static Integer processStartIndex(Integer startIndex) { } } + /** + * Process the cursor value. + * + * @param cursor Cursor value for pagination. + * @return String as the cursor. + */ + public static Cursor processCursor(String cursor) { + + //When using cursor pagination and the cursor is "", it means it is the first request. Therefore, cursor = "". + if (StringUtils.isEmpty(cursor)) { + return new Cursor(StringUtils.EMPTY, SCIMConstants.NEXT); + } else { + //Decode the base 64 encoded string and create a JSON object containing the cursor and the direction. + Base64.Decoder decoder = Base64.getDecoder(); + byte[] cursorBytes = decoder.decode(cursor.getBytes(StandardCharsets.UTF_8)); + String cursorString = new String(cursorBytes, StandardCharsets.UTF_8); + JSONObject jsonCursor = new JSONObject(cursorString); + return new Cursor(jsonCursor.getString(SCIMConstants.VALUE), jsonCursor.getString(SCIMConstants.DIRECTION)); + } + } + + /** + * Identify the type of pagination being used. + * + * @param startIndex Starting index in the request. + * @param cursor Cursor value used for cursor pagination. + * @return String of the type of pagination. + */ + public static String processPagination(Integer startIndex, String cursor) + throws CharonException { + + if (startIndex != null && cursor != null) { + throw new CharonException("Select One Type of Pagination (Offset or Cursor). Not both."); + } else if (cursor != null) { + return SCIMConstants.CURSOR; + } else { + return SCIMConstants.OFFSET; + } + } + /** * Process startIndex value according to SCIM 2.0 specification * @param startIndexStr diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/utils/codeutils/SearchRequest.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/utils/codeutils/SearchRequest.java index e5b587f68..144f6acc5 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/utils/codeutils/SearchRequest.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/utils/codeutils/SearchRequest.java @@ -16,7 +16,14 @@ package org.wso2.charon3.core.utils.codeutils; +import org.apache.commons.lang.StringUtils; +import org.json.JSONObject; +import org.wso2.charon3.core.objects.plainobjects.Cursor; +import org.wso2.charon3.core.schema.SCIMConstants; + +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Base64; /** * this corresponds to the /.search request object @@ -40,6 +47,7 @@ public class SearchRequest { private String sortBy; private String sortOder; private String domainName; + private Cursor cursor; public String getCountStr() { return countStr; @@ -57,6 +65,28 @@ public void setStartIndexStr(String startIndexStr) { this.startIndexStr = startIndexStr; } + public Cursor getCursor() { + return cursor; + } + + public void setCursor(String cursor) { + + Cursor tempCursor = new Cursor(null, null); + //When using cursor pagination and the cursor is null, it means it is the first request so cursor = "". + if (StringUtils.isEmpty(cursor)) { + tempCursor.setCursorValue(StringUtils.EMPTY); + tempCursor.setDirection(SCIMConstants.NEXT); + } else { + Base64.Decoder decoder = Base64.getDecoder(); + byte[] cursorBytes = decoder.decode(cursor.getBytes(StandardCharsets.UTF_8)); + String cursorString = new String(cursorBytes, StandardCharsets.UTF_8); + JSONObject jsonCursor = new JSONObject(cursorString); + tempCursor.setCursorValue(jsonCursor.getString(SCIMConstants.VALUE)); + tempCursor.setDirection(jsonCursor.getString(SCIMConstants.DIRECTION)); + } + this.cursor = tempCursor; + } + public String getSchema() { return schema; } diff --git a/modules/charon-core/src/test/java/org/wso2/charon3/core/protocol/endpoints/UserResourceManagerTest.java b/modules/charon-core/src/test/java/org/wso2/charon3/core/protocol/endpoints/UserResourceManagerTest.java index 5fa532e0b..fb9d2b64e 100644 --- a/modules/charon-core/src/test/java/org/wso2/charon3/core/protocol/endpoints/UserResourceManagerTest.java +++ b/modules/charon-core/src/test/java/org/wso2/charon3/core/protocol/endpoints/UserResourceManagerTest.java @@ -37,7 +37,9 @@ import org.wso2.charon3.core.exceptions.NotFoundException; import org.wso2.charon3.core.exceptions.NotImplementedException; import org.wso2.charon3.core.extensions.UserManager; +import org.wso2.charon3.core.objects.ListedResource; import org.wso2.charon3.core.objects.User; +import org.wso2.charon3.core.objects.plainobjects.Cursor; import org.wso2.charon3.core.objects.plainobjects.UsersGetResponse; import org.wso2.charon3.core.protocol.ResponseCodeConstants; import org.wso2.charon3.core.protocol.SCIMResponse; @@ -55,8 +57,10 @@ import java.util.Map; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -575,6 +579,62 @@ public void testListWithGetInteger(String filter, Integer startIndexInt, Assert.assertEquals(outputScimResponse.getResponseStatus(), ResponseCodeConstants.CODE_OK); } + @DataProvider(name = "dataForListWithGetCursor") + public Object[][] dataToGetListCursor() throws BadRequestException, CharonException, InternalErrorException { + + User user = getNewUser(); + List users = new ArrayList<>(); + users.add(user); + UsersGetResponse userResponse = new UsersGetResponse(1, "Adam_123", "Bob_456", users); + UsersGetResponse userResponse2 = new UsersGetResponse(1, users); + + return new Object[][]{ + {null, "eyJ2YWx1ZSI6IkFjaGFsYV8xMzEiLCJkaXJlY3Rpb24iOiJQUkVWSU9VUyJ9", 2, null, null, DOMAIN_NAME, + "emails", null, userResponse}, + {"userName sw A", "eyJ2YWx1ZSI6IkFjaGFsYV8xMzEiLCJkaXJlY3Rpb24iOiJQUkVWSU9VUyJ9", 10, null, null, + DOMAIN_NAME, "userName", null, userResponse2} + }; + } + + @Test(dataProvider = "dataForListWithGetCursor") + public void testListWithGetCursor(String filter, String cursorString, + Integer count, String sortBy, String sortOrder, String domainName, + String attributes, String excludeAttributes, UsersGetResponse usersResponse) + throws NotImplementedException, BadRequestException, CharonException { + + Mockito.when(userManager.listUsersWithGET(any(), any(Cursor.class), anyInt(), anyString(), anyString(), + anyString(), anyMap())).thenReturn(usersResponse); + SCIMResponse outputScimResponse = userResourceManager.listWithGET(userManager, filter, cursorString, + count, sortBy, sortOrder, domainName, attributes, excludeAttributes); + + Assert.assertEquals(outputScimResponse.getResponseStatus(), ResponseCodeConstants.CODE_OK); + } + + @DataProvider(name = "dataForTestCreateListedResource") + public Object[][] dataToTestCreateListedResource() + throws BadRequestException, CharonException, InternalErrorException { + + User user = getNewUser(); + List users = new ArrayList<>(); + users.add(user); + UsersGetResponse userResponse = new UsersGetResponse(1, "Adam_123", "Bob_456", users); + UsersGetResponse userResponse2 = new UsersGetResponse(1, users); + return new Object[][]{ + {userResponse, null, "Jake_5", 5}, + {userResponse2, 1, null, 5}, + {userResponse, null, "", 100} + }; + } + + @Test(dataProvider = "dataForTestCreateListedResource") + public void testCreateListedResource(UsersGetResponse usersGetResponse, Integer startIndex, String cursor, + Integer limit) throws NotFoundException, CharonException { + + ListedResource listedResource = userResourceManager.createListedResource(usersGetResponse, startIndex, + cursor, limit); + Assert.assertEquals(listedResource.getResources().size(), 1); + } + @DataProvider(name = "dataForTestCreateUserSuccess") public Object[][] dataToTestCreateUserSuccess() { diff --git a/modules/charon-core/src/test/java/org/wso2/charon3/core/utils/ResourceManagerUtilTest.java b/modules/charon-core/src/test/java/org/wso2/charon3/core/utils/ResourceManagerUtilTest.java index f6d688ab2..a772d6630 100644 --- a/modules/charon-core/src/test/java/org/wso2/charon3/core/utils/ResourceManagerUtilTest.java +++ b/modules/charon-core/src/test/java/org/wso2/charon3/core/utils/ResourceManagerUtilTest.java @@ -23,6 +23,7 @@ import org.testng.annotations.Test; import org.wso2.charon3.core.exceptions.BadRequestException; import org.wso2.charon3.core.exceptions.CharonException; +import org.wso2.charon3.core.objects.plainobjects.Cursor; import org.wso2.charon3.core.schema.AttributeSchema; import org.wso2.charon3.core.schema.SCIMAttributeSchema; import org.wso2.charon3.core.schema.SCIMResourceTypeSchema; @@ -265,6 +266,67 @@ public void testProcessStartIndex(Integer startIndex, Integer expectedIndex) { Assert.assertEquals(index, expectedIndex); } + @DataProvider(name = "dataForProcessCursor") + public Object[][] dataToProcessCursor() { + + return new Object[][]{ + + {"eyJ2YWx1ZSI6IkFjaGFsYV8xMzEiLCJkaXJlY3Rpb24iOiJQUkVWSU9VUyJ9", "Achala_131", "PREVIOUS"}, + {"", "" , "NEXT"}, + {null, "", "NEXT"} + }; + } + + @Test(dataProvider = "dataForProcessCursor") + public void testProcessCursor(String cursorStr, String expectedCursor, String expectedDirection) { + + Cursor cursor = ResourceManagerUtil.processCursor(cursorStr); + Assert.assertEquals(cursor.getCursorValue(), expectedCursor); + Assert.assertEquals(cursor.getDirection(), expectedDirection); + } + + @DataProvider(name = "dataForProcessPagination") + public Object[][] dataToProcessPagination() { + + return new Object[][]{ + + {null, "eyJ2YWx1ZSI6IkFjaGFsYV8xMzEiLCJkaXJlY3Rpb24iOiJQUkVWSU9VUyJ9", "cursor"}, + {null, "eyJ2YWx1ZSI6IkFjaGFsYV8xMzEiLCJkaXJlY3Rpb24iOiJQUkVWSU9VUyJ9", "cursor"}, + {0, null, "offset"}, + {null, null, "offset"}, + {10, null, "offset"}, + {-5, null, "offset"}, + }; + } + + @Test(dataProvider = "dataForProcessPagination") + public void testProcessPagination(Integer startIndex, String cursorString, String expectedResult) + throws CharonException { + + String paginationType = ResourceManagerUtil.processPagination(startIndex, cursorString); + Assert.assertEquals(paginationType, expectedResult); + } + + @DataProvider(name = "dataForProcessPaginationException") + public Object[][] dataToProcessPaginationException() { + + return new Object[][]{ + + {0, "eyJ2YWx1ZSI6IkFjaGFsYV8xMzEiLCJkaXJlY3Rpb24iOiJQUkVWSU9VUyJ9"}, + {10, "eyJ2YWx1ZSI6IkFjaGFsYV8xMzEiLCJkaXJlY3Rpb24iOiJQUkVWSU9VUyJ9"}, + {-3, "eyJ2YWx1ZSI6IkFjaGFsYV8xMzEiLCJkaXJlY3Rpb24iOiJQUkVWSU9VUyJ9"}, + }; + } + + @Test(dataProvider = "dataForProcessPaginationException", expectedExceptions = CharonException.class) + public void testProcessPaginationException(Integer startIndex, String cursorString) + throws CharonException { + + ResourceManagerUtil.processPagination(startIndex, cursorString); + // This method is for testing of throwing CharonException when both cursor and offset are given, + // hence no assertion. + } + @DataProvider(name = "dataForProcessStartIndexString") public Object[][] dataToProcessStartIndexString() { diff --git a/modules/charon-utils/src/main/java/org/wso2/charon3/utils/usermanager/InMemoryUserManager.java b/modules/charon-utils/src/main/java/org/wso2/charon3/utils/usermanager/InMemoryUserManager.java index 8db2ec714..026be5a86 100644 --- a/modules/charon-utils/src/main/java/org/wso2/charon3/utils/usermanager/InMemoryUserManager.java +++ b/modules/charon-utils/src/main/java/org/wso2/charon3/utils/usermanager/InMemoryUserManager.java @@ -83,9 +83,10 @@ public void deleteUser(String id) } @Override - public UsersGetResponse listUsersWithGET(Node rootNode, int startIndex, int count, String sortBy, - String sortOrder, String domainName, Map requiredAttributes) + public UsersGetResponse listUsersWithGET(Node rootNode, int startIndex, int count, String sortBy, String sortOrder, + String domainName, Map requiredAttributes) throws CharonException, NotImplementedException, BadRequestException { + if (sortBy != null || sortOrder != null) { throw new NotImplementedException("Sorting is not supported"); } else if (startIndex != 1) {