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) {