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