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 2744b881fe..33870f8051 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/BulkBodyCancel.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/BulkBodyCancel.java new file mode 100644 index 0000000000..c280255fd3 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/hold/BulkBodyCancel.java @@ -0,0 +1,41 @@ +/*- + * #%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; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +public class BulkBodyCancel +{ + private String reason; +} 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 0000000000..288163d9cc --- /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,59 @@ +/* + * #%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; +import org.alfresco.rest.search.RestRequestQueryModel; +import org.alfresco.utility.model.TestModel; + +/** + * POJO for hold bulk request + * + * @author Damian Ujma + */ +@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 0000000000..2709cff151 --- /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 0000000000..c8d1f8e8a2 --- /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,67 @@ +/* + * #%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 String status; + + private boolean isCancelled; + + private String cancellationReason; + + private HoldBulkOperation holdBulkOperation; +} 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 0000000000..b6f71b611a --- /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 0000000000..68291352cc --- /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 e8a65dea78..6b3cc4b36e 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 @@ import org.alfresco.rest.core.RestModels; 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 c06403290e..10bf6f3128 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 @@ -38,7 +38,12 @@ import static org.springframework.http.HttpMethod.POST; import static org.springframework.http.HttpMethod.PUT; import org.alfresco.rest.core.RMRestWrapper; +import org.alfresco.rest.rm.community.model.hold.BulkBodyCancel; 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 +292,155 @@ public class HoldsAPI extends RMModelRequest { 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); + } + + /** + * Cancels a bulk operation for a hold. + * + * @param holdId The identifier of a hold + * @param bulkStatusId The identifier of a bulk status operation + * @param bulkBodyCancel The bulk body cancel model + * @param parameters The URL parameters to add + * @throws RuntimeException for the following cases: + *
    + *
  • {@code holdId}, {@code bulkStatusId} or {@code bulkBodyCancel} is invalid
  • + *
  • authentication fails
  • + *
  • current user does not have permission to cancel the bulk operation for {@code bulkStatusId}
  • + *
  • {@code holdId} or {@code bulkStatusId} does not exist
  • + *
+ */ + public void cancelBulkOperation(String holdId, String bulkStatusId, BulkBodyCancel bulkBodyCancel, String parameters) + { + mandatoryString("holdId", holdId); + mandatoryString("bulkStatusId", bulkStatusId); + mandatoryObject("bulkBodyCancel", bulkBodyCancel); + + getRmRestWrapper().processEmptyModel(requestWithBody( + POST, + toJson(bulkBodyCancel), + "holds/{holdId}/bulk-statuses/{bulkStatusId}/cancel", + holdId, + bulkStatusId, + parameters + )); + } + + /** + * See {@link #cancelBulkOperation(String, String, BulkBodyCancel, String)} + */ + public void cancelBulkOperation(String holdId, String bulkStatusId, BulkBodyCancel bulkBodyCancel) + { + mandatoryString("holdId", holdId); + mandatoryString("bulkStatusId", bulkStatusId); + mandatoryObject("bulkBodyCancel", bulkBodyCancel); + + cancelBulkOperation(holdId, bulkStatusId, bulkBodyCancel, 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 0000000000..f7b3a7fb85 --- /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,614 @@ +/* + * #%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.OK; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +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.BulkBodyCancel; +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.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, holdBulkOperation); + + 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, bulkOperation); + + STEP("Check the bulk statuses."); + HoldBulkStatusCollection holdBulkStatusCollection = getRestAPIFactory().getHoldsAPI(userAddHoldPermission) + .getBulkStatuses(hold3.getId()); + assertEquals(List.of(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(() -> + Objects.equals(getRestAPIFactory().getHoldsAPI(userWithoutPermission) + .getBulkStatus(hold.getId(), bulkOperationEntry.getBulkStatusId()).getStatus(), "DONE")); + + HoldBulkStatus holdBulkStatus = getRestAPIFactory().getHoldsAPI(userWithoutPermission) + .getBulkStatus(hold.getId(), bulkOperationEntry.getBulkStatusId()); + assertBulkProcessStatus(holdBulkStatus, NUMBER_OF_FILES, NUMBER_OF_FILES, ACCESS_DENIED_ERROR_MESSAGE, + holdBulkOperation); + } + + /** + * 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.setPermissionForUser(getAdminUser().getUsername(), getAdminUser().getPassword(), + testSite.getId(), addedFiles.get(0).getName(), userAddHoldPermission.getUsername(), + UserRole.SiteConsumer.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(30, TimeUnit.SECONDS).until( + () -> getRestAPIFactory().getHoldsAPI(getAdminUser()).getChildren(hold2.getId()).getEntries().size() + == NUMBER_OF_FILES - 1); + await().atMost(30, TimeUnit.SECONDS).until( + () -> getRestAPIFactory().getHoldsAPI(userAddHoldPermission) + .getBulkStatus(hold2.getId(), bulkOperationEntry.getBulkStatusId()).getProcessedItems() + == NUMBER_OF_FILES); + 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, holdBulkOperation); + + STEP("Check the bulk statuses."); + HoldBulkStatusCollection holdBulkStatusCollection = getRestAPIFactory().getHoldsAPI(userAddHoldPermission) + .getBulkStatuses(hold2.getId()); + assertEquals(List.of(holdBulkStatus), + holdBulkStatusCollection.getEntries().stream().map(HoldBulkStatusEntry::getEntry).toList()); + + // Revert the permissions + contentActions.setPermissionForUser(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); + } + + /** + * 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 then the user cancels the bulk operation + * Then the user receives OK status code + */ + @Test + public void testBulkProcessCancellationWithAllowedUser() + { + Hold hold4 = getRestAPIFactory().getFilePlansAPI(getAdminUser()).createHold( + Hold.builder().name("HOLD" + generateTestPrefix(AddToHoldsV1Tests.class)).description(HOLD_DESCRIPTION) + .reason(HOLD_REASON).build(), FILE_PLAN_ALIAS); + holds.add(hold4); + + UserModel userAddHoldPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite, + UserRole.SiteCollaborator, hold4.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, hold4.getId()); + + // Verify the status code + assertStatusCode(ACCEPTED); + assertEquals(NUMBER_OF_FILES, bulkOperationEntry.getTotalItems()); + + STEP("Cancel the bulk operation."); + getRestAPIFactory().getHoldsAPI(userAddHoldPermission) + .cancelBulkOperation(hold4.getId(), bulkOperationEntry.getBulkStatusId(), new BulkBodyCancel()); + + // Verify the status code + assertStatusCode(OK); + } + + /** + * 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 a 2nd user without the add to hold capability cancels the bulk operation + * Then the 2nd user receives access denied error + */ + @Test + public void testBulkProcessCancellationWithUserWithoutAddToHoldCapability() + { + Hold hold5 = getRestAPIFactory().getFilePlansAPI(getAdminUser()).createHold( + Hold.builder().name("HOLD" + generateTestPrefix(AddToHoldsV1Tests.class)).description(HOLD_DESCRIPTION) + .reason(HOLD_REASON).build(), FILE_PLAN_ALIAS); + holds.add(hold5); + + UserModel userAddHoldPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite, + UserRole.SiteCollaborator, hold5.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, hold5.getId()); + + // Verify the status code + assertStatusCode(ACCEPTED); + assertEquals(NUMBER_OF_FILES, bulkOperationEntry.getTotalItems()); + + UserModel userWithoutAddToHoldCapability = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite, + UserRole + .SiteCollaborator, + hold5.getId(), UserRoles.ROLE_RM_POWER_USER, PERMISSION_FILING); + users.add(userWithoutAddToHoldCapability); + + STEP("Cancel the bulk operation."); + getRestAPIFactory().getHoldsAPI(userWithoutAddToHoldCapability) + .cancelBulkOperation(hold5.getId(), bulkOperationEntry.getBulkStatusId(), new BulkBodyCancel()); + + STEP("Verify the response status code and the error message."); + assertStatusCode(FORBIDDEN); + getRestAPIFactory().getRmRestWrapper().assertLastError().containsSummary(ACCESS_DENIED_ERROR_MESSAGE); + } + + private void assertBulkProcessStatus(HoldBulkStatus holdBulkStatus, long expectedProcessedItems, + int expectedErrorsCount, String expectedErrorMessage, HoldBulkOperation holdBulkOperation) + { + assertEquals("DONE", holdBulkStatus.getStatus()); + assertEquals(expectedProcessedItems, holdBulkStatus.getTotalItems()); + assertEquals(expectedProcessedItems, holdBulkStatus.getProcessedItems()); + assertEquals(expectedErrorsCount, holdBulkStatus.getErrorsCount()); + assertEquals(holdBulkStatus.getHoldBulkOperation(), holdBulkOperation); + 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())); + } +} \ No newline at end of file diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/resources/default.properties b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/resources/default.properties index 60d58a5b73..0586dd982d 100644 --- a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/resources/default.properties +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/resources/default.properties @@ -45,9 +45,9 @@ serverHealth.showTenants=false # testManagement.username= # testManagement.apiKey= # testManagement.project= +# testManagement.testRun= # testManagement.includeOnlyTestCasesExecuted=true #if you want to include in your run ONLY the test cases that you run, then set this value to false -# testManagement.rateLimitInSeconds=1 #is the default rate limit after what minimum time, should we upload the next request. http://docs.gurock.com/testrail-api2/introduction #Rate Limit +# testManagement.rateLimitInSeconds=1 #is the default rate limit after what minimum time, should we upload the next request. http://docs.gurock.com/testrail-api2/introduction #Rate Limit # testManagement.suiteId=23 (the id of the Master suite) # ------------------------------------------------------ testManagement.enabled=false @@ -72,7 +72,7 @@ reports.path=./target/reports # # MySQL: # db.url = jdbc:mysql://${alfresco.server}:3306/alfresco -# +# # PostgreSQL: # db.url = jdbc:postgresql://:3306/alfresco # 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 0b0936856c..13f938dc27 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,26 @@ 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 +# The maximum number of bulk requests we can process in parallel. +rm.hold.bulk.maxParallelRequests=10 + +cache.bulkHoldStatusCache.cluster.type=fully-distributed +cache.bulkHoldStatusCache.timeToLiveSeconds=2592000 +cache.bulkHoldRegistryCache.cluster.type=fully-distributed +cache.bulkHoldRegistryCache.timeToLiveSeconds=2592000 +cache.bulkCancellationsCache.cluster.type=fully-distributed +cache.bulkCancellationsCache.timeToLiveSeconds=2592000 \ No newline at end of file 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 4ccba8a2a0..70ddb82712 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 0000000000..c8c5dc3678 --- /dev/null +++ b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-bulk-context.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + ${rm.hold.bulk.threadCount} + + + ${rm.hold.bulk.batchSize} + + + ${rm.hold.bulk.maxItems} + + + ${rm.hold.bulk.logging.interval} + + + ${rm.hold.bulk.itemsPerTransaction} + + + ${rm.hold.bulk.maxParallelRequests} + + + + + + + + + + + + + + + + + + + + + + + \ 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 f969a458ea..f1f225c569 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,14 @@ + + + + + + + + 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 2b3b10a747..1d128c8634 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/pom.xml b/amps/ags/rm-community/rm-community-repo/pom.xml index 2a61e434d1..d0e6b9d12a 100644 --- a/amps/ags/rm-community/rm-community-repo/pom.xml +++ b/amps/ags/rm-community/rm-community-repo/pom.xml @@ -155,6 +155,12 @@ lombok provided
+ + org.awaitility + awaitility + ${dependency.awaitility.version} + test + 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 0000000000..aeedc1f67c --- /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,265 @@ +/* + * #%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 java.util.concurrent.Executors.newFixedThreadPool; + +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +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 ExecutorService executorService; + 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; + protected int maxParallelRequests; + + @Override + public void afterPropertiesSet() throws Exception + { + this.searchService = serviceRegistry.getSearchService(); + this.executorService = newFixedThreadPool(maxParallelRequests); + } + + /** + * 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, bulkOperation); + + BulkProgress bulkProgress = new BulkProgress(totalItems, processId, new AtomicBoolean(false), + new AtomicInteger(0)); + BatchProcessWorker batchProcessWorker = getWorkerProvider(nodeRef, bulkOperation, bulkProgress); + BulkStatusUpdater bulkStatusUpdater = getBulkStatusUpdater(); + + BatchProcessor batchProcessor = new BatchProcessor<>( + processId, + transactionService.getRetryingTransactionHelper(), + getWorkProvider(bulkOperation, bulkStatusUpdater, bulkProgress), + 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(); + } + }; + + executorService.submit(backgroundLogic); + } + + /** + * 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 bulkStatusUpdater bulk status updater + * @param bulkProgress bulk progress + * @return work provider + */ + protected abstract BatchProcessWorkProvider getWorkProvider(BulkOperation bulkOperation, + BulkStatusUpdater bulkStatusUpdater, BulkProgress bulkProgress); + + /** + * Get worker provider + * + * @param nodeRef node reference + * @param bulkOperation bulk operation + * @param bulkProgress bulk progress + * @return worker provider + */ + protected abstract BatchProcessWorker getWorkerProvider(NodeRef nodeRef, BulkOperation bulkOperation, + BulkProgress bulkProgress); + + /** + * 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); + searchParams.setLimit(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; + } + + public void setMaxParallelRequests(int maxParallelRequests) + { + this.maxParallelRequests = maxParallelRequests; + } +} diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/BulkCancellationRequest.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/BulkCancellationRequest.java new file mode 100644 index 0000000000..21f9c4a680 --- /dev/null +++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/BulkCancellationRequest.java @@ -0,0 +1,34 @@ +/* + * #%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; + +/** + * An immutable POJO to represent a bulk cancellation request + */ +public record BulkCancellationRequest(String reason) +{ +} 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 0000000000..cb5ce3e099 --- /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,83 @@ +/* + * #%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 + * @param bulkOperation the bulk operation + */ + void registerProcess(NodeRef nodeRef, String processId, BulkOperation bulkOperation); + + /** + * Get the bulk status + * + * @param bulkStatusId the bulk status id + * @return the bulk status + */ + T getBulkStatus(String bulkStatusId); + + /** + * Cancel a bulk operation + * + * @param bulkStatusId + * @param bulkCancellationRequest + */ + void cancelBulkOperation(String bulkStatusId, BulkCancellationRequest bulkCancellationRequest); + + /** + * Check if a bulk operation is cancelled + * + * @param bulkStatusId + * @return true if the bulk operation is cancelled + */ + boolean isCancelled(String bulkStatusId); + + /** + * Get the bulk cancellation request + * + * @param bulkStatusId + * @return cancellation reason + */ + BulkCancellationRequest getBulkCancellationRequest(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 0000000000..f5483220b3 --- /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,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; + +import java.io.Serializable; + +import org.alfresco.rest.api.search.model.Query; + +/** + * An immutable POJO to represent a bulk operation + */ +public record BulkOperation(Query searchQuery, String operationType) implements Serializable +{ + 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/BulkProgress.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/BulkProgress.java new file mode 100644 index 0000000000..3ee04df8ff --- /dev/null +++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/BulkProgress.java @@ -0,0 +1,37 @@ +/* + * #%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.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * An immutable POJO to represent the progress of a bulk operation + */ +public record BulkProgress(long totalItems, String processId, AtomicBoolean cancelled, AtomicInteger currentNodeNumber) +{ +} 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 0000000000..9dcc75a769 --- /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 0000000000..e02392a6c0 --- /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,165 @@ +/* + * #%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.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + +import org.alfresco.module.org_alfresco_module_rm.bulk.BulkCancellationRequest; +import org.alfresco.module.org_alfresco_module_rm.bulk.BulkOperation; +import org.alfresco.repo.cache.SimpleCache; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.util.Pair; +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 bulkCancellationsCache; + protected SimpleCache, HoldBulkProcessDetails> holdProcessRegistry; + + @Override + public void updateBulkStatus(HoldBulkStatus holdBulkStatus) + { + holdProgressCache.put(holdBulkStatus.bulkStatusId(), holdBulkStatus); + } + + @Override + public void registerProcess(NodeRef holdRef, String processId, BulkOperation bulkOperation) + { + if (holdRef != null && processId != null) + { + holdProcessRegistry.put(new Pair<>(holdRef.getId(), processId), + new HoldBulkProcessDetails(processId, getCurrentInstanceDetails(), bulkOperation)); + } + } + + @Override + public HoldBulkStatus getBulkStatus(String bulkStatusId) + { + return holdProgressCache.get(bulkStatusId); + } + + @Override + public void cancelBulkOperation(String bulkStatusId, BulkCancellationRequest bulkCancellationRequest) + { + bulkCancellationsCache.put(bulkStatusId, bulkCancellationRequest); + } + + @Override + public boolean isCancelled(String bulkStatusId) + { + return bulkCancellationsCache.contains(bulkStatusId); + } + + @Override + public BulkCancellationRequest getBulkCancellationRequest(String bulkStatusId) + { + return bulkCancellationsCache.get(bulkStatusId); + } + + @Override + public List getBulkStatusesWithProcessDetails(String holdId) + { + return holdProcessRegistry.getKeys().stream() + .filter(holdIdAndBulkStatusId -> holdId.equals(holdIdAndBulkStatusId.getFirst())) + .map(holdIdAndBulkStatusId -> holdProcessRegistry.get(holdIdAndBulkStatusId)) + .filter(Objects::nonNull) + .map(createHoldBulkStatusAndProcessDetails()) + .filter(statusAndProcess -> Objects.nonNull(statusAndProcess.holdBulkStatus())) + .sorted(sortBulkStatuses()) + .toList(); + } + + @Override + public HoldBulkStatusAndProcessDetails getBulkStatusWithProcessDetails(String holdId, String bulkStatusId) + { + return Optional.ofNullable(holdProcessRegistry.get(new Pair<>(holdId, bulkStatusId))) + .map(createHoldBulkStatusAndProcessDetails()) + .filter(statusAndProcess -> Objects.nonNull(statusAndProcess.holdBulkStatus())) + .orElse(null); + } + + protected String getCurrentInstanceDetails() + { + return null; + } + + protected Function createHoldBulkStatusAndProcessDetails() + { + return bulkProcessDetails -> new HoldBulkStatusAndProcessDetails( + getBulkStatus(bulkProcessDetails.bulkStatusId()), bulkProcessDetails); + } + + protected static Comparator sortBulkStatuses() + { + return Comparator.comparing( + statusAndProcess -> statusAndProcess.holdBulkStatus().endTime(), + Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(statusAndProcess -> statusAndProcess.holdBulkStatus().startTime(), + Comparator.nullsLast(Comparator.naturalOrder())) + .reversed(); + } + + public void setHoldProgressCache( + SimpleCache holdProgressCache) + { + this.holdProgressCache = holdProgressCache; + } + + public void setHoldProcessRegistry( + SimpleCache, HoldBulkProcessDetails> holdProcessRegistry) + { + this.holdProcessRegistry = holdProcessRegistry; + } + + public void setBulkCancellationsCache( + SimpleCache bulkCancellationsCache) + { + this.bulkCancellationsCache = bulkCancellationsCache; + } + + @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 0000000000..b7b094c3f9 --- /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,54 @@ +/* + * #%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; + +/** + * An interface for monitoring the progress of a bulk hold operation + */ +public interface HoldBulkMonitor extends BulkMonitor +{ + /** + * Get the bulk statuses with process details for a hold + * + * @param holdId the hold id + * @return the bulk statuses with process details + */ + List getBulkStatusesWithProcessDetails(String holdId); + + /** + * Get the bulk status with process details + * + * @param holdId the hold id + * @param bulkStatusId the bulk status id + * @return the bulk status with process details + */ + HoldBulkStatusAndProcessDetails getBulkStatusWithProcessDetails(String holdId, String bulkStatusId); +} 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 0000000000..f173e547cc --- /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,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.module.org_alfresco_module_rm.bulk.hold; + +import java.io.Serializable; + +import org.alfresco.module.org_alfresco_module_rm.bulk.BulkOperation; + +/** + * A simple immutable POJO to hold the details of a bulk process + */ +public record HoldBulkProcessDetails(String bulkStatusId, String creatorInstance, BulkOperation bulkOperation) 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 0000000000..c69c1453ad --- /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,55 @@ +/* + * #%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.BulkCancellationRequest; +import org.alfresco.module.org_alfresco_module_rm.bulk.BulkOperation; +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 + * @return The initial status of the bulk operation + */ + HoldBulkStatus execute(NodeRef holdRef, BulkOperation bulkOperation); + + /** + * Cancels a bulk operation. + * + * @param holdRef The hold reference + * @param bulkStatusId The bulk status id + * @param bulkCancellationRequest The bulk cancellation request + */ + void cancelBulkOperation(NodeRef holdRef, String bulkStatusId, BulkCancellationRequest bulkCancellationRequest); +} 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 0000000000..82df562e41 --- /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,286 @@ +/* + * #%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.Optional; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.bulk.BulkBaseService; +import org.alfresco.module.org_alfresco_module_rm.bulk.BulkCancellationRequest; +import org.alfresco.module.org_alfresco_module_rm.bulk.BulkOperation; +import org.alfresco.module.org_alfresco_module_rm.bulk.BulkProgress; +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.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 static final String MSG_ERR_ACCESS_DENIED = "permissions.err_access_denied"; + + private HoldService holdService; + 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, false, null); + } + + @Override + protected BulkStatusUpdater getBulkStatusUpdater() + { + return new HoldBulkStatusUpdater((HoldBulkMonitor) bulkMonitor); + } + + @Override + protected BatchProcessWorkProvider getWorkProvider(BulkOperation bulkOperation, + BulkStatusUpdater bulkStatusUpdater, BulkProgress bulkProgress) + { + return new AddToHoldWorkerProvider(bulkOperation, bulkStatusUpdater, bulkProgress, + (HoldBulkMonitor) bulkMonitor); + } + + @Override + protected BatchProcessWorker getWorkerProvider(NodeRef nodeRef, BulkOperation bulkOperation, + BulkProgress bulkProgress) + { + try + { + HoldBulkOperationType holdBulkOperationType = HoldBulkOperationType.valueOf(bulkOperation.operationType() + .toUpperCase(Locale.ENGLISH)); + return switch (holdBulkOperationType) + { + case ADD -> new AddToHoldWorkerBatch(nodeRef, bulkProgress); + }; + } + 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)); + } + + } + + @Override + public void cancelBulkOperation(NodeRef holdRef, String bulkStatusId, BulkCancellationRequest cancellationRequest) + { + if (bulkMonitor instanceof HoldBulkMonitor holdBulkMonitor) + { + HoldBulkStatusAndProcessDetails statusAndProcessDetails = holdBulkMonitor.getBulkStatusWithProcessDetails( + holdRef.getId(), bulkStatusId); + + Optional.ofNullable(statusAndProcessDetails).map(HoldBulkStatusAndProcessDetails::holdBulkProcessDetails) + .map(HoldBulkProcessDetails::bulkOperation).ifPresent(bulkOperation -> { + checkPermissions(holdRef, bulkOperation); + holdBulkMonitor.cancelBulkOperation(bulkStatusId, cancellationRequest); + }); + } + } + + private class AddToHoldWorkerBatch implements BatchProcessWorker + { + private final NodeRef holdRef; + private final String currentUser; + private final BulkProgress bulkProgress; + + public AddToHoldWorkerBatch(NodeRef holdRef, BulkProgress bulkProgress) + { + this.holdRef = holdRef; + this.bulkProgress = bulkProgress; + 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 + { + if (!bulkProgress.cancelled().get()) + { + AuthenticationUtil.setFullyAuthenticatedUser(currentUser); + holdService.addToHold(holdRef, entry); + } + } + + @Override + public void afterProcess() + { + AuthenticationUtil.popAuthentication(); + } + } + + private class AddToHoldWorkerProvider implements BatchProcessWorkProvider + { + private final HoldBulkMonitor holdBulkMonitor; + private final Query searchQuery; + private final String currentUser; + private final BulkProgress bulkProgress; + private final BulkStatusUpdater bulkStatusUpdater; + + public AddToHoldWorkerProvider(BulkOperation bulkOperation, + BulkStatusUpdater bulkStatusUpdater, BulkProgress bulkProgress, HoldBulkMonitor holdBulkMonitor) + { + this.searchQuery = bulkOperation.searchQuery(); + this.bulkProgress = bulkProgress; + this.bulkStatusUpdater = bulkStatusUpdater; + this.holdBulkMonitor = holdBulkMonitor; + currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + } + + @Override + public int getTotalEstimatedWorkSize() + { + return (int) bulkProgress.totalItems(); + } + + @Override + public long getTotalEstimatedWorkSizeLong() + { + return bulkProgress.totalItems(); + } + + @Override + public Collection getNextWork() + { + AuthenticationUtil.pushAuthentication(); + AuthenticationUtil.setFullyAuthenticatedUser(currentUser); + if (holdBulkMonitor.isCancelled(bulkProgress.processId())) + { + bulkProgress.cancelled().set(true); + return Collections.emptyList(); + } + 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()); + } + bulkProgress.currentNodeNumber().addAndGet(batchSize); + bulkStatusUpdater.update(); + return result.getNodeRefs(); + } + + private SearchParameters getNextPageParameters() + { + SearchParameters searchParams = new SearchParameters(); + searchMapper.setDefaults(searchParams); + searchMapper.fromQuery(searchParams, searchQuery); + searchParams.setSkipCount(bulkProgress.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/HoldBulkStatus.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkStatus.java new file mode 100644 index 0000000000..222d289635 --- /dev/null +++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkStatus.java @@ -0,0 +1,78 @@ +/* + * #%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; +import java.util.Date; + +/** + * An immutable POJO that contains the status of a hold bulk operation + */ +public record HoldBulkStatus(String bulkStatusId, Date startTime, Date endTime, long processedItems, long errorsCount, + long totalItems, String lastError, boolean isCancelled, String cancellationReason) + implements Serializable +{ + public enum Status + { + PENDING("PENDING"), + IN_PROGRESS("IN PROGRESS"), + DONE("DONE"), + CANCELLED("CANCELLED"); + + private final String value; + + Status(String value) + { + this.value = value; + } + + public String getValue() + { + return value; + } + } + + public String getStatus() + { + if (isCancelled) + { + return Status.CANCELLED.getValue(); + } + else 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/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkStatusAndProcessDetails.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkStatusAndProcessDetails.java new file mode 100644 index 0000000000..aa646e24be --- /dev/null +++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkStatusAndProcessDetails.java @@ -0,0 +1,35 @@ +/* + * #%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; + +/** + * An immutable POJO that contains the status of a hold bulk operation and the details of the process + */ +public record HoldBulkStatusAndProcessDetails(HoldBulkStatus holdBulkStatus, + HoldBulkProcessDetails holdBulkProcessDetails) +{ +} \ No newline at end of file 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 0000000000..909e8d59a1 --- /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,77 @@ +/* + * #%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.Optional; + +import org.alfresco.module.org_alfresco_module_rm.bulk.BulkCancellationRequest; +import org.alfresco.module.org_alfresco_module_rm.bulk.BulkStatusUpdater; +import org.alfresco.repo.batch.BatchMonitor; +import org.alfresco.repo.batch.BatchMonitorEvent; + +/** + * 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(), + holdBulkMonitor.isCancelled(batchMonitor.getProcessName()), + Optional.ofNullable(holdBulkMonitor.getBulkCancellationRequest(batchMonitor.getProcessName())) + .map(BulkCancellationRequest::reason) + .orElse(null))); + } + + @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/module/org_alfresco_module_rm/bulk/hold/HoldBulkUtils.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkUtils.java new file mode 100644 index 0000000000..9d4a0aed9b --- /dev/null +++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/bulk/hold/HoldBulkUtils.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.BulkOperation; +import org.alfresco.rest.api.search.model.Query; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rm.rest.api.model.HoldBulkOperation; +import org.alfresco.rm.rest.api.model.HoldBulkOperationType; +import org.alfresco.rm.rest.api.model.HoldBulkStatusEntry; + +/** + * Utility class for hold bulk operations + */ +@SuppressWarnings("PMD.PreserveStackTrace") +public final class HoldBulkUtils +{ + private HoldBulkUtils() + { + } + + public static HoldBulkStatusEntry toHoldBulkStatusEntry( + HoldBulkStatusAndProcessDetails holdBulkStatusAndProcessDetails) + { + HoldBulkStatus bulkStatus = holdBulkStatusAndProcessDetails.holdBulkStatus(); + BulkOperation bulkOperation = holdBulkStatusAndProcessDetails.holdBulkProcessDetails().bulkOperation(); + + try + { + HoldBulkOperation holdBulkOperation = new HoldBulkOperation( + new Query(bulkOperation.searchQuery().getLanguage(), + bulkOperation.searchQuery().getQuery(), bulkOperation.searchQuery().getUserQuery()), + HoldBulkOperationType.valueOf(bulkOperation.operationType())); + return new HoldBulkStatusEntry(bulkStatus.bulkStatusId(), bulkStatus.startTime(), + bulkStatus.endTime(), bulkStatus.processedItems(), bulkStatus.errorsCount(), + bulkStatus.totalItems(), bulkStatus.lastError(), bulkStatus.getStatus(), + bulkStatus.cancellationReason(), holdBulkOperation); + } + catch (IllegalArgumentException e) + { + String errorMsg = "Unsupported action type in the bulk operation: "; + throw new InvalidArgumentException(errorMsg + bulkOperation.operationType()); + } + } +} 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 0000000000..8f21bf2ee9 --- /dev/null +++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/holds/HoldsBulkStatusesRelation.java @@ -0,0 +1,163 @@ +/* + * #%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 jakarta.servlet.http.HttpServletResponse; +import org.alfresco.module.org_alfresco_module_rm.bulk.BulkCancellationRequest; +import org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkMonitor; +import org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkService; +import org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkStatusAndProcessDetails; +import org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkUtils; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.rest.framework.Operation; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +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.rest.framework.webscripts.WithResponse; +import org.alfresco.rm.rest.api.impl.FilePlanComponentsApiUtils; +import org.alfresco.rm.rest.api.model.BulkCancellationEntry; +import org.alfresco.rm.rest.api.model.HoldBulkStatusEntry; +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 HoldBulkService holdBulkService; + 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.getBulkStatusesWithProcessDetails(holdId); + List page = statuses.stream() + .map(HoldBulkUtils::toHoldBulkStatusEntry) + .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 HoldBulkStatusEntry 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.getBulkStatusWithProcessDetails(holdId, bulkStatusId)) + .map(HoldBulkUtils::toHoldBulkStatusEntry) + .orElseThrow(() -> new EntityNotFoundException(bulkStatusId)); + } + + @Operation("cancel") + @WebApiDescription(title = "Cancel a bulk operation", + successStatus = HttpServletResponse.SC_OK) + public void cancelBulkOperation(String holdId, String bulkStatusId, BulkCancellationEntry bulkCancellationEntry, + Parameters parameters, + WithResponse withResponse) + { + checkNotBlank("holdId", holdId); + checkNotBlank("bulkStatusId", bulkStatusId); + mandatory("parameters", parameters); + + NodeRef holdRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD); + + checkReadPermissions(holdRef); + + if (holdBulkMonitor.getBulkStatus(bulkStatusId) == null) + { + throw new NotFoundException("Bulk status not found"); + } + + holdBulkService.cancelBulkOperation(holdRef, bulkStatusId, new BulkCancellationRequest(bulkCancellationEntry.reason())); + } + + 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; + } + + 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/holds/HoldsEntityResource.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/holds/HoldsEntityResource.java index 2e8cdf9231..a7e8fe6dd5 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.module.org_alfresco_module_rm.util.RMParameterCheck.c 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.resource.parameters.Parameters; 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.module.org_alfresco_module_rm.bulk.hold.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 class HoldsEntityResource implements 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 class HoldsEntityResource implements { 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/BulkCancellationEntry.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/BulkCancellationEntry.java new file mode 100644 index 0000000000..940f910cbe --- /dev/null +++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/BulkCancellationEntry.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 BulkCancellationEntry(String reason) {} 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 0000000000..d6cd85edc0 --- /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 0000000000..a8d058eae9 --- /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 0000000000..3e8df3e134 --- /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/HoldBulkStatusEntry.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldBulkStatusEntry.java new file mode 100644 index 0000000000..2f37c9a3ea --- /dev/null +++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldBulkStatusEntry.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 java.util.Date; + +public record HoldBulkStatusEntry(String bulkStatusId, Date startTime, Date endTime, long processedItems, long errorsCount, + long totalItems, String lastError, String status, String cancellationReason, HoldBulkOperation holdBulkOperation) { +} diff --git a/amps/ags/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/bulk/hold/HoldBulkServiceTest.java b/amps/ags/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/bulk/hold/HoldBulkServiceTest.java new file mode 100644 index 0000000000..6eca9fcf26 --- /dev/null +++ b/amps/ags/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/bulk/hold/HoldBulkServiceTest.java @@ -0,0 +1,306 @@ +/* + * #%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.test.integration.bulk.hold; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.bulk.BulkCancellationRequest; +import org.alfresco.module.org_alfresco_module_rm.bulk.BulkOperation; +import org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkMonitor; +import org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkServiceImpl; +import org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkStatus; +import org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkStatus.Status; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase; +import org.alfresco.rest.api.search.model.Query; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchService; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; +import org.springframework.extensions.webscripts.GUID; + +/** + * Hold bulk service integration test. + */ +@SuppressWarnings({ "PMD.TestClassWithoutTestCases", "PMD.JUnit4TestShouldUseTestAnnotation" }) +public class HoldBulkServiceTest extends BaseRMTestCase +{ + private static final int RECORD_COUNT = 10; + private final SearchService searchServiceMock = mock(SearchService.class); + private final ResultSet resultSet = mock(ResultSet.class); + private HoldBulkServiceImpl holdBulkService; + private HoldBulkMonitor holdBulkMonitor; + + @Override + protected void initServices() + { + super.initServices(); + holdBulkMonitor = (HoldBulkMonitor) applicationContext.getBean("holdBulkMonitor"); + holdBulkService = (HoldBulkServiceImpl) applicationContext.getBean("holdBulkService"); + holdBulkService.setSearchService(searchServiceMock); + Mockito.when(searchServiceMock.query(any(SearchParameters.class))).thenReturn(resultSet); + } + + public void testCancelBulkOperation() + { + doBehaviourDrivenTest(new BehaviourDrivenTest() + { + private NodeRef hold; + private HoldBulkStatus holdBulkStatus; + private final ResultSet resultSet = mock(ResultSet.class); + + public void given() + { + Mockito.when(resultSet.getNumberFound()).thenReturn(4L); + Mockito.when(resultSet.hasMore()).thenReturn(false).thenReturn(true).thenReturn(false); + Mockito.when(resultSet.getNodeRefs()) + .thenAnswer((Answer>) invocationOnMock -> { + await().pollDelay(1, SECONDS).until(() -> true); + return List.of(new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, GUID.generate()), + new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, GUID.generate())); + }); + // create a hold + hold = holdService.createHold(filePlan, GUID.generate(), GUID.generate(), GUID.generate()); + } + + public void when() + { + BulkOperation bulkOperation = new BulkOperation(new Query("afts", "*", ""), "ADD"); + // execute the bulk operation + holdBulkStatus = holdBulkService.execute(hold, bulkOperation); + // cancel the bulk operation + holdBulkMonitor.cancelBulkOperation(holdBulkStatus.bulkStatusId(), + new BulkCancellationRequest("No reason")); + await().atMost(10, SECONDS) + .until(() -> Objects.equals( + holdBulkMonitor.getBulkStatus(holdBulkStatus.bulkStatusId()).getStatus(), + Status.CANCELLED.getValue())); + } + + public void then() + { + holdBulkStatus = holdBulkMonitor.getBulkStatus(holdBulkStatus.bulkStatusId()); + assertNotNull(holdBulkStatus.startTime()); + assertNotNull(holdBulkStatus.endTime()); + assertEquals(holdBulkMonitor.getBulkStatus(holdBulkStatus.bulkStatusId()).getStatus(), + HoldBulkStatus.Status.CANCELLED.getValue()); + assertEquals(holdBulkMonitor.getBulkStatus(holdBulkStatus.bulkStatusId()).cancellationReason(), + "No reason"); + } + }); + } + + public void testAddRecordsToHoldViaBulk() + { + doBehaviourDrivenTest(new BehaviourDrivenTest() + { + private NodeRef hold; + private NodeRef recordFolder; + private HoldBulkStatus holdBulkStatus; + private final List records = new ArrayList<>(RECORD_COUNT); + + public void given() + { + Mockito.when(resultSet.getNumberFound()).thenReturn(Long.valueOf(RECORD_COUNT)); + Mockito.when(resultSet.hasMore()).thenReturn(false).thenReturn(false); + // create a hold + hold = holdService.createHold(filePlan, GUID.generate(), GUID.generate(), GUID.generate()); + + // create a record folder that contains records + NodeRef recordCategory = filePlanService.createRecordCategory(filePlan, GUID.generate()); + recordFolder = recordFolderService.createRecordFolder(recordCategory, GUID.generate()); + for (int i = 0; i < RECORD_COUNT; i++) + { + records.add( + recordService.createRecordFromContent(recordFolder, GUID.generate(), ContentModel.TYPE_CONTENT, + null, null)); + } + Mockito.when(resultSet.getNodeRefs()).thenReturn(records).thenReturn(records) + .thenReturn(Collections.emptyList()); + + // assert current states + assertFalse(freezeService.isFrozen(recordFolder)); + assertFalse(freezeService.hasFrozenChildren(recordFolder)); + for (NodeRef record : records) + { + assertFalse(freezeService.isFrozen(record)); + } + + // additional check for child held caching + assertTrue(nodeService.hasAspect(recordFolder, ASPECT_HELD_CHILDREN)); + assertEquals(0, nodeService.getProperty(recordFolder, PROP_HELD_CHILDREN_COUNT)); + } + + public void when() + { + BulkOperation bulkOperation = new BulkOperation(new Query("afts", "*", ""), "ADD"); + // execute the bulk operation + holdBulkStatus = holdBulkService.execute(hold, bulkOperation); + await().atMost(10, SECONDS) + .until(() -> Objects.equals( + holdBulkMonitor.getBulkStatus(holdBulkStatus.bulkStatusId()).getStatus(), + Status.DONE.getValue())); + } + + public void then() + { + holdBulkStatus = holdBulkMonitor.getBulkStatus(holdBulkStatus.bulkStatusId()); + assertNotNull(holdBulkStatus.startTime()); + assertNotNull(holdBulkStatus.endTime()); + assertEquals(RECORD_COUNT, holdBulkStatus.totalItems()); + assertEquals(RECORD_COUNT, holdBulkStatus.processedItems()); + assertEquals(0, holdBulkStatus.errorsCount()); + assertEquals(holdBulkMonitor.getBulkStatus(holdBulkStatus.bulkStatusId()).getStatus(), + HoldBulkStatus.Status.DONE.getValue()); + + // record is held + for (NodeRef record : records) + { + assertTrue(freezeService.isFrozen(record)); + } + + // record folder has frozen children + assertFalse(freezeService.isFrozen(recordFolder)); + assertTrue(freezeService.hasFrozenChildren(recordFolder)); + + // record folder is not held + assertFalse(holdService.getHeld(hold).contains(recordFolder)); + assertFalse(holdService.heldBy(recordFolder, true).contains(hold)); + + for (NodeRef record : records) + { + // hold contains record + assertTrue(holdService.getHeld(hold).contains(record)); + assertTrue(holdService.heldBy(record, true).contains(hold)); + } + + // additional check for child held caching + assertTrue(nodeService.hasAspect(recordFolder, ASPECT_HELD_CHILDREN)); + assertEquals(RECORD_COUNT, nodeService.getProperty(recordFolder, PROP_HELD_CHILDREN_COUNT)); + } + }); + } + + public void testAddRecordFolderToHoldViaBulk() + { + doBehaviourDrivenTest(new BehaviourDrivenTest() + { + private NodeRef hold; + private NodeRef recordFolder; + private final List records = new ArrayList<>(RECORD_COUNT); + private HoldBulkStatus holdBulkStatus; + + public void given() + { + Mockito.when(resultSet.getNumberFound()).thenReturn(1L); + Mockito.when(resultSet.hasMore()).thenReturn(false).thenReturn(false); + // create a hold + hold = holdService.createHold(filePlan, GUID.generate(), GUID.generate(), GUID.generate()); + + // create a record folder that contains records + NodeRef recordCategory = filePlanService.createRecordCategory(filePlan, GUID.generate()); + recordFolder = recordFolderService.createRecordFolder(recordCategory, GUID.generate()); + for (int i = 0; i < RECORD_COUNT; i++) + { + records.add( + recordService.createRecordFromContent(recordFolder, GUID.generate(), ContentModel.TYPE_CONTENT, + null, null)); + } + Mockito.when(resultSet.getNodeRefs()).thenReturn(Collections.singletonList(recordFolder)) + .thenReturn(Collections.singletonList(recordFolder)).thenReturn(Collections.emptyList()); + + // assert current states + assertFalse(freezeService.isFrozen(recordFolder)); + assertFalse(freezeService.hasFrozenChildren(recordFolder)); + for (NodeRef record : records) + { + assertFalse(freezeService.isFrozen(record)); + } + + // additional check for child held caching + assertTrue(nodeService.hasAspect(recordFolder, ASPECT_HELD_CHILDREN)); + assertEquals(0, nodeService.getProperty(recordFolder, PROP_HELD_CHILDREN_COUNT)); + } + + public void when() + { + BulkOperation bulkOperation = new BulkOperation(new Query("afts", "*", ""), "ADD"); + // execute the bulk operation + holdBulkStatus = holdBulkService.execute(hold, bulkOperation); + await().atMost(10, SECONDS) + .until(() -> Objects.equals( + holdBulkMonitor.getBulkStatus(holdBulkStatus.bulkStatusId()).getStatus(), + Status.DONE.getValue())); + } + + public void then() + { + holdBulkStatus = holdBulkMonitor.getBulkStatus(holdBulkStatus.bulkStatusId()); + assertNotNull(holdBulkStatus.startTime()); + assertNotNull(holdBulkStatus.endTime()); + assertEquals(1, holdBulkStatus.totalItems()); + assertEquals(1, holdBulkStatus.processedItems()); + assertEquals(0, holdBulkStatus.errorsCount()); + assertEquals(holdBulkMonitor.getBulkStatus(holdBulkStatus.bulkStatusId()).getStatus(), + HoldBulkStatus.Status.DONE.getValue()); + + for (NodeRef record : records) + { + // record is held + assertTrue(freezeService.isFrozen(record)); + assertFalse(holdService.getHeld(hold).contains(record)); + assertTrue(holdService.heldBy(record, true).contains(hold)); + } + + // record folder has frozen children + assertTrue(freezeService.isFrozen(recordFolder)); + assertTrue(freezeService.hasFrozenChildren(recordFolder)); + + // hold contains record folder + assertTrue(holdService.getHeld(hold).contains(recordFolder)); + assertTrue(holdService.heldBy(recordFolder, true).contains(hold)); + + // additional check for child held caching + assertTrue(nodeService.hasAspect(recordFolder, ASPECT_HELD_CHILDREN)); + assertEquals(RECORD_COUNT, nodeService.getProperty(recordFolder, PROP_HELD_CHILDREN_COUNT)); + } + }); + + } +} 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 0000000000..fef0eb6e4d --- /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,164 @@ +/* + * #%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.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; + +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.module.org_alfresco_module_rm.bulk.hold.HoldBulkStatus; +import org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkStatusAndProcessDetails; +import org.alfresco.repo.cache.SimpleCache; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.util.Pair; +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, HoldBulkProcessDetails> 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, false, 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(new Pair<>(holdRef.getId(), processId))).thenReturn(null); + + holdBulkMonitor.registerProcess(holdRef, processId, null); + + Mockito.verify(holdProcessRegistry) + .put(new Pair<>(holdRef.getId(), processId), new HoldBulkProcessDetails(processId, null, null)); + } + + @Test + public void testGetBulkStatusesWithProcessDetailsReturnsEmptyListWhenNoProcessesWithProcessDetails() + { + when(holdProcessRegistry.getKeys()).thenReturn(Collections.emptyList()); + assertEquals(Collections.emptyList(), holdBulkMonitor.getBulkStatusesWithProcessDetails("holdId")); + } + + @Test + public void testGetBulkStatus() + { + BulkOperation bulkOperation = mock(BulkOperation.class); + HoldBulkStatus status1 = new HoldBulkStatus("process1", new Date(1000), new Date(2000), 0L, 0L, 0L, null, false, + null); + when(holdProcessRegistry.get(new Pair<>("holdId", "process1"))).thenReturn( + new HoldBulkProcessDetails("process1", null, bulkOperation)); + when(holdProgressCache.get("process1")).thenReturn(status1); + + assertEquals(new HoldBulkStatusAndProcessDetails(status1, + new HoldBulkProcessDetails(status1.bulkStatusId(), null, bulkOperation)), + holdBulkMonitor.getBulkStatusWithProcessDetails("holdId", "process1")); + } + + @Test + public void testGetNonExistingBulkStatus() + { + BulkOperation bulkOperation = mock(BulkOperation.class); + when(holdProcessRegistry.get(new Pair<>("holdId", "process1"))).thenReturn( + new HoldBulkProcessDetails("process1", null, bulkOperation)); + when(holdProgressCache.get("process1")).thenReturn(null); + + assertNull(holdBulkMonitor.getBulkStatusWithProcessDetails("holdId", "process1")); + } + + @Test + public void testGetBulkStatusesForHoldReturnsSortedStatusesWithProcessDetails() + { + BulkOperation bulkOperation = mock(BulkOperation.class); + HoldBulkStatus status1 = new HoldBulkStatus("process1", new Date(1000), new Date(2000), 0L, 0L, 0L, null, false, + null); + HoldBulkStatus status2 = new HoldBulkStatus("process2", new Date(3000), null, 0L, 0L, 0L, null, false, null); + HoldBulkStatus status3 = new HoldBulkStatus("process3", new Date(4000), null, 0L, 0L, 0L, null, false, null); + HoldBulkStatus status4 = new HoldBulkStatus("process4", new Date(500), new Date(800), 0L, 0L, 0L, null, false, + null); + HoldBulkStatus status5 = new HoldBulkStatus("process5", null, null, 0L, 0L, 0L, null, false, null); + + when(holdProcessRegistry.getKeys()).thenReturn( + Arrays.asList(new Pair<>("holdId", "process1"), new Pair<>("holdId", "process2"), + new Pair<>("holdId", "process3"), new Pair<>("holdId", "process4"), new Pair<>("holdId", "process5")) + ); + when(holdProcessRegistry.get(new Pair<>("holdId", "process1"))).thenReturn( + new HoldBulkProcessDetails("process1", null, bulkOperation)); + when(holdProcessRegistry.get(new Pair<>("holdId", "process2"))).thenReturn( + new HoldBulkProcessDetails("process2", null, bulkOperation)); + when(holdProcessRegistry.get(new Pair<>("holdId", "process3"))).thenReturn( + new HoldBulkProcessDetails("process3", null, bulkOperation)); + when(holdProcessRegistry.get(new Pair<>("holdId", "process4"))).thenReturn( + new HoldBulkProcessDetails("process4", null, bulkOperation)); + when(holdProcessRegistry.get(new Pair<>("holdId", "process5"))).thenReturn( + new HoldBulkProcessDetails("process5", null, bulkOperation)); + 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).stream().map( + status -> new HoldBulkStatusAndProcessDetails(status, + new HoldBulkProcessDetails(status.bulkStatusId(), null, bulkOperation))).toList(), + holdBulkMonitor.getBulkStatusesWithProcessDetails("holdId")); + } +} \ No newline at end of file diff --git a/amps/ags/rm-community/rm-community-rest-api-explorer/src/main/webapp/definitions/gs-core-api.yaml b/amps/ags/rm-community/rm-community-rest-api-explorer/src/main/webapp/definitions/gs-core-api.yaml index 92bac98ac2..41c728fc7e 100644 --- a/amps/ags/rm-community/rm-community-rest-api-explorer/src/main/webapp/definitions/gs-core-api.yaml +++ b/amps/ags/rm-community/rm-community-rest-api-explorer/src/main/webapp/definitions/gs-core-api.yaml @@ -2314,6 +2314,145 @@ paths: description: Unexpected error schema: $ref: '#/definitions/Error' + '/holds/{holdId}/bulk-statuses': + get: + tags: + - holds + operationId: listBulkStatuses + summary: Get bulk statuses + description: | + Gets bulk statuses for hold with id **holdId**. + parameters: + - $ref: '#/parameters/holdIdParam' + - $ref: '#/parameters/skipCountParam' + - $ref: '#/parameters/maxItemsParam' + responses: + '200': + description: Successful response + schema: + $ref: '#/definitions/HoldBulkStatusPaging' + '400': + description: | + Invalid parameter: **holdId** is not a valid format + '401': + description: Authentication failed + '403': + description: Current user does not have permission to read **holdId** + '404': + description: "**holdId** does not exist" + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + '/holds/{holdId}/bulk-statuses/{bulkStatusId}': + get: + tags: + - holds + operationId: getBulkStatus + summary: Get a bulk status + description: | + Gets a bulk status specified by **bulkStatusId** for **holdId**. + parameters: + - $ref: '#/parameters/holdIdParam' + - $ref: '#/parameters/bulkStatusId' + responses: + '200': + description: Successful response + schema: + $ref: '#/definitions/HoldBulkStatus' + '400': + description: | + Invalid parameter: **holdId** or **bulkStatusId** is not a valid format + '401': + description: Authentication failed + '403': + description: Current user does not have permission to read **holdId** + '404': + description: "**holdId** or **bulkStatusId** does not exist" + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + '/holds/{holdId}/bulk-statuses/{bulkStatusId}/cancel': + post: + tags: + - holds + operationId: cancelBulkStatus + summary: Cancel the bulk operation + description: | + Cancels the bulk operation specified by **bulkStatusId** for **holdId**. + parameters: + - $ref: '#/parameters/holdIdParam' + - $ref: '#/parameters/bulkStatusId' + - in: body + name: cancelReason + description: Cancel reason. + required: false + schema: + $ref: '#/definitions/BulkBodyCancel' + responses: + '200': + description: Successful response + '400': + description: | + Invalid parameter: **holdId** or **bulkStatusId** is not a valid format + '401': + description: Authentication failed + '403': + description: Current user does not have permission to cancel the bulk process for **holdId** and **bulkStatusId** + '404': + description: "**holdId** or **bulkStatusId** does not exist" + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + '/holds/{holdId}/bulk': + post: + tags: + - holds + operationId: startHoldBulkProcess + summary: Start the hold bulk process + description: | + Start the asynchronous bulk process for a hold with id **holdId** based on search query results. + + ```JSON + For example, the following JSON body starts the bulk process to add search query results + as children of a hold. + + { + "query": { + "query": "SITE:swsdp and TYPE:content", + "language": "afts" + }, + "op": "ADD" + } + ``` + parameters: + - $ref: '#/parameters/holdIdParam' + - in: body + name: holdBulkOperation + description: Bulk operation. + required: true + schema: + $ref: '#/definitions/HoldBulkOperation' + responses: + '202': + description: Successful response + schema: + $ref: '#/definitions/HoldBulkOperationEntry' + '400': + description: | + Invalid parameter: **holdId** is not a valid format or **HoldBulkOperation** is not valid + '401': + description: Authentication failed + '403': + description: Current user does not have permission to start the bulk process for **holdId** + '404': + description: "**holdId** does not exist" + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' '/holds/{holdId}/delete': post: tags: @@ -2862,6 +3001,12 @@ parameters: description: The identifier of a child of a hold. required: true type: string + bulkStatusId: + name: bulkStatusId + in: path + description: The identifier of a bulk process. + required: true + type: string ## Record recordIdParam: name: recordId @@ -4018,6 +4163,101 @@ definitions: properties: reason: type: string + SearchRequestQuery: + type: object + required: + - query + properties: + language: + description: The query language in which the query is written. + type: string + default: afts + enum: + - afts + - lucene + - cmis + userQuery: + description: The search request typed in by the user + type: string + query: + description: The query which may have been generated in some way from the userQuery + type: string + HoldBulkOperation: + type: object + properties: + query: + $ref: '#/definitions/SearchRequestQuery' + op: + description: The operation type. + type: string + default: ADD + enum: + - ADD + HoldBulkOperationEntry: + type: object + properties: + bulkStatusId: + type: string + totalItems: + type: integer + format: int64 + BulkBodyCancel: + type: object + properties: + reason: + type: string + HoldBulkStatus: + type: object + properties: + bulkStatusId: + type: string + startTime: + type: string + format: date-time + endTime: + type: string + format: date-time + processedItems: + type: integer + format: int64 + errorsCount: + type: integer + format: int64 + totalItems: + type: integer + format: int64 + lastError: + type: string + status: + type: string + enum: + - PENDING + - IN PROGRESS + - DONE + - CANCELLED + cancellationReason: + type: string + holdBulkOperation: + $ref: '#/definitions/HoldBulkOperation' + HoldBulkStatusEntry: + type: object + required: + - entry + properties: + entry: + $ref: '#/definitions/HoldBulkStatus' + HoldBulkStatusPaging: + type: object + properties: + list: + type: object + properties: + pagination: + $ref: '#/definitions/Pagination' + entries: + type: array + items: + $ref: '#/definitions/HoldBulkStatusEntry' ## RequestBodyFile: type: object diff --git a/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/search/RestRequestQueryModel.java b/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/search/RestRequestQueryModel.java index 3138a75c7d..e167e660e2 100644 --- a/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/search/RestRequestQueryModel.java +++ b/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/search/RestRequestQueryModel.java @@ -2,7 +2,7 @@ * #%L * alfresco-tas-restapi * %% - * Copyright (C) 2005 - 2022 Alfresco Software Limited + * 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 @@ -25,6 +25,8 @@ */ package org.alfresco.rest.search; +import java.util.Objects; + import com.fasterxml.jackson.annotation.JsonProperty; import org.alfresco.rest.core.IRestModel; @@ -93,6 +95,28 @@ public class RestRequestQueryModel extends TestModel implements IRestModel