From 0d32b804b99a0801ac128c334985cc9b51ad5c7f Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:37:51 -0400 Subject: [PATCH 01/13] Adding filtering and pagination to collectionList APIs --- ...3-collection-list-search-and-pagination.md | 7 ++ doc/sphinx-guides/source/api/native-api.rst | 14 ++++ .../iq/dataverse/PermissionServiceBean.java | 71 +++++++++++-------- .../iq/dataverse/api/AbstractApiBean.java | 14 ++++ .../edu/harvard/iq/dataverse/api/Users.java | 12 +++- .../GetUserPermittedCollectionsCommand.java | 10 ++- .../iq/dataverse/mydata/DataRetrieverAPI.java | 12 +++- .../harvard/iq/dataverse/mydata/Pager.java | 3 +- .../iq/dataverse/util/json/JsonPrinter.java | 18 ++++- .../iq/dataverse/api/DataRetrieverApiIT.java | 14 ++-- .../edu/harvard/iq/dataverse/api/UtilIT.java | 10 +++ 11 files changed, 142 insertions(+), 43 deletions(-) create mode 100644 doc/release-notes/12423-collection-list-search-and-pagination.md diff --git a/doc/release-notes/12423-collection-list-search-and-pagination.md b/doc/release-notes/12423-collection-list-search-and-pagination.md new file mode 100644 index 00000000000..2634a78aff4 --- /dev/null +++ b/doc/release-notes/12423-collection-list-search-and-pagination.md @@ -0,0 +1,7 @@ +## Feature ## + +Updated the following APIs to add ability to filter using `searchTerm` and added optional pagination parameters `offset` and `pageSize` to limit the results with each GET. + +GET `/api/users/$USERNAME/allowedCollections/$PERMISSION?pageSize=10&offset=0&searchTerm=bio` + +GET `/api/mydata/retrieve/collectionList?userIdentifier=anotherUser&pageSize=10&offset=11&searchTerm=bio` diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index eaff1c77057..659092860f3 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -8659,6 +8659,12 @@ The ``$identifier`` is the username of the requested user. The ``$permission`` is the permission (tied to the roles) that gives the user access to the collection. Passing ``$permission`` as 'any' will return the collection as long as the user has any access/permission on the collection +**For filtering and pagination these query parameters can be used:** + +- ``searchTerm``: To filter the results. +- ``offset``: Starting row ('nextOffset' or 'prevOffset' from the Json output can be used to get the next or previous page). +- ``pageSize``: Number of items to limit the output. + .. code-block:: bash export SERVER_URL=https://demo.dataverse.org @@ -8666,6 +8672,7 @@ Passing ``$permission`` as 'any' will return the collection as long as the user export PERMISSION=PublishDataverse curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/users/$USERNAME/allowedCollections/$PERMISSION" + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/users/$USERNAME/allowedCollections/$PERMISSION?pageSize=10&offset=0&searchTerm=bio" Show Role Assignee ~~~~~~~~~~~~~~~~~~ @@ -9253,6 +9260,12 @@ MyData Collection List The MyData Collection List API is used to get a list of the collections an authenticated user can create a Dataset in. Param userIdentifier={userName} is used by a superuser to get the collections for a specific user. +**For filtering and pagination these query parameters can be used:** + +- ``searchTerm``: To filter the results. +- ``offset``: Starting row ('nextOffset' or 'prevOffset' from the Json output can be used to get the next or previous page). +- ``pageSize``: Number of items to limit the output. + A curl example listing collections: .. code-block:: bash @@ -9262,4 +9275,5 @@ A curl example listing collections: curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/mydata/retrieve/collectionList" curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/mydata/retrieve/collectionList?userIdentifier=anotherUser" + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/mydata/retrieve/collectionList?userIdentifier=anotherUser&pageSize=10&offset=11&searchTerm=bio" diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index bd91363d2bb..7ae7c5e40fa 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -1,11 +1,9 @@ package edu.harvard.iq.dataverse; -import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IPv4Address; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IPv6Address; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; -import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.RoleAssignee; @@ -16,6 +14,8 @@ import java.util.*; import java.util.logging.Logger; + +import edu.harvard.iq.dataverse.mydata.Pager; import jakarta.ejb.EJB; import jakarta.ejb.Stateless; import jakarta.inject.Inject; @@ -59,20 +59,11 @@ public class PermissionServiceBean { .filter(Permission::requiresAuthenticatedUser) .collect(Collectors.toList())); - @EJB - BuiltinUserServiceBean userService; - - @EJB - AuthenticationServiceBean authenticationService; - @EJB DataverseRoleServiceBean roleService; @EJB RoleAssigneeServiceBean roleAssigneeService; - - @EJB - DataverseServiceBean dataverseService; @EJB DvObjectServiceBean dvObjectServiceBean; @@ -86,9 +77,6 @@ public class PermissionServiceBean { @EJB GroupServiceBean groupService; - @Inject - DataverseSession session; - @Inject DataverseRequestServiceBean dvRequestService; @@ -96,7 +84,7 @@ public class PermissionServiceBean { DatasetVersionFilesServiceBean datasetVersionFilesServiceBean; private static final String LIST_ALL_DATAVERSES_SUPERUSER_HAS_PERMISSION = """ - SELECT id, name, alias FROM DATAVERSE dv + SELECT id, name, alias, ROW_NUMBER() OVER (ORDER BY id) AS row_num FROM DATAVERSE dv """; private static final String LIST_ALL_DATAVERSES_USER_HAS_PERMISSION = """ @@ -104,8 +92,8 @@ WITH grouplist AS ( SELECT explicitgroup_authenticateduser.explicitgroup_id as id FROM explicitgroup_authenticateduser WHERE explicitgroup_authenticateduser.containedauthenticatedusers_id = @USERID ) - - SELECT * FROM DATAVERSE dv WHERE id IN ( + + SELECT *, ROW_NUMBER() OVER (ORDER BY id) AS row_num FROM DATAVERSE dv WHERE id IN ( SELECT definitionpoint_id FROM roleassignment WHERE roleassignment.assigneeidentifier IN ( @@ -179,6 +167,11 @@ AND EXISTS (SELECT id FROM dataverserole WHERE dataverserole.id = roleassignment or (LOWER(dv.name) LIKE ?)))) """; + private static final String PAGE_PARAMS = """ + select * from ( @SQL@ ) + where row_num BETWEEN @START AND @END + """; + /** * A request-level permission query (e.g includes IP ras). */ @@ -939,33 +932,51 @@ private boolean hasUnrestrictedReleasedFiles(DatasetVersion targetDatasetVersion } public List findPermittedCollections(DataverseRequest request, AuthenticatedUser user, Permission permission) { - return findPermittedCollections(request, user, 1 << permission.ordinal(), ""); + return findPermittedCollections(request, user, 1 << permission.ordinal(), "", null); } public List findPermittedCollections(DataverseRequest request, AuthenticatedUser user, Permission permission, String searchTerm) { - return findPermittedCollections(request, user, 1 << permission.ordinal(), searchTerm); + return findPermittedCollections(request, user, 1 << permission.ordinal(), searchTerm, null); } public List findPermittedCollections(DataverseRequest request, AuthenticatedUser user, int permissionBit) { - return findPermittedCollections(request, user, permissionBit, ""); + return findPermittedCollections(request, user, permissionBit, "", null); } - public List findPermittedCollections(DataverseRequest request, AuthenticatedUser user, int permissionBit, String searchTerm) { + public List findPermittedCollections(DataverseRequest request, AuthenticatedUser user, int permissionBit, String searchTerm, Pager pager) { if (user != null) { + List dataverses = new ArrayList<>(); var sqlCode = getBaseQueryForAllPermittedDataverses(request, user, permissionBit); - if (searchTerm == null || searchTerm.isEmpty()) { - return em.createNativeQuery(sqlCode, Dataverse.class).getResultList(); - } else if (user.isSuperuser()) { - Query query = em.createNativeQuery(sqlCode.concat(WHERE).concat(SEARCH_PARAMS), Dataverse.class); - setSearchParamValues(searchTerm, query); - return query.getResultList(); + if (searchTerm != null && !searchTerm.isEmpty()) { + if (user.isSuperuser()) { + sqlCode = sqlCode.concat(WHERE).concat(SEARCH_PARAMS); + } else { + sqlCode = sqlCode.concat(AND).concat(SEARCH_PARAMS); + } } - else { - Query query = em.createNativeQuery(sqlCode.concat(AND).concat(SEARCH_PARAMS), Dataverse.class); + if (pager != null) { + // Add a pagination wrapper around the sqlCode + int pageSize = pager.getDocsPerPage(); + int pageStart = (pager.getSelectedPageNumber()-1) * pageSize + 1; + int pageEnd = pageStart + pageSize - 1; + sqlCode = PAGE_PARAMS + .replace("@START", String.valueOf(pageStart)) + .replace("@END", String.valueOf(pageEnd)) + .replace("@SQL@", sqlCode); + } + + Query query = em.createNativeQuery(sqlCode, Dataverse.class); + if (searchTerm != null && !searchTerm.isEmpty()) { setSearchParamValues(searchTerm, query); - return query.getResultList(); } + + List resultList = query.getResultList(); + if (pager != null) { + pager.setNumResults((pager.getSelectedPageNumber()-1) * pager.getDocsPerPage() + resultList.size()); + } + dataverses.addAll(resultList); + return dataverses; } return null; } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index a23e2a92c07..804362415ea 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -25,6 +25,7 @@ import edu.harvard.iq.dataverse.externaltools.ExternalToolServiceBean; import edu.harvard.iq.dataverse.license.LicenseServiceBean; import edu.harvard.iq.dataverse.makedatacount.DatasetMetricsServiceBean; +import edu.harvard.iq.dataverse.mydata.Pager; import edu.harvard.iq.dataverse.pidproviders.FailedPIDResolutionLoggingServiceBean; import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.pidproviders.FailedPIDResolutionLoggingServiceBean.FailedPIDResolutionEntry; @@ -299,6 +300,19 @@ protected boolean parseBooleanOrDie( String input ) throws WrappedResponse { } } + // Get a Pager object for adding pagination to a list result + protected Pager getPager(Integer pageSize, Integer start) { + if (pageSize != null || start != null) { + int maxPageSize = pageSize != null ? Math.max(pageSize, 10) : 10; + int offset = start != null ? start : 0; + int selectedPageNumber = offset / maxPageSize + 1; + // Since a new Pager is created for each API call we need to default some values to make things work + int numResults = selectedPageNumber * maxPageSize; + return new Pager(numResults, maxPageSize, selectedPageNumber); + } + return null; + } + /** * Returns the {@code key} query parameter from the current request, or {@code null} if * the request has no such parameter. diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Users.java b/src/main/java/edu/harvard/iq/dataverse/api/Users.java index af6b533d46d..2c2c26d3e37 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Users.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Users.java @@ -12,6 +12,7 @@ import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.engine.command.impl.*; +import edu.harvard.iq.dataverse.mydata.Pager; import edu.harvard.iq.dataverse.settings.FeatureFlags; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.FileUtil; @@ -276,7 +277,11 @@ public Response getTracesElement(@Context ContainerRequestContext crc, @Context @AuthRequired @Path("{identifier}/allowedCollections/{permission}") @Produces("application/json") - public Response getUserPermittedCollections(@Context ContainerRequestContext crc, @Context Request req, @PathParam("identifier") String identifier, @PathParam("permission") String permission) { + public Response getUserPermittedCollections(@Context ContainerRequestContext crc, @Context Request req, + @PathParam("identifier") String identifier, + @PathParam("permission") String permission, + @QueryParam("offset") Integer start, + @QueryParam("pageSize") Integer pageSize) { AuthenticatedUser authenticatedUser = null; try { authenticatedUser = getRequestAuthenticatedUserOrDie(crc); @@ -288,8 +293,9 @@ public Response getUserPermittedCollections(@Context ContainerRequestContext crc } try { AuthenticatedUser userToQuery = authSvc.getAuthenticatedUser(identifier); - List collections = execCommand(new GetUserPermittedCollectionsCommand(createDataverseRequest(getRequestUser(crc)), userToQuery, permission)); - return ok(JsonPrinter.jsonArray(collections)); + Pager pager = getPager(pageSize, start); + List collections = execCommand(new GetUserPermittedCollectionsCommand(createDataverseRequest(getRequestUser(crc)), userToQuery, permission, null, pager)); + return ok(JsonPrinter.jsonArray(collections, pager)); } catch (WrappedResponse ex) { return ex.getResponse(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetUserPermittedCollectionsCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetUserPermittedCollectionsCommand.java index 28a924bea92..0bb732e496b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetUserPermittedCollectionsCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetUserPermittedCollectionsCommand.java @@ -10,6 +10,7 @@ import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.InvalidCommandArgumentsException; +import edu.harvard.iq.dataverse.mydata.Pager; import edu.harvard.iq.dataverse.util.BundleUtil; import java.util.List; @@ -40,12 +41,19 @@ public class GetUserPermittedCollectionsCommand extends AbstractCommand execute(CommandContext ctxt) throws CommandException { } catch (IllegalArgumentException e) { throw new InvalidCommandArgumentsException(BundleUtil.getStringFromBundle("getUserPermittedCollectionsCommand.errors.permissionNotValid"), this); } - return ctxt.permissions().findPermittedCollections(request, user, permissionBit); + return ctxt.permissions().findPermittedCollections(request, user, permissionBit, searchTerm, pager); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java b/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java index bdc6961358e..fe000e822a9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java +++ b/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java @@ -327,11 +327,17 @@ private JsonObjectBuilder myDataAsJson(String message, Pager pager, RoleTagRetri @AuthRequired @Path(retrieveDataPartialAPIPath + "/collectionList") @Produces("application/json") - public Response retrieveMyCollectionList(@Context ContainerRequestContext crc, @QueryParam("userIdentifier") String userIdentifier) { + public Response retrieveMyCollectionList(@Context ContainerRequestContext crc, + @QueryParam("userIdentifier") String userIdentifier, + @QueryParam("searchTerm") String searchTerm, + @QueryParam("offset") Integer start, + @QueryParam("pageSize") Integer pageSize) { try { verifyAuth(crc, userIdentifier); - List collections = execCommand(new GetUserPermittedCollectionsCommand(createDataverseRequest(getRequestUser(crc)), searchUser, Permission.AddDataset.name())); - return ok(JsonPrinter.jsonArray(collections)); + Pager pager = getPager(pageSize, start); + List collections = execCommand(new GetUserPermittedCollectionsCommand(createDataverseRequest(getRequestUser(crc)), searchUser, Permission.AddDataset.name(), + searchTerm, pager)); + return ok(JsonPrinter.jsonArray(collections, pager)); } catch (WrappedResponse wr) { return wr.getResponse(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/mydata/Pager.java b/src/main/java/edu/harvard/iq/dataverse/mydata/Pager.java index 096974b9d72..863dd8d9990 100644 --- a/src/main/java/edu/harvard/iq/dataverse/mydata/Pager.java +++ b/src/main/java/edu/harvard/iq/dataverse/mydata/Pager.java @@ -155,6 +155,7 @@ public int getNumResults(){ */ public void setNumResults(int numResults){ this.numResults = numResults; + makePageStats(); } @@ -482,4 +483,4 @@ private void msgt(String s){ msg(s); msg("-------------------------------"); } -} \ No newline at end of file +} diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index 55247f7d2af..a890de6919b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -26,6 +26,7 @@ import edu.harvard.iq.dataverse.globus.FileDetailsHolder; import edu.harvard.iq.dataverse.harvest.client.HarvestingClient; import edu.harvard.iq.dataverse.license.License; +import edu.harvard.iq.dataverse.mydata.Pager; import edu.harvard.iq.dataverse.privateurl.PrivateUrl; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; @@ -385,8 +386,12 @@ public static JsonObjectBuilder json(Dataverse dv, Boolean hideEmail, Boolean re } public static JsonObjectBuilder jsonArray(List dataverses) { + return jsonArray(dataverses, null); + } + public static JsonObjectBuilder jsonArray(List dataverses, Pager pager) { JsonObjectBuilder job = Json.createObjectBuilder(); - job.add("count", dataverses.size()); + int count = dataverses.size(); + job.add("count", count); JsonArrayBuilder jsonArrayBuilder = Json.createArrayBuilder(); for (Dataverse dataverse : dataverses) { NullSafeJsonBuilder jsonObject = NullSafeJsonBuilder.jsonObjectBuilder(); @@ -396,6 +401,17 @@ public static JsonObjectBuilder jsonArray(List dataverses) { jsonArrayBuilder.add(jsonObject); } job.add("items", jsonArrayBuilder); + if (pager != null) { + job.add("pageSize", pager.getDocsPerPage()); + int nextOffset = pager.getSelectedPageNumber() * pager.getDocsPerPage() + 1; + int prevOffset = nextOffset - (2 * pager.getDocsPerPage()); + if (count >= pager.getDocsPerPage()) { + job.add("nextOffset", nextOffset); + } + if (prevOffset > 0) { + job.add("prevOffset", prevOffset); + } + } return job; } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataRetrieverApiIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataRetrieverApiIT.java index 10aa145698b..5865cd459d0 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataRetrieverApiIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataRetrieverApiIT.java @@ -167,12 +167,18 @@ public void testRetrieveMyDataCollections() throws InterruptedException { // Sleep for indexing Thread.sleep(4000); - // User1 gets the list of Dataverses/Collections it has access to - retrieveMyCollectionListResponse = UtilIT.retrieveMyCollectionList(User1ApiToken, null); + // User1 gets the list of Dataverses/Collections it has access to (with pagination) + // Get the first page + retrieveMyCollectionListResponse = UtilIT.retrieveMyCollectionList(User1ApiToken, null, null, 10); + retrieveMyCollectionListResponse.prettyPrint(); + int count = retrieveMyCollectionListResponse.getBody().jsonPath().getInt("data.count"); + // get the second page + int offset = retrieveMyCollectionListResponse.getBody().jsonPath().getInt("data.nextOffset"); + retrieveMyCollectionListResponse = UtilIT.retrieveMyCollectionList(User1ApiToken, null, offset, 10); retrieveMyCollectionListResponse.prettyPrint(); + count = count + retrieveMyCollectionListResponse.getBody().jsonPath().getInt("data.count"); // The count should show the list size to be User1's + Root Dataverse count - items = retrieveMyCollectionListResponse.getBody().jsonPath().getList("data.items"); - assertEquals(rootCount + user1DataverseCount, items.size()); + assertEquals(rootCount + user1DataverseCount, count); // User2 gets the list of Dataverses/Collections it has access to retrieveMyCollectionListResponse = UtilIT.retrieveMyCollectionList(User2ApiToken, null); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index b6f8d66c0af..e9cd8bd4d85 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -4394,6 +4394,10 @@ static Response retrieveMyDataAsJsonString(String apiToken, String userIdentifie } static Response retrieveMyCollectionList(String apiToken, String userIdentifier) { + return retrieveMyCollectionList(apiToken, userIdentifier, null, null); + } + + static Response retrieveMyCollectionList(String apiToken, String userIdentifier, Integer offset, Integer pageSize) { RequestSpecification requestSpecification = given(); if (apiToken != null) { requestSpecification.header(API_TOKEN_HTTP_HEADER, apiToken); @@ -4401,6 +4405,12 @@ static Response retrieveMyCollectionList(String apiToken, String userIdentifier) if (userIdentifier != null) { requestSpecification.queryParam("userIdentifier", userIdentifier); } + if (offset != null) { + requestSpecification.queryParam("offset", offset); + } + if (pageSize != null) { + requestSpecification.queryParam("pageSize", pageSize); + } return requestSpecification.get("/api/mydata/retrieve/collectionList"); } From 08ef2bb717e08d411bfa5ce0e5bf0d7440c8f1c8 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:13:47 -0400 Subject: [PATCH 02/13] fix GetUserPermittedCollectionsCommandTest --- ...etUserPermittedCollectionsCommandTest.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/GetUserPermittedCollectionsCommandTest.java b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/GetUserPermittedCollectionsCommandTest.java index 21b267be39c..34917c538c3 100644 --- a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/GetUserPermittedCollectionsCommandTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/GetUserPermittedCollectionsCommandTest.java @@ -46,7 +46,9 @@ public void execute_shouldReturnCollections_whenAnyPermissionIsRequested() throw Mockito.when(permissionsServiceBean.findPermittedCollections( Mockito.any(DataverseRequest.class), Mockito.any(AuthenticatedUser.class), - Mockito.eq(Integer.MAX_VALUE) + Mockito.eq(Integer.MAX_VALUE), + Mockito.any(), + Mockito.any() )).thenReturn(expectedDataverses); GetUserPermittedCollectionsCommand sut = new GetUserPermittedCollectionsCommand( @@ -64,7 +66,9 @@ public void execute_shouldReturnCollections_whenAnyPermissionIsRequested() throw Mockito.verify(permissionsServiceBean).findPermittedCollections( dataverseRequest, authenticatedUser, - Integer.MAX_VALUE + Integer.MAX_VALUE, + null, + null ); } @@ -77,13 +81,17 @@ public void execute_shouldReturnCollections_whenSpecificPermissionIsRequested() Mockito.when(permissionsServiceBean.findPermittedCollections( Mockito.any(DataverseRequest.class), Mockito.any(AuthenticatedUser.class), - Mockito.eq(1 << Permission.AddDataset.ordinal()) + Mockito.eq(1 << Permission.AddDataset.ordinal()), + Mockito.any(), + Mockito.any() )).thenReturn(expectedDataverses); GetUserPermittedCollectionsCommand sut = new GetUserPermittedCollectionsCommand( dataverseRequest, authenticatedUser, - Permission.AddDataset.name() + Permission.AddDataset.name(), + null, + null ); // Act @@ -95,7 +103,9 @@ public void execute_shouldReturnCollections_whenSpecificPermissionIsRequested() Mockito.verify(permissionsServiceBean).findPermittedCollections( dataverseRequest, authenticatedUser, - 1 << Permission.AddDataset.ordinal() + 1 << Permission.AddDataset.ordinal(), + null, + null ); } From 06587a5b548f7174f3aeee65230bf2714cbdb97e Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:04:09 -0400 Subject: [PATCH 03/13] refactor pagination --- .../iq/dataverse/PermissionServiceBean.java | 32 +++++++++++-------- .../iq/dataverse/api/AbstractApiBean.java | 14 -------- .../edu/harvard/iq/dataverse/api/Users.java | 7 ++-- .../iq/dataverse/api/util/Pagination.java | 28 ++++++++++++++++ .../GetUserPermittedCollectionsCommand.java | 10 +++--- .../iq/dataverse/mydata/DataRetrieverAPI.java | 7 ++-- .../harvard/iq/dataverse/mydata/Pager.java | 3 +- .../iq/dataverse/util/json/JsonPrinter.java | 17 +++------- .../iq/dataverse/api/DataRetrieverApiIT.java | 5 +-- 9 files changed, 68 insertions(+), 55 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/api/util/Pagination.java diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index 7ae7c5e40fa..61185acc7a8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -1,5 +1,6 @@ package edu.harvard.iq.dataverse; +import edu.harvard.iq.dataverse.api.util.Pagination; import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IPv4Address; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IPv6Address; @@ -15,7 +16,6 @@ import java.util.*; import java.util.logging.Logger; -import edu.harvard.iq.dataverse.mydata.Pager; import jakarta.ejb.EJB; import jakarta.ejb.Stateless; import jakarta.inject.Inject; @@ -172,6 +172,9 @@ AND EXISTS (SELECT id FROM dataverserole WHERE dataverserole.id = roleassignment where row_num BETWEEN @START AND @END """; + private static final String TOTAL_COUNT = """ + select count(*) from ( @SQL@ ) + """; /** * A request-level permission query (e.g includes IP ras). */ @@ -939,12 +942,7 @@ public List findPermittedCollections(DataverseRequest request, Authen return findPermittedCollections(request, user, 1 << permission.ordinal(), searchTerm, null); } - public List findPermittedCollections(DataverseRequest request, AuthenticatedUser user, int permissionBit) { - return findPermittedCollections(request, user, permissionBit, "", null); - } - - - public List findPermittedCollections(DataverseRequest request, AuthenticatedUser user, int permissionBit, String searchTerm, Pager pager) { + public List findPermittedCollections(DataverseRequest request, AuthenticatedUser user, int permissionBit, String searchTerm, Pagination pagination) { if (user != null) { List dataverses = new ArrayList<>(); var sqlCode = getBaseQueryForAllPermittedDataverses(request, user, permissionBit); @@ -955,11 +953,20 @@ public List findPermittedCollections(DataverseRequest request, Authen sqlCode = sqlCode.concat(AND).concat(SEARCH_PARAMS); } } - if (pager != null) { + if (pagination != null) { + if (pagination.getNumResults() < 0) { + // Get the total count + String sqlCodeCount = TOTAL_COUNT.replace("@SQL@", sqlCode); + Query queryCount = em.createNativeQuery(sqlCodeCount); + if (searchTerm != null && !searchTerm.isEmpty()) { + setSearchParamValues(searchTerm, queryCount); + } + Number countResult = (Number) queryCount.getSingleResult(); + pagination.setNumResults(countResult.intValue()); + } // Add a pagination wrapper around the sqlCode - int pageSize = pager.getDocsPerPage(); - int pageStart = (pager.getSelectedPageNumber()-1) * pageSize + 1; - int pageEnd = pageStart + pageSize - 1; + int pageStart = pagination.getOffset(); + int pageEnd = pageStart + pagination.getLimitPerPage(); sqlCode = PAGE_PARAMS .replace("@START", String.valueOf(pageStart)) .replace("@END", String.valueOf(pageEnd)) @@ -972,9 +979,6 @@ public List findPermittedCollections(DataverseRequest request, Authen } List resultList = query.getResultList(); - if (pager != null) { - pager.setNumResults((pager.getSelectedPageNumber()-1) * pager.getDocsPerPage() + resultList.size()); - } dataverses.addAll(resultList); return dataverses; } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index bb0fbe5d5c4..562e5bb2c6f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -25,7 +25,6 @@ import edu.harvard.iq.dataverse.externaltools.ExternalToolServiceBean; import edu.harvard.iq.dataverse.license.LicenseServiceBean; import edu.harvard.iq.dataverse.makedatacount.DatasetMetricsServiceBean; -import edu.harvard.iq.dataverse.mydata.Pager; import edu.harvard.iq.dataverse.pidproviders.FailedPIDResolutionLoggingServiceBean; import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.pidproviders.FailedPIDResolutionLoggingServiceBean.FailedPIDResolutionEntry; @@ -301,19 +300,6 @@ protected boolean parseBooleanOrDie( String input ) throws WrappedResponse { } } - // Get a Pager object for adding pagination to a list result - protected Pager getPager(Integer pageSize, Integer start) { - if (pageSize != null || start != null) { - int maxPageSize = pageSize != null ? Math.max(pageSize, 10) : 10; - int offset = start != null ? start : 0; - int selectedPageNumber = offset / maxPageSize + 1; - // Since a new Pager is created for each API call we need to default some values to make things work - int numResults = selectedPageNumber * maxPageSize; - return new Pager(numResults, maxPageSize, selectedPageNumber); - } - return null; - } - /** * Returns the {@code key} query parameter from the current request, or {@code null} if * the request has no such parameter. diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Users.java b/src/main/java/edu/harvard/iq/dataverse/api/Users.java index ea9e08fceb7..2e69cf2e0f1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Users.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Users.java @@ -7,6 +7,7 @@ import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.api.auth.AuthRequired; +import edu.harvard.iq.dataverse.api.util.Pagination; import edu.harvard.iq.dataverse.authorization.users.ApiToken; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; @@ -340,9 +341,9 @@ public Response getUserPermittedCollections(@Context ContainerRequestContext crc } try { AuthenticatedUser userToQuery = authSvc.getAuthenticatedUser(identifier); - Pager pager = getPager(pageSize, start); - List collections = execCommand(new GetUserPermittedCollectionsCommand(createDataverseRequest(getRequestUser(crc)), userToQuery, permission, null, pager)); - return ok(JsonPrinter.jsonArray(collections, pager)); + Pagination pagination = (start != null || pageSize != null) ? new Pagination(pageSize, start) : null; + List collections = execCommand(new GetUserPermittedCollectionsCommand(createDataverseRequest(getRequestUser(crc)), userToQuery, permission, null, pagination)); + return ok(JsonPrinter.jsonArray(collections, pagination)); } catch (WrappedResponse ex) { return ex.getResponse(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/util/Pagination.java b/src/main/java/edu/harvard/iq/dataverse/api/util/Pagination.java new file mode 100644 index 00000000000..4e711f2d50d --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/api/util/Pagination.java @@ -0,0 +1,28 @@ +package edu.harvard.iq.dataverse.api.util; + +public class Pagination { + + private int numResults = -1; + private int offset; + private int limitPerPage; + + public Pagination(Integer limitPerPage, Integer offset) { + this.offset = offset != null ? Math.max(offset, 0) : 0; + this.limitPerPage = limitPerPage != null ? Math.max(limitPerPage, 1) : 10; + } + public void setNumResults(int numResults) { + this.numResults = numResults; + } + + public int getNumResults() { + return numResults; + } + + public int getOffset() { + return offset; + } + + public int getLimitPerPage() { + return limitPerPage; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetUserPermittedCollectionsCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetUserPermittedCollectionsCommand.java index 0bb732e496b..50b830bd37f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetUserPermittedCollectionsCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetUserPermittedCollectionsCommand.java @@ -2,6 +2,7 @@ import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.api.util.Pagination; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; @@ -10,7 +11,6 @@ import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.InvalidCommandArgumentsException; -import edu.harvard.iq.dataverse.mydata.Pager; import edu.harvard.iq.dataverse.util.BundleUtil; import java.util.List; @@ -42,18 +42,18 @@ public class GetUserPermittedCollectionsCommand extends AbstractCommand execute(CommandContext ctxt) throws CommandException { } catch (IllegalArgumentException e) { throw new InvalidCommandArgumentsException(BundleUtil.getStringFromBundle("getUserPermittedCollectionsCommand.errors.permissionNotValid"), this); } - return ctxt.permissions().findPermittedCollections(request, user, permissionBit, searchTerm, pager); + return ctxt.permissions().findPermittedCollections(request, user, permissionBit, searchTerm, pagination); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java b/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java index 550b03e7941..ec24be39228 100644 --- a/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java +++ b/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java @@ -5,6 +5,7 @@ import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.api.auth.AuthRequired; +import edu.harvard.iq.dataverse.api.util.Pagination; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; @@ -344,10 +345,10 @@ public Response retrieveMyCollectionList(@Context ContainerRequestContext crc, @QueryParam("pageSize") Integer pageSize) { try { verifyAuth(crc, userIdentifier); - Pager pager = getPager(pageSize, start); + Pagination pagination = (start != null || pageSize != null) ? new Pagination(pageSize, start) : null; List collections = execCommand(new GetUserPermittedCollectionsCommand(createDataverseRequest(getRequestUser(crc)), searchUser, Permission.AddDataset.name(), - searchTerm, pager)); - return ok(JsonPrinter.jsonArray(collections, pager)); + searchTerm, pagination)); + return ok(JsonPrinter.jsonArray(collections, pagination)); } catch (WrappedResponse wr) { return wr.getResponse(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/mydata/Pager.java b/src/main/java/edu/harvard/iq/dataverse/mydata/Pager.java index 863dd8d9990..096974b9d72 100644 --- a/src/main/java/edu/harvard/iq/dataverse/mydata/Pager.java +++ b/src/main/java/edu/harvard/iq/dataverse/mydata/Pager.java @@ -155,7 +155,6 @@ public int getNumResults(){ */ public void setNumResults(int numResults){ this.numResults = numResults; - makePageStats(); } @@ -483,4 +482,4 @@ private void msgt(String s){ msg(s); msg("-------------------------------"); } -} +} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index a890de6919b..76478c9a54c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -2,6 +2,7 @@ import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.api.Util; +import edu.harvard.iq.dataverse.api.util.Pagination; import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.RoleAssigneeDisplayInfo; @@ -388,10 +389,13 @@ public static JsonObjectBuilder json(Dataverse dv, Boolean hideEmail, Boolean re public static JsonObjectBuilder jsonArray(List dataverses) { return jsonArray(dataverses, null); } - public static JsonObjectBuilder jsonArray(List dataverses, Pager pager) { + public static JsonObjectBuilder jsonArray(List dataverses, Pagination pagination) { JsonObjectBuilder job = Json.createObjectBuilder(); int count = dataverses.size(); job.add("count", count); + if (pagination != null) { + job.add("totalCount", pagination.getNumResults()); + } JsonArrayBuilder jsonArrayBuilder = Json.createArrayBuilder(); for (Dataverse dataverse : dataverses) { NullSafeJsonBuilder jsonObject = NullSafeJsonBuilder.jsonObjectBuilder(); @@ -401,17 +405,6 @@ public static JsonObjectBuilder jsonArray(List dataverses, Pager page jsonArrayBuilder.add(jsonObject); } job.add("items", jsonArrayBuilder); - if (pager != null) { - job.add("pageSize", pager.getDocsPerPage()); - int nextOffset = pager.getSelectedPageNumber() * pager.getDocsPerPage() + 1; - int prevOffset = nextOffset - (2 * pager.getDocsPerPage()); - if (count >= pager.getDocsPerPage()) { - job.add("nextOffset", nextOffset); - } - if (prevOffset > 0) { - job.add("prevOffset", prevOffset); - } - } return job; } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataRetrieverApiIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataRetrieverApiIT.java index 5865cd459d0..62830517a2c 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataRetrieverApiIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataRetrieverApiIT.java @@ -169,12 +169,13 @@ public void testRetrieveMyDataCollections() throws InterruptedException { // User1 gets the list of Dataverses/Collections it has access to (with pagination) // Get the first page + int offset = 0; retrieveMyCollectionListResponse = UtilIT.retrieveMyCollectionList(User1ApiToken, null, null, 10); retrieveMyCollectionListResponse.prettyPrint(); int count = retrieveMyCollectionListResponse.getBody().jsonPath().getInt("data.count"); + int totalCount = retrieveMyCollectionListResponse.getBody().jsonPath().getInt("data.totalCount"); // get the second page - int offset = retrieveMyCollectionListResponse.getBody().jsonPath().getInt("data.nextOffset"); - retrieveMyCollectionListResponse = UtilIT.retrieveMyCollectionList(User1ApiToken, null, offset, 10); + retrieveMyCollectionListResponse = UtilIT.retrieveMyCollectionList(User1ApiToken, null, offset + count + 1, 10); retrieveMyCollectionListResponse.prettyPrint(); count = count + retrieveMyCollectionListResponse.getBody().jsonPath().getInt("data.count"); // The count should show the list size to be User1's + Root Dataverse count From 540a5c235ee1318948ce068463989676f2e9f40c Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:08:57 -0400 Subject: [PATCH 04/13] refactor pagination --- src/main/java/edu/harvard/iq/dataverse/api/Users.java | 1 - .../java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Users.java b/src/main/java/edu/harvard/iq/dataverse/api/Users.java index 2e69cf2e0f1..b5eff3e49da 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Users.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Users.java @@ -13,7 +13,6 @@ import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.engine.command.impl.*; -import edu.harvard.iq.dataverse.mydata.Pager; import edu.harvard.iq.dataverse.settings.FeatureFlags; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.FileUtil; diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index 76478c9a54c..07a15bbdb0b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -27,7 +27,6 @@ import edu.harvard.iq.dataverse.globus.FileDetailsHolder; import edu.harvard.iq.dataverse.harvest.client.HarvestingClient; import edu.harvard.iq.dataverse.license.License; -import edu.harvard.iq.dataverse.mydata.Pager; import edu.harvard.iq.dataverse.privateurl.PrivateUrl; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; From 0098ff11fe947064fa093e4a92c775d4622b5f49 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:14:31 -0400 Subject: [PATCH 05/13] refactor pagination --- .../12423-collection-list-search-and-pagination.md | 6 +++--- src/main/java/edu/harvard/iq/dataverse/api/Users.java | 2 +- src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/release-notes/12423-collection-list-search-and-pagination.md b/doc/release-notes/12423-collection-list-search-and-pagination.md index 2634a78aff4..ccabc085606 100644 --- a/doc/release-notes/12423-collection-list-search-and-pagination.md +++ b/doc/release-notes/12423-collection-list-search-and-pagination.md @@ -1,7 +1,7 @@ ## Feature ## -Updated the following APIs to add ability to filter using `searchTerm` and added optional pagination parameters `offset` and `pageSize` to limit the results with each GET. +Updated the following APIs to add ability to filter using `searchTerm` and added optional pagination parameters `offset` and `limit` to limit the results with each GET. -GET `/api/users/$USERNAME/allowedCollections/$PERMISSION?pageSize=10&offset=0&searchTerm=bio` +GET `/api/users/$USERNAME/allowedCollections/$PERMISSION?limit=10&offset=0&searchTerm=bio` -GET `/api/mydata/retrieve/collectionList?userIdentifier=anotherUser&pageSize=10&offset=11&searchTerm=bio` +GET `/api/mydata/retrieve/collectionList?userIdentifier=anotherUser&limit=10&offset=11&searchTerm=bio` diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Users.java b/src/main/java/edu/harvard/iq/dataverse/api/Users.java index b5eff3e49da..492099cea76 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Users.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Users.java @@ -328,7 +328,7 @@ public Response getUserPermittedCollections(@Context ContainerRequestContext crc @Parameter(description = "Offset used to override the starting point of the list.") @QueryParam("offset") Integer start, @Parameter(description = "Page size to limit the number of items in the list.") - @QueryParam("pageSize") Integer pageSize) { + @QueryParam("limit") Integer pageSize) { AuthenticatedUser authenticatedUser = null; try { authenticatedUser = getRequestAuthenticatedUserOrDie(crc); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index e9cd8bd4d85..b0eb093d35b 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -4397,7 +4397,7 @@ static Response retrieveMyCollectionList(String apiToken, String userIdentifier) return retrieveMyCollectionList(apiToken, userIdentifier, null, null); } - static Response retrieveMyCollectionList(String apiToken, String userIdentifier, Integer offset, Integer pageSize) { + static Response retrieveMyCollectionList(String apiToken, String userIdentifier, Integer offset, Integer limit) { RequestSpecification requestSpecification = given(); if (apiToken != null) { requestSpecification.header(API_TOKEN_HTTP_HEADER, apiToken); @@ -4408,8 +4408,8 @@ static Response retrieveMyCollectionList(String apiToken, String userIdentifier, if (offset != null) { requestSpecification.queryParam("offset", offset); } - if (pageSize != null) { - requestSpecification.queryParam("pageSize", pageSize); + if (limit != null) { + requestSpecification.queryParam("limit", limit); } return requestSpecification.get("/api/mydata/retrieve/collectionList"); From a5e8244577ef999c97b7c3a7c1b79a4e2b95d545 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:16:12 -0400 Subject: [PATCH 06/13] refactor pagination --- doc/sphinx-guides/source/api/native-api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 4c7c1c7baca..9db2a15c18f 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -8723,7 +8723,7 @@ Passing ``$permission`` as 'any' will return the collection as long as the user - ``searchTerm``: To filter the results. - ``offset``: Starting row ('nextOffset' or 'prevOffset' from the Json output can be used to get the next or previous page). -- ``pageSize``: Number of items to limit the output. +- ``limit``: Number of items to limit the output. .. code-block:: bash @@ -8732,7 +8732,7 @@ Passing ``$permission`` as 'any' will return the collection as long as the user export PERMISSION=PublishDataverse curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/users/$USERNAME/allowedCollections/$PERMISSION" - curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/users/$USERNAME/allowedCollections/$PERMISSION?pageSize=10&offset=0&searchTerm=bio" + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/users/$USERNAME/allowedCollections/$PERMISSION?limit=10&offset=0&searchTerm=bio" Show Role Assignee ~~~~~~~~~~~~~~~~~~ From d9b56474967c4e6fc96500537ec48ccc38ccb4f4 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:18:15 -0400 Subject: [PATCH 07/13] refactor pagination --- doc/sphinx-guides/source/api/native-api.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 9db2a15c18f..74c23a46d0e 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -8722,7 +8722,7 @@ Passing ``$permission`` as 'any' will return the collection as long as the user **For filtering and pagination these query parameters can be used:** - ``searchTerm``: To filter the results. -- ``offset``: Starting row ('nextOffset' or 'prevOffset' from the Json output can be used to get the next or previous page). +- ``offset``: Starting row. - ``limit``: Number of items to limit the output. .. code-block:: bash @@ -9323,8 +9323,8 @@ Param userIdentifier={userName} is used by a superuser to get the collections fo **For filtering and pagination these query parameters can be used:** - ``searchTerm``: To filter the results. -- ``offset``: Starting row ('nextOffset' or 'prevOffset' from the Json output can be used to get the next or previous page). -- ``pageSize``: Number of items to limit the output. +- ``offset``: Starting row. +- ``limit``: Number of items to limit the output. A curl example listing collections: @@ -9335,5 +9335,5 @@ A curl example listing collections: curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/mydata/retrieve/collectionList" curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/mydata/retrieve/collectionList?userIdentifier=anotherUser" - curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/mydata/retrieve/collectionList?userIdentifier=anotherUser&pageSize=10&offset=11&searchTerm=bio" + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/mydata/retrieve/collectionList?userIdentifier=anotherUser&limit=10&offset=11&searchTerm=bio" From 457f7e60c8e4b2e55c732ef04aed96b5877d558c Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:22:57 -0400 Subject: [PATCH 08/13] refactor pagination --- .../java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java | 2 +- .../java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java b/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java index ec24be39228..88f57e22527 100644 --- a/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java +++ b/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java @@ -342,7 +342,7 @@ public Response retrieveMyCollectionList(@Context ContainerRequestContext crc, @Parameter(description = "Offset used to override the starting point of the list.") @QueryParam("offset") Integer start, @Parameter(description = "Page size to limit the number of items in the list.") - @QueryParam("pageSize") Integer pageSize) { + @QueryParam("limit") Integer pageSize) { try { verifyAuth(crc, userIdentifier); Pagination pagination = (start != null || pageSize != null) ? new Pagination(pageSize, start) : null; diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index 07a15bbdb0b..9174717d4ed 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -390,8 +390,7 @@ public static JsonObjectBuilder jsonArray(List dataverses) { } public static JsonObjectBuilder jsonArray(List dataverses, Pagination pagination) { JsonObjectBuilder job = Json.createObjectBuilder(); - int count = dataverses.size(); - job.add("count", count); + job.add("count", dataverses.size()); if (pagination != null) { job.add("totalCount", pagination.getNumResults()); } From 627d2dc8fe223703d456d000e1ac09ebd8318676 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:53:52 -0400 Subject: [PATCH 09/13] triggering test workflow --- src/main/java/edu/harvard/iq/dataverse/api/util/Pagination.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/util/Pagination.java b/src/main/java/edu/harvard/iq/dataverse/api/util/Pagination.java index 4e711f2d50d..62b427090c9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/util/Pagination.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/util/Pagination.java @@ -10,6 +10,7 @@ public Pagination(Integer limitPerPage, Integer offset) { this.offset = offset != null ? Math.max(offset, 0) : 0; this.limitPerPage = limitPerPage != null ? Math.max(limitPerPage, 1) : 10; } + public void setNumResults(int numResults) { this.numResults = numResults; } From f2b9b0cb84a99bda28a1e0b47833edb50fab70ea Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Fri, 7 Aug 2026 09:42:56 -0400 Subject: [PATCH 10/13] triggering tests --- .../java/edu/harvard/iq/dataverse/api/util/Pagination.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/util/Pagination.java b/src/main/java/edu/harvard/iq/dataverse/api/util/Pagination.java index 62b427090c9..d81d30c954b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/util/Pagination.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/util/Pagination.java @@ -6,9 +6,11 @@ public class Pagination { private int offset; private int limitPerPage; + private static int DEFAULT_PAGE_SIZE = 10; + public Pagination(Integer limitPerPage, Integer offset) { this.offset = offset != null ? Math.max(offset, 0) : 0; - this.limitPerPage = limitPerPage != null ? Math.max(limitPerPage, 1) : 10; + this.limitPerPage = limitPerPage != null ? Math.max(limitPerPage, 1) : DEFAULT_PAGE_SIZE; } public void setNumResults(int numResults) { From e39aba2a97b1e3e4d7ad4d931ad6712c99cf96ab Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:29:06 -0400 Subject: [PATCH 11/13] Update JsonPrinter.java --- .../edu/harvard/iq/dataverse/util/json/JsonPrinter.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index d0e079ebf4e..0c3b17b0816 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -386,8 +386,14 @@ public static JsonObjectBuilder json(Dataverse dv, Boolean hideEmail, Boolean re } public static JsonObjectBuilder jsonArray(List dataverses) { + return jsonArray(dataverses, null); + } + public static JsonObjectBuilder jsonArray(List dataverses, Pagination pagination) { JsonObjectBuilder job = Json.createObjectBuilder(); job.add("count", dataverses.size()); + if (pagination != null) { + job.add("totalCount", pagination.getNumResults()); + } JsonArrayBuilder jsonArrayBuilder = Json.createArrayBuilder(); for (Dataverse dataverse : dataverses) { NullSafeJsonBuilder jsonObject = NullSafeJsonBuilder.jsonObjectBuilder(); From 73ad9b8c88b7d2c901170dfa150c34b05abbd70c Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:31:39 -0400 Subject: [PATCH 12/13] conflict fixes --- .../java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index 0c3b17b0816..6f194975569 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -146,7 +146,7 @@ public static JsonObjectBuilder json(FileAccessRequest fileAccessRequest) { } public static JsonArrayBuilder jsonRoleAssignments(List roleAssignments) { - JsonArrayBuilder bld = Json.createArrayBuilder(); + JsonArrayBuilder bld = JsonUtil.createArrayBuilder(); roleAssignments.forEach(roleAssignment -> bld.add(json(roleAssignment))); return bld; } From 07dcf06d9465dec858b0175afb712ee7528e87e7 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:33:57 -0400 Subject: [PATCH 13/13] conflict fixes --- .../java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index 6f194975569..655b9bda65d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -389,12 +389,12 @@ public static JsonObjectBuilder jsonArray(List dataverses) { return jsonArray(dataverses, null); } public static JsonObjectBuilder jsonArray(List dataverses, Pagination pagination) { - JsonObjectBuilder job = Json.createObjectBuilder(); + JsonObjectBuilder job = JsonUtil.createObjectBuilder(); job.add("count", dataverses.size()); if (pagination != null) { job.add("totalCount", pagination.getNumResults()); } - JsonArrayBuilder jsonArrayBuilder = Json.createArrayBuilder(); + JsonArrayBuilder jsonArrayBuilder = JsonUtil.createArrayBuilder(); for (Dataverse dataverse : dataverses) { NullSafeJsonBuilder jsonObject = NullSafeJsonBuilder.jsonObjectBuilder(); jsonObject.add("id", dataverse.getId());