Compare commits

...

27 Commits

Author SHA1 Message Date
Damian Ujma
cb863c646f ACS-7587 Improve permission test [ags][tas] 2024-05-29 13:05:54 +02:00
Damian Ujma
a2d1391aee ACS-7587 Increase timeout 2024-05-29 08:45:09 +02:00
Damian Ujma
818bcb09f3 ACS-7587 Fix intermittent failure 2024-05-27 14:01:49 +02:00
Damian Ujma
72e85076ad ACS-7587 Fix PMD issue 2024-05-27 12:19:43 +02:00
Damian Ujma
a69be867e8 ACS-7587 Fix PMD issues 2024-05-27 10:42:25 +02:00
Damian Ujma
fa6e0ded45 ACS-7587 Improve search query 2024-05-24 18:34:29 +02:00
Damian Ujma
ed0bbb6699 ACS-7587 Refactor code 2024-05-24 12:07:41 +02:00
Damian Ujma
8009a6d6dd ACS-7587 Add test files alternately 2024-05-20 13:17:48 +02:00
Damian Ujma
785bdb72ea ACS-7587 Refactor 2024-05-20 12:28:11 +02:00
Damian Ujma
66aef18862 ACS-7587 Fix PMD isues 2024-05-17 15:24:54 +02:00
Damian Ujma
7b516f24b6 ACS-7587 Fix PMD issues 2024-05-17 15:14:50 +02:00
Damian Ujma
0de1aca0f6 ACS-7587 Reimplement BulkStatusUpdater [ags][tas] 2024-05-16 17:16:47 +02:00
Damian Ujma
592dc35b6d ACS-7587 Change DefaultHoldBulkMonitor [ags][tas] 2024-05-15 16:22:21 +02:00
Damian Ujma
73724a8205 ACS-7587 Tests [ags][tas] 2024-05-15 15:16:54 +02:00
Damian Ujma
c8c1102431 ACS-7587 Refactor [ags] 2024-05-15 14:41:08 +02:00
Damian.Ujma@hyland.com
c31ae1fe33 ACS-7587 Remove merge leftovers 2024-05-15 13:12:10 +02:00
Damian Ujma
82e9f0452d Merge branch 'feature/ACS-7557_design_bulk_api' into feature/ACS-7587_implement_v1_bulk_api_to_add_items
# Conflicts:
#	amps/ags/rm-community/rm-community-rest-api-explorer/src/main/webapp/definitions/gs-core-api.yaml
2024-05-15 13:05:19 +02:00
Damian Ujma
377f546a9b Merge branch 'master' into feature/ACS-7556_bulk_update_in_legal_holds 2024-05-15 12:34:50 +02:00
Damian Ujma
07dcba972f ACS-7557 Refactor code 2024-05-15 12:33:21 +02:00
Damian Ujma
d7f9ed1cf0 ACS-7557 Reimplement task container 2024-05-10 16:33:53 +02:00
Damian Ujma
4eccb77fa8 ACS-7557 Add Legal Holds Bulk v1 API (#2624)
* ACS-7557 Add Legal Holds Bulk v1 API

* ACS-7557 Improve v1 API

* ACS-7557 Replace processId with bulkStatusId
2024-05-10 16:29:54 +02:00
Damian Ujma
07352336c5 ACS-7557 Refactor 2024-05-09 16:39:09 +02:00
Damian Ujma
b5ce847bb1 ACS-7557 Add comments + logging 2024-05-09 16:27:10 +02:00
Damian Ujma
34925b497b ACS-7557 Add IT tests 2024-05-09 15:11:59 +02:00
Damian.Ujma@hyland.com
8986d03a2f ACS-7557 Add permissions checks [ags] 2024-05-06 15:24:27 +02:00
Damian.Ujma@hyland.com
502c996c9e ACS-7557 Fix [ags] 2024-04-29 16:15:03 +02:00
Damian.Ujma@hyland.com
a22e7d23f0 ACS-7557 Add bulk API design 2024-04-29 16:00:40 +02:00
32 changed files with 2582 additions and 1 deletions

View File

@@ -84,6 +84,12 @@
<artifactId>okhttp</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>${dependency.awaitility.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>

View File

@@ -0,0 +1,60 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.rm.community.model.hold;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.alfresco.rest.search.RestRequestQueryModel;
import org.alfresco.utility.model.TestModel;
/**
* POJO for hold bulk request
*
* @author Damian Ujma
*/
@EqualsAndHashCode(callSuper = true)
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class HoldBulkOperation extends TestModel
{
public enum HoldBulkOperationType
{
ADD
}
@JsonProperty(required = true)
private RestRequestQueryModel query;
@JsonProperty(required = true)
private HoldBulkOperationType op;
}

View File

@@ -0,0 +1,50 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.rm.community.model.hold;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* POJO for hold bulk request entry
*
* @author Damian Ujma
*/
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class HoldBulkOperationEntry
{
private String bulkStatusId;
private long totalItems;
}

View File

@@ -0,0 +1,68 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.rm.community.model.hold;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.alfresco.utility.model.TestModel;
/**
* POJO for hold bulk request
*
* @author Damian Ujma
*/
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class HoldBulkStatus extends TestModel
{
private String bulkStatusId;
private String startTime;
private String endTime;
private long processedItems;
private long errorsCount;
private long totalItems;
private String lastError;
private Status status;
public enum Status
{
PENDING,
IN_PROGRESS,
DONE
}
}

View File

@@ -0,0 +1,38 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.rm.community.model.hold;
import org.alfresco.rest.core.RestModels;
/**
* Handle collection of {@link HoldBulkStatusEntry}
*
* @author Damian Ujma
*/
public class HoldBulkStatusCollection extends RestModels<HoldBulkStatusEntry, HoldBulkStatusCollection>
{
}

View File

@@ -0,0 +1,46 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.rm.community.model.hold;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.alfresco.rest.core.RestModels;
@Builder
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class HoldBulkStatusEntry extends RestModels<HoldBulkStatus, HoldBulkStatusEntry>
{
private HoldBulkStatus entry;
}

View File

@@ -48,5 +48,5 @@ import org.alfresco.rest.core.RestModels;
public class HoldChildEntry extends RestModels<Hold, HoldChildEntry>
{
@JsonProperty
private HoldChildEntry entry;
private HoldChild entry;
}

View File

@@ -39,6 +39,10 @@ import static org.springframework.http.HttpMethod.PUT;
import org.alfresco.rest.core.RMRestWrapper;
import org.alfresco.rest.rm.community.model.hold.Hold;
import org.alfresco.rest.rm.community.model.hold.HoldBulkOperation;
import org.alfresco.rest.rm.community.model.hold.HoldBulkOperationEntry;
import org.alfresco.rest.rm.community.model.hold.HoldBulkStatus;
import org.alfresco.rest.rm.community.model.hold.HoldBulkStatusCollection;
import org.alfresco.rest.rm.community.model.hold.HoldChild;
import org.alfresco.rest.rm.community.model.hold.HoldChildCollection;
import org.alfresco.rest.rm.community.model.hold.HoldDeletionReason;
@@ -287,4 +291,113 @@ public class HoldsAPI extends RMModelRequest
{
deleteHoldChild(holdId, holdChildId, EMPTY);
}
/**
* Starts a bulk process for a hold.
*
* @param holdBulkOperation The bulk operation details
* @param hold The identifier of a hold
* @param parameters The URL parameters to add
* @return The {@link HoldBulkOperationEntry} for the started bulk process
* @throws RuntimeException for the following cases:
* <ul>
* <li>{@code hold} or {@code holdBulkOperation} is invalid</li>
* <li>authentication fails</li>
* <li>current user does not have permission to start a bulk process for {@code hold}</li>
* <li>{@code hold} does not exist</li>
* </ul>
*/
public HoldBulkOperationEntry startBulkProcess(HoldBulkOperation holdBulkOperation, String hold, String parameters)
{
mandatoryObject("holdBulkOperation", holdBulkOperation);
mandatoryString("hold", hold);
return getRmRestWrapper().processModel(HoldBulkOperationEntry.class, requestWithBody(
POST,
toJson(holdBulkOperation),
"holds/{hold}/bulk",
hold,
parameters
));
}
/**
* See {@link #startBulkProcess(HoldBulkOperation, String, String)}
*/
public HoldBulkOperationEntry startBulkProcess(HoldBulkOperation holdBulkOperation, String hold)
{
return startBulkProcess(holdBulkOperation, hold, EMPTY);
}
/**
* Gets the status of a bulk process for a hold.
*
* @param holdId The identifier of a hold
* @param holdBulkStatusId The identifier of a bulk status operation
* @param parameters The URL parameters to add
* @return The {@link HoldBulkStatus} for the given {@code holdId} and {@code holdBulkStatusId}
* @throws RuntimeException for the following cases:
* <ul>
* <li>{@code holdId} or {@code holdBulkStatusId} is invalid</li>
* <li>authentication fails</li>
* <li>current user does not have permission to get the bulk status for {@code holdId}</li>
* <li>{@code holdId} or {@code holdBulkStatusId} does not exist</li>
* </ul>
*/
public HoldBulkStatus getBulkStatus(String holdId, String holdBulkStatusId, String parameters)
{
mandatoryString("holdId", holdId);
mandatoryString("holdBulkStatusId", holdBulkStatusId);
return getRmRestWrapper().processModel(HoldBulkStatus.class, simpleRequest(
GET,
"holds/{holdId}/bulk-statuses/{holdBulkStatusId}",
holdId,
holdBulkStatusId,
parameters
));
}
/**
* See {@link #getBulkStatus(String, String, String)}
*/
public HoldBulkStatus getBulkStatus(String holdId, String holdBulkStatusId)
{
return getBulkStatus(holdId, holdBulkStatusId, EMPTY);
}
/**
* Gets the statuses of all bulk processes for a hold.
*
* @param holdId The identifier of a hold
* @param parameters The URL parameters to add
* @return The {@link HoldBulkStatusCollection} for the given {@code holdId}
* @throws RuntimeException for the following cases:
* <ul>
* <li>{@code holdId} is invalid</li>
* <li>authentication fails</li>
* <li>current user does not have permission to get the bulk statuses for {@code holdId}</li>
* <li>{@code holdId} does not exist</li>
* </ul>
*/
public HoldBulkStatusCollection getBulkStatuses(String holdId, String parameters)
{
mandatoryString("holdId", holdId);
return getRmRestWrapper().processModels(HoldBulkStatusCollection.class, simpleRequest(
GET,
"holds/{holdId}/bulk-statuses",
holdId,
parameters
));
}
/**
* See {@link #getBulkStatuses(String, String)}
*/
public HoldBulkStatusCollection getBulkStatuses(String holdId)
{
return getBulkStatuses(holdId, EMPTY);
}
}

View File

@@ -0,0 +1,532 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.rm.community.hold;
import static org.alfresco.rest.rm.community.base.TestData.HOLD_DESCRIPTION;
import static org.alfresco.rest.rm.community.base.TestData.HOLD_REASON;
import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAlias.FILE_PLAN_ALIAS;
import static org.alfresco.rest.rm.community.model.user.UserPermissions.PERMISSION_FILING;
import static org.alfresco.rest.rm.community.model.user.UserPermissions.PERMISSION_READ_RECORDS;
import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix;
import static org.alfresco.utility.report.log.Step.STEP;
import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.springframework.http.HttpStatus.ACCEPTED;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.alfresco.dataprep.CMISUtil;
import org.alfresco.dataprep.ContentActions;
import org.alfresco.rest.rm.community.base.BaseRMRestTest;
import org.alfresco.rest.rm.community.model.hold.Hold;
import org.alfresco.rest.rm.community.model.hold.HoldBulkOperation;
import org.alfresco.rest.rm.community.model.hold.HoldBulkOperation.HoldBulkOperationType;
import org.alfresco.rest.rm.community.model.hold.HoldBulkOperationEntry;
import org.alfresco.rest.rm.community.model.hold.HoldBulkStatus;
import org.alfresco.rest.rm.community.model.hold.HoldBulkStatus.Status;
import org.alfresco.rest.rm.community.model.hold.HoldBulkStatusCollection;
import org.alfresco.rest.rm.community.model.hold.HoldBulkStatusEntry;
import org.alfresco.rest.rm.community.model.hold.HoldChild;
import org.alfresco.rest.rm.community.model.hold.HoldChildEntry;
import org.alfresco.rest.rm.community.model.user.UserRoles;
import org.alfresco.rest.search.RestRequestQueryModel;
import org.alfresco.rest.search.SearchRequest;
import org.alfresco.rest.v0.service.RoleService;
import org.alfresco.utility.constants.UserRole;
import org.alfresco.utility.model.FileModel;
import org.alfresco.utility.model.FolderModel;
import org.alfresco.utility.model.UserModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
/**
* API tests for adding items to holds via the bulk process
*/
public class AddToHoldsBulkV1Tests extends BaseRMRestTest
{
private static final String ACCESS_DENIED_ERROR_MESSAGE = "Access Denied. You do not have the appropriate " +
"permissions to perform this operation.";
private static final int NUMBER_OF_FILES = 5;
private final List<FileModel> addedFiles = new ArrayList<>();
private final List<UserModel> users = new ArrayList<>();
private final List<Hold> holds = new ArrayList<>();
private Hold hold;
private Hold hold2;
private Hold hold3;
private FolderModel rootFolder;
private HoldBulkOperation holdBulkOperation;
@Autowired
private RoleService roleService;
@Autowired
private ContentActions contentActions;
@BeforeClass(alwaysRun = true)
public void preconditionForAddContentToHold()
{
STEP("Create a hold.");
hold = getRestAPIFactory().getFilePlansAPI(getAdminUser()).createHold(
Hold.builder().name("HOLD" + generateTestPrefix(AddToHoldsV1Tests.class)).description(HOLD_DESCRIPTION)
.reason(HOLD_REASON).build(), FILE_PLAN_ALIAS);
holds.add(hold);
STEP("Create test files.");
testSite = dataSite.usingAdmin().createPublicRandomSite();
rootFolder = dataContent.usingAdmin().usingSite(testSite).createFolder();
FolderModel folder1 = dataContent.usingAdmin().usingResource(rootFolder).createFolder();
FolderModel folder2 = dataContent.usingAdmin().usingResource(folder1).createFolder();
// Add files to subfolders in the site
for (int i = 0; i < NUMBER_OF_FILES; i++)
{
FileModel documentHeld = dataContent.usingAdmin()
.usingResource(i % 2 == 0 ? folder1 : folder2)
.createContent(CMISUtil.DocumentType.TEXT_PLAIN);
addedFiles.add(documentHeld);
}
RestRequestQueryModel queryReq = getContentFromSiteQuery(testSite.getId());
SearchRequest searchRequest = new SearchRequest();
searchRequest.setQuery(queryReq);
STEP("Wait until all files are searchable.");
await().atMost(30, TimeUnit.SECONDS)
.until(() -> getRestAPIFactory().getSearchAPI(null).search(searchRequest).getPagination()
.getTotalItems() == NUMBER_OF_FILES);
holdBulkOperation = HoldBulkOperation.builder()
.query(queryReq)
.op(HoldBulkOperationType.ADD).build();
}
/**
* Given a user with the add to hold capability and hold filing permission
* When the user adds content from a site to a hold using the bulk API
* Then the content is added to the hold and the status of the bulk operation is DONE
*/
@Test
public void addContentFromTestSiteToHoldUsingBulkAPI()
{
UserModel userAddHoldPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite,
UserRole.SiteCollaborator, hold.getId(), UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING);
users.add(userAddHoldPermission);
STEP("Add content from the site to the hold using the bulk API.");
HoldBulkOperationEntry bulkOperationEntry = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
.startBulkProcess(holdBulkOperation, hold.getId());
// Verify the status code
assertStatusCode(ACCEPTED);
assertEquals(NUMBER_OF_FILES, bulkOperationEntry.getTotalItems());
STEP("Wait until all files are added to the hold.");
await().atMost(20, TimeUnit.SECONDS).until(
() -> getRestAPIFactory().getHoldsAPI(getAdminUser()).getChildren(hold.getId()).getEntries().size()
== NUMBER_OF_FILES);
List<String> holdChildrenNodeRefs = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
.getChildren(hold.getId()).getEntries().stream().map(HoldChildEntry::getEntry).map(
HoldChild::getId).toList();
assertEquals(addedFiles.stream().map(FileModel::getNodeRefWithoutVersion).sorted().toList(),
holdChildrenNodeRefs.stream().sorted().toList());
STEP("Check the bulk status.");
HoldBulkStatus holdBulkStatus = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
.getBulkStatus(hold.getId(), bulkOperationEntry.getBulkStatusId());
assertBulkProcessStatus(holdBulkStatus, NUMBER_OF_FILES, 0, null);
STEP("Check the bulk statuses.");
HoldBulkStatusCollection holdBulkStatusCollection = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
.getBulkStatuses(hold.getId());
assertEquals(Arrays.asList(holdBulkStatus), holdBulkStatusCollection.getEntries().stream().map(HoldBulkStatusEntry::getEntry).toList());
}
/**
* Given a user with the add to hold capability and hold filing permission
* When the user adds content from a folder and all subfolders to a hold using the bulk API
* Then the content is added to the hold and the status of the bulk operation is DONE
*/
@Test
public void addContentFromFolderAndAllSubfoldersToHoldUsingBulkAPI()
{
hold3 = getRestAPIFactory().getFilePlansAPI(getAdminUser()).createHold(
Hold.builder().name("HOLD" + generateTestPrefix(AddToHoldsV1Tests.class)).description(HOLD_DESCRIPTION)
.reason(HOLD_REASON).build(), FILE_PLAN_ALIAS);
holds.add(hold3);
UserModel userAddHoldPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite,
UserRole.SiteCollaborator, hold3.getId(), UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING);
users.add(userAddHoldPermission);
STEP("Add content from the site to the hold using the bulk API.");
// Get content from folder and all subfolders of the root folder
HoldBulkOperation bulkOperation = HoldBulkOperation.builder()
.query(getContentFromFolderAndAllSubfoldersQuery(rootFolder.getNodeRefWithoutVersion()))
.op(HoldBulkOperationType.ADD).build();
HoldBulkOperationEntry bulkOperationEntry = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
.startBulkProcess(bulkOperation, hold3.getId());
// Verify the status code
assertStatusCode(ACCEPTED);
assertEquals(NUMBER_OF_FILES, bulkOperationEntry.getTotalItems());
STEP("Wait until all files are added to the hold.");
await().atMost(20, TimeUnit.SECONDS).until(
() -> getRestAPIFactory().getHoldsAPI(getAdminUser()).getChildren(hold3.getId()).getEntries().size()
== NUMBER_OF_FILES);
List<String> holdChildrenNodeRefs = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
.getChildren(hold3.getId()).getEntries().stream().map(HoldChildEntry::getEntry).map(
HoldChild::getId).toList();
assertEquals(addedFiles.stream().map(FileModel::getNodeRefWithoutVersion).sorted().toList(),
holdChildrenNodeRefs.stream().sorted().toList());
STEP("Check the bulk status.");
HoldBulkStatus holdBulkStatus = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
.getBulkStatus(hold3.getId(), bulkOperationEntry.getBulkStatusId());
assertBulkProcessStatus(holdBulkStatus, NUMBER_OF_FILES, 0, null);
STEP("Check the bulk statuses.");
HoldBulkStatusCollection holdBulkStatusCollection = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
.getBulkStatuses(hold3.getId());
assertEquals(Arrays.asList(holdBulkStatus), holdBulkStatusCollection.getEntries().stream().map(HoldBulkStatusEntry::getEntry).toList());
}
/**
* Given a user without the add to hold capability
* When the user adds content from a site to a hold using the bulk API
* Then the user receives access denied error
*/
@Test
public void testBulkProcessWithUserWithoutAddToHoldCapability()
{
UserModel userWithoutAddToHoldCapability = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite,
UserRole
.SiteCollaborator,
hold.getId(), UserRoles.ROLE_RM_POWER_USER, PERMISSION_FILING);
users.add(userWithoutAddToHoldCapability);
STEP("Add content from the site to the hold using the bulk API.");
getRestAPIFactory().getHoldsAPI(userWithoutAddToHoldCapability)
.startBulkProcess(holdBulkOperation, hold.getId());
STEP("Verify the response status code and the error message.");
assertStatusCode(FORBIDDEN);
getRestAPIFactory().getRmRestWrapper().assertLastError().containsSummary(ACCESS_DENIED_ERROR_MESSAGE);
}
/**
* Given a user without the filing permission on a hold
* When the user adds content from a site to a hold using the bulk API
* Then the user receives access denied error
*/
@Test
public void testBulkProcessWithUserWithoutFilingPermissionOnAHold()
{
// User without filing permission on a hold
UserModel userWithoutPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite,
UserRole.SiteCollaborator, hold.getId(), UserRoles.ROLE_RM_MANAGER, PERMISSION_READ_RECORDS);
users.add(userWithoutPermission);
STEP("Add content from the site to the hold using the bulk API.");
getRestAPIFactory().getHoldsAPI(userWithoutPermission)
.startBulkProcess(holdBulkOperation, hold.getId());
STEP("Verify the response status code and the error message.");
assertStatusCode(FORBIDDEN);
getRestAPIFactory().getRmRestWrapper().assertLastError().containsSummary(ACCESS_DENIED_ERROR_MESSAGE);
}
/**
* Given a user without the write permission on all the content
* When the user adds content from a site to a hold using the bulk API
* Then all processed items are marked as errors and the last error message contains access denied error
*/
@Test
public void testBulkProcessWithUserWithoutWritePermissionOnTheContent()
{
// User without write permission on the content
UserModel userWithoutPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(
testSite, UserRole.SiteConsumer,
hold.getId(), UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING);
users.add(userWithoutPermission);
// Wait until permissions are reverted
SearchRequest searchRequest = new SearchRequest();
searchRequest.setQuery(holdBulkOperation.getQuery());
await().atMost(30, TimeUnit.SECONDS)
.until(() -> getRestAPIFactory().getSearchAPI(userWithoutPermission).search(searchRequest).getPagination()
.getTotalItems() == NUMBER_OF_FILES);
STEP("Add content from the site to the hold using the bulk API.");
HoldBulkOperationEntry bulkOperationEntry = getRestAPIFactory().getHoldsAPI(
userWithoutPermission).startBulkProcess(holdBulkOperation, hold.getId());
STEP("Verify the response.");
assertStatusCode(ACCEPTED);
await().atMost(20, TimeUnit.SECONDS).until(() ->
getRestAPIFactory().getHoldsAPI(userWithoutPermission)
.getBulkStatus(hold.getId(), bulkOperationEntry.getBulkStatusId()).getStatus() == Status.DONE);
HoldBulkStatus holdBulkStatus = getRestAPIFactory().getHoldsAPI(userWithoutPermission)
.getBulkStatus(hold.getId(), bulkOperationEntry.getBulkStatusId());
assertBulkProcessStatus(holdBulkStatus, NUMBER_OF_FILES, NUMBER_OF_FILES, ACCESS_DENIED_ERROR_MESSAGE);
}
/**
* Given a user without the write permission on one file
* When the user adds content from a site to a hold using the bulk API
* Then all processed items are added to the hold except the one that the user does not have write permission
* And the status of the bulk operation is DONE, contains the error message and the number of errors is 1
*/
@Test
public void testBulkProcessWithUserWithoutWritePermissionOnOneFile()
{
hold2 = getRestAPIFactory().getFilePlansAPI(getAdminUser()).createHold(
Hold.builder().name("HOLD" + generateTestPrefix(AddToHoldsV1Tests.class)).description(HOLD_DESCRIPTION)
.reason(HOLD_REASON).build(), FILE_PLAN_ALIAS);
holds.add(hold2);
UserModel userAddHoldPermission = roleService.createUserWithSiteRoleRMRoleAndPermission(testSite,
UserRole.SiteCollaborator, hold2.getId(), UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING);
users.add(userAddHoldPermission);
contentActions.setPermissionForUser(getAdminUser().getUsername(), getAdminUser().getPassword(),
testSite.getId(), addedFiles.get(0).getName(), userAddHoldPermission.getUsername(),
UserRole.SiteConsumer.getRoleId(), false);
STEP("Add content from the site to the hold using the bulk API.");
HoldBulkOperationEntry bulkOperationEntry = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
.startBulkProcess(holdBulkOperation, hold2.getId());
// Verify the status code
assertStatusCode(ACCEPTED);
assertEquals(NUMBER_OF_FILES, bulkOperationEntry.getTotalItems());
STEP("Wait until all files are added to the hold.");
await().atMost(30, TimeUnit.SECONDS).until(
() -> getRestAPIFactory().getHoldsAPI(getAdminUser()).getChildren(hold2.getId()).getEntries().size()
== NUMBER_OF_FILES - 1);
await().atMost(30, TimeUnit.SECONDS).until(
() -> getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
.getBulkStatus(hold2.getId(), bulkOperationEntry.getBulkStatusId()).getProcessedItems() == NUMBER_OF_FILES);
List<String> holdChildrenNodeRefs = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
.getChildren(hold2.getId()).getEntries().stream().map(HoldChildEntry::getEntry).map(
HoldChild::getId).toList();
assertEquals(addedFiles.stream().skip(1).map(FileModel::getNodeRefWithoutVersion).sorted().toList(),
holdChildrenNodeRefs.stream().sorted().toList());
STEP("Check the bulk status.");
HoldBulkStatus holdBulkStatus = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
.getBulkStatus(hold2.getId(), bulkOperationEntry.getBulkStatusId());
assertBulkProcessStatus(holdBulkStatus, NUMBER_OF_FILES, 1, ACCESS_DENIED_ERROR_MESSAGE);
STEP("Check the bulk statuses.");
HoldBulkStatusCollection holdBulkStatusCollection = getRestAPIFactory().getHoldsAPI(userAddHoldPermission)
.getBulkStatuses(hold2.getId());
assertEquals(Arrays.asList(holdBulkStatus), holdBulkStatusCollection.getEntries().stream().map(HoldBulkStatusEntry::getEntry).toList());
// Revert the permissions
contentActions.setPermissionForUser(getAdminUser().getUsername(), getAdminUser().getPassword(),
testSite.getId(), addedFiles.get(0).getName(), userAddHoldPermission.getUsername(),
UserRole.SiteCollaborator.getRoleId(), true);
}
/**
* Given an unauthenticated user
* When the user adds content from a site to a hold using the bulk API
* Then the user receives unauthorized error
*/
@Test
public void testBulkProcessAsUnauthenticatedUser()
{
STEP("Start bulk process as unauthenticated user");
getRestAPIFactory().getHoldsAPI(new UserModel(getAdminUser().getUsername(), "wrongPassword"))
.startBulkProcess(holdBulkOperation, hold.getId());
STEP("Verify the response status code.");
assertStatusCode(UNAUTHORIZED);
}
/**
* Given a user with the add to hold capability and hold filing permission
* When the user adds content from a site to a hold using the bulk API
* And the hold does not exist
* Then the user receives not found error
*/
@Test
public void testBulkProcessForNonExistentHold()
{
STEP("Start bulk process for non existent hold");
getRestAPIFactory().getHoldsAPI(getAdminUser()).startBulkProcess(holdBulkOperation, "nonExistentHoldId");
STEP("Verify the response status code.");
assertStatusCode(NOT_FOUND);
}
/**
* Given a user with the add to hold capability and hold filing permission
* When the user adds content from a site to a hold using the bulk API
* and the bulk operation is invalid
* Then the user receives bad request error
*/
@Test
public void testGetBulkStatusesForInvalidOperation()
{
STEP("Start bulk process for non existent hold");
HoldBulkOperation invalidHoldBulkOperation = HoldBulkOperation.builder().op(null)
.query(holdBulkOperation.getQuery()).build();
getRestAPIFactory().getHoldsAPI(getAdminUser()).startBulkProcess(invalidHoldBulkOperation, hold.getId());
STEP("Verify the response status code.");
assertStatusCode(BAD_REQUEST);
}
/**
* Given a user with the add to hold capability and hold filing permission
* When the user adds content from a site to a hold using the bulk API
* And the hold does not exist
* Then the user receives not found error
*/
@Test
public void testGetBulkStatusForNonExistentHold()
{
STEP("Start bulk process for non existent hold");
getRestAPIFactory().getHoldsAPI(getAdminUser()).getBulkStatus("nonExistentHoldId", "nonExistenBulkStatusId");
STEP("Verify the response status code.");
assertStatusCode(NOT_FOUND);
}
/**
* Given a user with the add to hold capability and hold filing permission
* When the user adds content from a site to a hold using the bulk API
* And the bulk status does not exist
* Then the user receives not found error
*/
@Test
public void testGetBulkStatusForNonExistentBulkStatus()
{
STEP("Start bulk process for non bulk status");
getRestAPIFactory().getHoldsAPI(getAdminUser()).getBulkStatus(hold.getId(), "nonExistenBulkStatusId");
STEP("Verify the response status code.");
assertStatusCode(NOT_FOUND);
}
/**
* Given a user with the add to hold capability and hold filing permission
* When the user adds content from a site to a hold using the bulk API
* And the hold does not exist
* Then the user receives not found error
*/
@Test
public void testGetBulkStatusesForNonExistentHold()
{
STEP("Start bulk process for non existent hold");
getRestAPIFactory().getHoldsAPI(getAdminUser()).getBulkStatuses("nonExistentHoldId");
STEP("Verify the response status code.");
assertStatusCode(NOT_FOUND);
}
/**
* Given a user with the add to hold capability and hold filing permission
* When the user adds content from all sites to a hold using the bulk API to exceed the limit (30 items)
* Then the user receives bad request error
*/
@Test
public void testExceedingBulkOperationLimit()
{
RestRequestQueryModel queryReq = new RestRequestQueryModel();
queryReq.setQuery("TYPE:content");
queryReq.setLanguage("afts");
HoldBulkOperation exceedLimitOp = HoldBulkOperation.builder()
.query(queryReq)
.op(HoldBulkOperationType.ADD).build();
STEP("Start bulk process to exceed the limit");
getRestAPIFactory().getHoldsAPI(getAdminUser()).startBulkProcess(exceedLimitOp, hold.getId());
STEP("Verify the response status code.");
assertStatusCode(BAD_REQUEST);
}
private void assertBulkProcessStatus(HoldBulkStatus holdBulkStatus, long expectedProcessedItems,
int expectedErrorsCount, String expectedErrorMessage)
{
assertEquals(Status.DONE, holdBulkStatus.getStatus());
assertEquals(expectedProcessedItems, holdBulkStatus.getTotalItems());
assertEquals(expectedProcessedItems, holdBulkStatus.getProcessedItems());
assertEquals(expectedErrorsCount, holdBulkStatus.getErrorsCount());
assertNotNull(holdBulkStatus.getStartTime());
assertNotNull(holdBulkStatus.getEndTime());
if (expectedErrorMessage != null)
{
assertTrue(holdBulkStatus.getLastError().contains(expectedErrorMessage));
}
}
private RestRequestQueryModel getContentFromSiteQuery(String siteId)
{
RestRequestQueryModel queryReq = new RestRequestQueryModel();
queryReq.setQuery("SITE:\"" + siteId + "\" and TYPE:content");
queryReq.setLanguage("afts");
return queryReq;
}
private RestRequestQueryModel getContentFromFolderAndAllSubfoldersQuery(String folderId)
{
RestRequestQueryModel queryReq = new RestRequestQueryModel();
queryReq.setQuery("ANCESTOR:\"workspace://SpacesStore/" + folderId + "\" and TYPE:content");
queryReq.setLanguage("afts");
return queryReq;
}
@AfterClass(alwaysRun = true)
public void cleanupAddToHoldsBulkV1Tests()
{
dataSite.usingAdmin().deleteSite(testSite);
users.forEach(user -> getDataUser().usingAdmin().deleteUser(user));
holds.forEach(hold -> getRestAPIFactory().getHoldsAPI(getAdminUser()).deleteHold(hold.getId()));
}
}

View File

@@ -139,3 +139,21 @@ content.metadata.async.extract.6.enabled=false
# Max number of entries returned in Record search view
rm.recordSearch.maxItems=500
#
# Hold bulk
#
# The number of worker threads.
rm.hold.bulk.threadCount=2
# The maximum number of total items to process in a single bulk operation.
rm.hold.bulk.maxItems=1000
# The number of entries to be fetched from the Search Service as a next set of work object to process.
rm.hold.bulk.batchSize=100
# The number of entries to process before reporting progress.
rm.hold.bulk.logging.interval=100
# The number of entries we process at a time in a transaction.
rm.hold.bulk.itemsPerTransaction=1
cache.bulkHoldStatusCache.cluster.type=fully-distributed
cache.bulkHoldRegistryCache.cluster.type=fully-distributed

View File

@@ -89,6 +89,9 @@
<!-- Import RM Audit -->
<import resource="classpath:alfresco/module/org_alfresco_module_rm/rm-audit-context.xml"/>
<!-- Import RM Bulk -->
<import resource="classpath:alfresco/module/org_alfresco_module_rm/rm-bulk-context.xml"/>
<!-- Import RM query context -->
<import resource="classpath:alfresco/module/org_alfresco_module_rm/query/rm-query-context.xml" />

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="holdBulkService"
class="org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkServiceImpl">
<property name="serviceRegistry" ref="ServiceRegistry" />
<property name="transactionService" ref="transactionService" />
<property name="searchMapper" ref="searchapiSearchMapper" />
<property name="bulkMonitor" ref="holdBulkMonitor" />
<property name="holdService" ref="HoldService" />
<property name="capabilityService" ref="CapabilityService" />
<property name="permissionService" ref="PermissionService" />
<property name="nodeService" ref="NodeService" />
<property name="threadCount">
<value>${rm.hold.bulk.threadCount}</value>
</property>
<property name="batchSize">
<value>${rm.hold.bulk.batchSize}</value>
</property>
<property name="maxItems">
<value>${rm.hold.bulk.maxItems}</value>
</property>
<property name="loggingInterval">
<value>${rm.hold.bulk.logging.interval}</value>
</property>
<property name="itemsPerTransaction">
<value>${rm.hold.bulk.itemsPerTransaction}</value>
</property>
</bean>
<bean id="holdBulkMonitor" class="org.alfresco.module.org_alfresco_module_rm.bulk.hold.DefaultHoldBulkMonitor">
<property name="holdProgressCache" ref="holdProgressCache" />
<property name="holdProcessRegistry" ref="holdProcessRegistry" />
</bean>
<bean name="holdProgressCache" factory-bean="cacheFactory" factory-method="createCache">
<constructor-arg value="cache.bulkHoldStatusCache" />
</bean>
<bean name="holdProcessRegistry" factory-bean="cacheFactory" factory-method="createCache">
<constructor-arg value="cache.bulkHoldRegistryCache" />
</bean>
</beans>

View File

@@ -83,6 +83,13 @@
<property name="nodesModelFactory" ref="nodesModelFactory" />
<property name="fileFolderService" ref="FileFolderService" />
<property name="transactionService" ref="transactionService" />
<property name="holdBulkService" ref="holdBulkService" />
</bean>
<bean class="org.alfresco.rm.rest.api.holds.HoldsBulkStatusesRelation" >
<property name="holdBulkMonitor" ref="holdBulkMonitor" />
<property name="apiUtils" ref="apiUtils" />
<property name="permissionService" ref="PermissionService" />
</bean>
<bean class="org.alfresco.rm.rest.api.holds.HoldsChildrenRelation">

View File

@@ -41,6 +41,8 @@ services:
-Daos.baseUrlOverwrite=http://localhost:8080/alfresco/aos
-Dmessaging.broker.url=\"failover:(tcp://activemq:61616)?timeout=3000&jms.useCompression=true\"
-DlocalTransform.core-aio.url=http://transform-core-aio:8090/
-Drm.hold.bulk.maxItems=5
-Drm.hold.bulk.batchSize=2
"
ports:
- 8080:8080

View File

@@ -0,0 +1,249 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk;
import java.util.UUID;
import org.alfresco.repo.batch.BatchProcessWorkProvider;
import org.alfresco.repo.batch.BatchProcessor;
import org.alfresco.repo.batch.BatchProcessor.BatchProcessWorker;
import org.alfresco.rest.api.search.impl.SearchMapper;
import org.alfresco.rest.api.search.model.Query;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.transaction.TransactionService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
/**
* A base class for executing bulk operations on nodes based on search query results
*/
public abstract class BulkBaseService<T> implements InitializingBean
{
private static final Log LOG = LogFactory.getLog(BulkBaseService.class);
protected ServiceRegistry serviceRegistry;
protected SearchService searchService;
protected TransactionService transactionService;
protected SearchMapper searchMapper;
protected BulkMonitor<T> bulkMonitor;
protected int threadCount;
protected int batchSize;
protected int itemsPerTransaction;
protected int maxItems;
protected int loggingInterval;
@Override
public void afterPropertiesSet() throws Exception
{
this.searchService = serviceRegistry.getSearchService();
}
/**
* Execute bulk operation on node based on the search query results
*
* @param nodeRef node reference
* @param bulkOperation bulk operation
* @return bulk status
*/
public T execute(NodeRef nodeRef, BulkOperation bulkOperation)
{
checkPermissions(nodeRef, bulkOperation);
ResultSet resultSet = getTotalItems(bulkOperation.searchQuery(), maxItems);
if (maxItems < resultSet.getNumberFound() || resultSet.hasMore())
{
throw new InvalidArgumentException("Too many items to process. Please refine your query.");
}
long totalItems = resultSet.getNumberFound();
// Generate a random process id
String processId = UUID.randomUUID().toString();
T initBulkStatus = getInitBulkStatus(processId, totalItems);
bulkMonitor.updateBulkStatus(initBulkStatus);
bulkMonitor.registerProcess(nodeRef, processId);
BatchProcessWorker<NodeRef> batchProcessWorker = getWorkerProvider(nodeRef, bulkOperation);
BulkStatusUpdater bulkStatusUpdater = getBulkStatusUpdater();
BatchProcessor<NodeRef> batchProcessor = new BatchProcessor<>(
processId,
transactionService.getRetryingTransactionHelper(),
getWorkProvider(bulkOperation, totalItems, bulkStatusUpdater),
threadCount,
itemsPerTransaction,
bulkStatusUpdater,
LOG,
loggingInterval);
runAsyncBatchProcessor(batchProcessor, batchProcessWorker, bulkStatusUpdater);
return initBulkStatus;
}
/**
* Run batch processor
*/
protected void runAsyncBatchProcessor(BatchProcessor<NodeRef> batchProcessor,
BatchProcessWorker<NodeRef> batchProcessWorker, BulkStatusUpdater bulkStatusUpdater)
{
Runnable backgroundLogic = () -> {
try
{
if (LOG.isDebugEnabled())
{
LOG.debug("Started processing batch with name: " + batchProcessor.getProcessName());
}
batchProcessor.processLong(batchProcessWorker, true);
if (LOG.isDebugEnabled())
{
LOG.debug("Processing batch with name: " + batchProcessor.getProcessName() + " completed");
}
}
catch (Exception exception)
{
LOG.error("Error processing batch with name: " + batchProcessor.getProcessName(), exception);
}
finally
{
bulkStatusUpdater.update();
}
};
Thread backgroundThread = new Thread(backgroundLogic, "BulkBaseService-BackgroundThread");
backgroundThread.start();
}
/**
* Get initial bulk status
*
* @param processId process id
* @param totalItems total items
* @return bulk status
*/
protected abstract T getInitBulkStatus(String processId, long totalItems);
/**
* Get bulk status updater
*
* @return bulk status updater
*/
protected abstract BulkStatusUpdater getBulkStatusUpdater();
/**
* Get work provider
*
* @param bulkOperation bulk operation
* @param totalItems total items
* @param bulkStatusUpdater bulk status updater
* @return work provider
*/
protected abstract BatchProcessWorkProvider<NodeRef> getWorkProvider(BulkOperation bulkOperation, long totalItems,
BulkStatusUpdater bulkStatusUpdater);
/**
* Get worker provider
*
* @param nodeRef node reference
* @param bulkOperation bulk operation
* @return worker provider
*/
protected abstract BatchProcessWorker<NodeRef> getWorkerProvider(NodeRef nodeRef, BulkOperation bulkOperation);
/**
* Check permissions
*
* @param nodeRef node reference
* @param bulkOperation bulk operation
*/
protected abstract void checkPermissions(NodeRef nodeRef, BulkOperation bulkOperation);
protected ResultSet getTotalItems(Query searchQuery, int skipCount)
{
SearchParameters searchParams = new SearchParameters();
searchMapper.setDefaults(searchParams);
searchMapper.fromQuery(searchParams, searchQuery);
searchParams.setSkipCount(skipCount);
searchParams.setMaxItems(1);
return searchService.query(searchParams);
}
public void setServiceRegistry(ServiceRegistry serviceRegistry)
{
this.serviceRegistry = serviceRegistry;
}
public void setSearchService(SearchService searchService)
{
this.searchService = searchService;
}
public void setTransactionService(TransactionService transactionService)
{
this.transactionService = transactionService;
}
public void setSearchMapper(SearchMapper searchMapper)
{
this.searchMapper = searchMapper;
}
public void setBulkMonitor(BulkMonitor<T> bulkMonitor)
{
this.bulkMonitor = bulkMonitor;
}
public void setThreadCount(int threadCount)
{
this.threadCount = threadCount;
}
public void setBatchSize(int batchSize)
{
this.batchSize = batchSize;
}
public void setMaxItems(int maxItems)
{
this.maxItems = maxItems;
}
public void setLoggingInterval(int loggingInterval)
{
this.loggingInterval = loggingInterval;
}
public void setItemsPerTransaction(int itemsPerTransaction)
{
this.itemsPerTransaction = itemsPerTransaction;
}
}

View File

@@ -0,0 +1,58 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk;
import org.alfresco.service.cmr.repository.NodeRef;
/**
* An interface for monitoring the progress of a bulk operation
*/
public interface BulkMonitor<T>
{
/**
* Update the bulk status
*
* @param bulkStatus the bulk status
*/
void updateBulkStatus(T bulkStatus);
/**
* Register a process
*
* @param nodeRef the node reference
* @param processId the process id
*/
void registerProcess(NodeRef nodeRef, String processId);
/**
* Get the bulk status
*
* @param bulkStatusId the bulk status id
* @return the bulk status
*/
T getBulkStatus(String bulkStatusId);
}

View File

@@ -0,0 +1,44 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk;
import org.alfresco.rest.api.search.model.Query;
/**
* An immutable POJO to represent a bulk operation
*/
public record BulkOperation(Query searchQuery, String operationType)
{
public BulkOperation
{
if (operationType == null || searchQuery == null)
{
throw new IllegalArgumentException("Operation type and search query must not be null");
}
}
}

View File

@@ -0,0 +1,40 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk;
import org.springframework.context.ApplicationEventPublisher;
/**
* An interface for updating the status of a bulk operation
*/
public interface BulkStatusUpdater extends ApplicationEventPublisher
{
/**
* Update the bulk status
*/
void update();
}

View File

@@ -0,0 +1,109 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk.hold;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.rm.rest.api.model.HoldBulkStatus;
import org.alfresco.service.cmr.repository.NodeRef;
import org.springframework.context.ApplicationEvent;
import org.springframework.extensions.surf.util.AbstractLifecycleBean;
/**
* Default hold bulk monitor implementation
*/
public class DefaultHoldBulkMonitor extends AbstractLifecycleBean implements HoldBulkMonitor
{
protected SimpleCache<String, HoldBulkStatus> holdProgressCache;
protected SimpleCache<String, List<HoldBulkProcessDetails>> holdProcessRegistry;
@Override
public void updateBulkStatus(HoldBulkStatus holdBulkStatus)
{
holdProgressCache.put(holdBulkStatus.bulkStatusId(), holdBulkStatus);
}
@Override
public void registerProcess(NodeRef holdRef, String processId)
{
List<HoldBulkProcessDetails> processIds = Optional.ofNullable(holdProcessRegistry.get(holdRef.getId()))
.orElse(new ArrayList<>());
processIds.add(new HoldBulkProcessDetails(processId, null));
holdProcessRegistry.put(holdRef.getId(), processIds);
}
@Override
public HoldBulkStatus getBulkStatus(String bulkStatusId)
{
return holdProgressCache.get(bulkStatusId);
}
@Override
public List<HoldBulkStatus> getBulkStatusesForHold(String holdId)
{
return Optional.ofNullable(holdProcessRegistry.get(holdId))
.map(bulkProcessDetailsList -> bulkProcessDetailsList.stream()
.map(HoldBulkProcessDetails::bulkStatusId)
.map(this::getBulkStatus)
.filter(Objects::nonNull)
.sorted(Comparator.comparing(HoldBulkStatus::endTime, Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(HoldBulkStatus::startTime, Comparator.nullsLast(Comparator.naturalOrder()))
.reversed())
.toList())
.orElse(Collections.emptyList());
}
public void setHoldProgressCache(
SimpleCache<String, HoldBulkStatus> holdProgressCache)
{
this.holdProgressCache = holdProgressCache;
}
public void setHoldProcessRegistry(
SimpleCache<String, List<HoldBulkProcessDetails>> holdProcessRegistry)
{
this.holdProcessRegistry = holdProcessRegistry;
}
@Override
protected void onBootstrap(ApplicationEvent applicationEvent)
{
// NOOP
}
@Override
protected void onShutdown(ApplicationEvent applicationEvent)
{
// NOOP
}
}

View File

@@ -0,0 +1,46 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk.hold;
import java.util.List;
import org.alfresco.module.org_alfresco_module_rm.bulk.BulkMonitor;
import org.alfresco.rm.rest.api.model.HoldBulkStatus;
/**
* An interface for monitoring the progress of a bulk hold operation
*/
public interface HoldBulkMonitor extends BulkMonitor<HoldBulkStatus>
{
/**
* Get the bulk statuses for a hold
*
* @param holdId the hold id
* @return the bulk statuses
*/
List<HoldBulkStatus> getBulkStatusesForHold(String holdId);
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk.hold;
import java.io.Serializable;
/**
* A simple immutable POJO to hold the details of a bulk hold process
*/
public record HoldBulkProcessDetails(String bulkStatusId, String creatorInstance) implements Serializable
{
}

View File

@@ -0,0 +1,45 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk.hold;
import org.alfresco.module.org_alfresco_module_rm.bulk.BulkOperation;
import org.alfresco.rm.rest.api.model.HoldBulkStatus;
import org.alfresco.service.cmr.repository.NodeRef;
/**
* Interface defining a hold bulk service.
*/
public interface HoldBulkService
{
/**
* Initiates a bulk operation on a hold.
*
* @param holdRef The hold reference
* @param bulkOperation The bulk operation
*/
HoldBulkStatus execute(NodeRef holdRef, BulkOperation bulkOperation);
}

View File

@@ -0,0 +1,258 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk.hold;
import static org.alfresco.model.ContentModel.PROP_NAME;
import static org.alfresco.rm.rest.api.model.HoldBulkOperationType.ADD;
import java.util.Collection;
import java.util.Collections;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicInteger;
import org.alfresco.model.ContentModel;
import org.alfresco.module.org_alfresco_module_rm.bulk.BulkBaseService;
import org.alfresco.module.org_alfresco_module_rm.bulk.BulkOperation;
import org.alfresco.module.org_alfresco_module_rm.bulk.BulkStatusUpdater;
import org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService;
import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel;
import org.alfresco.module.org_alfresco_module_rm.hold.HoldService;
import org.alfresco.repo.batch.BatchProcessWorkProvider;
import org.alfresco.repo.batch.BatchProcessor.BatchProcessWorker;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.rest.api.search.model.Query;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rm.rest.api.model.HoldBulkOperationType;
import org.alfresco.rm.rest.api.model.HoldBulkStatus;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.PermissionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.extensions.surf.util.I18NUtil;
/**
* Implementation of the {@link HoldBulkService} interface.
*/
@SuppressWarnings("PMD.PreserveStackTrace")
public class HoldBulkServiceImpl extends BulkBaseService<HoldBulkStatus> implements HoldBulkService
{
private static final Logger LOGGER = LoggerFactory.getLogger(HoldBulkServiceImpl.class);
private HoldService holdService;
private static final String MSG_ERR_ACCESS_DENIED = "permissions.err_access_denied";
private CapabilityService capabilityService;
private PermissionService permissionService;
private NodeService nodeService;
@Override
protected HoldBulkStatus getInitBulkStatus(String processId, long totalItems)
{
return new HoldBulkStatus(processId, null, null, 0, 0, totalItems, null);
}
@Override
protected BulkStatusUpdater getBulkStatusUpdater()
{
return new HoldBulkStatusUpdater((HoldBulkMonitor) bulkMonitor);
}
@Override
protected BatchProcessWorkProvider<NodeRef> getWorkProvider(BulkOperation bulkOperation, long totalItems,
BulkStatusUpdater bulkStatusUpdater)
{
return new AddToHoldWorkerProvider(new AtomicInteger(0), bulkOperation, totalItems, bulkStatusUpdater);
}
@Override
protected BatchProcessWorker<NodeRef> getWorkerProvider(NodeRef nodeRef, BulkOperation bulkOperation)
{
try
{
HoldBulkOperationType holdBulkOperationType = HoldBulkOperationType.valueOf(bulkOperation.operationType()
.toUpperCase(Locale.ENGLISH));
return switch (holdBulkOperationType)
{
case ADD -> new AddToHoldWorkerBatch(nodeRef);
};
}
catch (IllegalArgumentException e)
{
String errorMsg = "Unsupported action type when starting the bulk process: ";
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("{} {}", errorMsg, bulkOperation.operationType(), e);
}
throw new InvalidArgumentException(errorMsg + bulkOperation.operationType());
}
}
@Override
protected void checkPermissions(NodeRef holdRef, BulkOperation bulkOperation)
{
if (!holdService.isHold(holdRef))
{
final String holdName = (String) nodeService.getProperty(holdRef, PROP_NAME);
throw new InvalidArgumentException(I18NUtil.getMessage("rm.hold.not-hold", holdName), null);
}
if (ADD.name().equals(bulkOperation.operationType()) && (!AccessStatus.ALLOWED.equals(
capabilityService.getCapabilityAccessState(holdRef, RMPermissionModel.ADD_TO_HOLD)) ||
permissionService.hasPermission(holdRef, RMPermissionModel.FILING) == AccessStatus.DENIED))
{
throw new AccessDeniedException(I18NUtil.getMessage(MSG_ERR_ACCESS_DENIED));
}
}
private class AddToHoldWorkerBatch implements BatchProcessWorker<NodeRef>
{
private final NodeRef holdRef;
private final String currentUser;
public AddToHoldWorkerBatch(NodeRef holdRef)
{
this.holdRef = holdRef;
currentUser = AuthenticationUtil.getFullyAuthenticatedUser();
}
@Override
public String getIdentifier(NodeRef entry)
{
return entry.getId();
}
@Override
public void beforeProcess()
{
AuthenticationUtil.pushAuthentication();
}
@Override
public void process(NodeRef entry) throws Throwable
{
AuthenticationUtil.setFullyAuthenticatedUser(currentUser);
holdService.addToHold(holdRef, entry);
}
@Override
public void afterProcess()
{
AuthenticationUtil.popAuthentication();
}
}
private class AddToHoldWorkerProvider implements BatchProcessWorkProvider<NodeRef>
{
private final AtomicInteger currentNodeNumber;
private final Query searchQuery;
private final String currentUser;
private final long totalItems;
private final BulkStatusUpdater bulkStatusUpdater;
public AddToHoldWorkerProvider(AtomicInteger currentNodeNumber, BulkOperation bulkOperation, long totalItems,
BulkStatusUpdater bulkStatusUpdater)
{
this.currentNodeNumber = currentNodeNumber;
this.searchQuery = bulkOperation.searchQuery();
this.totalItems = totalItems;
this.bulkStatusUpdater = bulkStatusUpdater;
currentUser = AuthenticationUtil.getFullyAuthenticatedUser();
}
@Override
public int getTotalEstimatedWorkSize()
{
return (int) totalItems;
}
@Override
public long getTotalEstimatedWorkSizeLong()
{
return totalItems;
}
@Override
public Collection<NodeRef> getNextWork()
{
AuthenticationUtil.pushAuthentication();
AuthenticationUtil.setFullyAuthenticatedUser(currentUser);
SearchParameters searchParams = getNextPageParameters();
ResultSet result = searchService.query(searchParams);
if (result.getNodeRefs().isEmpty())
{
return Collections.emptyList();
}
AuthenticationUtil.popAuthentication();
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Processing the next work for the batch processor, skipCount={}, size={}",
searchParams.getSkipCount(), result.getNumberFound());
}
currentNodeNumber.addAndGet(batchSize);
bulkStatusUpdater.update();
return result.getNodeRefs();
}
private SearchParameters getNextPageParameters()
{
SearchParameters searchParams = new SearchParameters();
searchMapper.setDefaults(searchParams);
searchMapper.fromQuery(searchParams, searchQuery);
searchParams.setSkipCount(currentNodeNumber.get());
searchParams.setMaxItems(batchSize);
searchParams.setLimit(batchSize);
searchParams.addSort("@" + ContentModel.PROP_CREATED, true);
return searchParams;
}
}
public void setHoldService(HoldService holdService)
{
this.holdService = holdService;
}
public void setCapabilityService(CapabilityService capabilityService)
{
this.capabilityService = capabilityService;
}
public void setPermissionService(PermissionService permissionService)
{
this.permissionService = permissionService;
}
public void setNodeService(NodeService nodeService)
{
this.nodeService = nodeService;
}
}

View File

@@ -0,0 +1,69 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk.hold;
import org.alfresco.module.org_alfresco_module_rm.bulk.BulkStatusUpdater;
import org.alfresco.repo.batch.BatchMonitor;
import org.alfresco.repo.batch.BatchMonitorEvent;
import org.alfresco.rm.rest.api.model.HoldBulkStatus;
/**
* An implementation of {@link BulkStatusUpdater} for the hold bulk operation
*/
public class HoldBulkStatusUpdater implements BulkStatusUpdater
{
private final Runnable task;
private BatchMonitor batchMonitor;
public HoldBulkStatusUpdater(HoldBulkMonitor holdBulkMonitor)
{
this.task = () -> holdBulkMonitor.updateBulkStatus(
new HoldBulkStatus(batchMonitor.getProcessName(), batchMonitor.getStartTime(),
batchMonitor.getEndTime(),
batchMonitor.getSuccessfullyProcessedEntriesLong() + batchMonitor.getTotalErrorsLong(),
batchMonitor.getTotalErrorsLong(), batchMonitor.getTotalResultsLong(),
batchMonitor.getLastError()));
}
@Override
public void update()
{
if (task != null && batchMonitor != null)
{
task.run();
}
}
@Override
public void publishEvent(Object event)
{
if (event instanceof BatchMonitorEvent batchMonitorEvent)
{
batchMonitor = batchMonitorEvent.getBatchMonitor();
}
}
}

View File

@@ -0,0 +1,120 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rm.rest.api.holds;
import static org.alfresco.module.org_alfresco_module_rm.util.RMParameterCheck.checkNotBlank;
import static org.alfresco.util.ParameterCheck.mandatory;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkMonitor;
import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel;
import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
import org.alfresco.rest.framework.core.exceptions.RelationshipResourceNotFoundException;
import org.alfresco.rest.framework.resource.RelationshipResource;
import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.rm.rest.api.impl.FilePlanComponentsApiUtils;
import org.alfresco.rm.rest.api.model.HoldBulkStatus;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.PermissionService;
import org.springframework.extensions.surf.util.I18NUtil;
@RelationshipResource(name = "bulk-statuses", entityResource = HoldsEntityResource.class, title = "Bulk statuses of a hold")
public class HoldsBulkStatusesRelation
implements RelationshipResourceAction.Read<HoldBulkStatus>, RelationshipResourceAction.ReadById<HoldBulkStatus>
{
private HoldBulkMonitor holdBulkMonitor;
private FilePlanComponentsApiUtils apiUtils;
private PermissionService permissionService;
@Override
public CollectionWithPagingInfo<HoldBulkStatus> readAll(String holdId, Parameters parameters)
{
// validate parameters
checkNotBlank("holdId", holdId);
mandatory("parameters", parameters);
NodeRef holdRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
checkReadPermissions(holdRef);
List<HoldBulkStatus> statuses = holdBulkMonitor.getBulkStatusesForHold(holdId);
List<HoldBulkStatus> page = statuses.stream()
.skip(parameters.getPaging().getSkipCount())
.limit(parameters.getPaging().getMaxItems())
.collect(Collectors.toCollection(LinkedList::new));
int totalItems = statuses.size();
boolean hasMore = parameters.getPaging().getSkipCount() + parameters.getPaging().getMaxItems() < totalItems;
return CollectionWithPagingInfo.asPaged(parameters.getPaging(), page, hasMore, totalItems);
}
@Override
public HoldBulkStatus readById(String holdId, String bulkStatusId, Parameters parameters)
throws RelationshipResourceNotFoundException
{
checkNotBlank("holdId", holdId);
checkNotBlank("bulkStatusId", bulkStatusId);
mandatory("parameters", parameters);
NodeRef holdRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
checkReadPermissions(holdRef);
return Optional.ofNullable(holdBulkMonitor.getBulkStatus(bulkStatusId)).orElseThrow(() -> new EntityNotFoundException(bulkStatusId));
}
private void checkReadPermissions(NodeRef holdRef)
{
if (permissionService.hasReadPermission(holdRef) == AccessStatus.DENIED)
{
throw new PermissionDeniedException(I18NUtil.getMessage("permissions.err_access_denied"));
}
}
public void setHoldBulkMonitor(HoldBulkMonitor holdBulkMonitor)
{
this.holdBulkMonitor = holdBulkMonitor;
}
public void setApiUtils(FilePlanComponentsApiUtils apiUtils)
{
this.apiUtils = apiUtils;
}
public void setPermissionService(PermissionService permissionService)
{
this.permissionService = permissionService;
}
}

View File

@@ -30,6 +30,8 @@ import static org.alfresco.module.org_alfresco_module_rm.util.RMParameterCheck.c
import static org.alfresco.util.ParameterCheck.mandatory;
import jakarta.servlet.http.HttpServletResponse;
import org.alfresco.module.org_alfresco_module_rm.bulk.BulkOperation;
import org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkService;
import org.alfresco.module.org_alfresco_module_rm.hold.HoldService;
import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
@@ -42,6 +44,9 @@ import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.rest.framework.webscripts.WithResponse;
import org.alfresco.rm.rest.api.impl.ApiNodesModelFactory;
import org.alfresco.rm.rest.api.impl.FilePlanComponentsApiUtils;
import org.alfresco.rm.rest.api.model.HoldBulkOperation;
import org.alfresco.rm.rest.api.model.HoldBulkOperationEntry;
import org.alfresco.rm.rest.api.model.HoldBulkStatus;
import org.alfresco.rm.rest.api.model.HoldDeletionReason;
import org.alfresco.rm.rest.api.model.HoldModel;
import org.alfresco.service.cmr.model.FileFolderService;
@@ -68,6 +73,7 @@ public class HoldsEntityResource implements
private ApiNodesModelFactory nodesModelFactory;
private HoldService holdService;
private TransactionService transactionService;
private HoldBulkService holdBulkService;
@Override
public void afterPropertiesSet() throws Exception
@@ -157,6 +163,23 @@ public class HoldsEntityResource implements
return reason;
}
@Operation("bulk")
@WebApiDescription(title = "Start the hold bulk operation",
successStatus = HttpServletResponse.SC_ACCEPTED)
public HoldBulkOperationEntry bulk(String holdId, HoldBulkOperation holdBulkOperation, Parameters parameters,
WithResponse withResponse)
{
// validate parameters
checkNotBlank("holdId", holdId);
mandatory("parameters", parameters);
NodeRef parentNodeRef = apiUtils.lookupAndValidateNodeType(holdId, RecordsManagementModel.TYPE_HOLD);
HoldBulkStatus holdBulkStatus = holdBulkService.execute(parentNodeRef,
new BulkOperation(holdBulkOperation.query(), holdBulkOperation.op().name()));
return new HoldBulkOperationEntry(holdBulkStatus.bulkStatusId(), holdBulkStatus.totalItems());
}
public void setApiUtils(FilePlanComponentsApiUtils apiUtils)
{
this.apiUtils = apiUtils;
@@ -181,4 +204,9 @@ public class HoldsEntityResource implements
{
this.transactionService = transactionService;
}
public void setHoldBulkService(HoldBulkService holdBulkService)
{
this.holdBulkService = holdBulkService;
}
}

View File

@@ -0,0 +1,33 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rm.rest.api.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.alfresco.rest.api.search.model.Query;
public record HoldBulkOperation(@JsonProperty(required = true) Query query, @JsonProperty(required = true) HoldBulkOperationType op) {}

View File

@@ -0,0 +1,29 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rm.rest.api.model;
public record HoldBulkOperationEntry(String bulkStatusId, long totalItems){}

View File

@@ -0,0 +1,38 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rm.rest.api.model;
/**
* This enum represents the types of bulk operations that can be performed on holds
*/
public enum HoldBulkOperationType
{
/**
* The ADD operation represents adding items to a hold in bulk.
*/
ADD
}

View File

@@ -0,0 +1,69 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rm.rest.api.model;
import java.io.Serializable;
import java.util.Date;
public record HoldBulkStatus(String bulkStatusId, Date startTime, Date endTime, long processedItems, long errorsCount,
long totalItems, String lastError) implements Serializable
{
public enum Status
{
PENDING("PENDING"),
IN_PROGRESS("IN PROGRESS"),
DONE("DONE");
private final String value;
Status(String value)
{
this.value = value;
}
public String getValue()
{
return value;
}
}
public String getStatus()
{
if (startTime == null && endTime == null)
{
return Status.PENDING.getValue();
}
else if (startTime != null && endTime == null)
{
return Status.IN_PROGRESS.getValue();
}
else
{
return Status.DONE.getValue();
}
}
}

View File

@@ -0,0 +1,119 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.module.org_alfresco_module_rm.bulk;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import org.alfresco.module.org_alfresco_module_rm.bulk.hold.DefaultHoldBulkMonitor;
import org.alfresco.module.org_alfresco_module_rm.bulk.hold.HoldBulkProcessDetails;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.rm.rest.api.model.HoldBulkStatus;
import org.alfresco.service.cmr.repository.NodeRef;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
public class DefaultHoldBulkMonitorUnitTest
{
@Mock
private SimpleCache<String, HoldBulkStatus> holdProgressCache;
@Mock
private SimpleCache<String, List<HoldBulkProcessDetails>> holdProcessRegistry;
private DefaultHoldBulkMonitor holdBulkMonitor;
@Before
public void setUp()
{
MockitoAnnotations.openMocks(this);
holdBulkMonitor = new DefaultHoldBulkMonitor();
holdBulkMonitor.setHoldProgressCache(holdProgressCache);
holdBulkMonitor.setHoldProcessRegistry(holdProcessRegistry);
}
@Test
public void testUpdateBulkStatus()
{
HoldBulkStatus status = new HoldBulkStatus("bulkStatusId", null, null, 0L, 0L, 0L, null);
holdBulkMonitor.updateBulkStatus(status);
Mockito.verify(holdProgressCache).put("bulkStatusId", status);
}
@Test
public void testRegisterProcess()
{
NodeRef holdRef = new NodeRef("workspace://SpacesStore/holdId");
String processId = "processId";
when(holdProcessRegistry.get(holdRef.getId())).thenReturn(null);
holdBulkMonitor.registerProcess(holdRef, processId);
Mockito.verify(holdProcessRegistry)
.put(holdRef.getId(), Arrays.asList(new HoldBulkProcessDetails(processId, null)));
}
@Test
public void testGetBulkStatusesForHoldReturnsEmptyListWhenNoProcesses()
{
when(holdProcessRegistry.get("holdId")).thenReturn(null);
assertEquals(Collections.emptyList(), holdBulkMonitor.getBulkStatusesForHold("holdId"));
}
@Test
public void testGetBulkStatusesForHoldReturnsSortedStatuses()
{
HoldBulkStatus status1 = new HoldBulkStatus(null, new Date(1000), new Date(2000), 0L, 0L, 0L, null);
HoldBulkStatus status2 = new HoldBulkStatus(null, new Date(3000), null, 0L, 0L, 0L, null);
HoldBulkStatus status3 = new HoldBulkStatus(null, new Date(4000), null, 0L, 0L, 0L, null);
HoldBulkStatus status4 = new HoldBulkStatus(null, new Date(500), new Date(800), 0L, 0L, 0L, null);
HoldBulkStatus status5 = new HoldBulkStatus(null, null, null, 0L, 0L, 0L, null);
when(holdProcessRegistry.get("holdId")).thenReturn(
Arrays.asList("process1", "process2", "process3", "process4", "process5")
.stream().map(bulkStatusId -> new HoldBulkProcessDetails(bulkStatusId, null)).toList());
when(holdProgressCache.get("process1")).thenReturn(status1);
when(holdProgressCache.get("process2")).thenReturn(status2);
when(holdProgressCache.get("process3")).thenReturn(status3);
when(holdProgressCache.get("process4")).thenReturn(status4);
when(holdProgressCache.get("process5")).thenReturn(status5);
assertEquals(Arrays.asList(status5, status3, status2, status1, status4),
holdBulkMonitor.getBulkStatusesForHold("holdId"));
}
}

View File

@@ -2314,6 +2314,112 @@ paths:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
'/holds/{holdId}/bulk-statuses':
get:
tags:
- holds
operationId: listBulkStatuses
summary: Get bulk statuses
description: |
Gets bulk statuses for hold with id **holdId**.
parameters:
- $ref: '#/parameters/holdIdParam'
- $ref: '#/parameters/skipCountParam'
- $ref: '#/parameters/maxItemsParam'
responses:
'200':
description: Successful response
schema:
$ref: '#/definitions/HoldBulkStatusPaging'
'400':
description: |
Invalid parameter: **holdId** is not a valid format
'401':
description: Authentication failed
'403':
description: Current user does not have permission to read **holdId**
'404':
description: "**holdId** does not exist"
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
'/holds/{holdId}/bulk-statuses/{bulkStatusId}':
get:
tags:
- holds
operationId: getBulkStatus
summary: Get a bulk status
description: |
Gets a bulk status specified by **bulkStatusId** for **holdId**.
parameters:
- $ref: '#/parameters/holdIdParam'
- $ref: '#/parameters/bulkStatusId'
responses:
'200':
description: Successful response
schema:
$ref: '#/definitions/HoldBulkStatus'
'400':
description: |
Invalid parameter: **holdId** or **bulkStatusId** is not a valid format
'401':
description: Authentication failed
'403':
description: Current user does not have permission to read **holdId**
'404':
description: "**holdId** or **bulkStatusId** does not exist"
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
'/holds/{holdId}/bulk':
post:
tags:
- holds
operationId: startHoldBulkProcess
summary: Start the hold bulk process
description: |
Start the asynchronous bulk process for a hold with id **holdId** based on search query results.
```JSON
For example, the following JSON body starts the bulk process to add search query results
as children of a hold.
{
"query": {
"query": "SITE:swsdp and TYPE:content",
"language": "afts"
},
"op": "ADD"
}
```
parameters:
- $ref: '#/parameters/holdIdParam'
- in: body
name: holdBulkOperation
description: Bulk operation.
required: true
schema:
$ref: '#/definitions/HoldBulkOperation'
responses:
'202':
description: Successful response
schema:
$ref: '#/definitions/HoldBulkOperationEntry'
'400':
description: |
Invalid parameter: **holdId** is not a valid format or **HoldBulkOperation** is not valid
'401':
description: Authentication failed
'403':
description: Current user does not have permission to start the bulk process for **holdId**
'404':
description: "**holdId** does not exist"
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
'/holds/{holdId}/delete':
post:
tags:
@@ -2862,6 +2968,12 @@ parameters:
description: The identifier of a child of a hold.
required: true
type: string
bulkStatusId:
name: bulkStatusId
in: path
description: The identifier of a bulk process.
required: true
type: string
## Record
recordIdParam:
name: recordId
@@ -4018,6 +4130,91 @@ definitions:
properties:
reason:
type: string
SearchRequestQuery:
type: object
required:
- query
properties:
language:
description: The query language in which the query is written.
type: string
default: afts
enum:
- afts
- lucene
- cmis
userQuery:
description: The search request typed in by the user
type: string
query:
description: The query which may have been generated in some way from the userQuery
type: string
HoldBulkOperation:
type: object
properties:
query:
$ref: '#/definitions/SearchRequestQuery'
op:
description: The operation type.
type: string
default: ADD
enum:
- ADD
HoldBulkOperationEntry:
type: object
properties:
bulkStatusId:
type: string
totalItems:
type: integer
format: int64
HoldBulkStatus:
type: object
properties:
bulkStatusId:
type: string
startTime:
type: string
format: date-time
endTime:
type: string
format: date-time
processedItems:
type: integer
format: int64
errorsCount:
type: integer
format: int64
totalItems:
type: integer
format: int64
lastError:
type: string
status:
type: string
enum:
- PENDING
- IN PROGRESS
- DONE
HoldBulkStatusEntry:
type: object
required:
- entry
properties:
entry:
$ref: '#/definitions/HoldBulkStatus'
HoldBulkStatusPaging:
type: object
properties:
list:
type: object
properties:
pagination:
$ref: '#/definitions/Pagination'
entries:
type: array
items:
$ref: '#/definitions/HoldBulkStatusEntry'
##
RequestBodyFile:
type: object