holdsListRef = new ArrayList<>();
private FileModel heldContent;
private String hold1NodeRef;
+ private String hold2NodeRef;
+ private String hold3NodeRef;
@BeforeClass (alwaysRun = true)
public void preconditionForAuditRemoveFromHoldTests()
@@ -111,10 +111,18 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
privateSite = dataSite.usingUser(rmAdmin).createPrivateRandomSite();
STEP("Create new holds.");
- hold1NodeRef = holdsAPI.createHoldAndGetNodeRef(getAdminUser().getUsername(), getAdminUser().getPassword(),
- HOLD1, HOLD_REASON, HOLD_DESCRIPTION);
- String hold2NodeRef = holdsAPI.createHoldAndGetNodeRef(getAdminUser().getUsername(), getAdminUser().getPassword(), HOLD2, HOLD_REASON, HOLD_DESCRIPTION);
- String hold3NodeRef = holdsAPI.createHoldAndGetNodeRef(getAdminUser().getUsername(), getAdminUser().getPassword(), HOLD3, HOLD_REASON, HOLD_DESCRIPTION);
+ hold1NodeRef = getRestAPIFactory()
+ .getFilePlansAPI(rmAdmin)
+ .createHold(Hold.builder().name(HOLD1).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), FILE_PLAN_ALIAS)
+ .getId();
+ hold2NodeRef = getRestAPIFactory()
+ .getFilePlansAPI(rmAdmin)
+ .createHold(Hold.builder().name(HOLD2).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), FILE_PLAN_ALIAS)
+ .getId();
+ hold3NodeRef = getRestAPIFactory()
+ .getFilePlansAPI(rmAdmin)
+ .createHold(Hold.builder().name(HOLD3).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), FILE_PLAN_ALIAS)
+ .getId();
holdsListRef = asList(hold1NodeRef, hold2NodeRef, hold3NodeRef);
STEP("Create a new record category with a record folder.");
@@ -127,9 +135,12 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
heldRecordFolder = createRecordFolder(recordCategory.getId(), PREFIX + "heldRecFolder");
heldRecord = createElectronicRecord(recordFolder.getId(), PREFIX + "record");
- holdsAPI.addItemsToHolds(getAdminUser().getUsername(), getAdminUser().getPassword(),
- asList(heldContent.getNodeRefWithoutVersion(), heldRecordFolder.getId(), heldRecord.getId()),
- holdsList);
+ holdsListRef.forEach(holdRef ->
+ {
+ getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(heldContent.getNodeRefWithoutVersion()).build(), holdRef);
+ getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(heldRecordFolder.getId()).build(), holdRef);
+ getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(heldRecord.getId()).build(), holdRef);
+ });
STEP("Create users without rights to remove content from a hold.");
rmManagerNoReadOnHold = roleService.createUserWithSiteRoleRMRoleAndPermission(privateSite,
@@ -179,7 +190,7 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
rmAuditService.clearAuditLog();
STEP("Remove node from hold.");
- holdsAPI.removeItemFromHold(rmAdmin.getUsername(), rmAdmin.getPassword(), nodeId, HOLD3);
+ getRestAPIFactory().getHoldsAPI(rmAdmin).deleteHoldChild(hold3NodeRef, nodeId);
STEP("Check the audit log contains the entry for the remove from hold event.");
rmAuditService.checkAuditLogForEvent(getAdminUser(), REMOVE_FROM_HOLD, rmAdmin, nodeName, nodePath,
@@ -198,9 +209,8 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
rmAuditService.clearAuditLog();
STEP("Try to remove the record from a hold by an user with no rights.");
- holdsAPI.removeItemsFromHolds(rmManagerNoReadOnHold.getUsername(), rmManagerNoReadOnHold.getPassword(),
- SC_INTERNAL_SERVER_ERROR, Collections.singletonList(heldRecord.getId()),
- Collections.singletonList(hold1NodeRef));
+ getRestAPIFactory().getHoldsAPI(rmManagerNoReadOnHold).deleteHoldChild(hold1NodeRef, heldRecord.getId());
+ assertStatusCode(FORBIDDEN);
STEP("Check the audit log doesn't contain the entry for the unsuccessful remove from hold.");
assertTrue("The list of events should not contain remove from hold entry ",
@@ -220,12 +230,12 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
Record record = createElectronicRecord(notEmptyRecFolder.getId(), PREFIX + "record");
STEP("Add the record folder to a hold.");
- holdsAPI.addItemToHold(rmAdmin.getUsername(), rmAdmin.getPassword(), notEmptyRecFolder.getId(), HOLD1);
+ getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(notEmptyRecFolder.getId()).build(), hold1NodeRef);
rmAuditService.clearAuditLog();
STEP("Remove record folder from hold.");
- holdsAPI.removeItemFromHold(rmAdmin.getUsername(), rmAdmin.getPassword(), notEmptyRecFolder.getId(), HOLD1);
+ getRestAPIFactory().getHoldsAPI(rmAdmin).deleteHoldChild(hold1NodeRef, notEmptyRecFolder.getId());
STEP("Get the list of audit entries for the remove from hold event.");
auditEntries = rmAuditService.getAuditEntriesFilteredByEvent(getAdminUser(), REMOVE_FROM_HOLD);
@@ -247,8 +257,8 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
rmAuditService.clearAuditLog();
STEP("Remove record folder from multiple holds.");
- holdsAPI.removeItemsFromHolds(rmAdmin.getUsername(), rmAdmin.getPassword(),
- Collections.singletonList(heldRecordFolder.getId()), asList(HOLD1, HOLD2));
+ getRestAPIFactory().getHoldsAPI(rmAdmin).deleteHoldChild(hold1NodeRef, heldRecordFolder.getId());
+ getRestAPIFactory().getHoldsAPI(rmAdmin).deleteHoldChild(hold2NodeRef, heldRecordFolder.getId());
STEP("Get the list of audit entries for the remove from hold event.");
auditEntries = rmAuditService.getAuditEntriesFilteredByEvent(getAdminUser(), REMOVE_FROM_HOLD);
@@ -275,12 +285,12 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
STEP("Add content to a hold.");
FileModel heldFile = dataContent.usingAdmin().usingSite(privateSite)
.createContent(CMISUtil.DocumentType.TEXT_PLAIN);
- holdsAPI.addItemToHold(rmAdmin.getUsername(), rmAdmin.getPassword(), heldFile.getNodeRefWithoutVersion(), HOLD1);
+ getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(heldFile.getNodeRefWithoutVersion()).build(), hold1NodeRef);
rmAuditService.clearAuditLog();
STEP("Remove held content from the hold.");
- holdsAPI.removeItemFromHold(rmAdmin.getUsername(), rmAdmin.getPassword(), heldFile.getNodeRefWithoutVersion(), HOLD1);
+ getRestAPIFactory().getHoldsAPI(rmAdmin).deleteHoldChild(hold1NodeRef, heldFile.getNodeRefWithoutVersion());
STEP("Check that an user with no Read permissions can't see the entry for the remove from hold event.");
assertTrue("The list of events should not contain Remove from Hold entry ",
@@ -298,12 +308,12 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
STEP("Add content to a hold.");
FileModel heldFile = dataContent.usingAdmin().usingSite(privateSite)
.createContent(CMISUtil.DocumentType.TEXT_PLAIN);
- holdsAPI.addItemToHold(rmAdmin.getUsername(), rmAdmin.getPassword(), heldFile.getNodeRefWithoutVersion(), HOLD1);
+ getRestAPIFactory().getHoldsAPI(rmAdmin).addChildToHold(HoldChild.builder().id(heldFile.getNodeRefWithoutVersion()).build(), hold1NodeRef);
rmAuditService.clearAuditLog();
STEP("Remove held content from the hold.");
- holdsAPI.removeItemFromHold(rmAdmin.getUsername(), rmAdmin.getPassword(), heldFile.getNodeRefWithoutVersion(), HOLD1);
+ getRestAPIFactory().getHoldsAPI(rmAdmin).deleteHoldChild(hold1NodeRef, heldFile.getNodeRefWithoutVersion());
auditEntries = rmAuditService.getAuditEntriesFilteredByEvent(rmManagerNoReadOnHold, REMOVE_FROM_HOLD);
@@ -318,7 +328,7 @@ public class AuditRemoveFromHoldTests extends BaseRMRestTest
@AfterClass (alwaysRun = true)
public void cleanUpAuditRemoveFromHoldTests()
{
- holdsListRef.forEach(holdRef -> holdsAPI.deleteHold(getAdminUser(), holdRef));
+ holdsListRef.forEach(holdRef -> getRestAPIFactory().getHoldsAPI(rmAdmin).deleteHold(holdRef));
dataSite.usingAdmin().deleteSite(privateSite);
asList(rmAdmin, rmManagerNoReadOnHold, rmManagerNoReadOnNode).forEach(user -> getDataUser().usingAdmin().deleteUser(user));
deleteRecordCategory(recordCategory.getId());
diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/fileplans/FilePlanTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/fileplans/FilePlanTests.java
index 3ed53671dd..4ad252ef94 100644
--- a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/fileplans/FilePlanTests.java
+++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/fileplans/FilePlanTests.java
@@ -60,12 +60,15 @@ import static org.testng.Assert.fail;
import static org.testng.AssertJUnit.assertEquals;
import java.util.ArrayList;
+import java.util.List;
import java.util.NoSuchElementException;
import org.alfresco.rest.rm.community.base.BaseRMRestTest;
import org.alfresco.rest.rm.community.base.DataProviderClass;
import org.alfresco.rest.rm.community.model.fileplan.FilePlan;
import org.alfresco.rest.rm.community.model.fileplan.FilePlanProperties;
+import org.alfresco.rest.rm.community.model.hold.Hold;
+import org.alfresco.rest.rm.community.model.hold.HoldCollection;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryCollection;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryProperties;
@@ -514,5 +517,97 @@ public class FilePlanTests extends BaseRMRestTest
);
}
+ /**
+ *
+ * Given that a file plan exists
+ * When I ask the API to create a hold
+ * Then it is created
+ *
+ */
+ @Test
+ public void createHolds()
+ {
+ String holdName = "Hold" + getRandomAlphanumeric();
+ String holdDescription = "Description" + getRandomAlphanumeric();
+ String holdReason = "Reason" + getRandomAlphanumeric();
+ // Create the hold
+ Hold hold = Hold.builder()
+ .name(holdName)
+ .description(holdDescription)
+ .reason(holdReason)
+ .build();
+ Hold createdHold = getRestAPIFactory().getFilePlansAPI()
+ .createHold(hold, FILE_PLAN_ALIAS);
+
+ // Verify the status code
+ assertStatusCode(CREATED);
+
+ assertEquals(createdHold.getName(), holdName);
+ assertEquals(createdHold.getDescription(), holdDescription);
+ assertEquals(createdHold.getReason(), holdReason);
+ assertNotNull(createdHold.getId());
+ }
+
+
+ @Test
+ public void listHolds()
+ {
+ // Delete all holds
+ getRestAPIFactory().getFilePlansAPI().getHolds(FILE_PLAN_ALIAS).getEntries().forEach(holdEntry ->
+ getRestAPIFactory().getHoldsAPI().deleteHold(holdEntry.getEntry().getId()));
+
+ // Add holds
+ List filePlanHolds = new ArrayList<>();
+ for (int i = 0; i < NUMBER_OF_CHILDREN; i++)
+ {
+ String holdName = "Hold name " + getRandomAlphanumeric();
+ String holdDescription = "Hold Description " + getRandomAlphanumeric();
+ String holdReason = "Reason " + getRandomAlphanumeric();
+ // Create a hold
+ Hold hold = Hold.builder()
+ .name(holdName)
+ .description(holdDescription)
+ .reason(holdReason)
+ .build();
+ Hold createdHold = getRestAPIFactory().getFilePlansAPI()
+ .createHold(hold, FILE_PLAN_ALIAS);
+ assertNotNull(createdHold.getId());
+ filePlanHolds.add(createdHold);
+ }
+
+ // Get holds of a file plan
+ HoldCollection holdCollection = getRestAPIFactory().getFilePlansAPI()
+ .getHolds(FILE_PLAN_ALIAS);
+
+ // Check status code
+ assertStatusCode(OK);
+
+ // Check holds against created list
+ holdCollection.getEntries().forEach(c ->
+ {
+ Hold hold = c.getEntry();
+ String holdId = hold.getId();
+ assertNotNull(holdId);
+ logger.info("Checking hold " + holdId);
+
+ try
+ {
+ // Find this hold in created holds list
+ Hold createdHold = filePlanHolds.stream()
+ .filter(child -> child.getId().equals(holdId))
+ .findFirst()
+ .orElseThrow();
+
+ assertEquals(createdHold.getName(), hold.getName());
+ assertEquals(createdHold.getDescription(), hold.getDescription());
+ assertEquals(createdHold.getReason(), hold.getReason());
+ }
+ catch (NoSuchElementException e)
+ {
+ fail("No child element for " + hold);
+ }
+ }
+ );
+ }
}
diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/AddToHoldsTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/AddToHoldsTests.java
index 28f9cada61..aaa52f71c0 100644
--- a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/AddToHoldsTests.java
+++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/AddToHoldsTests.java
@@ -60,7 +60,7 @@ import org.alfresco.dataprep.CMISUtil;
import org.alfresco.dataprep.ContentActions;
import org.alfresco.rest.model.RestNodeModel;
import org.alfresco.rest.rm.community.base.BaseRMRestTest;
-import org.alfresco.rest.rm.community.model.hold.HoldEntry;
+import org.alfresco.rest.rm.community.model.hold.v0.HoldEntry;
import org.alfresco.rest.rm.community.model.record.Record;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild;
diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/AddToHoldsV1Tests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/AddToHoldsV1Tests.java
new file mode 100644
index 0000000000..3c240e9704
--- /dev/null
+++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/AddToHoldsV1Tests.java
@@ -0,0 +1,386 @@
+/*-
+ * #%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.fileplancomponents.FilePlanComponentAlias.TRANSFERS_ALIAS;
+import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAlias.UNFILED_RECORDS_CONTAINER_ALIAS;
+import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAspects.FROZEN_ASPECT;
+import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentType.UNFILED_RECORD_FOLDER_TYPE;
+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.model.user.UserRoles.ROLE_RM_MANAGER;
+import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix;
+import static org.alfresco.rest.rm.community.utils.CoreUtil.toContentModel;
+import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.IMAGE_FILE;
+import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.createElectronicRecordModel;
+import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.createNonElectronicRecordModel;
+import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.getFile;
+import static org.alfresco.utility.data.RandomData.getRandomAlphanumeric;
+import static org.alfresco.utility.report.log.Step.STEP;
+import static org.apache.commons.httpclient.HttpStatus.SC_BAD_REQUEST;
+import static org.springframework.http.HttpStatus.CREATED;
+import static org.springframework.http.HttpStatus.FORBIDDEN;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.AssertJUnit.assertFalse;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.alfresco.dataprep.CMISUtil;
+import org.alfresco.dataprep.ContentActions;
+import org.alfresco.rest.model.RestNodeAssociationModelCollection;
+import org.alfresco.rest.model.RestNodeModel;
+import org.alfresco.rest.rm.community.base.BaseRMRestTest;
+import org.alfresco.rest.rm.community.model.hold.Hold;
+import org.alfresco.rest.rm.community.model.hold.HoldChild;
+import org.alfresco.rest.rm.community.model.record.Record;
+import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory;
+import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild;
+import org.alfresco.rest.rm.community.model.user.UserRoles;
+import org.alfresco.rest.rm.community.requests.gscore.api.FilePlanAPI;
+import org.alfresco.rest.rm.community.requests.gscore.api.RecordFolderAPI;
+import org.alfresco.rest.v0.service.RoleService;
+import org.alfresco.utility.constants.UserRole;
+import org.alfresco.utility.model.FileModel;
+import org.alfresco.utility.model.SiteModel;
+import org.alfresco.utility.model.UserModel;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * V1 API tests for adding content/record folder/records to holds
+ *
+ * @author Damian Ujma
+ */
+public class AddToHoldsV1Tests 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 String INVALID_TYPE_ERROR_MESSAGE = "Only records, record folders or content can be added to a hold.";
+ private static final String LOCKED_FILE_ERROR_MESSAGE = "Locked content can't be added to a hold.";
+
+ private static final String HOLD = "HOLD" + generateTestPrefix(AddToHoldsV1Tests.class);
+ private String holdNodeRef;
+ private SiteModel testSite;
+ private FileModel documentHeld;
+ private FileModel contentToAddToHold;
+ private FileModel contentAddToHoldNoPermission;
+ private Hold hold;
+
+ private UserModel userAddHoldPermission;
+ private final List users = new ArrayList<>();
+ private final List nodesToBeClean = new ArrayList<>();
+
+ @Autowired
+ private RoleService roleService;
+ @Autowired
+ private ContentActions contentActions;
+
+ @BeforeClass(alwaysRun = true)
+ public void preconditionForAddContentToHold()
+ {
+ STEP("Create a hold.");
+ hold = createHold(FILE_PLAN_ALIAS,
+ Hold.builder().name(HOLD).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), getAdminUser());
+ holdNodeRef = hold.getId();
+ STEP("Create test files.");
+ testSite = dataSite.usingAdmin().createPublicRandomSite();
+ documentHeld = dataContent.usingAdmin().usingSite(testSite)
+ .createContent(CMISUtil.DocumentType.TEXT_PLAIN);
+ contentToAddToHold = dataContent.usingAdmin().usingSite(testSite)
+ .createContent(CMISUtil.DocumentType.TEXT_PLAIN);
+ contentAddToHoldNoPermission = dataContent.usingAdmin().usingSite(testSite)
+ .createContent(CMISUtil.DocumentType.TEXT_PLAIN);
+
+ STEP("Add the content to the hold.");
+ getRestAPIFactory()
+ .getHoldsAPI(getAdminUser())
+ .addChildToHold(HoldChild.builder().id(documentHeld.getNodeRefWithoutVersion()).build(), hold.getId());
+
+ STEP("Create users");
+ userAddHoldPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite,
+ UserRole.SiteCollaborator, holdNodeRef, UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING);
+ users.add(userAddHoldPermission);
+ }
+
+ /**
+ * Given a hold that contains at least one active content
+ * When I use the existing REST API to retrieve the contents of the hold
+ * Then I should see all the active content on hold
+ */
+ @Test
+ public void retrieveTheContentOfTheHoldUsingV1API()
+ {
+ STEP("Retrieve the list of children from the hold and collect the entries that have the name of the active " +
+ "content held");
+ List documentNames = restClient.authenticateUser(getAdminUser()).withCoreAPI()
+ .usingNode(toContentModel(holdNodeRef))
+ .listChildren().getEntries().stream()
+ .map(RestNodeModel::onModel)
+ .map(RestNodeModel::getName)
+ .filter(documentName -> documentName.equals(documentHeld.getName()))
+ .toList();
+
+ STEP("Check the list of active content");
+ assertEquals(documentNames, Set.of(documentHeld.getName()));
+ }
+
+ /**
+ * Given a hold that contains at least one active content
+ * When I use the existing REST API to retrieve the holds the content is added
+ * Then the hold where the content held is returned
+ */
+ @Test
+ public void retrieveTheHoldWhereTheContentIsAdded()
+ {
+ RestNodeAssociationModelCollection holdsEntries = getRestAPIFactory()
+ .getNodeAPI(documentHeld).usingParams("where=(assocType='rma:frozenContent')").getParents();
+ Hold retrievedHold = getRestAPIFactory().getHoldsAPI(getAdminUser())
+ .getHold(holdsEntries.getEntries().get(0).getModel().getId());
+ assertEquals(retrievedHold, hold, "Holds are not equal");
+ }
+
+ /**
+ * Valid nodes to be added to hold
+ */
+ @DataProvider(name = "validNodesForAddToHold")
+ public Object[][] getValidNodesForAddToHold()
+ {
+ //create electronic and nonElectronic record in record folder
+ RecordCategoryChild recordFolder = createCategoryFolderInFilePlan();
+ RecordFolderAPI recordFolderAPI = getRestAPIFactory().getRecordFolderAPI();
+ nodesToBeClean.add(recordFolder.getParentId());
+ Record electronicRecord = recordFolderAPI.createRecord(createElectronicRecordModel(), recordFolder.getId(),
+ getFile
+ (IMAGE_FILE));
+ assertStatusCode(CREATED);
+
+ Record nonElectronicRecord = recordFolderAPI.createRecord(createNonElectronicRecordModel(),
+ recordFolder.getId());
+ assertStatusCode(CREATED);
+ getRestAPIFactory().getRMUserAPI().addUserPermission(recordFolder.getId(), userAddHoldPermission,
+ PERMISSION_FILING);
+
+ RecordCategoryChild folderToHold = createCategoryFolderInFilePlan();
+ getRestAPIFactory().getRMUserAPI().addUserPermission(folderToHold.getId(), userAddHoldPermission,
+ PERMISSION_FILING);
+ nodesToBeClean.add(folderToHold.getParentId());
+
+ return new String[][]
+ { // record folder
+ { folderToHold.getId() },
+ //electronic record
+ { electronicRecord.getId() },
+ // non electronic record
+ { nonElectronicRecord.getId() },
+ // document from collaboration site
+ { contentToAddToHold.getNodeRefWithoutVersion() },
+ };
+ }
+
+ /**
+ * Given record folder/record/document not on hold
+ * And a hold
+ * And file permission on the hold
+ * And the appropriate capability to add to hold
+ * When I use the existing REST API to add the node to the hold
+ * Then the record folder/record/document is added to the hold
+ * And the item is frozen
+ *
+ * @throws Exception
+ */
+ @Test(dataProvider = "validNodesForAddToHold")
+ public void addValidNodesToHoldWithAllowedUser(String nodeId) throws Exception
+ {
+ STEP("Add node to hold with user with permission.");
+ getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
+ .addChildToHold(HoldChild.builder().id(nodeId).build(), hold.getId());
+
+ STEP("Check the node is frozen.");
+ assertTrue(hasAspect(nodeId, FROZEN_ASPECT));
+ }
+
+ /**
+ * Data provider with user without correct permission to add to hold and the node ref to be added to hold
+ *
+ * @return object with user model and the node ref to be added to hold
+ */
+ @DataProvider(name = "userWithoutPermissionForAddToHold")
+ public Object[][] getUserWithoutPermissionForAddToHold()
+ {
+ //create record folder
+ RecordCategoryChild recordFolder = createCategoryFolderInFilePlan();
+ //create a rm manager and grant read permission over the record folder created
+ UserModel user = roleService.createUserWithRMRoleAndRMNodePermission(ROLE_RM_MANAGER.roleId,
+ recordFolder.getId(),
+ PERMISSION_READ_RECORDS);
+ getRestAPIFactory().getRMUserAPI().addUserPermission(holdNodeRef, user, PERMISSION_FILING);
+ nodesToBeClean.add(recordFolder.getParentId());
+ return new Object[][]
+ { // user without write permission on the content
+ {
+ roleService.createUserWithSiteRoleRMRoleAndPermission(testSite, UserRole.SiteConsumer,
+ holdNodeRef, UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING),
+ contentAddToHoldNoPermission.getNodeRefWithoutVersion()
+ },
+ // user with write permission on the content and without filling permission on a hold
+ {
+ roleService.createUserWithSiteRoleRMRoleAndPermission(testSite, UserRole
+ .SiteCollaborator,
+ holdNodeRef, UserRoles.ROLE_RM_MANAGER, PERMISSION_READ_RECORDS),
+ contentAddToHoldNoPermission.getNodeRefWithoutVersion()
+ },
+ // user with write permission on the content, filling permission on a hold without add to
+ // hold capability
+ {
+ roleService.createUserWithSiteRoleRMRoleAndPermission(testSite, UserRole
+ .SiteCollaborator,
+ holdNodeRef, UserRoles.ROLE_RM_POWER_USER, PERMISSION_READ_RECORDS),
+ contentAddToHoldNoPermission.getNodeRefWithoutVersion()
+ },
+ //user without write permission on RM record folder
+ {
+ user, recordFolder.getId()
+ },
+
+ };
+ }
+
+ /**
+ * Given a node not on hold
+ * And a hold
+ * And user without right permission to add to hold
+ * When I use the existing REST API to add the node to the hold
+ * Then the node is not added to the hold
+ * And the node is not frozen
+ *
+ * @throws Exception
+ */
+ @Test(dataProvider = "userWithoutPermissionForAddToHold")
+ public void addContentToHoldWithUserWithoutHoldPermission(UserModel userModel, String nodeToBeAddedToHold)
+ throws Exception
+ {
+ users.add(userModel);
+ STEP("Add the node to the hold with user without permission.");
+
+ getRestAPIFactory()
+ .getHoldsAPI(userModel)
+ .addChildToHold(HoldChild.builder().id(nodeToBeAddedToHold).build(), holdNodeRef);
+
+ assertStatusCode(FORBIDDEN);
+ getRestAPIFactory().getRmRestWrapper().assertLastError().containsSummary(ACCESS_DENIED_ERROR_MESSAGE);
+
+ STEP("Check the node is not frozen.");
+ assertFalse(hasAspect(nodeToBeAddedToHold, FROZEN_ASPECT));
+ }
+
+ /**
+ * Data provider with invalid node types that can be added to a hold
+ */
+ @DataProvider(name = "invalidNodesForAddToHold")
+ public Object[][] getInvalidNodesForAddToHold()
+ {
+ //create locked file
+ FileModel contentLocked = dataContent.usingAdmin().usingSite(testSite)
+ .createContent(CMISUtil.DocumentType.TEXT_PLAIN);
+
+ contentActions.checkOut(getAdminUser().getUsername(), getAdminUser().getPassword(),
+ testSite.getId(), contentLocked.getName());
+ RecordCategory category = createRootCategory(getRandomAlphanumeric());
+ nodesToBeClean.add(category.getId());
+ return new Object[][]
+ { // file plan node id
+ { getFilePlan(FILE_PLAN_ALIAS).getId(), SC_BAD_REQUEST, INVALID_TYPE_ERROR_MESSAGE },
+ //transfer container
+ { getTransferContainer(TRANSFERS_ALIAS).getId(), SC_BAD_REQUEST, INVALID_TYPE_ERROR_MESSAGE },
+ // a record category
+ { category.getId(), SC_BAD_REQUEST, INVALID_TYPE_ERROR_MESSAGE },
+ // unfiled records root
+ { getUnfiledContainer(UNFILED_RECORDS_CONTAINER_ALIAS).getId(), SC_BAD_REQUEST,
+ INVALID_TYPE_ERROR_MESSAGE },
+ // an arbitrary unfiled records folder
+ { createUnfiledContainerChild(UNFILED_RECORDS_CONTAINER_ALIAS, "Unfiled Folder " +
+ getRandomAlphanumeric(), UNFILED_RECORD_FOLDER_TYPE).getId(), SC_BAD_REQUEST,
+ INVALID_TYPE_ERROR_MESSAGE },
+ //folder,
+ { dataContent.usingAdmin().usingSite(testSite).createFolder().getNodeRef(), SC_BAD_REQUEST,
+ INVALID_TYPE_ERROR_MESSAGE },
+ //document locked
+ { contentLocked.getNodeRefWithoutVersion(), SC_BAD_REQUEST, LOCKED_FILE_ERROR_MESSAGE }
+ };
+ }
+
+ /**
+ * Given a node that is not a document/record/ record folder ( a valid node type to be added to hold)
+ * And a hold
+ * And user without right permission to add to hold
+ * When I use the existing REST API to add the node to the hold
+ * Then the node is not added to the hold
+ * And the node is not frozen
+ *
+ * @throws Exception
+ */
+ @Test(dataProvider = "invalidNodesForAddToHold")
+ public void addInvalidNodesToHold(String itemNodeRef, int responseCode, String errorMessage) throws Exception
+ {
+ STEP("Add the node to the hold ");
+
+ getRestAPIFactory()
+ .getHoldsAPI(getAdminUser())
+ .addChildToHold(HoldChild.builder().id(itemNodeRef).build(), holdNodeRef);
+
+ assertStatusCode(HttpStatus.valueOf(responseCode));
+ getRestAPIFactory().getRmRestWrapper().assertLastError().containsSummary(errorMessage);
+
+ STEP("Check node is not frozen.");
+ assertFalse(hasAspect(itemNodeRef, FROZEN_ASPECT));
+ }
+
+ private Hold createHold(String parentId, Hold hold, UserModel user)
+ {
+ FilePlanAPI filePlanAPI = getRestAPIFactory().getFilePlansAPI(user);
+ return filePlanAPI.createHold(hold, parentId);
+ }
+
+ @AfterClass(alwaysRun = true)
+ public void cleanUpAddContentToHold()
+ {
+ getRestAPIFactory().getHoldsAPI(getAdminUser()).deleteHold(holdNodeRef);
+ dataSite.usingAdmin().deleteSite(testSite);
+ users.forEach(user -> getDataUser().usingAdmin().deleteUser(user));
+ nodesToBeClean.forEach(this::deleteRecordCategory);
+ }
+}
diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/HoldsTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/HoldsTests.java
new file mode 100644
index 0000000000..661dc247ef
--- /dev/null
+++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/HoldsTests.java
@@ -0,0 +1,186 @@
+/*-
+ * #%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.model.fileplancomponents.FilePlanComponentAlias.FILE_PLAN_ALIAS;
+import static org.alfresco.utility.data.RandomData.getRandomAlphanumeric;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.springframework.http.HttpStatus.NOT_FOUND;
+import static org.springframework.http.HttpStatus.NO_CONTENT;
+import static org.springframework.http.HttpStatus.OK;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.alfresco.rest.rm.community.base.BaseRMRestTest;
+import org.alfresco.rest.rm.community.model.hold.Hold;
+import org.alfresco.rest.rm.community.model.hold.HoldDeletionReason;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.Test;
+
+/**
+ * This class contains the tests for the Holds CRUD V1 API
+ *
+ * @author Damian Ujma
+ */
+public class HoldsTests extends BaseRMRestTest
+{
+
+ private final List nodeRefs = new ArrayList<>();
+
+ @Test
+ public void testGetHold()
+ {
+ String holdName = "Hold" + getRandomAlphanumeric();
+ String holdDescription = "Description" + getRandomAlphanumeric();
+ String holdReason = "Reason" + getRandomAlphanumeric();
+
+ // Create the hold
+ Hold hold = Hold.builder()
+ .name(holdName)
+ .description(holdDescription)
+ .reason(holdReason)
+ .build();
+ Hold createdHold = getRestAPIFactory().getFilePlansAPI()
+ .createHold(hold, FILE_PLAN_ALIAS);
+
+ // Get the hold
+ Hold receivedHold = getRestAPIFactory().getHoldsAPI().getHold(createdHold.getId());
+ nodeRefs.add(receivedHold.getId());
+
+ // Verify the status code
+ assertStatusCode(OK);
+
+ assertEquals(receivedHold.getName(), holdName);
+ assertEquals(receivedHold.getDescription(), holdDescription);
+ assertEquals(receivedHold.getReason(), holdReason);
+ assertNotNull(receivedHold.getId());
+ }
+
+ @Test
+ public void testUpdateHold()
+ {
+ String holdName = "Hold" + getRandomAlphanumeric();
+ String holdDescription = "Description" + getRandomAlphanumeric();
+ String holdReason = "Reason" + getRandomAlphanumeric();
+
+ // Create the hold
+ Hold hold = Hold.builder()
+ .name(holdName)
+ .description(holdDescription)
+ .reason(holdReason)
+ .build();
+ Hold createdHold = getRestAPIFactory().getFilePlansAPI()
+ .createHold(hold, FILE_PLAN_ALIAS);
+ nodeRefs.add(createdHold.getId());
+
+ Hold holdModel = Hold.builder()
+ .name("Updated" + holdName)
+ .description("Updated" + holdDescription)
+ .reason("Updated" + holdReason)
+ .build();
+
+ // Update the hold
+ Hold updatedHold = getRestAPIFactory().getHoldsAPI().updateHold(holdModel, createdHold.getId());
+
+ // Verify the status code
+ assertStatusCode(OK);
+
+ assertEquals(updatedHold.getName(), "Updated" + holdName);
+ assertEquals(updatedHold.getDescription(), "Updated" + holdDescription);
+ assertEquals(updatedHold.getReason(), "Updated" + holdReason);
+ assertNotNull(updatedHold.getId());
+ }
+
+ @Test
+ public void testDeleteHold()
+ {
+ String holdName = "Hold" + getRandomAlphanumeric();
+ String holdDescription = "Description" + getRandomAlphanumeric();
+ String holdReason = "Reason" + getRandomAlphanumeric();
+
+ // Create the hold
+ Hold hold = Hold.builder()
+ .name(holdName)
+ .description(holdDescription)
+ .reason(holdReason)
+ .build();
+ Hold createdHold = getRestAPIFactory().getFilePlansAPI()
+ .createHold(hold, FILE_PLAN_ALIAS);
+ nodeRefs.add(createdHold.getId());
+
+ // Delete the hold
+ getRestAPIFactory().getHoldsAPI().deleteHold(createdHold.getId());
+
+ // Verify the status code
+ assertStatusCode(NO_CONTENT);
+
+ // Try to get the hold
+ getRestAPIFactory().getHoldsAPI().getHold(createdHold.getId());
+
+ // Verify the status code
+ assertStatusCode(NOT_FOUND);
+ }
+
+ @Test
+ public void testDeleteHoldWithReason()
+ {
+ String holdName = "Hold" + getRandomAlphanumeric();
+ String holdDescription = "Description" + getRandomAlphanumeric();
+ String holdReason = "Reason" + getRandomAlphanumeric();
+
+ // Create the hold
+ Hold hold = Hold.builder()
+ .name(holdName)
+ .description(holdDescription)
+ .reason(holdReason)
+ .build();
+ Hold createdHold = getRestAPIFactory().getFilePlansAPI()
+ .createHold(hold, FILE_PLAN_ALIAS);
+ nodeRefs.add(createdHold.getId());
+
+ // Delete the hold with the reason
+ getRestAPIFactory().getHoldsAPI()
+ .deleteHoldWithReason(HoldDeletionReason.builder().reason("Example reason").build(), createdHold.getId());
+
+ // Verify the status code
+ assertStatusCode(OK);
+
+ // Try to get the hold
+ getRestAPIFactory().getHoldsAPI().getHold(createdHold.getId());
+
+ // Verify the status code
+ assertStatusCode(NOT_FOUND);
+ }
+
+ @AfterClass(alwaysRun = true)
+ public void cleanUpHoldsTests()
+ {
+ nodeRefs.forEach(nodeRef -> getRestAPIFactory().getHoldsAPI(getAdminUser()).deleteHold(nodeRef));
+ }
+}
diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/PreventActionsOnFrozenContentV1Tests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/PreventActionsOnFrozenContentV1Tests.java
new file mode 100644
index 0000000000..defad62b04
--- /dev/null
+++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/PreventActionsOnFrozenContentV1Tests.java
@@ -0,0 +1,337 @@
+/*-
+ * #%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.fileplancomponents.FilePlanComponentAspects.ASPECTS_VITAL_RECORD;
+import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAspects.ASPECTS_VITAL_RECORD_DEFINITION;
+import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix;
+import static org.alfresco.rest.rm.community.utils.CoreUtil.createBodyForMoveCopy;
+import static org.alfresco.utility.data.RandomData.getRandomName;
+import static org.alfresco.utility.report.log.Step.STEP;
+import static org.apache.commons.httpclient.HttpStatus.SC_INTERNAL_SERVER_ERROR;
+import static org.springframework.http.HttpStatus.CREATED;
+import static org.springframework.http.HttpStatus.FORBIDDEN;
+import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
+import static org.springframework.http.HttpStatus.OK;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.AssertJUnit.assertFalse;
+
+import java.io.File;
+
+import jakarta.json.Json;
+import jakarta.json.JsonObject;
+import org.alfresco.dataprep.CMISUtil;
+import org.alfresco.rest.core.JsonBodyGenerator;
+import org.alfresco.rest.core.v0.BaseAPI.RM_ACTIONS;
+import org.alfresco.rest.rm.community.base.BaseRMRestTest;
+import org.alfresco.rest.rm.community.model.common.ReviewPeriod;
+import org.alfresco.rest.rm.community.model.hold.Hold;
+import org.alfresco.rest.rm.community.model.hold.HoldChild;
+import org.alfresco.rest.rm.community.model.record.Record;
+import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory;
+import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild;
+import org.alfresco.rest.rm.community.model.recordfolder.RecordFolder;
+import org.alfresco.rest.rm.community.model.recordfolder.RecordFolderProperties;
+import org.alfresco.rest.rm.community.requests.gscore.api.FilePlanAPI;
+import org.alfresco.rest.v0.RMRolesAndActionsAPI;
+import org.alfresco.rest.v0.service.DispositionScheduleService;
+import org.alfresco.utility.Utility;
+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;
+
+/**
+ * V1 API tests to check actions on frozen content
+ *
+ * @author Damian Ujma
+ */
+public class PreventActionsOnFrozenContentV1Tests extends BaseRMRestTest
+{
+ private static String holdNodeRef;
+ private static FileModel contentHeld;
+ private static File updatedFile;
+ private static FolderModel folderModel;
+ private static RecordCategoryChild recordFolder;
+ private static Record recordFrozen;
+ private static Record recordNotHeld;
+ private static RecordCategory categoryWithRS;
+
+ private Hold hold;
+
+ @Autowired
+ private DispositionScheduleService dispositionScheduleService;
+
+ @Autowired
+ private RMRolesAndActionsAPI rmRolesAndActionsAPI;
+
+ @BeforeClass(alwaysRun = true)
+ public void preconditionForPreventActionsOnFrozenContent()
+ {
+ String holdOne = "HOLD" + generateTestPrefix(PreventActionsOnFrozenContentV1Tests.class);
+
+ STEP("Create a hold.");
+ hold = createHold(FILE_PLAN_ALIAS,
+ Hold.builder().name(holdOne).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(), getAdminUser());
+ holdNodeRef = hold.getId();
+
+ STEP("Create a test file.");
+ testSite = dataSite.usingAdmin().createPublicRandomSite();
+ contentHeld = dataContent.usingAdmin().usingSite(testSite)
+ .createContent(CMISUtil.DocumentType.TEXT_PLAIN);
+
+ STEP("Add the file to the hold.");
+ getRestAPIFactory()
+ .getHoldsAPI(getAdminUser())
+ .addChildToHold(HoldChild.builder().id(contentHeld.getNodeRefWithoutVersion()).build(), hold.getId());
+
+ STEP("Get a file resource.");
+ updatedFile = Utility.getResourceTestDataFile("SampleTextFile_10kb.txt");
+
+ STEP("Create a folder withing the test site .");
+ folderModel = dataContent.usingAdmin().usingSite(testSite)
+ .createFolder();
+
+ STEP("Create a record folder with some records");
+ recordFolder = createCategoryFolderInFilePlan();
+ recordFrozen = createElectronicRecord(recordFolder.getId(), getRandomName("elRecordFrozen"));
+ recordNotHeld = createElectronicRecord(recordFolder.getId(), getRandomName("elRecordNotHeld"));
+ assertStatusCode(CREATED);
+
+ STEP("Add the record to the hold.");
+ getRestAPIFactory()
+ .getHoldsAPI(getAdminUser())
+ .addChildToHold(HoldChild.builder().id(recordFrozen.getId()).build(), hold.getId());
+ }
+
+ /**
+ * Given active content on hold
+ * When I try to edit the properties
+ * Or perform an action that edits the properties
+ * Then I am not successful
+ */
+ @Test
+ public void editPropertiesForContentHeld() throws Exception
+ {
+ STEP("Update name property of the held content");
+ JsonObject nameUpdated = Json.createObjectBuilder().add("name", "HeldNameUpdated").build();
+ restClient.authenticateUser(getAdminUser()).withCoreAPI().usingNode(contentHeld)
+ .updateNode(nameUpdated.toString());
+
+ STEP("Check the request failed.");
+ restClient.assertStatusCodeIs(FORBIDDEN);
+ restClient.assertLastError().containsSummary("Frozen content can't be updated.");
+ }
+
+ /*
+ * Given active content on hold
+ * When I try to update the content
+ * Then I am not successful
+ */
+ @Test
+ public void updateContentForFrozenFile() throws Exception
+ {
+ STEP("Update content of the held file");
+ restClient.authenticateUser(getAdminUser()).withCoreAPI().usingNode(contentHeld).updateNodeContent(updatedFile);
+
+ STEP("Check the request failed.");
+ restClient.assertStatusCodeIs(INTERNAL_SERVER_ERROR);
+ restClient.assertLastError().containsSummary("Frozen content can't be updated.");
+ }
+
+ /*
+ * Given active content on hold
+ * When I try to delete the content
+ * Then I am not successful
+ */
+ @Test
+ public void deleteFrozenFile() throws Exception
+ {
+ STEP("Delete frozen file");
+ restClient.authenticateUser(getAdminUser()).withCoreAPI().usingNode(contentHeld)
+ .deleteNode(contentHeld.getNodeRefWithoutVersion());
+
+ STEP("Check the request failed.");
+ restClient.assertStatusCodeIs(FORBIDDEN);
+ restClient.assertLastError().containsSummary("Frozen content can't be deleted.");
+ }
+
+ /**
+ * Given active content on hold
+ * When I try to copy the content
+ * Then I am not successful
+ */
+ @Test
+ public void copyFrozenFile()
+ {
+ STEP("Copy frozen file");
+ String postBody = JsonBodyGenerator.keyValueJson("targetParentId", folderModel.getNodeRef());
+ getRestAPIFactory().getNodeAPI(contentHeld).copyNode(postBody);
+
+ STEP("Check the request failed.");
+ assertStatusCode(FORBIDDEN);
+ getRestAPIFactory().getRmRestWrapper().assertLastError().containsSummary("Permission was denied");
+ }
+
+ /**
+ * Given active content on hold
+ * When I try to move the content
+ * Then I am not successful
+ */
+ @Test
+ public void moveFrozenFile() throws Exception
+ {
+ STEP("Move frozen file");
+ getRestAPIFactory().getNodeAPI(contentHeld).move(createBodyForMoveCopy(folderModel.getNodeRef()));
+
+ STEP("Check the request failed.");
+ assertStatusCode(FORBIDDEN);
+ getRestAPIFactory().getRmRestWrapper().assertLastError().containsSummary("Frozen content can't be moved.");
+ }
+
+ /**
+ * Given a record folder with a frozen record and another record not held
+ * When I update the record folder and make the records as vital
+ * Then I am successful and the records not held are marked as vital
+ * And the frozen nodes have the vital record search properties updated
+ */
+ @Test
+ public void updateRecordFolderVitalProperties()
+ {
+ STEP("Update the vital record properties for the record folder");
+ // Create the record folder properties to update
+ RecordFolder recordFolderToUpdate = RecordFolder.builder()
+ .properties(RecordFolderProperties.builder()
+ .vitalRecordIndicator(true)
+ .reviewPeriod(new ReviewPeriod("month", "1"))
+ .build())
+ .build();
+
+ // Update the record folder
+ RecordFolder updatedRecordFolder = getRestAPIFactory().getRecordFolderAPI().updateRecordFolder
+ (recordFolderToUpdate,
+ recordFolder.getId());
+ assertStatusCode(OK);
+ assertTrue(updatedRecordFolder.getAspectNames().contains(ASPECTS_VITAL_RECORD_DEFINITION));
+
+ STEP("Check the frozen record was not marked as vital");
+ recordFrozen = getRestAPIFactory().getRecordsAPI().getRecord(recordFrozen.getId());
+ assertFalse(recordFrozen.getAspectNames().contains(ASPECTS_VITAL_RECORD));
+ assertTrue(recordFrozen.getProperties().getRecordSearchVitalRecordReviewPeriod().contains("month"));
+ assertTrue(recordFrozen.getProperties().getRecordSearchVitalRecordReviewPeriodExpression().contains("1"));
+
+ STEP("Check the record not held was marked as vital");
+ recordNotHeld = getRestAPIFactory().getRecordsAPI().getRecord(recordNotHeld.getId());
+ assertTrue(recordNotHeld.getAspectNames().contains(ASPECTS_VITAL_RECORD));
+ assertNotNull(recordNotHeld.getProperties().getReviewAsOf());
+ assertTrue(recordNotHeld.getProperties().getRecordSearchVitalRecordReviewPeriod().contains("month"));
+ assertTrue(recordNotHeld.getProperties().getRecordSearchVitalRecordReviewPeriodExpression().contains("1"));
+ }
+
+ /**
+ * Given a record folder with a frozen record and another record not held
+ * When I add a disposition schedule
+ * Then I am successful
+ * And the record search disposition schedule properties are updated
+ */
+ @Test
+ public void createDispositionScheduleOnCategoryWithHeldChildren()
+ {
+ STEP("Create a retention schedule on the category with frozen children");
+ RecordCategory categoryWithRS = getRestAPIFactory().getRecordCategoryAPI()
+ .getRecordCategory(recordFolder.getParentId());
+ dispositionScheduleService.createCategoryRetentionSchedule(categoryWithRS.getName(), false);
+ dispositionScheduleService.addCutOffImmediatelyStep(categoryWithRS.getName());
+ dispositionScheduleService.addDestroyWithGhostingImmediatelyAfterCutOff(categoryWithRS.getName());
+
+ STEP("Check the record folder has a disposition schedule");
+ RecordFolder folderWithRS = getRestAPIFactory().getRecordFolderAPI().getRecordFolder(recordFolder.getId());
+ assertNotNull(folderWithRS.getProperties().getRecordSearchDispositionAuthority());
+ assertNotNull(folderWithRS.getProperties().getRecordSearchDispositionInstructions());
+
+ }
+
+ /**
+ * Given a record category with a disposition schedule applied to records
+ * And the disposition schedule has a retain step immediately and destroy step immediately
+ * And a complete record added to one hold
+ * When I execute the retain action
+ * Then the action is executed
+ * And the record search disposition schedule properties are updated
+ */
+ @Test
+ public void retainActionOnFrozenHeldRecords()
+ {
+ STEP("Add a category with a disposition schedule.");
+ categoryWithRS = createRootCategory(getRandomName("CategoryWithRS"));
+ dispositionScheduleService.createCategoryRetentionSchedule(categoryWithRS.getName(), true);
+ dispositionScheduleService.addRetainAfterPeriodStep(categoryWithRS.getName(), "immediately");
+ dispositionScheduleService.addDestroyWithGhostingImmediatelyAfterCutOff(categoryWithRS.getName());
+
+ STEP("Create record folder with a record.");
+ RecordCategoryChild folder = createFolder(categoryWithRS.getId(), getRandomName("RecFolder"));
+ Record record = createElectronicRecord(folder.getId(), getRandomName("elRecord"));
+ completeRecord(record.getId());
+
+ STEP("Add the record to the hold");
+ getRestAPIFactory()
+ .getHoldsAPI(getAdminUser())
+ .addChildToHold(HoldChild.builder().id(record.getId()).build(), hold.getId());
+
+ STEP("Execute the retain action");
+ rmRolesAndActionsAPI.executeAction(getAdminUser().getUsername(), getAdminUser().getPassword(), record.getName(),
+ RM_ACTIONS.END_RETENTION, null, SC_INTERNAL_SERVER_ERROR);
+
+ STEP("Check the record search disposition properties");
+ Record recordUpdated = getRestAPIFactory().getRecordsAPI().getRecord(record.getId());
+ assertTrue(recordUpdated.getProperties().getRecordSearchDispositionActionName()
+ .contains(RM_ACTIONS.END_RETENTION.getAction()));
+ assertTrue(recordUpdated.getProperties().getRecordSearchDispositionPeriod().contains("immediately"));
+ }
+
+ private Hold createHold(String parentId, Hold hold, UserModel user)
+ {
+ FilePlanAPI filePlanAPI = getRestAPIFactory().getFilePlansAPI(user);
+ return filePlanAPI.createHold(hold, parentId);
+ }
+
+ @AfterClass(alwaysRun = true)
+ public void cleanUpPreventActionsOnFrozenContent()
+ {
+ getRestAPIFactory().getHoldsAPI(getAdminUser()).deleteHold(holdNodeRef);
+ dataSite.usingAdmin().deleteSite(testSite);
+ deleteRecordCategory(recordFolder.getParentId());
+ deleteRecordCategory(categoryWithRS.getId());
+ }
+}
diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/RemoveFromHoldsTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/RemoveFromHoldsTests.java
index a48e9e16d3..8f317c2ff0 100644
--- a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/RemoveFromHoldsTests.java
+++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/RemoveFromHoldsTests.java
@@ -52,7 +52,7 @@ import java.util.Set;
import org.alfresco.dataprep.CMISUtil;
import org.alfresco.rest.rm.community.base.BaseRMRestTest;
-import org.alfresco.rest.rm.community.model.hold.HoldEntry;
+import org.alfresco.rest.rm.community.model.hold.v0.HoldEntry;
import org.alfresco.rest.rm.community.model.record.Record;
import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild;
import org.alfresco.rest.rm.community.model.user.UserRoles;
diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/RemoveFromHoldsV1Tests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/RemoveFromHoldsV1Tests.java
new file mode 100644
index 0000000000..9f76269ce0
--- /dev/null
+++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/hold/RemoveFromHoldsV1Tests.java
@@ -0,0 +1,374 @@
+/*-
+ * #%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 java.util.Arrays.asList;
+
+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.fileplancomponents.FilePlanComponentAspects.FROZEN_ASPECT;
+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.model.user.UserRoles.ROLE_RM_MANAGER;
+import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix;
+import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.IMAGE_FILE;
+import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.createElectronicRecordModel;
+import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.createNonElectronicRecordModel;
+import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.getFile;
+import static org.alfresco.utility.report.log.Step.STEP;
+import static org.springframework.http.HttpStatus.CREATED;
+import static org.springframework.http.HttpStatus.FORBIDDEN;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import org.alfresco.dataprep.CMISUtil;
+import org.alfresco.rest.model.RestNodeAssociationModelCollection;
+import org.alfresco.rest.rm.community.base.BaseRMRestTest;
+import org.alfresco.rest.rm.community.model.hold.Hold;
+import org.alfresco.rest.rm.community.model.hold.HoldChild;
+import org.alfresco.rest.rm.community.model.record.Record;
+import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild;
+import org.alfresco.rest.rm.community.model.user.UserRoles;
+import org.alfresco.rest.rm.community.requests.gscore.api.FilePlanAPI;
+import org.alfresco.rest.rm.community.requests.gscore.api.RecordFolderAPI;
+import org.alfresco.rest.rm.community.utils.CoreUtil;
+import org.alfresco.rest.v0.service.RoleService;
+import org.alfresco.utility.constants.UserRole;
+import org.alfresco.utility.model.FileModel;
+import org.alfresco.utility.model.SiteModel;
+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.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * V1 API tests for removing content/record folder/record from holds
+ *
+ * @author Damian Ujma
+ */
+public class RemoveFromHoldsV1Tests extends BaseRMRestTest
+{
+ private static final String HOLD_ONE = "HOLD_ONE" + generateTestPrefix(RemoveFromHoldsV1Tests.class);
+ private static final String HOLD_TWO = "HOLD_TWO" + generateTestPrefix(RemoveFromHoldsV1Tests.class);
+ private static final String ACCESS_DENIED_ERROR_MESSAGE = "Access Denied. You do not have the appropriate " +
+ "permissions to perform this operation.";
+
+ private SiteModel testSite;
+ private SiteModel privateSite;
+ private String holdNodeRefOne;
+ private FileModel contentHeld;
+ private FileModel contentAddToManyHolds;
+ private List holdsListRef = new ArrayList<>();
+ private final Set usersToBeClean = new HashSet<>();
+ private final Set nodesToBeClean = new HashSet<>();
+
+ @Autowired
+ private RoleService roleService;
+
+ @BeforeClass(alwaysRun = true)
+ public void preconditionForRemoveContentFromHold()
+ {
+ STEP("Create two holds.");
+
+ holdNodeRefOne = createHold(FILE_PLAN_ALIAS,
+ Hold.builder().name(HOLD_ONE).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(),
+ getAdminUser()).getId();
+ String holdNodeRefTwo = createHold(FILE_PLAN_ALIAS,
+ Hold.builder().name(HOLD_TWO).description(HOLD_DESCRIPTION).reason(HOLD_REASON).build(),
+ getAdminUser()).getId();
+ holdsListRef = asList(holdNodeRefOne, holdNodeRefTwo);
+
+ STEP("Create test files.");
+ testSite = dataSite.usingAdmin().createPublicRandomSite();
+ privateSite = dataSite.usingAdmin().createPrivateRandomSite();
+ contentHeld = dataContent.usingAdmin().usingSite(testSite)
+ .createContent(CMISUtil.DocumentType.TEXT_PLAIN);
+ contentAddToManyHolds = dataContent.usingSite(testSite)
+ .createContent(CMISUtil.DocumentType.TEXT_PLAIN);
+
+ STEP("Add content to the holds.");
+ getRestAPIFactory()
+ .getHoldsAPI(getAdminUser())
+ .addChildToHold(HoldChild.builder().id(contentHeld.getNodeRefWithoutVersion()).build(), holdNodeRefOne);
+ getRestAPIFactory()
+ .getHoldsAPI(getAdminUser())
+ .addChildToHold(HoldChild.builder().id(contentAddToManyHolds.getNodeRefWithoutVersion()).build(),
+ holdNodeRefOne);
+ getRestAPIFactory()
+ .getHoldsAPI(getAdminUser())
+ .addChildToHold(HoldChild.builder().id(contentAddToManyHolds.getNodeRefWithoutVersion()).build(),
+ holdNodeRefTwo);
+ }
+
+ /**
+ * Valid nodes to be removed from hold
+ */
+ @DataProvider(name = "validNodesToRemoveFromHold")
+ public Object[][] getValidNodesToRemoveFromHold()
+ {
+ //create electronic and nonElectronic record in record folder
+ RecordCategoryChild recordFolder = createCategoryFolderInFilePlan();
+ RecordFolderAPI recordFolderAPI = getRestAPIFactory().getRecordFolderAPI();
+ nodesToBeClean.add(recordFolder.getParentId());
+ Record electronicRecord = recordFolderAPI.createRecord(createElectronicRecordModel(), recordFolder.getId(),
+ getFile
+ (IMAGE_FILE));
+ assertStatusCode(CREATED);
+ Record nonElectronicRecord = recordFolderAPI.createRecord(createNonElectronicRecordModel(),
+ recordFolder.getId());
+ assertStatusCode(CREATED);
+
+ RecordCategoryChild folderToHeld = createCategoryFolderInFilePlan();
+ nodesToBeClean.add(folderToHeld.getParentId());
+ Stream.of(electronicRecord.getId(), nonElectronicRecord.getId(), folderToHeld.getId())
+ .forEach(id -> getRestAPIFactory()
+ .getHoldsAPI(getAdminUser())
+ .addChildToHold(HoldChild.builder().id(id).build(), holdNodeRefOne));
+
+ return new String[][]
+ { // record folder
+ { folderToHeld.getId() },
+ //electronic record
+ { electronicRecord.getId() },
+ // non electronic record
+ { nonElectronicRecord.getId() },
+ // document from collaboration site
+ { contentHeld.getNodeRefWithoutVersion() },
+ };
+ }
+
+ /**
+ * Given content/record folder/record that is held
+ * And the corresponding hold
+ * When I use the existing REST API to remove the node from the hold
+ * Then the node is removed from the hold
+ * And is no longer frozen
+ */
+ @Test(dataProvider = "validNodesToRemoveFromHold")
+ public void removeContentFromHold(String nodeId) throws Exception
+ {
+ STEP("Remove node from hold");
+ getRestAPIFactory()
+ .getHoldsAPI(getAdminUser()).deleteHoldChild(holdNodeRefOne, nodeId);
+
+ STEP("Check the node is not held");
+ assertFalse(hasAspect(nodeId, FROZEN_ASPECT));
+
+ STEP("Check node is not in any hold");
+ RestNodeAssociationModelCollection holdsEntries = getRestAPIFactory()
+ .getNodeAPI(CoreUtil.toContentModel(nodeId)).usingParams("where=(assocType='rma:frozenContent')")
+ .getParents();
+ assertTrue(holdsEntries.getEntries().isEmpty(), "Content held is still added to a hold.");
+ }
+
+ /**
+ * Given active content that is held on many holds
+ * When I use the existing REST API to remove the active content from one hold
+ * Then the active content is removed from the specific hold
+ * And is frozen
+ * And in the other holds
+ */
+ @Test
+ public void removeContentAddedToManyHolds() throws Exception
+ {
+ STEP("Remove content from hold. ");
+
+ getRestAPIFactory().getHoldsAPI(getAdminUser())
+ .deleteHoldChild(holdNodeRefOne, contentAddToManyHolds.getNodeRefWithoutVersion());
+
+ STEP("Check the content is held. ");
+ assertTrue(hasAspect(contentAddToManyHolds.getNodeRefWithoutVersion(), FROZEN_ASPECT));
+
+ STEP("Check node is in hold HOLD_TWO. ");
+
+ RestNodeAssociationModelCollection holdsEntries = getRestAPIFactory()
+ .getNodeAPI(CoreUtil.toContentModel(contentAddToManyHolds.getNodeRefWithoutVersion()))
+ .usingParams("where=(assocType='rma:frozenContent')").getParents();
+ assertFalse(holdsEntries.getEntries().isEmpty(), "Content held is not held after removing from one hold.");
+ assertTrue(holdsEntries.getEntries().stream()
+ .anyMatch(restNodeModel -> restNodeModel.getModel().getName().equals(HOLD_TWO)),
+ "Content held is not held after removing from one hold.");
+ }
+
+ /**
+ * Data provider with user without right permission or capability to remove from hold a specific node
+ *
+ * @return user model and the node ref to be removed from hold
+ */
+ @DataProvider(name = "userWithoutPermissionForRemoveFromHold")
+ public Object[][] getUserWithoutPermissionForAddToHold()
+ {
+ //create record folder
+ RecordCategoryChild recordFolder = createCategoryFolderInFilePlan();
+ nodesToBeClean.add(recordFolder.getParentId());
+ UserModel user = roleService.createUserWithRMRole(ROLE_RM_MANAGER.roleId);
+ getRestAPIFactory().getRMUserAPI().addUserPermission(holdNodeRefOne, user, PERMISSION_FILING);
+ //create files that will be removed from hold
+ FileModel contentNoHoldPerm = dataContent.usingAdmin().usingSite(testSite)
+ .createContent(CMISUtil.DocumentType.TEXT_PLAIN);
+ FileModel contentNoHoldCap = dataContent.usingAdmin().usingSite(testSite)
+ .createContent(CMISUtil.DocumentType.TEXT_PLAIN);
+ FileModel privateFile = dataContent.usingAdmin().usingSite(privateSite)
+ .createContent(CMISUtil.DocumentType.TEXT_PLAIN);
+ //add files to hold
+ asList(recordFolder.getId(), contentNoHoldCap.getNodeRefWithoutVersion(),
+ contentNoHoldPerm.getNodeRefWithoutVersion(), privateFile.getNodeRefWithoutVersion())
+ .forEach(id -> getRestAPIFactory()
+ .getHoldsAPI(getAdminUser())
+ .addChildToHold(HoldChild.builder().id(id).build(), holdNodeRefOne));
+
+ return new Object[][]
+ {
+ // user with read permission on the content, with remove from hold capability and without
+ // filling permission on a hold
+ {
+ roleService.createUserWithSiteRoleRMRoleAndPermission(testSite, UserRole.SiteCollaborator,
+ holdNodeRefOne, UserRoles.ROLE_RM_MANAGER, PERMISSION_READ_RECORDS),
+ contentNoHoldPerm.getNodeRefWithoutVersion()
+ },
+ // user with write permission on the content, filling permission on a hold without remove from
+ // hold capability
+ {
+ roleService.createUserWithSiteRoleRMRoleAndPermission(testSite, UserRole
+ .SiteCollaborator,
+ holdNodeRefOne, UserRoles.ROLE_RM_POWER_USER, PERMISSION_FILING),
+ contentNoHoldCap.getNodeRefWithoutVersion()
+ },
+ //user without read permission on RM record folder
+ {
+ user, recordFolder.getId()
+ },
+ //user without read permission over the content from the private site
+ {
+ user, privateFile.getNodeRefWithoutVersion()
+ }
+ };
+ }
+
+ /**
+ * Given node on hold in a single hold location
+ * And the user does not have sufficient permissions or capabilities to remove the node from the hold
+ * When the user tries to remove the node from the hold
+ * Then it's unsuccessful
+ *
+ * @throws Exception
+ */
+ @Test(dataProvider = "userWithoutPermissionForRemoveFromHold")
+ public void removeFromHoldWithUserWithoutPermission(UserModel userModel, String nodeIdToBeRemoved) throws Exception
+ {
+ STEP("Update the list of users to be deleted after running the tests");
+ usersToBeClean.add(userModel);
+
+ STEP("Remove node from hold with user without right permission or capability");
+ getRestAPIFactory().getHoldsAPI(userModel).deleteHoldChild(holdNodeRefOne, nodeIdToBeRemoved);
+
+ assertStatusCode(FORBIDDEN);
+ getRestAPIFactory().getRmRestWrapper().assertLastError().containsSummary(ACCESS_DENIED_ERROR_MESSAGE);
+
+ STEP("Check node is frozen.");
+ assertTrue(hasAspect(nodeIdToBeRemoved, FROZEN_ASPECT));
+ }
+
+ /**
+ * Data provider with user with right permission or capability to remove from hold a specific node
+ *
+ * @return user model and the node ref to be removed from hold
+ */
+ @DataProvider(name = "userWithPermissionForRemoveFromHold")
+ public Object[][] getUserWithPermissionForAddToHold()
+ {
+ //create record folder
+ RecordCategoryChild recordFolder = createCategoryFolderInFilePlan();
+ nodesToBeClean.add(recordFolder.getParentId());
+ UserModel user = roleService.createUserWithRMRoleAndRMNodePermission(ROLE_RM_MANAGER.roleId,
+ recordFolder.getId(),
+ PERMISSION_READ_RECORDS);
+ getRestAPIFactory().getRMUserAPI().addUserPermission(holdNodeRefOne, user, PERMISSION_FILING);
+ //create file that will be removed from hold
+ FileModel contentPermission = dataContent.usingAdmin().usingSite(testSite)
+ .createContent(CMISUtil.DocumentType.TEXT_PLAIN);
+
+ //add files to hold
+ asList(recordFolder.getId(), contentPermission.getNodeRefWithoutVersion())
+ .forEach(id -> getRestAPIFactory()
+ .getHoldsAPI(getAdminUser())
+ .addChildToHold(HoldChild.builder().id(id).build(), holdNodeRefOne));
+
+ return new Object[][]
+ {
+ // user with write permission on the content
+ {
+ roleService.createUserWithSiteRoleRMRoleAndPermission(testSite, UserRole.SiteConsumer,
+ holdNodeRefOne, UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING),
+ contentPermission.getNodeRefWithoutVersion()
+ },
+ //user with read permission on RM record folder
+ {
+ user, recordFolder.getId()
+ },
+
+ };
+ }
+
+ @Test(dataProvider = "userWithPermissionForRemoveFromHold")
+ public void removeFromHoldWithUserWithPermission(UserModel userModel, String nodeIdToBeRemoved) throws Exception
+ {
+ STEP("Update the list of users to be deleted after running the tests");
+ usersToBeClean.add(userModel);
+
+ STEP("Remove node from hold with user with right permission and capability");
+ getRestAPIFactory().getHoldsAPI(userModel).deleteHoldChild(holdNodeRefOne, nodeIdToBeRemoved);
+
+ STEP("Check node is not frozen.");
+ assertFalse(hasAspect(nodeIdToBeRemoved, FROZEN_ASPECT));
+ }
+
+ private Hold createHold(String parentId, Hold hold, UserModel user)
+ {
+ FilePlanAPI filePlanAPI = getRestAPIFactory().getFilePlansAPI(user);
+ return filePlanAPI.createHold(hold, parentId);
+ }
+
+ @AfterClass(alwaysRun = true)
+ public void cleanUpRemoveContentFromHold()
+ {
+ holdsListRef.forEach(holdRef -> getRestAPIFactory().getHoldsAPI(getAdminUser()).deleteHold(holdRef));
+ dataSite.usingAdmin().deleteSite(testSite);
+ dataSite.usingAdmin().deleteSite(privateSite);
+ usersToBeClean.forEach(user -> getDataUser().usingAdmin().deleteUser(user));
+ nodesToBeClean.forEach(this::deleteRecordCategory);
+ }
+}
diff --git a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml
index a667c393da..368492e785 100644
--- a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml
+++ b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml
@@ -538,6 +538,11 @@
d:text
true
+
+ Hold Deletion Reason
+ d:text
+ false
+
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 b711326ba6..f969a458ea 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
@@ -69,7 +69,32 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml
index ae80740b66..dfc436146d 100644
--- a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml
+++ b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml
@@ -1614,6 +1614,8 @@
org.alfresco.module.org_alfresco_module_rm.hold.HoldService.createHold=RM_CAP.0.rma:filePlanComponent.CreateHold
org.alfresco.module.org_alfresco_module_rm.hold.HoldService.getHoldReason=RM.Read.0
org.alfresco.module.org_alfresco_module_rm.hold.HoldService.setHoldReason=RM_CAP.0.rma:filePlanComponent.EditHold
+ org.alfresco.module.org_alfresco_module_rm.hold.HoldService.setHoldDeletionReason=RM_CAP.0.rma:filePlanComponent.EditHold
+ org.alfresco.module.org_alfresco_module_rm.hold.HoldService.updateHold=RM_CAP.0.rma:filePlanComponent.EditHold
org.alfresco.module.org_alfresco_module_rm.hold.HoldService.deleteHold=RM_CAP.0.rma:filePlanComponent.DeleteHold
org.alfresco.module.org_alfresco_module_rm.hold.HoldService.addToHold=RM_CAP.0.rma:filePlanComponent.AddToHold
org.alfresco.module.org_alfresco_module_rm.hold.HoldService.addToHolds=RM_ALLOW
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/event/DeleteHoldAuditEvent.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/event/DeleteHoldAuditEvent.java
index 01cb1063f6..faa2273808 100644
--- a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/event/DeleteHoldAuditEvent.java
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/event/DeleteHoldAuditEvent.java
@@ -27,6 +27,7 @@
package org.alfresco.module.org_alfresco_module_rm.audit.event;
+import static org.alfresco.module.org_alfresco_module_rm.audit.event.HoldUtils.HOLD_DELETION_REASON;
import static org.alfresco.repo.policy.Behaviour.NotificationFrequency.EVERY_EVENT;
import java.io.Serializable;
@@ -77,6 +78,8 @@ public class DeleteHoldAuditEvent extends AuditEvent implements NodeServicePolic
public void beforeDeleteNode(NodeRef holdNodeRef)
{
Map auditProperties = HoldUtils.makePropertiesMap(holdNodeRef, nodeService);
+ auditProperties.put(HOLD_DELETION_REASON, nodeService.getProperty(holdNodeRef, PROP_HOLD_DELETION_REASON));
+
recordsManagementAuditService.auditEvent(holdNodeRef, getName(), auditProperties, null, true, false);
}
}
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/event/HoldUtils.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/event/HoldUtils.java
index f293a593be..65e4732e08 100644
--- a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/event/HoldUtils.java
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/audit/event/HoldUtils.java
@@ -47,6 +47,7 @@ class HoldUtils
{
/** A QName to display for the hold name. */
public static final QName HOLD_NAME = QName.createQName(RecordsManagementModel.RM_URI, "Hold Name");
+ public static final QName HOLD_DELETION_REASON = QName.createQName(RecordsManagementModel.RM_URI, "Hold deletion reason");
/** A QName to display for the hold node ref. */
public static final QName HOLD_NODEREF = QName.createQName(RecordsManagementModel.RM_URI, "Hold NodeRef");
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/hold/HoldService.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/hold/HoldService.java
index eb80e730a2..ef392b03bf 100644
--- a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/hold/HoldService.java
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/hold/HoldService.java
@@ -111,6 +111,24 @@ public interface HoldService
*/
void setHoldReason(NodeRef hold, String reason);
+ /**
+ * Sets the reason for the hold deletion
+ *
+ * @param hold The {@link NodeRef} of the hold
+ * @param reason {@link String} The reason for the hold
+ */
+ void setHoldDeletionReason(NodeRef hold, String reason);
+
+ /**
+ * Updates a hold with the given name, reason and description
+ *
+ * @param hold The {@link NodeRef} of the hold
+ * @param name {@link String} The name of the hold
+ * @param reason {@link String} The reason of the hold
+ * @param description {@link String} The description of the hold
+ */
+ void updateHold(NodeRef hold, String name, String reason, String description);
+
/**
* Deletes the hold
*
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/hold/HoldServiceImpl.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/hold/HoldServiceImpl.java
index 636eb2101d..1bcc1f9cf4 100644
--- a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/hold/HoldServiceImpl.java
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/hold/HoldServiceImpl.java
@@ -29,6 +29,7 @@ package org.alfresco.module.org_alfresco_module_rm.hold;
import static org.alfresco.model.ContentModel.ASPECT_LOCKABLE;
import static org.alfresco.model.ContentModel.ASSOC_CONTAINS;
+import static org.alfresco.model.ContentModel.PROP_DESCRIPTION;
import static org.alfresco.model.ContentModel.PROP_NAME;
import java.io.Serializable;
@@ -458,11 +459,11 @@ public class HoldServiceImpl extends ServiceBaseImpl
// create map of properties
Map properties = new HashMap<>(3);
- properties.put(ContentModel.PROP_NAME, name);
+ properties.put(PROP_NAME, name);
properties.put(PROP_HOLD_REASON, reason);
if (description != null && !description.isEmpty())
{
- properties.put(ContentModel.PROP_DESCRIPTION, description);
+ properties.put(PROP_DESCRIPTION, description);
}
// create assoc name
@@ -512,6 +513,39 @@ public class HoldServiceImpl extends ServiceBaseImpl
}
}
+ /**
+ * @see org.alfresco.module.org_alfresco_module_rm.hold.HoldService#setHoldDeletionReason(org.alfresco.service.cmr.repository.NodeRef, java.lang.String)
+ */
+ @Override
+ public void setHoldDeletionReason(NodeRef hold, String reason)
+ {
+ ParameterCheck.mandatory("hold", hold);
+ ParameterCheck.mandatory("reason", reason);
+
+ if (nodeService.exists(hold) && isHold(hold))
+ {
+ nodeService.setProperty(hold, PROP_HOLD_DELETION_REASON, reason);
+ }
+ }
+
+ /**
+ * @see org.alfresco.module.org_alfresco_module_rm.hold.HoldService#updateHold(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, java.lang.String, java.lang.String) (org.alfresco.service.cmr.repository.NodeRef, java.lang.String, java.lang.String, java.lang.String)
+ */
+ @Override
+ public void updateHold(NodeRef hold, String name, String reason, String description)
+ {
+ ParameterCheck.mandatory("hold", hold);
+ ParameterCheck.mandatory("name", name);
+ ParameterCheck.mandatory("reason", reason);
+
+ if (nodeService.exists(hold) && isHold(hold))
+ {
+ nodeService.setProperty(hold, PROP_NAME, name);
+ nodeService.setProperty(hold, PROP_HOLD_REASON, reason);
+ nodeService.setProperty(hold, PROP_DESCRIPTION, description);
+ }
+ }
+
/**
* @see org.alfresco.module.org_alfresco_module_rm.hold.HoldService#deleteHold(org.alfresco.service.cmr.repository.NodeRef)
*/
@@ -563,7 +597,7 @@ public class HoldServiceImpl extends ServiceBaseImpl
if (permissionService.hasPermission(nodeRef, permission) == AccessStatus.DENIED)
{
- heldNames.add((String) nodeService.getProperty(nodeRef, ContentModel.PROP_NAME));
+ heldNames.add((String) nodeService.getProperty(nodeRef, PROP_NAME));
}
}
catch (AccessDeniedException ade)
@@ -630,7 +664,7 @@ public class HoldServiceImpl extends ServiceBaseImpl
{
if (!isHold(hold))
{
- final String holdName = (String) nodeService.getProperty(hold, ContentModel.PROP_NAME);
+ final String holdName = (String) nodeService.getProperty(hold, PROP_NAME);
throw new IntegrityException(I18NUtil.getMessage("rm.hold.not-hold", holdName), null);
}
@@ -688,7 +722,7 @@ public class HoldServiceImpl extends ServiceBaseImpl
{
if (!isRecordFolder(nodeRef) && !instanceOf(nodeRef, ContentModel.TYPE_CONTENT))
{
- final String nodeName = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);
+ final String nodeName = (String) nodeService.getProperty(nodeRef, PROP_NAME);
throw new IntegrityException(I18NUtil.getMessage("rm.hold.add-to-hold-invalid-type", nodeName), null);
}
@@ -795,7 +829,7 @@ public class HoldServiceImpl extends ServiceBaseImpl
{
if (!isHold(hold))
{
- final String holdName = (String) nodeService.getProperty(hold, ContentModel.PROP_NAME);
+ final String holdName = (String) nodeService.getProperty(hold, PROP_NAME);
throw new IntegrityException(I18NUtil.getMessage("rm.hold.not-hold", holdName), null);
}
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java
index 51d20f93c7..8386e949fa 100644
--- a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java
@@ -35,6 +35,7 @@ import org.alfresco.service.namespace.QName;
*
* @author Roy Wetherall
*/
+@SuppressWarnings("PMD.ConstantsInInterface")
@AlfrescoPublicApi
public interface RecordsManagementModel extends RecordsManagementCustomModel
{
@@ -200,6 +201,7 @@ public interface RecordsManagementModel extends RecordsManagementCustomModel
// Hold type
QName TYPE_HOLD = QName.createQName(RM_URI, "hold");
QName PROP_HOLD_REASON = QName.createQName(RM_URI, "holdReason");
+ QName PROP_HOLD_DELETION_REASON = QName.createQName(RM_URI, "holdDeletionReason");
//since 3.2
@Deprecated
QName ASSOC_FROZEN_RECORDS = QName.createQName(RM_URI, "frozenRecords");
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/fileplans/FilePlanHoldsRelation.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/fileplans/FilePlanHoldsRelation.java
new file mode 100644
index 0000000000..cc92610176
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/fileplans/FilePlanHoldsRelation.java
@@ -0,0 +1,155 @@
+/*
+ * #%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.fileplans;
+
+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.stream.Collectors;
+
+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;
+import org.alfresco.rest.framework.WebApiDescription;
+import org.alfresco.rest.framework.resource.RelationshipResource;
+import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction;
+import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
+import org.alfresco.rest.framework.resource.parameters.Parameters;
+import org.alfresco.rm.rest.api.impl.ApiNodesModelFactory;
+import org.alfresco.rm.rest.api.impl.FilePlanComponentsApiUtils;
+import org.alfresco.rm.rest.api.model.HoldModel;
+import org.alfresco.service.cmr.model.FileFolderService;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.transaction.TransactionService;
+import org.springframework.beans.factory.InitializingBean;
+
+/**
+ * File plan holds relation
+ *
+ * @author Damian Ujma
+ */
+@RelationshipResource(name = "holds", entityResource = FilePlanEntityResource.class, title = "Holds in a file plan")
+public class FilePlanHoldsRelation implements
+ RelationshipResourceAction.Create,
+ RelationshipResourceAction.Read,
+ InitializingBean
+{
+ private FilePlanComponentsApiUtils apiUtils;
+ private ApiNodesModelFactory nodesModelFactory;
+ private HoldService holdService;
+ private FileFolderService fileFolderService;
+ private TransactionService transactionService;
+
+ @Override
+ public void afterPropertiesSet() throws Exception
+ {
+ mandatory("apiUtils", this.apiUtils);
+ mandatory("nodesModelFactory", this.nodesModelFactory);
+ mandatory("holdService", this.holdService);
+ mandatory("fileFolderService", this.fileFolderService);
+ mandatory("transactionService", this.transactionService);
+ }
+
+ @Override
+ @WebApiDescription(title = "Return a paged list of holds for the file plan identified by 'filePlanId'")
+ public CollectionWithPagingInfo readAll(String filePlanId, Parameters parameters)
+ {
+ checkNotBlank("filePlanId", filePlanId);
+ mandatory("parameters", parameters);
+
+ NodeRef parentNodeRef = apiUtils.lookupAndValidateNodeType(filePlanId, RecordsManagementModel.TYPE_FILE_PLAN);
+ List holds = holdService.getHolds(parentNodeRef);
+
+ List page = holds.stream()
+ .map(hold -> fileFolderService.getFileInfo(hold))
+ .map(nodesModelFactory::createHoldModel)
+ .skip(parameters.getPaging().getSkipCount())
+ .limit(parameters.getPaging().getMaxItems())
+ .collect(Collectors.toCollection(LinkedList::new));
+
+ int totalItems = holds.size();
+ boolean hasMore = parameters.getPaging().getSkipCount() + parameters.getPaging().getMaxItems() < totalItems;
+ return CollectionWithPagingInfo.asPaged(parameters.getPaging(), page, hasMore, totalItems);
+ }
+
+ @Override
+ @WebApiDescription(title = "Create one (or more) holds in a file plan identified by 'filePlanId'")
+ public List create(String filePlanId, List holds, Parameters parameters)
+ {
+ checkNotBlank("filePlanId", filePlanId);
+ mandatory("holds", holds);
+ mandatory("parameters", parameters);
+
+ NodeRef parentNodeRef = apiUtils.lookupAndValidateNodeType(filePlanId, RecordsManagementModel.TYPE_FILE_PLAN);
+
+ RetryingTransactionCallback> callback = () -> {
+ List createdNodes = new LinkedList<>();
+ for (HoldModel nodeInfo : holds)
+ {
+ NodeRef newNodeRef = holdService.createHold(parentNodeRef, nodeInfo.name(), nodeInfo.reason(),
+ nodeInfo.description());
+ createdNodes.add(newNodeRef);
+ }
+ return createdNodes;
+ };
+
+ List createdNodes = transactionService.getRetryingTransactionHelper()
+ .doInTransaction(callback, false, true);
+
+ return createdNodes.stream()
+ .map(hold -> fileFolderService.getFileInfo(hold))
+ .map(nodesModelFactory::createHoldModel)
+ .collect(Collectors.toCollection(LinkedList::new));
+ }
+
+ public void setApiUtils(FilePlanComponentsApiUtils apiUtils)
+ {
+ this.apiUtils = apiUtils;
+ }
+
+ public void setNodesModelFactory(ApiNodesModelFactory nodesModelFactory)
+ {
+ this.nodesModelFactory = nodesModelFactory;
+ }
+
+ public void setHoldService(HoldService holdService)
+ {
+ this.holdService = holdService;
+ }
+
+ public void setFileFolderService(FileFolderService fileFolderService)
+ {
+ this.fileFolderService = fileFolderService;
+ }
+
+ public void setTransactionService(TransactionService transactionService)
+ {
+ this.transactionService = transactionService;
+ }
+}
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/holds/HoldsChildrenRelation.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/holds/HoldsChildrenRelation.java
new file mode 100644
index 0000000000..753b05471b
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/holds/HoldsChildrenRelation.java
@@ -0,0 +1,207 @@
+/*
+ * #%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.stream.Collectors;
+
+import org.alfresco.module.org_alfresco_module_rm.hold.HoldService;
+import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel;
+import org.alfresco.repo.node.integrity.IntegrityException;
+import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
+import org.alfresco.rest.framework.WebApiDescription;
+import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
+import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
+import org.alfresco.rest.framework.resource.RelationshipResource;
+import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction;
+import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
+import org.alfresco.rest.framework.resource.parameters.Parameters;
+import org.alfresco.rm.rest.api.impl.ApiNodesModelFactory;
+import org.alfresco.rm.rest.api.impl.FilePlanComponentsApiUtils;
+import org.alfresco.rm.rest.api.model.HoldChild;
+import org.alfresco.service.cmr.model.FileFolderService;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.StoreRef;
+import org.alfresco.service.cmr.security.AccessStatus;
+import org.alfresco.service.cmr.security.PermissionService;
+import org.alfresco.service.transaction.TransactionService;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.extensions.surf.util.I18NUtil;
+
+/**
+ * Hold children relation
+ *
+ * @author Damian Ujma
+ */
+@RelationshipResource(name = "children", entityResource = HoldsEntityResource.class, title = "Children of a hold")
+public class HoldsChildrenRelation implements
+ RelationshipResourceAction.Create,
+ RelationshipResourceAction.Read,
+ RelationshipResourceAction.Delete,
+ InitializingBean
+{
+ private HoldService holdService;
+ private FilePlanComponentsApiUtils apiUtils;
+ private ApiNodesModelFactory nodesModelFactory;
+ private TransactionService transactionService;
+ private FileFolderService fileFolderService;
+ private PermissionService permissionService;
+
+ @Override
+ public void afterPropertiesSet() throws Exception
+ {
+ mandatory("holdService", holdService);
+ mandatory("apiUtils", apiUtils);
+ mandatory("nodesModelFactory", nodesModelFactory);
+ mandatory("transactionService", transactionService);
+ mandatory("fileFolderService", fileFolderService);
+ }
+
+ @Override
+ @WebApiDescription(title = "Add one (or more) children as children of a hold identified by 'holdId'")
+ public List create(String holdId, List children, Parameters parameters)
+ {
+ // validate parameters
+ checkNotBlank("holdId", holdId);
+ mandatory("children", children);
+ mandatory("parameters", parameters);
+
+ NodeRef parentNodeRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
+
+ RetryingTransactionCallback> callback = () -> {
+ List createdNodes = children.stream()
+ .map(holdChild -> new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, holdChild.id()))
+ .collect(Collectors.toList());
+ try
+ {
+ holdService.addToHold(parentNodeRef, createdNodes);
+ }
+ catch (IntegrityException exception)
+ {
+ // Throw 400 Bad Request when a node with id 'holdId' is not a hold or a child cannot be added to a hold
+ throw new InvalidArgumentException(exception.getMsgId()).initCause(exception);
+ }
+ return createdNodes;
+ };
+
+ List nodeInfos = transactionService.getRetryingTransactionHelper()
+ .doInTransaction(callback, false, true);
+
+ return nodeInfos.stream()
+ .map(nodeRef -> new HoldChild(nodeRef.getId()))
+ .collect(Collectors.toCollection(LinkedList::new));
+ }
+
+ @Override
+ @WebApiDescription(title = "Return a paged list of hold children for the hold identified by 'holdId'")
+ public CollectionWithPagingInfo readAll(String holdId, Parameters parameters)
+ {
+ checkNotBlank("holdId", holdId);
+ mandatory("parameters", parameters);
+
+ NodeRef parentNodeRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
+ List children = holdService.getHeld(parentNodeRef);
+
+ List page = children.stream()
+ .map(NodeRef::getId)
+ .map(HoldChild::new)
+ .skip(parameters.getPaging().getSkipCount())
+ .limit(parameters.getPaging().getMaxItems())
+ .collect(Collectors.toCollection(LinkedList::new));
+
+ int totalItems = children.size();
+ boolean hasMore = parameters.getPaging().getSkipCount() + parameters.getPaging().getMaxItems() < totalItems;
+ return CollectionWithPagingInfo.asPaged(parameters.getPaging(), page, hasMore, totalItems);
+
+ }
+
+ @Override
+ @WebApiDescription(title = "Remove a child from a hold", description = "Remove a child with id 'childId' from a hold with id 'holdId'")
+ public void delete(String holdId, String childId, Parameters parameters)
+ {
+ checkNotBlank("holdId", holdId);
+ checkNotBlank("childId", childId);
+ mandatory("parameters", parameters);
+
+ NodeRef nodeRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
+ NodeRef childRef = apiUtils.lookupByPlaceholder(childId);
+
+ if (permissionService.hasReadPermission(childRef) == AccessStatus.DENIED)
+ {
+ throw new PermissionDeniedException(I18NUtil.getMessage("permissions.err_access_denied"));
+ }
+
+ RetryingTransactionCallback> callback = () -> {
+ try
+ {
+ holdService.removeFromHold(nodeRef, childRef);
+ }
+ catch (IntegrityException exception)
+ {
+ // Throw 400 Bad Request when a node with id 'holdId' is not a hold
+ throw new InvalidArgumentException(exception.getMsgId()).initCause(exception);
+ }
+ return null;
+ };
+
+ transactionService.getRetryingTransactionHelper().doInTransaction(callback, false, true);
+ }
+
+ public void setHoldService(HoldService holdService)
+ {
+ this.holdService = holdService;
+ }
+
+ public void setApiUtils(FilePlanComponentsApiUtils apiUtils)
+ {
+ this.apiUtils = apiUtils;
+ }
+
+ public void setTransactionService(TransactionService transactionService)
+ {
+ this.transactionService = transactionService;
+ }
+
+ public void setNodesModelFactory(ApiNodesModelFactory nodesModelFactory)
+ {
+ this.nodesModelFactory = nodesModelFactory;
+ }
+
+ public void setFileFolderService(FileFolderService fileFolderService)
+ {
+ this.fileFolderService = fileFolderService;
+ }
+
+ public void setPermissionService(PermissionService permissionService)
+ {
+ this.permissionService = permissionService;
+ }
+}
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/holds/HoldsEntityResource.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/holds/HoldsEntityResource.java
new file mode 100644
index 0000000000..2e8cdf9231
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/holds/HoldsEntityResource.java
@@ -0,0 +1,184 @@
+/*
+ * #%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 jakarta.servlet.http.HttpServletResponse;
+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;
+import org.alfresco.rest.framework.Operation;
+import org.alfresco.rest.framework.WebApiDescription;
+import org.alfresco.rest.framework.WebApiParam;
+import org.alfresco.rest.framework.resource.EntityResource;
+import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction;
+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.HoldDeletionReason;
+import org.alfresco.rm.rest.api.model.HoldModel;
+import org.alfresco.service.cmr.model.FileFolderService;
+import org.alfresco.service.cmr.model.FileInfo;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.transaction.TransactionService;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.InitializingBean;
+
+/**
+ * Hold entity resource
+ *
+ * @author Damian Ujma
+ */
+@EntityResource(name = "holds", title = "Holds")
+public class HoldsEntityResource implements
+ EntityResourceAction.ReadById,
+ EntityResourceAction.Update,
+ EntityResourceAction.Delete,
+ InitializingBean
+{
+ private FilePlanComponentsApiUtils apiUtils;
+ private FileFolderService fileFolderService;
+ private ApiNodesModelFactory nodesModelFactory;
+ private HoldService holdService;
+ private TransactionService transactionService;
+
+ @Override
+ public void afterPropertiesSet() throws Exception
+ {
+ mandatory("nodesModelFactory", nodesModelFactory);
+ mandatory("apiUtils", apiUtils);
+ mandatory("fileFolderService", fileFolderService);
+ mandatory("holdService", holdService);
+ mandatory("transactionService", transactionService);
+ }
+
+ @Override
+ @WebApiDescription(title = "Get hold information", description = "Get information for a hold with id 'holdId'")
+ @WebApiParam(name = "holdId", title = "The hold id")
+ public HoldModel readById(String holdId, Parameters parameters)
+ {
+ checkNotBlank("holdId", holdId);
+ mandatory("parameters", parameters);
+
+ NodeRef hold = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
+ FileInfo info = fileFolderService.getFileInfo(hold);
+ return nodesModelFactory.createHoldModel(info);
+ }
+
+ @Override
+ @WebApiDescription(title = "Update a hold", description = "Updates a hold with id 'holdId'")
+ public HoldModel update(String holdId, HoldModel holdModel, Parameters parameters)
+ {
+ checkNotBlank("holdId", holdId);
+ mandatory("holdModel", holdModel);
+ mandatory("holdModel.name", holdModel.name());
+ mandatory("holdModel.reason", holdModel.reason());
+ mandatory("parameters", parameters);
+
+ NodeRef nodeRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
+
+ RetryingTransactionCallback callback = () -> {
+ holdService.updateHold(nodeRef, holdModel.name(), holdModel.reason(), holdModel.description());
+ return null;
+ };
+ transactionService.getRetryingTransactionHelper().doInTransaction(callback, false, true);
+
+ RetryingTransactionCallback readCallback = () -> fileFolderService.getFileInfo(nodeRef);
+ FileInfo info = transactionService.getRetryingTransactionHelper().doInTransaction(readCallback, false, true);
+
+ return nodesModelFactory.createHoldModel(info);
+ }
+
+ @Override
+ @WebApiDescription(title = "Delete hold", description = "Deletes a hold with id 'holdId'")
+ public void delete(String holdId, Parameters parameters)
+ {
+ checkNotBlank("holdId", holdId);
+ mandatory("parameters", parameters);
+
+ NodeRef hold = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
+ RetryingTransactionCallback callback = () -> {
+ holdService.deleteHold(hold);
+ return null;
+ };
+ transactionService.getRetryingTransactionHelper().doInTransaction(callback, false, true);
+ }
+
+ @Operation("delete")
+ @WebApiDescription(title = "Delete hold with a reason",
+ successStatus = HttpServletResponse.SC_OK)
+ public HoldDeletionReason deleteHoldWithReason(String holdId, HoldDeletionReason reason, Parameters parameters,
+ WithResponse withResponse)
+ {
+ checkNotBlank("holdId", holdId);
+ mandatory("reason", reason);
+ mandatory("parameters", parameters);
+
+ NodeRef hold = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
+ String deletionReason = reason.reason();
+
+ RetryingTransactionCallback callback = () -> {
+ if (StringUtils.isNotBlank(deletionReason))
+ {
+ holdService.setHoldDeletionReason(hold, deletionReason);
+ }
+ holdService.deleteHold(hold);
+ return null;
+ };
+ transactionService.getRetryingTransactionHelper().doInTransaction(callback, false, true);
+
+ return reason;
+ }
+
+ public void setApiUtils(FilePlanComponentsApiUtils apiUtils)
+ {
+ this.apiUtils = apiUtils;
+ }
+
+ public void setFileFolderService(FileFolderService fileFolderService)
+ {
+ this.fileFolderService = fileFolderService;
+ }
+
+ public void setNodesModelFactory(ApiNodesModelFactory nodesModelFactory)
+ {
+ this.nodesModelFactory = nodesModelFactory;
+ }
+
+ public void setHoldService(HoldService holdService)
+ {
+ this.holdService = holdService;
+ }
+
+ public void setTransactionService(TransactionService transactionService)
+ {
+ this.transactionService = transactionService;
+ }
+}
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/holds/package-info.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/holds/package-info.java
new file mode 100644
index 0000000000..905b0fc480
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/holds/package-info.java
@@ -0,0 +1,36 @@
+/*
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+
+/**
+ * Package info that defines the Information Governance Holds REST API
+ *
+ * @author Damian Ujma
+ */
+@WebApi(name="gs", scope=Api.SCOPE.PUBLIC, version=1)
+package org.alfresco.rm.rest.api.holds;
+import org.alfresco.rest.framework.Api;
+import org.alfresco.rest.framework.WebApi;
\ No newline at end of file
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/impl/ApiNodesModelFactory.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/impl/ApiNodesModelFactory.java
index ef22c88ee3..a48a7ae4cf 100644
--- a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/impl/ApiNodesModelFactory.java
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/impl/ApiNodesModelFactory.java
@@ -47,6 +47,7 @@ import org.alfresco.rest.api.model.UserInfo;
import org.alfresco.rest.framework.jacksonextensions.BeanPropertiesFilter;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.rm.rest.api.model.FilePlan;
+import org.alfresco.rm.rest.api.model.HoldModel;
import org.alfresco.rm.rest.api.model.RMNode;
import org.alfresco.rm.rest.api.model.Record;
import org.alfresco.rm.rest.api.model.RecordCategory;
@@ -637,6 +638,21 @@ public class ApiNodesModelFactory
}
}
+
+ /**
+ * Creates an object of type HoldModel
+ *
+ * @param info info of the hold
+ * @return HoldModel object
+ */
+ public HoldModel createHoldModel(FileInfo info)
+ {
+ return new HoldModel(info.getNodeRef().getId(),
+ (String) info.getProperties().get(ContentModel.PROP_NAME),
+ (String) info.getProperties().get(ContentModel.PROP_DESCRIPTION),
+ (String) info.getProperties().get(RecordsManagementModel.PROP_HOLD_REASON));
+ }
+
/**
* Creates an object of type FilePlan
*
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldChild.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldChild.java
new file mode 100644
index 0000000000..30aa86bf3a
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldChild.java
@@ -0,0 +1,36 @@
+/*-
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rm.rest.api.model;
+
+/**
+ * Hold Child POJO for use in the v1 REST API.
+ *
+ * @author Damian Ujma
+ */
+public record HoldChild(String id)
+{
+}
\ No newline at end of file
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldDeletionReason.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldDeletionReason.java
new file mode 100644
index 0000000000..c0d73f58b5
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldDeletionReason.java
@@ -0,0 +1,36 @@
+/*-
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rm.rest.api.model;
+
+/**
+ * Hold Deletion Reason POJO for use in the v1 REST API.
+ *
+ * @author Damian Ujma
+ */
+public record HoldDeletionReason(String reason)
+{
+}
diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldModel.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldModel.java
new file mode 100644
index 0000000000..f3a11cf84e
--- /dev/null
+++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/HoldModel.java
@@ -0,0 +1,36 @@
+/*-
+ * #%L
+ * Alfresco Records Management Module
+ * %%
+ * Copyright (C) 2005 - 2024 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rm.rest.api.model;
+
+/**
+ * Hold POJO for use in the v1 REST API.
+ *
+ * @author Damian Ujma
+ */
+public record HoldModel(String id, String name, String description, String reason)
+{
+}
diff --git a/amps/ags/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/hold/HoldServiceImplUnitTest.java b/amps/ags/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/hold/HoldServiceImplUnitTest.java
index 03b61adbce..8ac6fece18 100644
--- a/amps/ags/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/hold/HoldServiceImplUnitTest.java
+++ b/amps/ags/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/hold/HoldServiceImplUnitTest.java
@@ -87,6 +87,7 @@ public class HoldServiceImplUnitTest extends BaseUnitTest
/** test values */
private static final String HOLD_NAME = "holdname";
private static final String HOLD_REASON = "holdreason";
+ private static final String HOLD_DELETION_REASON = "holddeletionreason";
private static final String HOLD_DESCRIPTION = "holddescription";
private static final String GENERIC_ERROR_MSG = "any error message text";
@@ -173,7 +174,7 @@ public class HoldServiceImplUnitTest extends BaseUnitTest
}
@Test (expected=AlfrescoRuntimeException.class)
- public void getHold()
+ public void testGetHold()
{
// setup node service interactions
when(mockedNodeService.getChildByName(eq(holdContainer), eq(ContentModel.ASSOC_CONTAINS), anyString())).thenReturn(null)
@@ -194,19 +195,19 @@ public class HoldServiceImplUnitTest extends BaseUnitTest
}
@Test (expected=RuntimeException.class)
- public void getHeldNotAHold()
+ public void testGetHeldNotAHold()
{
holdService.getHeld(recordFolder);
}
@Test
- public void getHeldNoResults()
+ public void testGetHeldNoResults()
{
assertTrue(holdService.getHeld(hold).isEmpty());
}
@Test
- public void getHeldWithResults()
+ public void testGetHeldWithResults()
{
// setup record folder in hold
List holds = new ArrayList<>(2);
@@ -259,7 +260,7 @@ public class HoldServiceImplUnitTest extends BaseUnitTest
}
@Test
- public void getHoldReason()
+ public void testGetHoldReason()
{
// setup node service interactions
when(mockedNodeService.exists(hold))
@@ -306,6 +307,80 @@ public class HoldServiceImplUnitTest extends BaseUnitTest
verify(mockedNodeService).setProperty(hold, PROP_HOLD_REASON, HOLD_REASON);
}
+ @Test
+ public void setHoldDeletionReasonForNodeDoesNotExist()
+ {
+ // setup node service interactions
+ when(mockedNodeService.exists(hold))
+ .thenReturn(false);
+
+ // node does not exist
+ holdService.setHoldDeletionReason(hold, HOLD_DELETION_REASON);
+ verify(mockedNodeService, never()).setProperty(hold, PROP_HOLD_DELETION_REASON, HOLD_DELETION_REASON);
+ }
+
+ @Test
+ public void setHoldDeletionReasonForNodeIsNotAHold()
+ {
+ // setup node service interactions
+ when(mockedNodeService.exists(hold))
+ .thenReturn(true);
+
+ // node isn't a hold
+ holdService.setHoldDeletionReason(recordFolder, HOLD_DELETION_REASON);
+ verify(mockedNodeService, never()).setProperty(hold, PROP_HOLD_DELETION_REASON, HOLD_DELETION_REASON);
+ }
+
+ @Test
+ public void setHoldDeletionReason()
+ {
+ // setup node service interactions
+ when(mockedNodeService.exists(hold))
+ .thenReturn(true);
+
+ // set hold deletion reason
+ holdService.setHoldDeletionReason(hold, HOLD_DELETION_REASON);
+ verify(mockedNodeService).setProperty(hold, PROP_HOLD_DELETION_REASON, HOLD_DELETION_REASON);
+ }
+
+ @Test
+ public void updateHoldThatDoesNotExist()
+ {
+ // setup node service interactions
+ when(mockedNodeService.exists(hold))
+ .thenReturn(false);
+
+ // node does not exist
+ holdService.updateHold(hold, HOLD_NAME, HOLD_REASON, HOLD_DESCRIPTION);
+ verify(mockedNodeService, never()).setProperty(any(NodeRef.class), any(QName.class), any(String.class));
+ }
+
+ @Test
+ public void updateHoldThatIsNotAHold()
+ {
+ // setup node service interactions
+ when(mockedNodeService.exists(hold))
+ .thenReturn(true);
+
+ // node isn't a hold
+ holdService.updateHold(recordFolder, HOLD_NAME, HOLD_REASON, HOLD_DESCRIPTION);
+ verify(mockedNodeService, never()).setProperty(any(NodeRef.class), any(QName.class), any(String.class));
+ }
+
+ @Test
+ public void updateHold()
+ {
+ // setup node service interactions
+ when(mockedNodeService.exists(hold))
+ .thenReturn(true);
+
+ // update hold
+ holdService.updateHold(hold, HOLD_NAME, HOLD_REASON, HOLD_DESCRIPTION);
+ verify(mockedNodeService).setProperty(hold, ContentModel.PROP_NAME, HOLD_NAME);
+ verify(mockedNodeService).setProperty(hold, ContentModel.PROP_DESCRIPTION, HOLD_DESCRIPTION);
+ verify(mockedNodeService).setProperty(hold, PROP_HOLD_REASON, HOLD_REASON);
+ }
+
@Test (expected=AlfrescoRuntimeException.class)
public void deleteHoldNotAHold()
{
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 2236659422..92bac98ac2 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
@@ -38,6 +38,8 @@ tags:
description: Retrieve and manage unfiled records containers
- name: unfiled-record-folders
description: Retrieve and manage unfiled record folders
+ - name: holds
+ description: Retrieve and manage holds
paths:
## GS sites
@@ -418,6 +420,124 @@ paths:
description: New name clashes with an existing node in the current parent container
'422':
description: Model integrity exception, including node name with invalid characters
+ '/file-plans/{filePlanId}/holds':
+ get:
+ tags:
+ - file-plans
+ summary: Get all holds in a file plan
+ description: |
+ Returns a list of holds.
+ operationId: getHolds
+ parameters:
+ - $ref: '#/parameters/filePlanIdWithAliasParam'
+ - $ref: '#/parameters/skipCountParam'
+ - $ref: '#/parameters/maxItemsParam'
+ consumes:
+ - application/json
+ produces:
+ - application/json
+ responses:
+ '200':
+ description: Successful response
+ schema:
+ $ref: '#/definitions/HoldPaging'
+ '401':
+ description: Authentication failed
+ '403':
+ description: Current user does not have permission to read **filePlanId**
+ '404':
+ description: "**filePlanId** does not exist"
+ default:
+ description: Unexpected error
+ schema:
+ $ref: '#/definitions/Error'
+ post:
+ tags:
+ - file-plans
+ summary: Create holds for a file plan
+ description: |
+ Creates a new hold.
+
+ You must specify at least a **name** and a **reason** property.
+
+ **Note:** You can create more than one hold by specifying a list of holds in the JSON body.
+ For example, the following JSON body creates two holds:
+ ```JSON
+ [
+ {
+ "name":"Hold1",
+ "description": "Description1",
+ "reason": "Reason1"
+ },
+ {
+ "name":"Hold2",
+ "description": "Description2",
+ "reason": "Reason2"
+ }
+ ]
+ ```
+
+ If you specify a list as input, then a paginated list rather than an entry is returned in the response body. For example:
+
+ ```JSON
+ {
+ "list": {
+ "pagination": {
+ "count": 2,
+ "hasMoreItems": false,
+ "totalItems": 2,
+ "skipCount": 0,
+ "maxItems": 100
+ },
+ "entries": [
+ {
+ "entry": {
+ ...
+ }
+ },
+ {
+ "entry": {
+ ...
+ }
+ }
+ ]
+ }
+ }
+ ```
+ operationId: addHold
+ parameters:
+ - $ref: '#/parameters/filePlanIdWithAliasParam'
+ - in: body
+ name: nodeBodyCreate
+ description: The node information to create.
+ required: true
+ schema:
+ $ref: '#/definitions/HoldCreateBodyModel'
+ consumes:
+ - application/json
+ produces:
+ - application/json
+ responses:
+ '201':
+ description: Successful response
+ schema:
+ $ref: '#/definitions/HoldModelEntry'
+ '400':
+ description: |
+ Invalid parameter: **filePlanId** is not a valid format or **HoldCreateBodyModel** is not valid
+ '401':
+ description: Authentication failed
+ '403':
+ description: Current user does not have permission to create a hold
+ '404':
+ description: |
+ **filePlanId** does not exist
+ '409':
+ description: A hold with the name **name** already exists
+ default:
+ description: Unexpected error
+ schema:
+ $ref: '#/definitions/Error'
## Unfiled records containers
'/unfiled-containers/{unfiledContainerId}':
get:
@@ -2092,6 +2212,289 @@ paths:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
+ ## Holds
+ '/holds/{holdId}':
+ get:
+ tags:
+ - holds
+ summary: Get a hold
+ description: |
+ Gets information for hold with id **holdId**.
+ operationId: getHold
+ parameters:
+ - $ref: '#/parameters/holdIdParam'
+ produces:
+ - application/json
+ responses:
+ '200':
+ description: Successful response
+ schema:
+ $ref: '#/definitions/HoldModelEntry'
+ '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'
+ put:
+ tags:
+ - holds
+ summary: Update a hold
+ description: |
+ Updates the hold with id **holdId**. For example, you can rename a hold:
+ ```JSON
+ {
+ "name":"My new name",
+ "description":"Existing description",
+ "reason":"Existing reason"
+ }
+ ```
+ operationId: updateHold
+ parameters:
+ - $ref: '#/parameters/holdIdParam'
+ - in: body
+ name: holdBodyUpdate
+ description: The hold information to update.
+ required: true
+ schema:
+ $ref: '#/definitions/HoldCreateBodyModel'
+ produces:
+ - application/json
+ responses:
+ '200':
+ description: Successful response
+ schema:
+ $ref: '#/definitions/HoldModelEntry'
+ '400':
+ description: |
+ Invalid parameter: the update request is invalid or **holdId** is not a valid format or **holdBodyUpdate** is invalid
+ '401':
+ description: Authentication failed
+ '403':
+ description: Current user does not have permission to update **holdId**
+ '404':
+ description: "**holdId** does not exist"
+ '409':
+ description: Updated name clashes with an existing node in the current parent folder
+ default:
+ description: Unexpected error
+ schema:
+ $ref: '#/definitions/Error'
+ delete:
+ tags:
+ - holds
+ summary: Delete a hold
+ description: |
+ Deletes the hold with id **holdId**.
+ operationId: deleteHold
+ parameters:
+ - $ref: '#/parameters/holdIdParam'
+ produces:
+ - application/json
+ responses:
+ '204':
+ description: Successful response
+ '400':
+ description: |
+ Invalid parameter: **holdId** is not a valid format
+ '401':
+ description: Authentication failed
+ '403':
+ description: Current user does not have permission to delete **holdId**
+ '404':
+ description: "**holdId** does not exist"
+ default:
+ description: Unexpected error
+ schema:
+ $ref: '#/definitions/Error'
+ '/holds/{holdId}/delete':
+ post:
+ tags:
+ - holds
+ summary: Delete a hold with a reason
+ description: |
+ Deletes the hold with id **holdId** and stores a reason for deletion in the audit log.
+
+ A **reason** must be specified in the request body.
+ operationId: deleteHoldWithReason
+ parameters:
+ - $ref: '#/parameters/holdIdParam'
+ - in: body
+ name: holdDeletionReason
+ description: Reason for deletion.
+ required: true
+ schema:
+ $ref: '#/definitions/HoldDeletionReason'
+ produces:
+ - application/json
+ responses:
+ '200':
+ description: Successful response
+ schema:
+ $ref: '#/definitions/HoldDeletionReasonEntry'
+ '400':
+ description: |
+ Invalid parameter: **holdId** is not a valid format
+ '401':
+ description: Authentication failed
+ '403':
+ description: Current user does not have permission to update or delete **holdId**
+ '404':
+ description: "**holdId** does not exist"
+ default:
+ description: Unexpected error
+ schema:
+ $ref: '#/definitions/Error'
+ '/holds/{holdId}/children':
+ post:
+ tags:
+ - holds
+ summary: Add children to a hold
+ description: |
+ Add a child of a hold with id **holdId**.
+
+ You must specify the child **id**.
+
+ The API returns a 201 Created if the child is already a child of the hold.
+
+ **Note:** You can add more than one child by specifying a list of children in the JSON body.
+ For example, the following JSON body adds two children:
+ ```JSON
+ [
+ {
+ "id":"a7c10f46-b85b-4de5-af1c-930056b736a7"
+ },
+ {
+ "id":"e0d79b71-be2b-4ce7-a846-a7c50cba20fb"
+ }
+ ]
+ ```
+
+ If you specify a list as input, then a paginated list rather than an entry is returned in the response body. For example:
+
+ ```JSON
+ {
+ "list": {
+ "pagination": {
+ "count": 2,
+ "hasMoreItems": false,
+ "totalItems": 2,
+ "skipCount": 0,
+ "maxItems": 100
+ },
+ "entries": [
+ {
+ "entry": {
+ ...
+ }
+ },
+ {
+ "entry": {
+ ...
+ }
+ }
+ ]
+ }
+ }
+ ```
+ operationId: addChildToHold
+ parameters:
+ - $ref: '#/parameters/holdIdParam'
+ - in: body
+ name: nodeId
+ description: The node id.
+ required: true
+ schema:
+ $ref: '#/definitions/HoldChild'
+ consumes:
+ - application/json
+ produces:
+ - application/json
+ responses:
+ '201':
+ description: Successful response
+ schema:
+ $ref: '#/definitions/HoldChildEntry'
+ '400':
+ description: |
+ Invalid parameter: **holdId** is not a valid format or **HoldChild** is not valid
+ '401':
+ description: Authentication failed
+ '403':
+ description: Current user does not have permission to add items to the hold
+ '404':
+ description: |
+ **holdId** does not exist
+ default:
+ description: Unexpected error
+ schema:
+ $ref: '#/definitions/Error'
+ get:
+ tags:
+ - holds
+ summary: Get children of a hold
+ description: |
+ Returns a list of children of a hold with id **holdId**.
+ operationId: getHoldChildren
+ parameters:
+ - $ref: '#/parameters/holdIdParam'
+ - $ref: '#/parameters/skipCountParam'
+ - $ref: '#/parameters/maxItemsParam'
+ consumes:
+ - application/json
+ produces:
+ - application/json
+ responses:
+ '200':
+ description: Successful response
+ schema:
+ $ref: '#/definitions/HoldChildPaging'
+ '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}/children/{holdChildId}':
+ delete:
+ tags:
+ - holds
+ summary: Delete a child of a hold
+ description: |
+ Deletes the relationship between a child with id **holdChildId** and a parent hold with id **holdId**.
+ operationId: removeHoldChild
+ parameters:
+ - $ref: '#/parameters/holdIdParam'
+ - $ref: '#/parameters/holdChildIdParam'
+ produces:
+ - application/json
+ responses:
+ '204':
+ description: Successful response
+ '400':
+ description: |
+ Invalid parameter: **holdChildId** or **holdId** is not a valid format
+ '401':
+ description: Authentication failed
+ '403':
+ description: Current user does not have permission to delete **holdChildId**
+ '404':
+ description: " **holdChildId** or **holdId** does not exist"
+ default:
+ description: Unexpected error
+ schema:
+ $ref: '#/definitions/Error'
parameters:
## File plans
@@ -2175,7 +2578,7 @@ parameters:
description: Also include **source** (in addition to **entries**) with folder information on the parent node – the specified parent **unfiledContainerId**
required: false
type: boolean
-## Unfiled record folders
+ ## Unfiled record folders
unfiledRecordFolderIdParam:
name: unfiledRecordFolderId
in: path
@@ -2446,6 +2849,19 @@ parameters:
items:
type: string
collectionFormat: csv
+ # Holds
+ holdIdParam:
+ name: holdId
+ in: path
+ description: The identifier of a hold.
+ required: true
+ type: string
+ holdChildIdParam:
+ name: holdChildId
+ in: path
+ description: The identifier of a child of a hold.
+ required: true
+ type: string
## Record
recordIdParam:
name: recordId
@@ -3519,6 +3935,89 @@ definitions:
properties:
association:
$ref: '#/definitions/ChildAssociationInfo'
+ ## Holds
+ HoldModelEntry:
+ type: object
+ required:
+ - entry
+ properties:
+ entry:
+ $ref: '#/definitions/HoldModel'
+ HoldModel:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ description:
+ type: string
+ reason:
+ type: string
+ HoldCreateBodyModel:
+ type: object
+ required:
+ - name
+ - reason
+ properties:
+ name:
+ type: string
+ description:
+ type: string
+ reason:
+ type: string
+ HoldPaging:
+ type: object
+ properties:
+ list:
+ type: object
+ properties:
+ pagination:
+ $ref: '#/definitions/Pagination'
+ entries:
+ type: array
+ items:
+ $ref: '#/definitions/HoldModelEntry'
+ HoldChild:
+ type: object
+ required:
+ - id
+ properties:
+ id:
+ type: string
+ HoldChildEntry:
+ type: object
+ required:
+ - entry
+ properties:
+ entry:
+ $ref: '#/definitions/HoldChild'
+ HoldChildPaging:
+ type: object
+ properties:
+ list:
+ type: object
+ properties:
+ pagination:
+ $ref: '#/definitions/Pagination'
+ entries:
+ type: array
+ items:
+ $ref: '#/definitions/HoldChildEntry'
+ HoldDeletionReasonEntry:
+ type: object
+ required:
+ - entry
+ properties:
+ entry:
+ $ref: '#/definitions/HoldDeletionReason'
+ HoldDeletionReason:
+ type: object
+ required:
+ - reason
+ properties:
+ reason:
+ type: string
##
RequestBodyFile:
type: object