diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/pom.xml b/amps/ags/rm-automation/rm-automation-community-rest-api/pom.xml
index d14ab1fb8ae..b9b7ed21ec9 100644
--- a/amps/ags/rm-automation/rm-automation-community-rest-api/pom.xml
+++ b/amps/ags/rm-automation/rm-automation-community-rest-api/pom.xml
@@ -84,6 +84,12 @@
okhttp
test
+
+ org.awaitility
+ awaitility
+ ${dependency.awaitility.version}
+ test
+
org.apache.commons
commons-collections4
diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldBulkOperation.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldBulkOperation.java
new file mode 100644
index 00000000000..55fe0ed118e
--- /dev/null
+++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldBulkOperation.java
@@ -0,0 +1,60 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rest.rm.community.model.hold;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.alfresco.rest.search.RestRequestQueryModel;
+import org.alfresco.utility.model.TestModel;
+
+/**
+ * POJO for hold bulk request
+ *
+ * @author Damian Ujma
+ */
+@EqualsAndHashCode(callSuper = true)
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class HoldBulkOperation extends TestModel
+{
+ public enum HoldBulkOperationType
+ {
+ ADD
+ }
+
+ @JsonProperty(required = true)
+ private RestRequestQueryModel query;
+ @JsonProperty(required = true)
+ private HoldBulkOperationType op;
+}
diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldBulkOperationEntry.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldBulkOperationEntry.java
new file mode 100644
index 00000000000..2709cff1514
--- /dev/null
+++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldBulkOperationEntry.java
@@ -0,0 +1,50 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rest.rm.community.model.hold;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * POJO for hold bulk request entry
+ *
+ * @author Damian Ujma
+ */
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class HoldBulkOperationEntry
+{
+ private String bulkStatusId;
+
+ private long totalItems;
+}
diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldBulkStatus.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldBulkStatus.java
new file mode 100644
index 00000000000..88258bea45b
--- /dev/null
+++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldBulkStatus.java
@@ -0,0 +1,68 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rest.rm.community.model.hold;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.alfresco.utility.model.TestModel;
+
+/**
+ * POJO for hold bulk request
+ *
+ * @author Damian Ujma
+ */
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class HoldBulkStatus extends TestModel
+{
+ private String bulkStatusId;
+
+ private String startTime;
+
+ private String endTime;
+
+ private long processedItems;
+
+ private long errorsCount;
+
+ private long totalItems;
+
+ private String lastError;
+
+ private Status status;
+
+ public enum Status
+ {
+ PENDING,
+ IN_PROGRESS,
+ DONE
+ }
+}
diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldBulkStatusCollection.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldBulkStatusCollection.java
new file mode 100644
index 00000000000..b6f71b611a5
--- /dev/null
+++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldBulkStatusCollection.java
@@ -0,0 +1,38 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rest.rm.community.model.hold;
+
+import org.alfresco.rest.core.RestModels;
+
+/**
+ * Handle collection of {@link HoldBulkStatusEntry}
+ *
+ * @author Damian Ujma
+ */
+public class HoldBulkStatusCollection extends RestModels
+{
+}
diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldBulkStatusEntry.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldBulkStatusEntry.java
new file mode 100644
index 00000000000..68291352ccf
--- /dev/null
+++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldBulkStatusEntry.java
@@ -0,0 +1,46 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rest.rm.community.model.hold;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.alfresco.rest.core.RestModels;
+
+@Builder
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+@AllArgsConstructor
+public class HoldBulkStatusEntry extends RestModels
+{
+ private HoldBulkStatus entry;
+}
diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldChildEntry.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldChildEntry.java
index e8a65dea783..6b3cc4b36e9 100644
--- a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldChildEntry.java
+++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/HoldChildEntry.java
@@ -48,5 +48,5 @@
public class HoldChildEntry extends RestModels
{
@JsonProperty
- private HoldChildEntry entry;
+ private HoldChild entry;
}
\ No newline at end of file
diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/requests/gscore/api/HoldsAPI.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/requests/gscore/api/HoldsAPI.java
index c06403290ee..b30eed527c1 100644
--- a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/requests/gscore/api/HoldsAPI.java
+++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/requests/gscore/api/HoldsAPI.java
@@ -39,6 +39,10 @@
import org.alfresco.rest.core.RMRestWrapper;
import org.alfresco.rest.rm.community.model.hold.Hold;
+import org.alfresco.rest.rm.community.model.hold.HoldBulkOperation;
+import org.alfresco.rest.rm.community.model.hold.HoldBulkOperationEntry;
+import org.alfresco.rest.rm.community.model.hold.HoldBulkStatus;
+import org.alfresco.rest.rm.community.model.hold.HoldBulkStatusCollection;
import org.alfresco.rest.rm.community.model.hold.HoldChild;
import org.alfresco.rest.rm.community.model.hold.HoldChildCollection;
import org.alfresco.rest.rm.community.model.hold.HoldDeletionReason;
@@ -287,4 +291,113 @@ public void deleteHoldChild(String holdId, String holdChildId)
{
deleteHoldChild(holdId, holdChildId, EMPTY);
}
+
+ /**
+ * Starts a bulk process for a hold.
+ *
+ * @param holdBulkOperation The bulk operation details
+ * @param hold The identifier of a hold
+ * @param parameters The URL parameters to add
+ * @return The {@link HoldBulkOperationEntry} for the started bulk process
+ * @throws RuntimeException for the following cases:
+ *
+ * - {@code hold} or {@code holdBulkOperation} is invalid
+ * - authentication fails
+ * - current user does not have permission to start a bulk process for {@code hold}
+ * - {@code hold} does not exist
+ *
+ */
+ public HoldBulkOperationEntry startBulkProcess(HoldBulkOperation holdBulkOperation, String hold, String parameters)
+ {
+ mandatoryObject("holdBulkOperation", holdBulkOperation);
+ mandatoryString("hold", hold);
+
+ return getRmRestWrapper().processModel(HoldBulkOperationEntry.class, requestWithBody(
+ POST,
+ toJson(holdBulkOperation),
+ "holds/{hold}/bulk",
+ hold,
+ parameters
+ ));
+ }
+
+ /**
+ * See {@link #startBulkProcess(HoldBulkOperation, String, String)}
+ */
+ public HoldBulkOperationEntry startBulkProcess(HoldBulkOperation holdBulkOperation, String hold)
+ {
+ return startBulkProcess(holdBulkOperation, hold, EMPTY);
+ }
+
+ /**
+ * Gets the status of a bulk process for a hold.
+ *
+ * @param holdId The identifier of a hold
+ * @param holdBulkStatusId The identifier of a bulk status operation
+ * @param parameters The URL parameters to add
+ * @return The {@link HoldBulkStatus} for the given {@code holdId} and {@code holdBulkStatusId}
+ * @throws RuntimeException for the following cases:
+ *
+ * - {@code holdId} or {@code holdBulkStatusId} is invalid
+ * - authentication fails
+ * - current user does not have permission to get the bulk status for {@code holdId}
+ * - {@code holdId} or {@code holdBulkStatusId} does not exist
+ *
+ */
+ public HoldBulkStatus getBulkStatus(String holdId, String holdBulkStatusId, String parameters)
+ {
+ mandatoryString("holdId", holdId);
+ mandatoryString("holdBulkStatusId", holdBulkStatusId);
+
+ return getRmRestWrapper().processModel(HoldBulkStatus.class, simpleRequest(
+ GET,
+ "holds/{holdId}/bulk-statuses/{holdBulkStatusId}",
+ holdId,
+ holdBulkStatusId,
+ parameters
+ ));
+ }
+
+ /**
+ * See {@link #getBulkStatus(String, String, String)}
+ */
+ public HoldBulkStatus getBulkStatus(String holdId, String holdBulkStatusId)
+ {
+ return getBulkStatus(holdId, holdBulkStatusId, EMPTY);
+ }
+
+ /**
+ * Gets the statuses of all bulk processes for a hold.
+ *
+ * @param holdId The identifier of a hold
+ * @param parameters The URL parameters to add
+ * @return The {@link HoldBulkStatusCollection} for the given {@code holdId}
+ * @throws RuntimeException for the following cases:
+ *
+ * - {@code holdId} is invalid
+ * - authentication fails
+ * - current user does not have permission to get the bulk statuses for {@code holdId}
+ * - {@code holdId} does not exist
+ *
+ */
+ public HoldBulkStatusCollection getBulkStatuses(String holdId, String parameters)
+ {
+ mandatoryString("holdId", holdId);
+
+ return getRmRestWrapper().processModels(HoldBulkStatusCollection.class, simpleRequest(
+ GET,
+ "holds/{holdId}/bulk-statuses",
+ holdId,
+ parameters
+ ));
+ }
+
+ /**
+ * See {@link #getBulkStatuses(String, String)}
+ */
+ public HoldBulkStatusCollection getBulkStatuses(String holdId)
+ {
+ return getBulkStatuses(holdId, EMPTY);
+ }
+
}
diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/AddToHoldsBulkV1Tests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/AddToHoldsBulkV1Tests.java
new file mode 100644
index 00000000000..11cb4d4eb16
--- /dev/null
+++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/AddToHoldsBulkV1Tests.java
@@ -0,0 +1,532 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rest.rm.community.hold;
+
+import static org.alfresco.rest.rm.community.base.TestData.HOLD_DESCRIPTION;
+import static org.alfresco.rest.rm.community.base.TestData.HOLD_REASON;
+import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAlias.FILE_PLAN_ALIAS;
+import static org.alfresco.rest.rm.community.model.user.UserPermissions.PERMISSION_FILING;
+import static org.alfresco.rest.rm.community.model.user.UserPermissions.PERMISSION_READ_RECORDS;
+import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix;
+import static org.alfresco.utility.report.log.Step.STEP;
+import static org.awaitility.Awaitility.await;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.springframework.http.HttpStatus.ACCEPTED;
+import static org.springframework.http.HttpStatus.BAD_REQUEST;
+import static org.springframework.http.HttpStatus.FORBIDDEN;
+import static org.springframework.http.HttpStatus.NOT_FOUND;
+import static org.springframework.http.HttpStatus.UNAUTHORIZED;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.alfresco.dataprep.CMISUtil;
+import org.alfresco.dataprep.ContentActions;
+import org.alfresco.rest.rm.community.base.BaseRMRestTest;
+import org.alfresco.rest.rm.community.model.hold.Hold;
+import org.alfresco.rest.rm.community.model.hold.HoldBulkOperation;
+import org.alfresco.rest.rm.community.model.hold.HoldBulkOperation.HoldBulkOperationType;
+import org.alfresco.rest.rm.community.model.hold.HoldBulkOperationEntry;
+import org.alfresco.rest.rm.community.model.hold.HoldBulkStatus;
+import org.alfresco.rest.rm.community.model.hold.HoldBulkStatus.Status;
+import org.alfresco.rest.rm.community.model.hold.HoldBulkStatusCollection;
+import org.alfresco.rest.rm.community.model.hold.HoldBulkStatusEntry;
+import org.alfresco.rest.rm.community.model.hold.HoldChild;
+import org.alfresco.rest.rm.community.model.hold.HoldChildEntry;
+import org.alfresco.rest.rm.community.model.user.UserRoles;
+import org.alfresco.rest.search.RestRequestQueryModel;
+import org.alfresco.rest.search.SearchRequest;
+import org.alfresco.rest.v0.service.RoleService;
+import org.alfresco.utility.constants.UserRole;
+import org.alfresco.utility.model.FileModel;
+import org.alfresco.utility.model.FolderModel;
+import org.alfresco.utility.model.UserModel;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+/**
+ * API tests for adding items to holds via the bulk process
+ */
+public class AddToHoldsBulkV1Tests extends BaseRMRestTest
+{
+ private static final String ACCESS_DENIED_ERROR_MESSAGE = "Access Denied. You do not have the appropriate " +
+ "permissions to perform this operation.";
+ private static final int NUMBER_OF_FILES = 5;
+ private final List addedFiles = new ArrayList<>();
+ private final List users = new ArrayList<>();
+ private final List holds = new ArrayList<>();
+ private Hold hold;
+ private Hold hold2;
+ private Hold hold3;
+ private FolderModel rootFolder;
+ private HoldBulkOperation holdBulkOperation;
+ @Autowired
+ private RoleService roleService;
+ @Autowired
+ private ContentActions contentActions;
+
+ @BeforeClass(alwaysRun = true)
+ public void preconditionForAddContentToHold()
+ {
+ STEP("Create a hold.");
+ hold = getRestAPIFactory().getFilePlansAPI(getAdminUser()).createHold(
+ Hold.builder().name("HOLD" + generateTestPrefix(AddToHoldsV1Tests.class)).description(HOLD_DESCRIPTION)
+ .reason(HOLD_REASON).build(), FILE_PLAN_ALIAS);
+ holds.add(hold);
+
+ STEP("Create test files.");
+ testSite = dataSite.usingAdmin().createPublicRandomSite();
+
+ rootFolder = dataContent.usingAdmin().usingSite(testSite).createFolder();
+ FolderModel folder1 = dataContent.usingAdmin().usingResource(rootFolder).createFolder();
+ FolderModel folder2 = dataContent.usingAdmin().usingResource(folder1).createFolder();
+
+ // Add files to subfolders in the site
+ for (int i = 0; i < NUMBER_OF_FILES; i++)
+ {
+ FileModel documentHeld = dataContent.usingAdmin()
+ .usingResource(i % 2 == 0 ? folder1 : folder2)
+ .createContent(CMISUtil.DocumentType.TEXT_PLAIN);
+ addedFiles.add(documentHeld);
+ }
+
+ RestRequestQueryModel queryReq = getContentFromSiteQuery(testSite.getId());
+ SearchRequest searchRequest = new SearchRequest();
+ searchRequest.setQuery(queryReq);
+
+ STEP("Wait until all files are searchable.");
+ await().atMost(30, TimeUnit.SECONDS)
+ .until(() -> getRestAPIFactory().getSearchAPI(null).search(searchRequest).getPagination()
+ .getTotalItems() == NUMBER_OF_FILES);
+
+ holdBulkOperation = HoldBulkOperation.builder()
+ .query(queryReq)
+ .op(HoldBulkOperationType.ADD).build();
+ }
+
+ /**
+ * Given a user with the add to hold capability and hold filing permission
+ * When the user adds content from a site to a hold using the bulk API
+ * Then the content is added to the hold and the status of the bulk operation is DONE
+ */
+ @Test
+ public void addContentFromTestSiteToHoldUsingBulkAPI()
+ {
+ UserModel userAddHoldPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite,
+ UserRole.SiteCollaborator, hold.getId(), UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING);
+ users.add(userAddHoldPermission);
+
+ STEP("Add content from the site to the hold using the bulk API.");
+ HoldBulkOperationEntry bulkOperationEntry = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
+ .startBulkProcess(holdBulkOperation, hold.getId());
+
+ // Verify the status code
+ assertStatusCode(ACCEPTED);
+ assertEquals(NUMBER_OF_FILES, bulkOperationEntry.getTotalItems());
+
+ STEP("Wait until all files are added to the hold.");
+ await().atMost(20, TimeUnit.SECONDS).until(
+ () -> getRestAPIFactory().getHoldsAPI(getAdminUser()).getChildren(hold.getId()).getEntries().size()
+ == NUMBER_OF_FILES);
+ List holdChildrenNodeRefs = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
+ .getChildren(hold.getId()).getEntries().stream().map(HoldChildEntry::getEntry).map(
+ HoldChild::getId).toList();
+ assertEquals(addedFiles.stream().map(FileModel::getNodeRefWithoutVersion).sorted().toList(),
+ holdChildrenNodeRefs.stream().sorted().toList());
+
+ STEP("Check the bulk status.");
+ HoldBulkStatus holdBulkStatus = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
+ .getBulkStatus(hold.getId(), bulkOperationEntry.getBulkStatusId());
+ assertBulkProcessStatus(holdBulkStatus, NUMBER_OF_FILES, 0, null);
+
+ STEP("Check the bulk statuses.");
+ HoldBulkStatusCollection holdBulkStatusCollection = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
+ .getBulkStatuses(hold.getId());
+ assertEquals(Arrays.asList(holdBulkStatus), holdBulkStatusCollection.getEntries().stream().map(HoldBulkStatusEntry::getEntry).toList());
+ }
+
+ /**
+ * Given a user with the add to hold capability and hold filing permission
+ * When the user adds content from a folder and all subfolders to a hold using the bulk API
+ * Then the content is added to the hold and the status of the bulk operation is DONE
+ */
+ @Test
+ public void addContentFromFolderAndAllSubfoldersToHoldUsingBulkAPI()
+ {
+ hold3 = getRestAPIFactory().getFilePlansAPI(getAdminUser()).createHold(
+ Hold.builder().name("HOLD" + generateTestPrefix(AddToHoldsV1Tests.class)).description(HOLD_DESCRIPTION)
+ .reason(HOLD_REASON).build(), FILE_PLAN_ALIAS);
+ holds.add(hold3);
+
+ UserModel userAddHoldPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite,
+ UserRole.SiteCollaborator, hold3.getId(), UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING);
+ users.add(userAddHoldPermission);
+
+ STEP("Add content from the site to the hold using the bulk API.");
+ // Get content from folder and all subfolders of the root folder
+ HoldBulkOperation bulkOperation = HoldBulkOperation.builder()
+ .query(getContentFromFolderAndAllSubfoldersQuery(rootFolder.getNodeRefWithoutVersion()))
+ .op(HoldBulkOperationType.ADD).build();
+ HoldBulkOperationEntry bulkOperationEntry = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
+ .startBulkProcess(bulkOperation, hold3.getId());
+
+ // Verify the status code
+ assertStatusCode(ACCEPTED);
+ assertEquals(NUMBER_OF_FILES, bulkOperationEntry.getTotalItems());
+
+ STEP("Wait until all files are added to the hold.");
+ await().atMost(20, TimeUnit.SECONDS).until(
+ () -> getRestAPIFactory().getHoldsAPI(getAdminUser()).getChildren(hold3.getId()).getEntries().size()
+ == NUMBER_OF_FILES);
+ List holdChildrenNodeRefs = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
+ .getChildren(hold3.getId()).getEntries().stream().map(HoldChildEntry::getEntry).map(
+ HoldChild::getId).toList();
+ assertEquals(addedFiles.stream().map(FileModel::getNodeRefWithoutVersion).sorted().toList(),
+ holdChildrenNodeRefs.stream().sorted().toList());
+
+ STEP("Check the bulk status.");
+ HoldBulkStatus holdBulkStatus = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
+ .getBulkStatus(hold3.getId(), bulkOperationEntry.getBulkStatusId());
+ assertBulkProcessStatus(holdBulkStatus, NUMBER_OF_FILES, 0, null);
+
+ STEP("Check the bulk statuses.");
+ HoldBulkStatusCollection holdBulkStatusCollection = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
+ .getBulkStatuses(hold3.getId());
+ assertEquals(Arrays.asList(holdBulkStatus), holdBulkStatusCollection.getEntries().stream().map(HoldBulkStatusEntry::getEntry).toList());
+ }
+
+ /**
+ * Given a user without the add to hold capability
+ * When the user adds content from a site to a hold using the bulk API
+ * Then the user receives access denied error
+ */
+ @Test
+ public void testBulkProcessWithUserWithoutAddToHoldCapability()
+ {
+ UserModel userWithoutAddToHoldCapability = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite,
+ UserRole
+ .SiteCollaborator,
+ hold.getId(), UserRoles.ROLE_RM_POWER_USER, PERMISSION_FILING);
+ users.add(userWithoutAddToHoldCapability);
+
+ STEP("Add content from the site to the hold using the bulk API.");
+ getRestAPIFactory().getHoldsAPI(userWithoutAddToHoldCapability)
+ .startBulkProcess(holdBulkOperation, hold.getId());
+
+ STEP("Verify the response status code and the error message.");
+ assertStatusCode(FORBIDDEN);
+ getRestAPIFactory().getRmRestWrapper().assertLastError().containsSummary(ACCESS_DENIED_ERROR_MESSAGE);
+ }
+
+ /**
+ * Given a user without the filing permission on a hold
+ * When the user adds content from a site to a hold using the bulk API
+ * Then the user receives access denied error
+ */
+ @Test
+ public void testBulkProcessWithUserWithoutFilingPermissionOnAHold()
+ {
+ // User without filing permission on a hold
+ UserModel userWithoutPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite,
+ UserRole.SiteCollaborator, hold.getId(), UserRoles.ROLE_RM_MANAGER, PERMISSION_READ_RECORDS);
+ users.add(userWithoutPermission);
+
+ STEP("Add content from the site to the hold using the bulk API.");
+ getRestAPIFactory().getHoldsAPI(userWithoutPermission)
+ .startBulkProcess(holdBulkOperation, hold.getId());
+
+ STEP("Verify the response status code and the error message.");
+ assertStatusCode(FORBIDDEN);
+ getRestAPIFactory().getRmRestWrapper().assertLastError().containsSummary(ACCESS_DENIED_ERROR_MESSAGE);
+
+ }
+
+ /**
+ * Given a user without the write permission on all the content
+ * When the user adds content from a site to a hold using the bulk API
+ * Then all processed items are marked as errors and the last error message contains access denied error
+ */
+ @Test
+ public void testBulkProcessWithUserWithoutWritePermissionOnTheContent()
+ {
+ // User without write permission on the content
+ UserModel userWithoutPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(
+ testSite, UserRole.SiteConsumer,
+ hold.getId(), UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING);
+ users.add(userWithoutPermission);
+
+ // Wait until permissions are reverted
+ SearchRequest searchRequest = new SearchRequest();
+ searchRequest.setQuery(holdBulkOperation.getQuery());
+ await().atMost(30, TimeUnit.SECONDS)
+ .until(() -> getRestAPIFactory().getSearchAPI(userWithoutPermission).search(searchRequest).getPagination()
+ .getTotalItems() == NUMBER_OF_FILES);
+
+ STEP("Add content from the site to the hold using the bulk API.");
+ HoldBulkOperationEntry bulkOperationEntry = getRestAPIFactory().getHoldsAPI(
+ userWithoutPermission).startBulkProcess(holdBulkOperation, hold.getId());
+
+ STEP("Verify the response.");
+ assertStatusCode(ACCEPTED);
+
+ await().atMost(20, TimeUnit.SECONDS).until(() ->
+ getRestAPIFactory().getHoldsAPI(userWithoutPermission)
+ .getBulkStatus(hold.getId(), bulkOperationEntry.getBulkStatusId()).getStatus() == Status.DONE);
+
+ HoldBulkStatus holdBulkStatus = getRestAPIFactory().getHoldsAPI(userWithoutPermission)
+ .getBulkStatus(hold.getId(), bulkOperationEntry.getBulkStatusId());
+ assertBulkProcessStatus(holdBulkStatus, NUMBER_OF_FILES, NUMBER_OF_FILES, ACCESS_DENIED_ERROR_MESSAGE);
+ }
+
+ /**
+ * Given a user without the write permission on one file
+ * When the user adds content from a site to a hold using the bulk API
+ * Then all processed items are added to the hold except the one that the user does not have write permission
+ * And the status of the bulk operation is DONE, contains the error message and the number of errors is 1
+ */
+ @Test
+ public void testBulkProcessWithUserWithoutWritePermissionOnOneFile()
+ {
+ hold2 = getRestAPIFactory().getFilePlansAPI(getAdminUser()).createHold(
+ Hold.builder().name("HOLD" + generateTestPrefix(AddToHoldsV1Tests.class)).description(HOLD_DESCRIPTION)
+ .reason(HOLD_REASON).build(), FILE_PLAN_ALIAS);
+ holds.add(hold2);
+
+ UserModel userAddHoldPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite,
+ UserRole.SiteCollaborator, hold2.getId(), UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING);
+ users.add(userAddHoldPermission);
+
+ contentActions.removePermissionForUser(getAdminUser().getUsername(), getAdminUser().getPassword(),
+ testSite.getId(), addedFiles.get(0).getName(), userAddHoldPermission.getUsername(),
+ UserRole.SiteCollaborator.getRoleId(), false);
+
+ STEP("Add content from the site to the hold using the bulk API.");
+ HoldBulkOperationEntry bulkOperationEntry = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
+ .startBulkProcess(holdBulkOperation, hold2.getId());
+
+ // Verify the status code
+ assertStatusCode(ACCEPTED);
+ assertEquals(NUMBER_OF_FILES, bulkOperationEntry.getTotalItems());
+
+ STEP("Wait until all files are added to the hold.");
+ await().atMost(20, TimeUnit.SECONDS).until(
+ () -> getRestAPIFactory().getHoldsAPI(getAdminUser()).getChildren(hold2.getId()).getEntries().size()
+ == NUMBER_OF_FILES - 1);
+ await().atMost(20, TimeUnit.SECONDS).until(
+ () -> getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
+ .getBulkStatus(hold2.getId(), bulkOperationEntry.getBulkStatusId()).getStatus() == Status.DONE);
+ List holdChildrenNodeRefs = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
+ .getChildren(hold2.getId()).getEntries().stream().map(HoldChildEntry::getEntry).map(
+ HoldChild::getId).toList();
+ assertEquals(addedFiles.stream().skip(1).map(FileModel::getNodeRefWithoutVersion).sorted().toList(),
+ holdChildrenNodeRefs.stream().sorted().toList());
+
+ STEP("Check the bulk status.");
+ HoldBulkStatus holdBulkStatus = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
+ .getBulkStatus(hold2.getId(), bulkOperationEntry.getBulkStatusId());
+ assertBulkProcessStatus(holdBulkStatus, NUMBER_OF_FILES, 1, ACCESS_DENIED_ERROR_MESSAGE);
+
+ STEP("Check the bulk statuses.");
+ HoldBulkStatusCollection holdBulkStatusCollection = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
+ .getBulkStatuses(hold2.getId());
+ assertEquals(Arrays.asList(holdBulkStatus), holdBulkStatusCollection.getEntries().stream().map(HoldBulkStatusEntry::getEntry).toList());
+
+ //revert the permissions
+ contentActions.removePermissionForUser(getAdminUser().getUsername(), getAdminUser().getPassword(),
+ testSite.getId(), addedFiles.get(0).getName(), userAddHoldPermission.getUsername(),
+ UserRole.SiteCollaborator.getRoleId(), true);
+ }
+
+ /**
+ * Given an unauthenticated user
+ * When the user adds content from a site to a hold using the bulk API
+ * Then the user receives unauthorized error
+ */
+ @Test
+ public void testBulkProcessAsUnauthenticatedUser()
+ {
+ STEP("Start bulk process as unauthenticated user");
+ getRestAPIFactory().getHoldsAPI(new UserModel(getAdminUser().getUsername(), "wrongPassword"))
+ .startBulkProcess(holdBulkOperation, hold.getId());
+
+ STEP("Verify the response status code.");
+ assertStatusCode(UNAUTHORIZED);
+ }
+
+ /**
+ * Given a user with the add to hold capability and hold filing permission
+ * When the user adds content from a site to a hold using the bulk API
+ * And the hold does not exist
+ * Then the user receives not found error
+ */
+ @Test
+ public void testBulkProcessForNonExistentHold()
+ {
+ STEP("Start bulk process for non existent hold");
+ getRestAPIFactory().getHoldsAPI(getAdminUser()).startBulkProcess(holdBulkOperation, "nonExistentHoldId");
+
+ STEP("Verify the response status code.");
+ assertStatusCode(NOT_FOUND);
+ }
+
+ /**
+ * Given a user with the add to hold capability and hold filing permission
+ * When the user adds content from a site to a hold using the bulk API
+ * and the bulk operation is invalid
+ * Then the user receives bad request error
+ */
+ @Test
+ public void testGetBulkStatusesForInvalidOperation()
+ {
+ STEP("Start bulk process for non existent hold");
+
+ HoldBulkOperation invalidHoldBulkOperation = HoldBulkOperation.builder().op(null)
+ .query(holdBulkOperation.getQuery()).build();
+ getRestAPIFactory().getHoldsAPI(getAdminUser()).startBulkProcess(invalidHoldBulkOperation, hold.getId());
+
+ STEP("Verify the response status code.");
+ assertStatusCode(BAD_REQUEST);
+ }
+
+ /**
+ * Given a user with the add to hold capability and hold filing permission
+ * When the user adds content from a site to a hold using the bulk API
+ * And the hold does not exist
+ * Then the user receives not found error
+ */
+ @Test
+ public void testGetBulkStatusForNonExistentHold()
+ {
+ STEP("Start bulk process for non existent hold");
+ getRestAPIFactory().getHoldsAPI(getAdminUser()).getBulkStatus("nonExistentHoldId", "nonExistenBulkStatusId");
+
+ STEP("Verify the response status code.");
+ assertStatusCode(NOT_FOUND);
+ }
+
+ /**
+ * Given a user with the add to hold capability and hold filing permission
+ * When the user adds content from a site to a hold using the bulk API
+ * And the bulk status does not exist
+ * Then the user receives not found error
+ */
+ @Test
+ public void testGetBulkStatusForNonExistentBulkStatus()
+ {
+ STEP("Start bulk process for non bulk status");
+ getRestAPIFactory().getHoldsAPI(getAdminUser()).getBulkStatus(hold.getId(), "nonExistenBulkStatusId");
+
+ STEP("Verify the response status code.");
+ assertStatusCode(NOT_FOUND);
+ }
+
+ /**
+ * Given a user with the add to hold capability and hold filing permission
+ * When the user adds content from a site to a hold using the bulk API
+ * And the hold does not exist
+ * Then the user receives not found error
+ */
+ @Test
+ public void testGetBulkStatusesForNonExistentHold()
+ {
+ STEP("Start bulk process for non existent hold");
+ getRestAPIFactory().getHoldsAPI(getAdminUser()).getBulkStatuses("nonExistentHoldId");
+
+ STEP("Verify the response status code.");
+ assertStatusCode(NOT_FOUND);
+ }
+
+ /**
+ * Given a user with the add to hold capability and hold filing permission
+ * When the user adds content from all sites to a hold using the bulk API to exceed the limit (30 items)
+ * Then the user receives bad request error
+ */
+ @Test
+ public void testExceedingBulkOperationLimit()
+ {
+ RestRequestQueryModel queryReq = new RestRequestQueryModel();
+ queryReq.setQuery("TYPE:content");
+ queryReq.setLanguage("afts");
+
+ HoldBulkOperation exceedLimitOp = HoldBulkOperation.builder()
+ .query(queryReq)
+ .op(HoldBulkOperationType.ADD).build();
+
+ STEP("Start bulk process to exceed the limit");
+ getRestAPIFactory().getHoldsAPI(getAdminUser()).startBulkProcess(exceedLimitOp, hold.getId());
+
+ STEP("Verify the response status code.");
+ assertStatusCode(BAD_REQUEST);
+ }
+
+
+ private void assertBulkProcessStatus(HoldBulkStatus holdBulkStatus, long expectedProcessedItems,
+ int expectedErrorsCount, String expectedErrorMessage)
+ {
+ assertEquals(Status.DONE, holdBulkStatus.getStatus());
+ assertEquals(expectedProcessedItems, holdBulkStatus.getTotalItems());
+ assertEquals(expectedProcessedItems, holdBulkStatus.getProcessedItems());
+ assertEquals(expectedErrorsCount, holdBulkStatus.getErrorsCount());
+ assertNotNull(holdBulkStatus.getStartTime());
+ assertNotNull(holdBulkStatus.getEndTime());
+
+ if (expectedErrorMessage != null)
+ {
+ assertTrue(holdBulkStatus.getLastError().contains(expectedErrorMessage));
+ }
+ }
+
+ private RestRequestQueryModel getContentFromSiteQuery(String siteId)
+ {
+ RestRequestQueryModel queryReq = new RestRequestQueryModel();
+ queryReq.setQuery("SITE:\"" + siteId + "\" and TYPE:content");
+ queryReq.setLanguage("afts");
+ return queryReq;
+ }
+
+ private RestRequestQueryModel getContentFromFolderAndAllSubfoldersQuery(String folderId)
+ {
+ RestRequestQueryModel queryReq = new RestRequestQueryModel();
+ queryReq.setQuery("ANCESTOR:\"workspace://SpacesStore/" + folderId + "\" and TYPE:content");
+ queryReq.setLanguage("afts");
+ return queryReq;
+ }
+
+ @AfterClass(alwaysRun = true)
+ public void cleanupAddToHoldsBulkV1Tests()
+ {
+ dataSite.usingAdmin().deleteSite(testSite);
+ users.forEach(user -> getDataUser().usingAdmin().deleteUser(user));
+ holds.forEach(hold -> getRestAPIFactory().getHoldsAPI(getAdminUser()).deleteHold(hold.getId()));
+ }
+}
diff --git a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/alfresco-global.properties b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/alfresco-global.properties
index 0b0936856c7..d8acf185091 100644
--- a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/alfresco-global.properties
+++ b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/alfresco-global.properties
@@ -139,3 +139,21 @@ content.metadata.async.extract.6.enabled=false
# Max number of entries returned in Record search view
rm.recordSearch.maxItems=500
+
+#
+# Hold bulk
+#
+# The number of worker threads.
+rm.hold.bulk.threadCount=2
+# The maximum number of total items to process in a single bulk operation.
+rm.hold.bulk.maxItems=1000
+# The number of entries to be fetched from the Search Service as a next set of work object to process.
+rm.hold.bulk.batchSize=100
+# The number of entries to process before reporting progress.
+rm.hold.bulk.logging.interval=100
+# The number of entries we process at a time in a transaction.
+rm.hold.bulk.itemsPerTransaction=1
+
+cache.bulkHoldStatusCache.cluster.type=fully-distributed
+cache.bulkHoldRegistryCache.cluster.type=fully-distributed
+
diff --git a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/module-context.xml b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/module-context.xml
index 4ccba8a2a02..70ddb827122 100644
--- a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/module-context.xml
+++ b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/module-context.xml
@@ -89,6 +89,9 @@
+
+
+
diff --git a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-bulk-context.xml b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-bulk-context.xml
new file mode 100644
index 00000000000..a6a310167fc
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-bulk-context.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${rm.hold.bulk.threadCount}
+
+
+ ${rm.hold.bulk.batchSize}
+
+
+ ${rm.hold.bulk.maxItems}
+
+
+ ${rm.hold.bulk.logging.interval}
+
+
+ ${rm.hold.bulk.itemsPerTransaction}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-public-rest-context.xml b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-public-rest-context.xml
index f969a458ea2..0f4b156497f 100644
--- a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-public-rest-context.xml
+++ b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-public-rest-context.xml
@@ -83,6 +83,13 @@
+
+
+
+
+
+
+
diff --git a/amps/ags/rm-community/rm-community-repo/docker-compose.yml b/amps/ags/rm-community/rm-community-repo/docker-compose.yml
index 2b3b10a7476..1d128c86344 100644
--- a/amps/ags/rm-community/rm-community-repo/docker-compose.yml
+++ b/amps/ags/rm-community/rm-community-repo/docker-compose.yml
@@ -41,6 +41,8 @@ services:
-Daos.baseUrlOverwrite=http://localhost:8080/alfresco/aos
-Dmessaging.broker.url=\"failover:(tcp://activemq:61616)?timeout=3000&jms.useCompression=true\"
-DlocalTransform.core-aio.url=http://transform-core-aio:8090/
+ -Drm.hold.bulk.maxItems=5
+ -Drm.hold.bulk.batchSize=2
"
ports:
- 8080:8080
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/BulkBaseService.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/BulkBaseService.java
new file mode 100644
index 00000000000..48efdde185d
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/BulkBaseService.java
@@ -0,0 +1,249 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.module.org_alfresco_module_rm.bulk;
+
+import java.util.UUID;
+
+import org.alfresco.repo.batch.BatchProcessWorkProvider;
+import org.alfresco.repo.batch.BatchProcessor;
+import org.alfresco.repo.batch.BatchProcessor.BatchProcessWorker;
+import org.alfresco.rest.api.search.impl.SearchMapper;
+import org.alfresco.rest.api.search.model.Query;
+import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
+import org.alfresco.service.ServiceRegistry;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.search.ResultSet;
+import org.alfresco.service.cmr.search.SearchParameters;
+import org.alfresco.service.cmr.search.SearchService;
+import org.alfresco.service.transaction.TransactionService;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.InitializingBean;
+
+/**
+ * A base class for executing bulk operations on nodes based on search query results
+ */
+public abstract class BulkBaseService implements InitializingBean
+{
+ private static final Log LOG = LogFactory.getLog(BulkBaseService.class);
+
+ protected ServiceRegistry serviceRegistry;
+ protected SearchService searchService;
+ protected TransactionService transactionService;
+ protected SearchMapper searchMapper;
+ protected BulkMonitor bulkMonitor;
+
+ protected int threadCount;
+ protected int batchSize;
+ protected int itemsPerTransaction;
+ protected int maxItems;
+ protected int loggingInterval;
+
+ @Override
+ public void afterPropertiesSet() throws Exception
+ {
+ this.searchService = serviceRegistry.getSearchService();
+ }
+
+ /**
+ * Execute bulk operation on node based on the search query results
+ *
+ * @param nodeRef node reference
+ * @param bulkOperation bulk operation
+ * @return bulk status
+ */
+ public T execute(NodeRef nodeRef, BulkOperation bulkOperation)
+ {
+ checkPermissions(nodeRef, bulkOperation);
+
+ ResultSet resultSet = getTotalItems(bulkOperation.searchQuery(), maxItems);
+ if (maxItems < resultSet.getNumberFound() || resultSet.hasMore())
+ {
+ throw new InvalidArgumentException("Too many items to process. Please refine your query.");
+ }
+ long totalItems = resultSet.getNumberFound();
+ // Generate a random process id
+ String processId = UUID.randomUUID().toString();
+
+ T initBulkStatus = getInitBulkStatus(processId, totalItems);
+ bulkMonitor.updateBulkStatus(initBulkStatus);
+ bulkMonitor.registerProcess(nodeRef, processId);
+
+ BatchProcessWorker batchProcessWorker = getWorkerProvider(nodeRef, bulkOperation);
+ BulkStatusUpdater bulkStatusUpdater = getBulkStatusUpdater();
+
+ BatchProcessor batchProcessor = new BatchProcessor<>(
+ processId,
+ transactionService.getRetryingTransactionHelper(),
+ getWorkProvider(bulkOperation, totalItems, bulkStatusUpdater),
+ threadCount,
+ itemsPerTransaction,
+ bulkStatusUpdater,
+ LOG,
+ loggingInterval);
+
+ runAsyncBatchProcessor(batchProcessor, batchProcessWorker, bulkStatusUpdater);
+ return initBulkStatus;
+ }
+
+ /**
+ * Run batch processor
+ */
+ protected void runAsyncBatchProcessor(BatchProcessor batchProcessor,
+ BatchProcessWorker batchProcessWorker, BulkStatusUpdater bulkStatusUpdater)
+ {
+ Runnable backgroundLogic = () -> {
+ try
+ {
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("Started processing batch with name: " + batchProcessor.getProcessName());
+ }
+ batchProcessor.processLong(batchProcessWorker, true);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("Processing batch with name: " + batchProcessor.getProcessName() + " completed");
+ }
+ }
+ catch (Exception exception)
+ {
+ LOG.error("Error processing batch with name: " + batchProcessor.getProcessName(), exception);
+ }
+ finally
+ {
+ bulkStatusUpdater.update();
+ }
+ };
+
+ Thread backgroundThread = new Thread(backgroundLogic, "BulkBaseService-BackgroundThread");
+ backgroundThread.start();
+ }
+
+ /**
+ * Get initial bulk status
+ *
+ * @param processId process id
+ * @param totalItems total items
+ * @return bulk status
+ */
+ protected abstract T getInitBulkStatus(String processId, long totalItems);
+
+ /**
+ * Get bulk status updater
+ *
+ * @return bulk status updater
+ */
+ protected abstract BulkStatusUpdater getBulkStatusUpdater();
+
+ /**
+ * Get work provider
+ *
+ * @param bulkOperation bulk operation
+ * @param totalItems total items
+ * @param bulkStatusUpdater bulk status updater
+ * @return work provider
+ */
+ protected abstract BatchProcessWorkProvider getWorkProvider(BulkOperation bulkOperation, long totalItems,
+ BulkStatusUpdater bulkStatusUpdater);
+
+ /**
+ * Get worker provider
+ *
+ * @param nodeRef node reference
+ * @param bulkOperation bulk operation
+ * @return worker provider
+ */
+ protected abstract BatchProcessWorker getWorkerProvider(NodeRef nodeRef, BulkOperation bulkOperation);
+
+ /**
+ * Check permissions
+ *
+ * @param nodeRef node reference
+ * @param bulkOperation bulk operation
+ */
+ protected abstract void checkPermissions(NodeRef nodeRef, BulkOperation bulkOperation);
+
+ protected ResultSet getTotalItems(Query searchQuery, int skipCount)
+ {
+ SearchParameters searchParams = new SearchParameters();
+ searchMapper.setDefaults(searchParams);
+ searchMapper.fromQuery(searchParams, searchQuery);
+ searchParams.setSkipCount(skipCount);
+ searchParams.setMaxItems(1);
+ return searchService.query(searchParams);
+ }
+
+ public void setServiceRegistry(ServiceRegistry serviceRegistry)
+ {
+ this.serviceRegistry = serviceRegistry;
+ }
+
+ public void setSearchService(SearchService searchService)
+ {
+ this.searchService = searchService;
+ }
+
+ public void setTransactionService(TransactionService transactionService)
+ {
+ this.transactionService = transactionService;
+ }
+
+ public void setSearchMapper(SearchMapper searchMapper)
+ {
+ this.searchMapper = searchMapper;
+ }
+
+ public void setBulkMonitor(BulkMonitor bulkMonitor)
+ {
+ this.bulkMonitor = bulkMonitor;
+ }
+
+ public void setThreadCount(int threadCount)
+ {
+ this.threadCount = threadCount;
+ }
+
+ public void setBatchSize(int batchSize)
+ {
+ this.batchSize = batchSize;
+ }
+
+ public void setMaxItems(int maxItems)
+ {
+ this.maxItems = maxItems;
+ }
+
+ public void setLoggingInterval(int loggingInterval)
+ {
+ this.loggingInterval = loggingInterval;
+ }
+
+ public void setItemsPerTransaction(int itemsPerTransaction)
+ {
+ this.itemsPerTransaction = itemsPerTransaction;
+ }
+}
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/BulkMonitor.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/BulkMonitor.java
new file mode 100644
index 00000000000..9c21fc06a68
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/BulkMonitor.java
@@ -0,0 +1,58 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.module.org_alfresco_module_rm.bulk;
+
+import org.alfresco.service.cmr.repository.NodeRef;
+
+/**
+ * An interface for monitoring the progress of a bulk operation
+ */
+public interface BulkMonitor
+{
+ /**
+ * Update the bulk status
+ *
+ * @param bulkStatus the bulk status
+ */
+ void updateBulkStatus(T bulkStatus);
+
+ /**
+ * Register a process
+ *
+ * @param nodeRef the node reference
+ * @param processId the process id
+ */
+ void registerProcess(NodeRef nodeRef, String processId);
+
+ /**
+ * Get the bulk status
+ *
+ * @param bulkStatusId the bulk status id
+ * @return the bulk status
+ */
+ T getBulkStatus(String bulkStatusId);
+}
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/BulkOperation.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/BulkOperation.java
new file mode 100644
index 00000000000..0f9973eb269
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/BulkOperation.java
@@ -0,0 +1,44 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.module.org_alfresco_module_rm.bulk;
+
+import org.alfresco.rest.api.search.model.Query;
+
+/**
+ * An immutable POJO to represent a bulk operation
+ */
+public record BulkOperation(Query searchQuery, String operationType)
+{
+ public BulkOperation
+ {
+ if (operationType == null || searchQuery == null)
+ {
+ throw new IllegalArgumentException("Operation type and search query must not be null");
+ }
+ }
+}
+
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/BulkStatusUpdater.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/BulkStatusUpdater.java
new file mode 100644
index 00000000000..9dcc75a7695
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/BulkStatusUpdater.java
@@ -0,0 +1,40 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.module.org_alfresco_module_rm.bulk;
+
+import org.springframework.context.ApplicationEventPublisher;
+
+/**
+ * An interface for updating the status of a bulk operation
+ */
+public interface BulkStatusUpdater extends ApplicationEventPublisher
+{
+ /**
+ * Update the bulk status
+ */
+ void update();
+}
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/DefaultHoldBulkMonitor.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/DefaultHoldBulkMonitor.java
new file mode 100644
index 00000000000..9adade5ab33
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/DefaultHoldBulkMonitor.java
@@ -0,0 +1,109 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.module.org_alfresco_module_rm.bulk.hold;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.alfresco.repo.cache.SimpleCache;
+import org.alfresco.rm.rest.api.model.HoldBulkStatus;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.springframework.context.ApplicationEvent;
+import org.springframework.extensions.surf.util.AbstractLifecycleBean;
+
+/**
+ * Default hold bulk monitor implementation
+ */
+public class DefaultHoldBulkMonitor extends AbstractLifecycleBean implements HoldBulkMonitor
+{
+ protected SimpleCache holdProgressCache;
+ protected SimpleCache> holdProcessRegistry;
+
+ @Override
+ public void updateBulkStatus(HoldBulkStatus holdBulkStatus)
+ {
+ holdProgressCache.put(holdBulkStatus.bulkStatusId(), holdBulkStatus);
+ }
+
+ @Override
+ public void registerProcess(NodeRef holdRef, String processId)
+ {
+ List processIds = Optional.ofNullable(holdProcessRegistry.get(holdRef.getId()))
+ .orElse(new ArrayList<>());
+ processIds.add(new HoldBulkProcessDetails(processId, null));
+ holdProcessRegistry.put(holdRef.getId(), processIds);
+ }
+
+ @Override
+ public HoldBulkStatus getBulkStatus(String bulkStatusId)
+ {
+ return holdProgressCache.get(bulkStatusId);
+ }
+
+ @Override
+ public List getBulkStatusesForHold(String holdId)
+ {
+ return Optional.ofNullable(holdProcessRegistry.get(holdId))
+ .map(bulkProcessDetailsList -> bulkProcessDetailsList.stream()
+ .map(HoldBulkProcessDetails::bulkStatusId)
+ .map(this::getBulkStatus)
+ .filter(Objects::nonNull)
+ .sorted(Comparator.comparing(HoldBulkStatus::endTime, Comparator.nullsLast(Comparator.naturalOrder()))
+ .thenComparing(HoldBulkStatus::startTime, Comparator.nullsLast(Comparator.naturalOrder()))
+ .reversed())
+ .toList())
+ .orElse(Collections.emptyList());
+ }
+
+ public void setHoldProgressCache(
+ SimpleCache holdProgressCache)
+ {
+ this.holdProgressCache = holdProgressCache;
+ }
+
+ public void setHoldProcessRegistry(
+ SimpleCache> holdProcessRegistry)
+ {
+ this.holdProcessRegistry = holdProcessRegistry;
+ }
+
+ @Override
+ protected void onBootstrap(ApplicationEvent applicationEvent)
+ {
+ // NOOP
+ }
+
+ @Override
+ protected void onShutdown(ApplicationEvent applicationEvent)
+ {
+ // NOOP
+ }
+}
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkMonitor.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkMonitor.java
new file mode 100644
index 00000000000..68c15c65957
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkMonitor.java
@@ -0,0 +1,46 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.module.org_alfresco_module_rm.bulk.hold;
+
+import java.util.List;
+
+import org.alfresco.module.org_alfresco_module_rm.bulk.BulkMonitor;
+import org.alfresco.rm.rest.api.model.HoldBulkStatus;
+
+/**
+ * An interface for monitoring the progress of a bulk hold operation
+ */
+public interface HoldBulkMonitor extends BulkMonitor
+{
+ /**
+ * Get the bulk statuses for a hold
+ *
+ * @param holdId the hold id
+ * @return the bulk statuses
+ */
+ List getBulkStatusesForHold(String holdId);
+}
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkProcessDetails.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkProcessDetails.java
new file mode 100644
index 00000000000..98d425e8734
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkProcessDetails.java
@@ -0,0 +1,36 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.module.org_alfresco_module_rm.bulk.hold;
+
+import java.io.Serializable;
+
+/**
+ * A simple immutable POJO to hold the details of a bulk hold process
+ */
+public record HoldBulkProcessDetails(String bulkStatusId, String creatorInstance) implements Serializable
+{
+}
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkService.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkService.java
new file mode 100644
index 00000000000..7cb5072c39b
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkService.java
@@ -0,0 +1,45 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.module.org_alfresco_module_rm.bulk.hold;
+
+import org.alfresco.module.org_alfresco_module_rm.bulk.BulkOperation;
+import org.alfresco.rm.rest.api.model.HoldBulkStatus;
+import org.alfresco.service.cmr.repository.NodeRef;
+
+/**
+ * Interface defining a hold bulk service.
+ */
+public interface HoldBulkService
+{
+ /**
+ * Initiates a bulk operation on a hold.
+ *
+ * @param holdRef The hold reference
+ * @param bulkOperation The bulk operation
+ */
+ HoldBulkStatus execute(NodeRef holdRef, BulkOperation bulkOperation);
+}
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkServiceImpl.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkServiceImpl.java
new file mode 100644
index 00000000000..c751a8e86b9
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkServiceImpl.java
@@ -0,0 +1,258 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.module.org_alfresco_module_rm.bulk.hold;
+
+import static org.alfresco.model.ContentModel.PROP_NAME;
+import static org.alfresco.rm.rest.api.model.HoldBulkOperationType.ADD;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.module.org_alfresco_module_rm.bulk.BulkBaseService;
+import org.alfresco.module.org_alfresco_module_rm.bulk.BulkOperation;
+import org.alfresco.module.org_alfresco_module_rm.bulk.BulkStatusUpdater;
+import org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService;
+import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel;
+import org.alfresco.module.org_alfresco_module_rm.hold.HoldService;
+import org.alfresco.repo.batch.BatchProcessWorkProvider;
+import org.alfresco.repo.batch.BatchProcessor.BatchProcessWorker;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.repo.security.permissions.AccessDeniedException;
+import org.alfresco.rest.api.search.model.Query;
+import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
+import org.alfresco.rm.rest.api.model.HoldBulkOperationType;
+import org.alfresco.rm.rest.api.model.HoldBulkStatus;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.search.ResultSet;
+import org.alfresco.service.cmr.search.SearchParameters;
+import org.alfresco.service.cmr.security.AccessStatus;
+import org.alfresco.service.cmr.security.PermissionService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.extensions.surf.util.I18NUtil;
+
+/**
+ * Implementation of the {@link HoldBulkService} interface.
+ */
+@SuppressWarnings("PMD.PreserveStackTrace")
+public class HoldBulkServiceImpl extends BulkBaseService implements HoldBulkService
+{
+ private static final Logger LOGGER = LoggerFactory.getLogger(HoldBulkServiceImpl.class);
+
+ private HoldService holdService;
+ private static final String MSG_ERR_ACCESS_DENIED = "permissions.err_access_denied";
+
+ private CapabilityService capabilityService;
+ private PermissionService permissionService;
+ private NodeService nodeService;
+
+ @Override
+ protected HoldBulkStatus getInitBulkStatus(String processId, long totalItems)
+ {
+ return new HoldBulkStatus(processId, null, null, 0, 0, totalItems, null);
+ }
+
+ @Override
+ protected BulkStatusUpdater getBulkStatusUpdater()
+ {
+ return new HoldBulkStatusUpdater((HoldBulkMonitor) bulkMonitor);
+ }
+
+ @Override
+ protected BatchProcessWorkProvider getWorkProvider(BulkOperation bulkOperation, long totalItems,
+ BulkStatusUpdater bulkStatusUpdater)
+ {
+ return new AddToHoldWorkerProvider(new AtomicInteger(0), bulkOperation, totalItems, bulkStatusUpdater);
+ }
+
+ @Override
+ protected BatchProcessWorker getWorkerProvider(NodeRef nodeRef, BulkOperation bulkOperation)
+ {
+ try
+ {
+ HoldBulkOperationType holdBulkOperationType = HoldBulkOperationType.valueOf(bulkOperation.operationType()
+ .toUpperCase(Locale.ENGLISH));
+ return switch (holdBulkOperationType)
+ {
+ case ADD -> new AddToHoldWorkerBatch(nodeRef);
+ };
+ }
+ catch (IllegalArgumentException e)
+ {
+ String errorMsg = "Unsupported action type when starting the bulk process: ";
+ if (LOGGER.isDebugEnabled())
+ {
+ LOGGER.debug("{} {}", errorMsg, bulkOperation.operationType(), e);
+ }
+ throw new InvalidArgumentException(errorMsg + bulkOperation.operationType());
+ }
+ }
+
+ @Override
+ protected void checkPermissions(NodeRef holdRef, BulkOperation bulkOperation)
+ {
+ if (!holdService.isHold(holdRef))
+ {
+ final String holdName = (String) nodeService.getProperty(holdRef, PROP_NAME);
+ throw new InvalidArgumentException(I18NUtil.getMessage("rm.hold.not-hold", holdName), null);
+ }
+ if (ADD.name().equals(bulkOperation.operationType()) && (!AccessStatus.ALLOWED.equals(
+ capabilityService.getCapabilityAccessState(holdRef, RMPermissionModel.ADD_TO_HOLD)) ||
+ permissionService.hasPermission(holdRef, RMPermissionModel.FILING) == AccessStatus.DENIED))
+ {
+ throw new AccessDeniedException(I18NUtil.getMessage(MSG_ERR_ACCESS_DENIED));
+ }
+
+ }
+
+ private class AddToHoldWorkerBatch implements BatchProcessWorker
+ {
+ private final NodeRef holdRef;
+ private final String currentUser;
+
+ public AddToHoldWorkerBatch(NodeRef holdRef)
+ {
+ this.holdRef = holdRef;
+ currentUser = AuthenticationUtil.getFullyAuthenticatedUser();
+ }
+
+ @Override
+ public String getIdentifier(NodeRef entry)
+ {
+ return entry.getId();
+ }
+
+ @Override
+ public void beforeProcess()
+ {
+ AuthenticationUtil.pushAuthentication();
+ }
+
+ @Override
+ public void process(NodeRef entry) throws Throwable
+ {
+ AuthenticationUtil.setFullyAuthenticatedUser(currentUser);
+ holdService.addToHold(holdRef, entry);
+ }
+
+ @Override
+ public void afterProcess()
+ {
+ AuthenticationUtil.popAuthentication();
+ }
+ }
+
+ private class AddToHoldWorkerProvider implements BatchProcessWorkProvider
+ {
+ private final AtomicInteger currentNodeNumber;
+ private final Query searchQuery;
+ private final String currentUser;
+ private final long totalItems;
+ private final BulkStatusUpdater bulkStatusUpdater;
+
+ public AddToHoldWorkerProvider(AtomicInteger currentNodeNumber, BulkOperation bulkOperation, long totalItems,
+ BulkStatusUpdater bulkStatusUpdater)
+ {
+ this.currentNodeNumber = currentNodeNumber;
+ this.searchQuery = bulkOperation.searchQuery();
+ this.totalItems = totalItems;
+ this.bulkStatusUpdater = bulkStatusUpdater;
+ currentUser = AuthenticationUtil.getFullyAuthenticatedUser();
+ }
+
+ @Override
+ public int getTotalEstimatedWorkSize()
+ {
+ return (int) totalItems;
+ }
+
+ @Override
+ public long getTotalEstimatedWorkSizeLong()
+ {
+ return totalItems;
+ }
+
+ @Override
+ public Collection getNextWork()
+ {
+ AuthenticationUtil.pushAuthentication();
+ AuthenticationUtil.setFullyAuthenticatedUser(currentUser);
+ SearchParameters searchParams = getNextPageParameters();
+ ResultSet result = searchService.query(searchParams);
+ if (result.getNodeRefs().isEmpty())
+ {
+ return Collections.emptyList();
+ }
+ AuthenticationUtil.popAuthentication();
+ if (LOGGER.isDebugEnabled())
+ {
+ LOGGER.debug("Processing the next work for the batch processor, skipCount={}, size={}",
+ searchParams.getSkipCount(), result.getNumberFound());
+ }
+ currentNodeNumber.addAndGet(batchSize);
+ bulkStatusUpdater.update();
+ return result.getNodeRefs();
+ }
+
+ private SearchParameters getNextPageParameters()
+ {
+ SearchParameters searchParams = new SearchParameters();
+ searchMapper.setDefaults(searchParams);
+ searchMapper.fromQuery(searchParams, searchQuery);
+ searchParams.setSkipCount(currentNodeNumber.get());
+ searchParams.setMaxItems(batchSize);
+ searchParams.setLimit(batchSize);
+ searchParams.addSort("@" + ContentModel.PROP_CREATED, true);
+ return searchParams;
+ }
+
+ }
+
+ public void setHoldService(HoldService holdService)
+ {
+ this.holdService = holdService;
+ }
+
+ public void setCapabilityService(CapabilityService capabilityService)
+ {
+ this.capabilityService = capabilityService;
+ }
+
+ public void setPermissionService(PermissionService permissionService)
+ {
+ this.permissionService = permissionService;
+ }
+
+ public void setNodeService(NodeService nodeService)
+ {
+ this.nodeService = nodeService;
+ }
+}
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkStatusUpdater.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkStatusUpdater.java
new file mode 100644
index 00000000000..75df3433da9
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkStatusUpdater.java
@@ -0,0 +1,69 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.module.org_alfresco_module_rm.bulk.hold;
+
+import org.alfresco.module.org_alfresco_module_rm.bulk.BulkStatusUpdater;
+import org.alfresco.repo.batch.BatchMonitor;
+import org.alfresco.repo.batch.BatchMonitorEvent;
+import org.alfresco.rm.rest.api.model.HoldBulkStatus;
+
+/**
+ * An implementation of {@link BulkStatusUpdater} for the hold bulk operation
+ */
+public class HoldBulkStatusUpdater implements BulkStatusUpdater
+{
+ private final Runnable task;
+ private BatchMonitor batchMonitor;
+
+ public HoldBulkStatusUpdater(HoldBulkMonitor holdBulkMonitor)
+ {
+ this.task = () -> holdBulkMonitor.updateBulkStatus(
+ new HoldBulkStatus(batchMonitor.getProcessName(), batchMonitor.getStartTime(),
+ batchMonitor.getEndTime(),
+ batchMonitor.getSuccessfullyProcessedEntriesLong() + batchMonitor.getTotalErrorsLong(),
+ batchMonitor.getTotalErrorsLong(), batchMonitor.getTotalResultsLong(),
+ batchMonitor.getLastError()));
+ }
+
+ @Override
+ public void update()
+ {
+ if (task != null && batchMonitor != null)
+ {
+ task.run();
+ }
+ }
+
+ @Override
+ public void publishEvent(Object event)
+ {
+ if (event instanceof BatchMonitorEvent batchMonitorEvent)
+ {
+ batchMonitor = batchMonitorEvent.getBatchMonitor();
+ }
+ }
+}
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/holds/HoldsBulkStatusesRelation.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/holds/HoldsBulkStatusesRelation.java
new file mode 100644
index 00000000000..c82f7a1d3a2
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/holds/HoldsBulkStatusesRelation.java
@@ -0,0 +1,120 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rm.rest.api.holds;
+
+import static org.alfresco.module.org_alfresco_module_rm.util.RMParameterCheck.checkNotBlank;
+import static org.alfresco.util.ParameterCheck.mandatory;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkMonitor;
+import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel;
+import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
+import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
+import org.alfresco.rest.framework.core.exceptions.RelationshipResourceNotFoundException;
+import org.alfresco.rest.framework.resource.RelationshipResource;
+import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction;
+import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
+import org.alfresco.rest.framework.resource.parameters.Parameters;
+import org.alfresco.rm.rest.api.impl.FilePlanComponentsApiUtils;
+import org.alfresco.rm.rest.api.model.HoldBulkStatus;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.security.AccessStatus;
+import org.alfresco.service.cmr.security.PermissionService;
+import org.springframework.extensions.surf.util.I18NUtil;
+
+@RelationshipResource(name = "bulk-statuses", entityResource = HoldsEntityResource.class, title = "Bulk statuses of a hold")
+public class HoldsBulkStatusesRelation
+ implements RelationshipResourceAction.Read, RelationshipResourceAction.ReadById
+{
+ private HoldBulkMonitor holdBulkMonitor;
+ private FilePlanComponentsApiUtils apiUtils;
+ private PermissionService permissionService;
+
+ @Override
+ public CollectionWithPagingInfo readAll(String holdId, Parameters parameters)
+ {
+ // validate parameters
+ checkNotBlank("holdId", holdId);
+ mandatory("parameters", parameters);
+
+ NodeRef holdRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
+
+ checkReadPermissions(holdRef);
+
+ List statuses = holdBulkMonitor.getBulkStatusesForHold(holdId);
+ List page = statuses.stream()
+ .skip(parameters.getPaging().getSkipCount())
+ .limit(parameters.getPaging().getMaxItems())
+ .collect(Collectors.toCollection(LinkedList::new));
+
+ int totalItems = statuses.size();
+ boolean hasMore = parameters.getPaging().getSkipCount() + parameters.getPaging().getMaxItems() < totalItems;
+ return CollectionWithPagingInfo.asPaged(parameters.getPaging(), page, hasMore, totalItems);
+ }
+
+ @Override
+ public HoldBulkStatus readById(String holdId, String bulkStatusId, Parameters parameters)
+ throws RelationshipResourceNotFoundException
+ {
+ checkNotBlank("holdId", holdId);
+ checkNotBlank("bulkStatusId", bulkStatusId);
+ mandatory("parameters", parameters);
+
+ NodeRef holdRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
+
+ checkReadPermissions(holdRef);
+
+ return Optional.ofNullable(holdBulkMonitor.getBulkStatus(bulkStatusId)).orElseThrow(() -> new EntityNotFoundException(bulkStatusId));
+ }
+
+ private void checkReadPermissions(NodeRef holdRef)
+ {
+ if (permissionService.hasReadPermission(holdRef) == AccessStatus.DENIED)
+ {
+ throw new PermissionDeniedException(I18NUtil.getMessage("permissions.err_access_denied"));
+ }
+ }
+
+ public void setHoldBulkMonitor(HoldBulkMonitor holdBulkMonitor)
+ {
+ this.holdBulkMonitor = holdBulkMonitor;
+ }
+
+ public void setApiUtils(FilePlanComponentsApiUtils apiUtils)
+ {
+ this.apiUtils = apiUtils;
+ }
+
+ public void setPermissionService(PermissionService permissionService)
+ {
+ this.permissionService = permissionService;
+ }
+}
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/holds/HoldsEntityResource.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/holds/HoldsEntityResource.java
index 2e8cdf92315..d8bee078fc3 100644
--- a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/holds/HoldsEntityResource.java
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/holds/HoldsEntityResource.java
@@ -30,6 +30,8 @@
import static org.alfresco.util.ParameterCheck.mandatory;
import jakarta.servlet.http.HttpServletResponse;
+import org.alfresco.module.org_alfresco_module_rm.bulk.BulkOperation;
+import org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkService;
import org.alfresco.module.org_alfresco_module_rm.hold.HoldService;
import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
@@ -42,6 +44,9 @@
import org.alfresco.rest.framework.webscripts.WithResponse;
import org.alfresco.rm.rest.api.impl.ApiNodesModelFactory;
import org.alfresco.rm.rest.api.impl.FilePlanComponentsApiUtils;
+import org.alfresco.rm.rest.api.model.HoldBulkOperation;
+import org.alfresco.rm.rest.api.model.HoldBulkOperationEntry;
+import org.alfresco.rm.rest.api.model.HoldBulkStatus;
import org.alfresco.rm.rest.api.model.HoldDeletionReason;
import org.alfresco.rm.rest.api.model.HoldModel;
import org.alfresco.service.cmr.model.FileFolderService;
@@ -68,6 +73,7 @@ public class HoldsEntityResource implements
private ApiNodesModelFactory nodesModelFactory;
private HoldService holdService;
private TransactionService transactionService;
+ private HoldBulkService holdBulkService;
@Override
public void afterPropertiesSet() throws Exception
@@ -157,6 +163,23 @@ public HoldDeletionReason deleteHoldWithReason(String holdId, HoldDeletionReason
return reason;
}
+ @Operation("bulk")
+ @WebApiDescription(title = "Start the hold bulk operation",
+ successStatus = HttpServletResponse.SC_ACCEPTED)
+ public HoldBulkOperationEntry bulk(String holdId, HoldBulkOperation holdBulkOperation, Parameters parameters,
+ WithResponse withResponse)
+ {
+ // validate parameters
+ checkNotBlank("holdId", holdId);
+ mandatory("parameters", parameters);
+
+ NodeRef parentNodeRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
+
+ HoldBulkStatus holdBulkStatus = holdBulkService.execute(parentNodeRef,
+ new BulkOperation(holdBulkOperation.query(), holdBulkOperation.op().name()));
+ return new HoldBulkOperationEntry(holdBulkStatus.bulkStatusId(), holdBulkStatus.totalItems());
+ }
+
public void setApiUtils(FilePlanComponentsApiUtils apiUtils)
{
this.apiUtils = apiUtils;
@@ -181,4 +204,9 @@ public void setTransactionService(TransactionService transactionService)
{
this.transactionService = transactionService;
}
+
+ public void setHoldBulkService(HoldBulkService holdBulkService)
+ {
+ this.holdBulkService = holdBulkService;
+ }
}
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldBulkOperation.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldBulkOperation.java
new file mode 100644
index 00000000000..d6cd85edc0b
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldBulkOperation.java
@@ -0,0 +1,33 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rm.rest.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import org.alfresco.rest.api.search.model.Query;
+
+public record HoldBulkOperation(@JsonProperty(required = true) Query query, @JsonProperty(required = true) HoldBulkOperationType op) {}
\ No newline at end of file
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldBulkOperationEntry.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldBulkOperationEntry.java
new file mode 100644
index 00000000000..a8d058eae91
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldBulkOperationEntry.java
@@ -0,0 +1,29 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rm.rest.api.model;
+
+public record HoldBulkOperationEntry(String bulkStatusId, long totalItems){}
\ No newline at end of file
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldBulkOperationType.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldBulkOperationType.java
new file mode 100644
index 00000000000..3e8df3e1346
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldBulkOperationType.java
@@ -0,0 +1,38 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rm.rest.api.model;
+
+/**
+ * This enum represents the types of bulk operations that can be performed on holds
+ */
+public enum HoldBulkOperationType
+{
+ /**
+ * The ADD operation represents adding items to a hold in bulk.
+ */
+ ADD
+}
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldBulkStatus.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldBulkStatus.java
new file mode 100644
index 00000000000..2d36d18075a
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldBulkStatus.java
@@ -0,0 +1,69 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rm.rest.api.model;
+
+import java.io.Serializable;
+import java.util.Date;
+
+public record HoldBulkStatus(String bulkStatusId, Date startTime, Date endTime, long processedItems, long errorsCount,
+ long totalItems, String lastError) implements Serializable
+{
+ public enum Status
+ {
+ PENDING("PENDING"),
+ IN_PROGRESS("IN PROGRESS"),
+ DONE("DONE");
+
+ private final String value;
+
+ Status(String value)
+ {
+ this.value = value;
+ }
+
+ public String getValue()
+ {
+ return value;
+ }
+ }
+
+ public String getStatus()
+ {
+ if (startTime == null && endTime == null)
+ {
+ return Status.PENDING.getValue();
+ }
+ else if (startTime != null && endTime == null)
+ {
+ return Status.IN_PROGRESS.getValue();
+ }
+ else
+ {
+ return Status.DONE.getValue();
+ }
+ }
+}
diff --git a/amps/ags/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/bulk/DefaultHoldBulkMonitorUnitTest.java b/amps/ags/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/bulk/DefaultHoldBulkMonitorUnitTest.java
new file mode 100644
index 00000000000..e46d49b1958
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/bulk/DefaultHoldBulkMonitorUnitTest.java
@@ -0,0 +1,119 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.module.org_alfresco_module_rm.bulk;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import org.alfresco.module.org_alfresco_module_rm.bulk.hold.DefaultHoldBulkMonitor;
+import org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkProcessDetails;
+import org.alfresco.repo.cache.SimpleCache;
+import org.alfresco.rm.rest.api.model.HoldBulkStatus;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+public class DefaultHoldBulkMonitorUnitTest
+{
+
+ @Mock
+ private SimpleCache holdProgressCache;
+
+ @Mock
+ private SimpleCache> holdProcessRegistry;
+
+ private DefaultHoldBulkMonitor holdBulkMonitor;
+
+ @Before
+ public void setUp()
+ {
+ MockitoAnnotations.openMocks(this);
+ holdBulkMonitor = new DefaultHoldBulkMonitor();
+ holdBulkMonitor.setHoldProgressCache(holdProgressCache);
+ holdBulkMonitor.setHoldProcessRegistry(holdProcessRegistry);
+ }
+
+ @Test
+ public void testUpdateBulkStatus()
+ {
+ HoldBulkStatus status = new HoldBulkStatus("bulkStatusId", null, null, 0L, 0L, 0L, null);
+
+ holdBulkMonitor.updateBulkStatus(status);
+
+ Mockito.verify(holdProgressCache).put("bulkStatusId", status);
+ }
+
+ @Test
+ public void testRegisterProcess()
+ {
+ NodeRef holdRef = new NodeRef("workspace://SpacesStore/holdId");
+ String processId = "processId";
+ when(holdProcessRegistry.get(holdRef.getId())).thenReturn(null);
+
+ holdBulkMonitor.registerProcess(holdRef, processId);
+
+ Mockito.verify(holdProcessRegistry)
+ .put(holdRef.getId(), Arrays.asList(new HoldBulkProcessDetails(processId, null)));
+ }
+
+ @Test
+ public void testGetBulkStatusesForHoldReturnsEmptyListWhenNoProcesses()
+ {
+ when(holdProcessRegistry.get("holdId")).thenReturn(null);
+ assertEquals(Collections.emptyList(), holdBulkMonitor.getBulkStatusesForHold("holdId"));
+ }
+
+ @Test
+ public void testGetBulkStatusesForHoldReturnsSortedStatuses()
+ {
+ HoldBulkStatus status1 = new HoldBulkStatus(null, new Date(1000), new Date(2000), 0L, 0L, 0L, null);
+ HoldBulkStatus status2 = new HoldBulkStatus(null, new Date(3000), null, 0L, 0L, 0L, null);
+ HoldBulkStatus status3 = new HoldBulkStatus(null, new Date(4000), null, 0L, 0L, 0L, null);
+ HoldBulkStatus status4 = new HoldBulkStatus(null, new Date(500), new Date(800), 0L, 0L, 0L, null);
+ HoldBulkStatus status5 = new HoldBulkStatus(null, null, null, 0L, 0L, 0L, null);
+
+ when(holdProcessRegistry.get("holdId")).thenReturn(
+ Arrays.asList("process1", "process2", "process3", "process4", "process5")
+ .stream().map(bulkStatusId -> new HoldBulkProcessDetails(bulkStatusId, null)).toList());
+ when(holdProgressCache.get("process1")).thenReturn(status1);
+ when(holdProgressCache.get("process2")).thenReturn(status2);
+ when(holdProgressCache.get("process3")).thenReturn(status3);
+ when(holdProgressCache.get("process4")).thenReturn(status4);
+ when(holdProgressCache.get("process5")).thenReturn(status5);
+
+ assertEquals(Arrays.asList(status5, status3, status2, status1, status4),
+ holdBulkMonitor.getBulkStatusesForHold("holdId"));
+ }
+}
\ No newline at end of file