From 1df144fb411aedd6701b7e67889e1f0035168505 Mon Sep 17 00:00:00 2001 From: jannikfuellgraf Date: Mon, 9 Dec 2024 12:38:48 +0100 Subject: [PATCH] Closes #372 - Add Distribution for tasks --- .../distribute/DistributeTaskAccTest.java | 592 ++++++++++++++++++ .../common/internal/InternalKadaiEngine.java | 8 + .../common/internal/KadaiEngineImpl.java | 8 + .../task/api/TaskDistributionProvider.java | 79 +++ .../DefaultTaskDistributionProvider.java | 42 ++ .../internal/TaskDistributionManager.java | 77 +++ .../java/io/kadai/task/api/TaskService.java | 240 +++++++ .../kadai/task/internal/TaskDistributor.java | 297 +++++++++ .../kadai/task/internal/TaskServiceImpl.java | 124 +++- .../util/ServiceProviderExtractor.java | 2 + .../io/kadai/common/rest/RestEndpoints.java | 2 + .../main/java/io/kadai/task/rest/TaskApi.java | 129 ++++ .../io/kadai/task/rest/TaskController.java | 67 ++ .../DistributionTasksRepresentationModel.java | 57 ++ .../task/rest/TaskControllerIntTest.java | 174 +++++ 15 files changed, 1891 insertions(+), 7 deletions(-) create mode 100644 lib/kadai-core-test/src/test/java/acceptance/task/distribute/DistributeTaskAccTest.java create mode 100644 lib/kadai-core/src/main/java/io/kadai/spi/task/api/TaskDistributionProvider.java create mode 100644 lib/kadai-core/src/main/java/io/kadai/spi/task/internal/DefaultTaskDistributionProvider.java create mode 100644 lib/kadai-core/src/main/java/io/kadai/spi/task/internal/TaskDistributionManager.java create mode 100644 lib/kadai-core/src/main/java/io/kadai/task/internal/TaskDistributor.java create mode 100644 rest/kadai-rest-spring/src/main/java/io/kadai/task/rest/models/DistributionTasksRepresentationModel.java diff --git a/lib/kadai-core-test/src/test/java/acceptance/task/distribute/DistributeTaskAccTest.java b/lib/kadai-core-test/src/test/java/acceptance/task/distribute/DistributeTaskAccTest.java new file mode 100644 index 000000000..24123d303 --- /dev/null +++ b/lib/kadai-core-test/src/test/java/acceptance/task/distribute/DistributeTaskAccTest.java @@ -0,0 +1,592 @@ +package acceptance.task.distribute; + +import static io.kadai.testapi.DefaultTestEntities.defaultTestClassification; +import static io.kadai.testapi.DefaultTestEntities.defaultTestObjectReference; +import static io.kadai.testapi.DefaultTestEntities.defaultTestWorkbasket; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import acceptance.task.distribute.DistributeTaskAccTest.TaskDistributionProviderForTest; +import acceptance.task.distribute.DistributeTaskAccTest.TaskDistributionProviderForTest2; +import io.kadai.classification.api.ClassificationService; +import io.kadai.classification.api.models.ClassificationSummary; +import io.kadai.common.api.BulkOperationResults; +import io.kadai.common.api.KadaiEngine; +import io.kadai.common.api.exceptions.InvalidArgumentException; +import io.kadai.common.api.exceptions.KadaiException; +import io.kadai.common.api.exceptions.NotAuthorizedException; +import io.kadai.spi.task.api.TaskDistributionProvider; +import io.kadai.task.api.TaskService; +import io.kadai.task.api.exceptions.InvalidTaskStateException; +import io.kadai.task.api.exceptions.TaskNotFoundException; +import io.kadai.task.api.models.ObjectReference; +import io.kadai.task.api.models.TaskSummary; +import io.kadai.testapi.KadaiInject; +import io.kadai.testapi.KadaiIntegrationTest; +import io.kadai.testapi.WithServiceProvider; +import io.kadai.testapi.builder.TaskBuilder; +import io.kadai.testapi.builder.WorkbasketAccessItemBuilder; +import io.kadai.testapi.security.WithAccessId; +import io.kadai.workbasket.api.WorkbasketPermission; +import io.kadai.workbasket.api.WorkbasketService; +import io.kadai.workbasket.api.exceptions.NotAuthorizedOnWorkbasketException; +import io.kadai.workbasket.api.exceptions.WorkbasketAccessItemAlreadyExistException; +import io.kadai.workbasket.api.exceptions.WorkbasketNotFoundException; +import io.kadai.workbasket.api.models.WorkbasketSummary; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +@KadaiIntegrationTest +@WithServiceProvider( + serviceProviderInterface = TaskDistributionProvider.class, + serviceProviders = { + TaskDistributionProviderForTest.class, + TaskDistributionProviderForTest2.class + }) +class DistributeTaskAccTest { + @KadaiInject TaskService taskService; + @KadaiInject WorkbasketService workbasketService; + @KadaiInject ClassificationService classificationService; + + ClassificationSummary classificationSummary; + ObjectReference objectReference; + + WorkbasketSummary workbasketSummary1; + WorkbasketSummary workbasketSummary2; + WorkbasketSummary workbasketSummary3; + WorkbasketSummary workbasketSummary4; + WorkbasketSummary workbasketSummary5; + WorkbasketSummary workbasketSummary6; + List taskSummaries; + TaskSummary taskSummaryForWb5; + TaskSummary taskSummaryForWb6; + + @WithAccessId(user = "admin") + @BeforeAll + void setup() throws Exception { + + classificationSummary = + defaultTestClassification().buildAndStoreAsSummary(classificationService); + + objectReference = defaultTestObjectReference().build(); + + workbasketSummary1 = defaultTestWorkbasket().buildAndStoreAsSummary(workbasketService); + workbasketSummary2 = defaultTestWorkbasket().buildAndStoreAsSummary(workbasketService); + workbasketSummary3 = defaultTestWorkbasket().buildAndStoreAsSummary(workbasketService); + workbasketSummary4 = defaultTestWorkbasket().buildAndStoreAsSummary(workbasketService); + workbasketSummary5 = defaultTestWorkbasket().buildAndStoreAsSummary(workbasketService); + workbasketSummary6 = defaultTestWorkbasket().buildAndStoreAsSummary(workbasketService); + + workbasketService.setDistributionTargets( + workbasketSummary1.getId(), + List.of( + workbasketSummary2.getId(), workbasketSummary3.getId(), workbasketSummary4.getId())); + workbasketService.setDistributionTargets( + workbasketSummary5.getId(), + List.of( + workbasketSummary2.getId(), workbasketSummary3.getId(), workbasketSummary4.getId())); + + taskSummaries = new ArrayList<>(); + for (int i = 1; i <= 6; i++) { + taskSummaries.add( + new TaskBuilder() + .classificationSummary(classificationSummary) + .workbasketSummary(workbasketSummary1) + .primaryObjRef(objectReference) + .buildAndStoreAsSummary(taskService)); + } + + taskSummaryForWb5 = + new TaskBuilder() + .classificationSummary(classificationSummary) + .workbasketSummary(workbasketSummary5) + .primaryObjRef(objectReference) + .buildAndStoreAsSummary(taskService); + + taskSummaryForWb6 = + new TaskBuilder() + .classificationSummary(classificationSummary) + .workbasketSummary(workbasketSummary6) + .primaryObjRef(objectReference) + .buildAndStoreAsSummary(taskService); + + createAccessItemForUser(workbasketSummary1.getId()); + createAccessItemForUser(workbasketSummary2.getId()); + createAccessItemForUser(workbasketSummary3.getId()); + createAccessItemForUser(workbasketSummary4.getId()); + createAccessItemForUser(workbasketSummary5.getId()); + createAccessItemForUser(workbasketSummary6.getId()); + } + + @WithAccessId(user = "admin") + @AfterEach + void cleanWorkbaskets() throws NotAuthorizedOnWorkbasketException, WorkbasketNotFoundException { + transferAllTasksBackToWorkbasket1(); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_DistributeTasksCorrectly_When_WorkbasketIdOnly() + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + InvalidTaskStateException, + TaskNotFoundException { + + taskService.distribute(workbasketSummary1.getId()); + + List tasksInWb1 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary1.getId()).list(); + List tasksInWb2 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary2.getId()).list(); + List tasksInWb3 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary3.getId()).list(); + List tasksInWb4 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary4.getId()).list(); + + assertThat(tasksInWb1).isEmpty(); + assertThat(tasksInWb2).hasSize(2); + assertThat(tasksInWb3).hasSize(2); + assertThat(tasksInWb4).hasSize(2); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_DistributeTasksCorrectly_When_TaskListOnly() + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + InvalidTaskStateException, + TaskNotFoundException { + String sourceWorkbasketId = workbasketSummary1.getId(); + List taskIds = taskSummaries.subList(0, 3).stream().map(TaskSummary::getId).toList(); + + taskService.distribute(sourceWorkbasketId, taskIds); + + List tasksInWb1 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary1.getId()).list(); + List tasksInWb2 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary2.getId()).list(); + List tasksInWb3 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary3.getId()).list(); + List tasksInWb4 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary4.getId()).list(); + + assertThat(tasksInWb1).hasSize(3); + assertThat(tasksInWb2).hasSize(1); + assertThat(tasksInWb3).hasSize(1); + assertThat(tasksInWb4).hasSize(1); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_DistributeTasksCorrectly_When_SourceWorkbasketAndDestinationWorkbaskets() + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + InvalidTaskStateException, + TaskNotFoundException { + String sourceWorkbasketId = workbasketSummary1.getId(); + List destinationWorkbasketIds = + List.of(workbasketSummary2.getId(), workbasketSummary3.getId()); + + taskService.distributeWithDestinations(sourceWorkbasketId, destinationWorkbasketIds); + + List tasksInWb1 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary1.getId()).list(); + List tasksInWb2 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary2.getId()).list(); + List tasksInWb3 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary3.getId()).list(); + List tasksInWb4 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary4.getId()).list(); + + assertThat(tasksInWb1).isEmpty(); + assertThat(tasksInWb2).hasSize(3); + assertThat(tasksInWb3).hasSize(3); + assertThat(tasksInWb4).isEmpty(); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_DistributeTasksCorrectly_When_TaskListAndDestinationWorkbaskets() + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + InvalidTaskStateException, + TaskNotFoundException { + String sourceWorkbasketId = workbasketSummary1.getId(); + List taskIds = taskSummaries.subList(0, 2).stream().map(TaskSummary::getId).toList(); + List destinationWorkbasketIds = + List.of(workbasketSummary2.getId(), workbasketSummary3.getId()); + + taskService.distributeWithDestinations(sourceWorkbasketId, taskIds, destinationWorkbasketIds); + + List tasksInWb1 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary1.getId()).list(); + List tasksInWb2 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary2.getId()).list(); + List tasksInWb3 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary3.getId()).list(); + List tasksInWb4 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary4.getId()).list(); + + assertThat(tasksInWb1).hasSize(4); + assertThat(tasksInWb2).hasSize(1); + assertThat(tasksInWb3).hasSize(1); + assertThat(tasksInWb4).isEmpty(); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_DistributeTasksCorrectly_When_SourceWorkbasketAndDestinationWorkbasketsAndStrategy() + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + InvalidTaskStateException, + TaskNotFoundException { + String sourceWorkbasketId = workbasketSummary1.getId(); + List destinationWorkbasketIds = + List.of(workbasketSummary2.getId(), workbasketSummary3.getId()); + + taskService.distribute( + sourceWorkbasketId, destinationWorkbasketIds, "TaskDistributionProviderForTest", null); + + List tasksInWb1 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary1.getId()).list(); + List tasksInWb2 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary2.getId()).list(); + List tasksInWb3 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary3.getId()).list(); + List tasksInWb4 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary4.getId()).list(); + + assertThat(tasksInWb1).hasSize(1); + assertThat(tasksInWb2).hasSize(3); + assertThat(tasksInWb3).hasSize(2); + assertThat(tasksInWb4).isEmpty(); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_DistributeTasksCorrectly_When_TaskListAndDestinationWorkbasketsAndStrategy() + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + InvalidTaskStateException, + TaskNotFoundException { + String sourceWorkbasketId = workbasketSummary1.getId(); + List taskIds = taskSummaries.stream().map(TaskSummary::getId).toList(); + List destinationWorkbasketIds = + List.of(workbasketSummary2.getId(), workbasketSummary3.getId()); + + taskService.distribute( + sourceWorkbasketId, + taskIds, + destinationWorkbasketIds, + "TaskDistributionProviderForTest", + null); + + List tasksInWb1 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary1.getId()).list(); + List tasksInWb2 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary2.getId()).list(); + List tasksInWb3 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary3.getId()).list(); + List tasksInWb4 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary4.getId()).list(); + + assertThat(tasksInWb1).hasSize(1); + + List actualDistribution = + List.of(tasksInWb2.size(), tasksInWb3.size(), tasksInWb4.size()); + List expectedValues = List.of(0, 2, 3); + assertThat(actualDistribution).containsAll(expectedValues); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_DistributeTasksCorrectly_When_TaskListAndAndStrategy() + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + InvalidTaskStateException, + TaskNotFoundException { + String sourceWorkbasketId = workbasketSummary1.getId(); + List taskIds = taskSummaries.stream().map(TaskSummary::getId).toList(); + + taskService.distributeWithStrategy( + sourceWorkbasketId, taskIds, "TaskDistributionProviderForTest", null); + + List tasksInWb1 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary1.getId()).list(); + List tasksInWb2 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary2.getId()).list(); + List tasksInWb3 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary3.getId()).list(); + List tasksInWb4 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary4.getId()).list(); + + List actualDistribution = + List.of(tasksInWb2.size(), tasksInWb3.size(), tasksInWb4.size()); + List expectedValues = List.of(1, 2, 3); + assertThat(actualDistribution).containsAll(expectedValues); + assertThat(tasksInWb1).isEmpty(); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_DistributeTasksCorrectly_When_SourceWorkbasketAndAndStrategy() + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + InvalidTaskStateException, + TaskNotFoundException { + + taskService.distributeWithStrategy( + workbasketSummary1.getId(), "TaskDistributionProviderForTest", null); + + List tasksInWb1 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary1.getId()).list(); + List tasksInWb2 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary2.getId()).list(); + List tasksInWb3 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary3.getId()).list(); + List tasksInWb4 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary4.getId()).list(); + + List actualDistribution = + List.of(tasksInWb2.size(), tasksInWb3.size(), tasksInWb4.size()); + + List expectedValues = List.of(1, 2, 3); + + assertThat(actualDistribution).containsAll(expectedValues); + assertThat(tasksInWb1).isEmpty(); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_ResetOwner_When_TasksDistributed() + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + InvalidTaskStateException, + TaskNotFoundException { + + taskService.distribute(workbasketSummary1.getId()); + + List tasksInWb2 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary2.getId()).list(); + + assertThat(tasksInWb2.get(0).getOwner()).isNull(); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_NotSetTransferFlag_When_TasksDistributed() + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + InvalidTaskStateException, + TaskNotFoundException { + + taskService.distributeWithDestinations( + workbasketSummary1.getId(), List.of(workbasketSummary2.getId())); + + List tasksInWb2 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary2.getId()).list(); + + assertThat(tasksInWb2.get(0).isTransferred()).isTrue(); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_ThrowInvalidArgumentException_When_TasksFromDifferentSourceWorkbaskets() { + String sourceWorkbasketId = workbasketSummary1.getId(); + List taskIds = + Stream.concat( + taskSummaries.subList(0, 2).stream().map(TaskSummary::getId), + Stream.of(taskSummaryForWb5.getId())) + .toList(); + + assertThatThrownBy(() -> taskService.distribute(sourceWorkbasketId, taskIds)) + .isInstanceOf(InvalidArgumentException.class) + .hasMessageContaining("Not all tasks are in the same workbasket."); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_ThrowException_When_WorkbasketHasNoDistributionTargets() { + String workbasketSummary6Id = workbasketSummary6.getId(); + assertThatThrownBy(() -> taskService.distributeWithDestinations(workbasketSummary6Id, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Ids of destinationWorkbaskets cannot be null or empty."); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_ThrowNotAuthorizedOnWorkbasketException_When_NotAuthorized() { + + String expectedWorkbasketId = workbasketSummary1.getId(); + + assertThatThrownBy(() -> taskService.distribute(expectedWorkbasketId, null)) + .isInstanceOf(NotAuthorizedOnWorkbasketException.class) + .hasMessageContaining( + String.format( + "Not authorized. The current user 'user-1-1' has no '[DISTRIBUTE]' permission(s) " + + "for Workbasket '%s'.", + expectedWorkbasketId)); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_ThrowInvalidArgumentException_When_DistributionStrategyDoesNotExist() { + + String nonExistingStrategy = "NoExistingStrategy"; + String workbasketSummary1Id = workbasketSummary1.getId(); + + assertThatThrownBy( + () -> + taskService.distributeWithStrategy(workbasketSummary1Id, nonExistingStrategy, null)) + .isInstanceOf(InvalidArgumentException.class) + .hasMessageContaining( + "The distribution strategy '%s' does not exist.", nonExistingStrategy); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_DoNothing_When_TaskIdsAreEmptyViaSourceWorkbasketId() throws Exception { + + BulkOperationResults result = + taskService.distribute(workbasketSummary2.getId()); + + assertThat(result.getFailedIds()).isEmpty(); + + List tasksInWb2 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary2.getId()).list(); + List tasksInWb3 = + taskService.createTaskQuery().workbasketIdIn(workbasketSummary3.getId()).list(); + + assertThat(tasksInWb2).isEmpty(); + assertThat(tasksInWb3).isEmpty(); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_ThrowWorkbasketNotFoundException_When_WorkbasketDoesNotExist() { + + String nonExistentWorkbasketId = "NonExistentWorkbasket"; + + assertThatThrownBy(() -> taskService.distribute(nonExistentWorkbasketId, null)) + .isInstanceOf(WorkbasketNotFoundException.class) + .hasMessageContaining( + String.format("Workbasket with id '%s' was not found.", nonExistentWorkbasketId)); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_ThrowTaskNotFoundException_When_TaskDoesNotExist() { + String sourceWorkbasketId = workbasketSummary1.getId(); + String nonExistingTask = "NonExistingTaskId"; + List taskIds = List.of(nonExistingTask, taskSummaries.get(0).getId()); + assertThatThrownBy(() -> taskService.distribute(sourceWorkbasketId, taskIds)) + .isInstanceOf(TaskNotFoundException.class) + .hasMessageContaining(String.format("Task with id '%s' was not found.", nonExistingTask)); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_ThrowWorkbasketNotFoundExceptionn_When_DestinationWorkbasketDoesNotExist() { + String sourceWorkbasketId = workbasketSummary1.getId(); + List nonExistingDestinationWorkbasket = List.of("NonExistingDestinationWorkbasket"); + List taskIds = taskSummaries.subList(0, 2).stream().map(TaskSummary::getId).toList(); + assertThatThrownBy( + () -> + taskService.distributeWithDestinations( + sourceWorkbasketId, taskIds, nonExistingDestinationWorkbasket)) + .isInstanceOf(WorkbasketNotFoundException.class) + .hasMessageContaining( + String.format( + "Workbasket with id '%s' was not found.", nonExistingDestinationWorkbasket.get(0))); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_ThrowInvalidArgumentException_When_TasksFromDifferentWorkbasketsProvided() { + String sourceWorkbasketId = workbasketSummary1.getId(); + List taskIds = List.of(taskSummaries.get(0).getId(), taskSummaryForWb5.getId()); + + assertThatThrownBy(() -> taskService.distribute(sourceWorkbasketId, taskIds)) + .isInstanceOf(InvalidArgumentException.class) + .hasMessageContaining("Not all tasks are in the same workbasket."); + } + + void createAccessItemForUser(String workbasketSummaryId) + throws WorkbasketAccessItemAlreadyExistException, + WorkbasketNotFoundException, + NotAuthorizedException { + WorkbasketAccessItemBuilder.newWorkbasketAccessItem() + .workbasketId(workbasketSummaryId) + .accessId("user-1-2") + .permission(WorkbasketPermission.OPEN) + .permission(WorkbasketPermission.DISTRIBUTE) + .permission(WorkbasketPermission.READ) + .permission(WorkbasketPermission.READTASKS) + .permission(WorkbasketPermission.TRANSFER) + .permission(WorkbasketPermission.APPEND) + .buildAndStore(workbasketService); + } + + void transferAllTasksBackToWorkbasket1() + throws NotAuthorizedOnWorkbasketException, WorkbasketNotFoundException { + taskService.transferTasks( + workbasketSummary1.getId(), taskSummaries.stream().map(TaskSummary::getId).toList()); + } + + public static class TaskDistributionProviderForTest implements TaskDistributionProvider { + + @Override + public void initialize(KadaiEngine kadaiEngine) { + // NOOP + } + + public Map> distributeTasks( + List taskIds, + List destinationWorkbasketIds, + Map additionalInformation) { + + if (taskIds == null || taskIds.isEmpty()) { + throw new IllegalArgumentException("Task IDs list cannot be null or empty."); + } + if (destinationWorkbasketIds == null || destinationWorkbasketIds.isEmpty()) { + throw new IllegalArgumentException("Workbasket IDs list cannot be null or empty."); + } + + Map> distributedTaskIds = new HashMap<>(); + for (String workbasketId : destinationWorkbasketIds) { + distributedTaskIds.put(workbasketId, new ArrayList<>()); + } + + distributedTaskIds.get(destinationWorkbasketIds.get(0)).addAll(taskIds.subList(0, 3)); + distributedTaskIds.get(destinationWorkbasketIds.get(1)).addAll(taskIds.subList(3, 5)); + + if (destinationWorkbasketIds.size() == 3) { + distributedTaskIds.get(destinationWorkbasketIds.get(2)).add(taskIds.get(5)); + } + + return distributedTaskIds; + } + } + + public static class TaskDistributionProviderForTest2 implements TaskDistributionProvider { + + @Override + public void initialize(KadaiEngine kadaiEngine) { + // NOOP + } + + @Override + public Map> distributeTasks( + List taskIds, + List destinationWorkbasketIds, + Map additionalInformation) { + return Map.of(); + } + } +} diff --git a/lib/kadai-core/src/main/java/io/kadai/common/internal/InternalKadaiEngine.java b/lib/kadai-core/src/main/java/io/kadai/common/internal/InternalKadaiEngine.java index 254307471..c0a9d3616 100644 --- a/lib/kadai-core/src/main/java/io/kadai/common/internal/InternalKadaiEngine.java +++ b/lib/kadai-core/src/main/java/io/kadai/common/internal/InternalKadaiEngine.java @@ -28,6 +28,7 @@ import io.kadai.spi.task.internal.BeforeRequestReviewManager; import io.kadai.spi.task.internal.CreateTaskPreprocessorManager; import io.kadai.spi.task.internal.ReviewRequiredManager; +import io.kadai.spi.task.internal.TaskDistributionManager; import io.kadai.spi.task.internal.TaskEndstatePreprocessorManager; import java.util.function.Supplier; import org.apache.ibatis.session.SqlSession; @@ -115,6 +116,13 @@ default void executeInDatabaseConnection(Runnable runnable) { */ TaskRoutingManager getTaskRoutingManager(); + /** + * Retrieve TaskDistributionManager. + * + * @return the TaskDistributionManager instance. + */ + TaskDistributionManager getTaskDistributionManager(); + /** * Retrieve CreateTaskPreprocessorManager. * diff --git a/lib/kadai-core/src/main/java/io/kadai/common/internal/KadaiEngineImpl.java b/lib/kadai-core/src/main/java/io/kadai/common/internal/KadaiEngineImpl.java index 762656560..908fe7cc9 100644 --- a/lib/kadai-core/src/main/java/io/kadai/common/internal/KadaiEngineImpl.java +++ b/lib/kadai-core/src/main/java/io/kadai/common/internal/KadaiEngineImpl.java @@ -58,6 +58,7 @@ import io.kadai.spi.task.internal.BeforeRequestReviewManager; import io.kadai.spi.task.internal.CreateTaskPreprocessorManager; import io.kadai.spi.task.internal.ReviewRequiredManager; +import io.kadai.spi.task.internal.TaskDistributionManager; import io.kadai.spi.task.internal.TaskEndstatePreprocessorManager; import io.kadai.task.api.TaskService; import io.kadai.task.internal.AttachmentMapper; @@ -109,6 +110,7 @@ public class KadaiEngineImpl implements KadaiEngine { private static final SessionStack SESSION_STACK = new SessionStack(); protected final KadaiConfiguration kadaiConfiguration; private final TaskRoutingManager taskRoutingManager; + private final TaskDistributionManager taskDistributionManager; private final CreateTaskPreprocessorManager createTaskPreprocessorManager; private final PriorityServiceManager priorityServiceManager; private final ReviewRequiredManager reviewRequiredManager; @@ -198,6 +200,7 @@ protected KadaiEngineImpl( priorityServiceManager = new PriorityServiceManager(this); historyEventManager = new HistoryEventManager(this); taskRoutingManager = new TaskRoutingManager(this); + taskDistributionManager = new TaskDistributionManager(this); reviewRequiredManager = new ReviewRequiredManager(this); beforeRequestReviewManager = new BeforeRequestReviewManager(this); afterRequestReviewManager = new AfterRequestReviewManager(this); @@ -609,6 +612,11 @@ public TaskRoutingManager getTaskRoutingManager() { return taskRoutingManager; } + @Override + public TaskDistributionManager getTaskDistributionManager() { + return taskDistributionManager; + } + @Override public CreateTaskPreprocessorManager getCreateTaskPreprocessorManager() { return createTaskPreprocessorManager; diff --git a/lib/kadai-core/src/main/java/io/kadai/spi/task/api/TaskDistributionProvider.java b/lib/kadai-core/src/main/java/io/kadai/spi/task/api/TaskDistributionProvider.java new file mode 100644 index 000000000..e3cebce5f --- /dev/null +++ b/lib/kadai-core/src/main/java/io/kadai/spi/task/api/TaskDistributionProvider.java @@ -0,0 +1,79 @@ +package io.kadai.spi.task.api; + +import io.kadai.common.api.KadaiEngine; +import io.kadai.task.api.models.Task; +import io.kadai.workbasket.api.models.Workbasket; +import java.util.List; +import java.util.Map; + +/** + * The {@code TaskDistributionProvider} interface defines a Service Provider Interface for + * implementing custom {@linkplain Task} distribution strategies within the {@linkplain + * KadaiEngine}. + * + *

This interface allows the system to distribute {@linkplain Task}s from a source to one or more + * destination {@linkplain Workbasket}s based on a given strategy and additional context-specific + * information. + * + *

The implementation of this interface must be registered as a service provider and will be + * called by KADAI during task distribution operations. + */ +public interface TaskDistributionProvider { + + /** + * Initializes the {@linkplain KadaiEngine} for the current KADAI installation. + * + *

This method is called during KADAI startup and allows the service provider to access and + * store the active {@linkplain KadaiEngine} instance for later use during task distribution. + * + * @param kadaiEngine the active {@linkplain KadaiEngine} instance initialized for this + * installation + */ + void initialize(KadaiEngine kadaiEngine); + + /** + * Determines the distribution of tasks to one or more destination workbaskets based on the + * provided parameters. + * + *

This method is invoked by KADAI to calculate the assignment of a set of task IDs to specific + * destination workbaskets using a custom distribution strategy. The method does not directly + * perform the distribution but returns the intended mapping of tasks to workbaskets. + * + *

Input Parameters: + * + *

    + *
  • {@code taskIds (List)}: A list of task IDs to be analyzed for distribution. + *
  • {@code destinationWorkbasketIds (List)}: A list of destination workbasket IDs + * where the tasks are intended to be assigned. + *
  • {@code additionalInformation (Map)}: Additional context-specific details + * that can influence the distribution logic. + *
+ * + *

Output: + * + *

    + *
  • The method returns a {@code Map>}, where each key represents a + * destination workbasket ID, and the corresponding value is a list of task IDs that should + * be assigned to that workbasket. + *
  • The returned mapping provides the intended distribution but does not execute any changes + * in the system. + *
+ * + *

Contract: + * + *

    + *
  • The {@code taskIds} and {@code destinationWorkbasketIds} must not be null. + *
+ * + * @param taskIds a list of task IDs to be analyzed for distribution + * @param destinationWorkbasketIds a list of destination workbasket IDs where tasks are intended + * to be distributed + * @param additionalInformation a map of additional details for customizing the distribution logic + * @return a {@code Map>} containing the destination workbasket IDs as keys + * and the corresponding task IDs to be assigned as values + */ + Map> distributeTasks( + List taskIds, + List destinationWorkbasketIds, + Map additionalInformation); +} diff --git a/lib/kadai-core/src/main/java/io/kadai/spi/task/internal/DefaultTaskDistributionProvider.java b/lib/kadai-core/src/main/java/io/kadai/spi/task/internal/DefaultTaskDistributionProvider.java new file mode 100644 index 000000000..f0dc2179f --- /dev/null +++ b/lib/kadai-core/src/main/java/io/kadai/spi/task/internal/DefaultTaskDistributionProvider.java @@ -0,0 +1,42 @@ +package io.kadai.spi.task.internal; + +import io.kadai.common.api.KadaiEngine; +import io.kadai.spi.task.api.TaskDistributionProvider; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DefaultTaskDistributionProvider implements TaskDistributionProvider { + + @Override + public void initialize(KadaiEngine kadaiEngine) { + // NOOP + } + + @Override + public Map> distributeTasks( + List taskIds, List workbasketIds, Map additionalInformation) { + + if (taskIds == null || taskIds.isEmpty()) { + throw new IllegalArgumentException("Task Ids list cannot be null or empty."); + } + if (workbasketIds == null || workbasketIds.isEmpty()) { + throw new IllegalArgumentException("Ids of destinationWorkbaskets cannot be null or empty."); + } + + Map> distributedTaskIds = new HashMap<>(); + for (String workbasketId : workbasketIds) { + distributedTaskIds.put(workbasketId, new ArrayList<>()); + } + + int workbasketCount = workbasketIds.size(); + for (int i = 0; i < taskIds.size(); i++) { + String taskId = taskIds.get(i); + String targetWorkbasketId = workbasketIds.get(i % workbasketCount); + distributedTaskIds.get(targetWorkbasketId).add(taskId); + } + + return distributedTaskIds; + } +} diff --git a/lib/kadai-core/src/main/java/io/kadai/spi/task/internal/TaskDistributionManager.java b/lib/kadai-core/src/main/java/io/kadai/spi/task/internal/TaskDistributionManager.java new file mode 100644 index 000000000..863b4d9f4 --- /dev/null +++ b/lib/kadai-core/src/main/java/io/kadai/spi/task/internal/TaskDistributionManager.java @@ -0,0 +1,77 @@ +package io.kadai.spi.task.internal; + +import io.kadai.common.api.KadaiEngine; +import io.kadai.common.api.exceptions.InvalidArgumentException; +import io.kadai.common.internal.util.LogSanitizer; +import io.kadai.common.internal.util.SpiLoader; +import io.kadai.spi.task.api.TaskDistributionProvider; +import java.util.List; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class TaskDistributionManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(TaskDistributionManager.class); + private final List taskDistributionProviderList; + + public TaskDistributionManager(KadaiEngine kadaiEngine) { + taskDistributionProviderList = SpiLoader.load(TaskDistributionProvider.class); + for (TaskDistributionProvider taskDistributionProvider : taskDistributionProviderList) { + taskDistributionProvider.initialize(kadaiEngine); + LOGGER.info( + "Registered TaskDistribution provider: {}", + taskDistributionProvider.getClass().getName()); + } + + if (taskDistributionProviderList.isEmpty()) { + LOGGER.info( + "No Custom TaskDistribution Provider found. Running with " + + "DefaultTaskDistributionProvider"); + } + } + + public TaskDistributionProvider getProviderByName(String name) { + return taskDistributionProviderList.stream() + .filter(provider -> provider.getClass().getSimpleName().equals(name)) + .findFirst() + .orElseThrow( + () -> + new InvalidArgumentException( + String.format("The distribution strategy '%s' does not exist.", name))); + } + + public Map> distributeTasks( + List taskIds, + List destinationWorkbasketIds, + Map additionalInformation, + String distributionStrategyName) { + + Map> newTaskDistribution; + + if (distributionStrategyName != null) { + + TaskDistributionProvider taskDistributionProvider = + this.getProviderByName(distributionStrategyName); + + String sanitizedDistributionStrategyName = + LogSanitizer.stripLineBreakingChars(distributionStrategyName); + LOGGER.info("Using TaskDistributionProvider: {}", sanitizedDistributionStrategyName); + + newTaskDistribution = + taskDistributionProvider.distributeTasks( + taskIds, destinationWorkbasketIds, additionalInformation); + } else { + LOGGER.info("No distribution strategy specified. Using default distribution."); + newTaskDistribution = + new DefaultTaskDistributionProvider() + .distributeTasks(taskIds, destinationWorkbasketIds, additionalInformation); + } + + if (newTaskDistribution == null || newTaskDistribution.isEmpty()) { + throw new InvalidArgumentException( + "The distribution strategy resulted in no task assignments. Please verify the input."); + } + return newTaskDistribution; + } +} diff --git a/lib/kadai-core/src/main/java/io/kadai/task/api/TaskService.java b/lib/kadai-core/src/main/java/io/kadai/task/api/TaskService.java index 9ed6937d1..2d8467ec7 100644 --- a/lib/kadai-core/src/main/java/io/kadai/task/api/TaskService.java +++ b/lib/kadai-core/src/main/java/io/kadai/task/api/TaskService.java @@ -893,6 +893,246 @@ BulkOperationResults transferTasksWithOwner( WorkbasketNotFoundException, NotAuthorizedOnWorkbasketException; + /** + * Distributes {@linkplain Task} instances from a source {@linkplain Workbasket} to one or more + * destination {@linkplain Workbasket}s based on a custom distribution strategy specified by its + * name. + * + * @param sourceWorkbasketId The {@linkplain Workbasket#getId() Id} of the source workbasket + * containing the tasks to be distributed. + * @param taskIds A list of task IDs to be distributed. All tasks must belong to the source + * workbasket. + * @param destinationWorkbasketIds A list of {@linkplain Workbasket#getId() Ids} of the + * destination workbaskets where tasks will be distributed. + * @param distributionStrategyName The name of the custom distribution strategy to be applied. The + * strategy must be registered in the SPI. If {@code null}, the default strategy is applied. + * @param additionalInformation A map containing additional context-specific information for the + * distribution strategy. This parameter may be {@code null}. + * @return BulkResult with {@linkplain Task#getId() ids} and Error for each failed transaction. + * @throws InvalidArgumentException If any input data is invalid or incompatible with the + * specified strategy. + * @throws WorkbasketNotFoundException If the source or any destination workbasket cannot be + * found. + * @throws NotAuthorizedOnWorkbasketException If the user is not authorized to access or modify + * the specified workbaskets. + * @throws InvalidTaskStateException If any {@linkplain Task} is in an {@linkplain + * TaskState#END_STATES EndState}. + * @throws TaskNotFoundException If any {@linkplain Task} in the list cannot be found. + */ + BulkOperationResults distribute( + String sourceWorkbasketId, + List taskIds, + List destinationWorkbasketIds, + String distributionStrategyName, + Map additionalInformation) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + TaskNotFoundException, + InvalidTaskStateException; + + /** + * Distributes {@linkplain Task} instances from a source {@linkplain Workbasket} to one or more + * destination {@linkplain Workbasket}s based on a custom distribution strategy. + * + * @param sourceWorkbasketId The {@linkplain Workbasket#getId() Id} of the source workbasket + * containing the tasks to be distributed. + * @param destinationWorkbasketIds A list of {@linkplain Workbasket#getId() Ids} of the + * destination workbaskets where tasks will be distributed. + * @param distributionStrategyName The name of the custom distribution strategy to be applied. The + * strategy must be registered in the SPI. If {@code null}, the default strategy is applied. + * @param additionalInformation A map containing additional context-specific information for the + * distribution strategy. This parameter may be {@code null}. + * @return BulkResult with {@linkplain Task#getId() ids} and Error for each failed transaction. + * @throws InvalidArgumentException If any input data is invalid or incompatible with the + * specified strategy. + * @throws WorkbasketNotFoundException If the source or any destination workbasket cannot be + * found. + * @throws NotAuthorizedOnWorkbasketException If the user is not authorized to access or modify + * the specified workbaskets. + * @throws InvalidTaskStateException If any {@linkplain Task} is in an {@linkplain + * TaskState#END_STATES EndState}. + * @throws TaskNotFoundException If any {@linkplain Task} cannot be found. + */ + BulkOperationResults distribute( + String sourceWorkbasketId, + List destinationWorkbasketIds, + String distributionStrategyName, + Map additionalInformation) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + TaskNotFoundException, + InvalidTaskStateException; + + /** + * Distributes {@linkplain Task} instances from a source {@linkplain Workbasket} using the default + * distribution strategy. + * + * @param sourceWorkbasketId The {@linkplain Workbasket#getId() Id} of the source workbasket + * containing the tasks to be distributed. + * @return BulkResult with {@linkplain Task#getId() ids} and Error for each failed transaction. + * @throws InvalidArgumentException If the input data is invalid. + * @throws WorkbasketNotFoundException If the source or any destination workbasket cannot be + * found. + * @throws NotAuthorizedOnWorkbasketException If the user is not authorized to access or modify + * the source workbasket. + * @throws InvalidTaskStateException If any {@linkplain Task} is in an {@linkplain + * TaskState#END_STATES EndState}. + * @throws TaskNotFoundException If any {@linkplain Task} in the source workbasket cannot be + * found. + */ + BulkOperationResults distribute(String sourceWorkbasketId) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + TaskNotFoundException, + InvalidTaskStateException; + + /** + * Distributes a list of {@linkplain Task} instances from a source {@linkplain Workbasket} using + * the default distribution strategy. + * + * @param sourceWorkbasketId The {@linkplain Workbasket#getId() Id} of the source workbasket + * containing the tasks to be distributed. + * @param taskIds A list of task IDs to be distributed. + * @return BulkResult with {@linkplain Task#getId() ids} and Error for each failed transaction. + * @throws InvalidArgumentException If the input data is invalid. + * @throws WorkbasketNotFoundException If the source or any destination workbasket cannot be + * found. + * @throws NotAuthorizedOnWorkbasketException If the user is not authorized to access or modify + * the source workbasket. + * @throws InvalidTaskStateException If any {@linkplain Task} is in an {@linkplain + * TaskState#END_STATES EndState}. + * @throws TaskNotFoundException If any {@linkplain Task} in the list cannot be found. + */ + BulkOperationResults distribute( + String sourceWorkbasketId, List taskIds) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + TaskNotFoundException, + InvalidTaskStateException; + + /** + * Distributes {@linkplain Task} instances from a source {@linkplain Workbasket} using a custom + * distribution strategy. + * + * @param sourceWorkbasketId The {@linkplain Workbasket#getId() Id} of the source workbasket + * containing the tasks to be distributed. + * @param distributionStrategyName The name of the custom distribution strategy. The strategy must + * be registered in the SPI. If no matching strategy is found, an exception will be thrown. + * @param additionalInformation A map containing additional context-specific information for the + * distribution strategy. This parameter may be {@code null}. + * @return BulkResult with {@linkplain Task#getId() ids} and Error for each failed transaction. + * @throws InvalidArgumentException If the specified strategy is invalid or incompatible with the + * input data. + * @throws WorkbasketNotFoundException If the source or any destination workbasket cannot be + * found. + * @throws NotAuthorizedOnWorkbasketException If the user is not authorized to access or modify + * the source workbasket. + * @throws InvalidTaskStateException If any {@linkplain Task} is in an {@linkplain + * TaskState#END_STATES EndState}. + * @throws TaskNotFoundException If any {@linkplain Task} in the source workbasket cannot be + * found. + */ + BulkOperationResults distributeWithStrategy( + String sourceWorkbasketId, + String distributionStrategyName, + Map additionalInformation) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + TaskNotFoundException, + InvalidTaskStateException; + + /** + * Distributes a list of {@linkplain Task} instances from a source {@linkplain Workbasket} using a + * custom distribution strategy. + * + * @param sourceWorkbasketId The {@linkplain Workbasket#getId() Id} of the source workbasket + * containing the tasks to be distributed. + * @param taskIds A list of task IDs to be distributed. + * @param distributionStrategyName The name of the custom distribution strategy. The strategy must + * be registered in the SPI. If no matching strategy is found, an exception will be thrown. + * @param additionalInformation A map containing additional context-specific information for the + * distribution strategy. This parameter may be {@code null}. + * @return BulkResult with {@linkplain Task#getId() ids} and Error for each failed transaction. + * @throws InvalidArgumentException If the specified strategy is invalid or incompatible with the + * input data. + * @throws WorkbasketNotFoundException If the source or any destination workbasket cannot be + * found. + * @throws NotAuthorizedOnWorkbasketException If the user is not authorized to access or modify + * the source workbasket. + * @throws InvalidTaskStateException If any {@linkplain Task} is in an {@linkplain + * TaskState#END_STATES EndState}. + * @throws TaskNotFoundException If any {@linkplain Task} in the list cannot be found. + */ + BulkOperationResults distributeWithStrategy( + String sourceWorkbasketId, + List taskIds, + String distributionStrategyName, + Map additionalInformation) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + TaskNotFoundException, + InvalidTaskStateException; + + /** + * Distributes {@linkplain Task} instances from a source {@linkplain Workbasket} to specified + * destination {@linkplain Workbasket}s using the default strategy. + * + * @param sourceWorkbasketId The {@linkplain Workbasket#getId() Id} of the source workbasket + * containing the tasks to be distributed. + * @param destinationWorkbasketIds A list of {@linkplain Workbasket#getId() Ids} of the + * destination workbaskets where tasks will be distributed. + * @return BulkResult with {@linkplain Task#getId() ids} and Error for each failed transaction. + * @throws InvalidArgumentException If the input data is invalid. + * @throws WorkbasketNotFoundException If the source or any destination workbasket cannot be + * found. + * @throws NotAuthorizedOnWorkbasketException If the user is not authorized to access or modify + * the specified workbaskets. + * @throws InvalidTaskStateException If any {@linkplain Task} is in an {@linkplain + * TaskState#END_STATES EndState}. + * @throws TaskNotFoundException If any {@linkplain Task} in the source workbasket cannot be + * found. + */ + BulkOperationResults distributeWithDestinations( + String sourceWorkbasketId, List destinationWorkbasketIds) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + TaskNotFoundException, + InvalidTaskStateException; + + /** + * Distributes a list of {@linkplain Task} instances from a source {@linkplain Workbasket} to + * specified destination {@linkplain Workbasket}s using the default strategy. + * + * @param sourceWorkbasketId The {@linkplain Workbasket#getId() Id} of the source workbasket + * containing the tasks to be distributed. + * @param taskIds A list of task IDs to be distributed. + * @param destinationWorkbasketIds A list of {@linkplain Workbasket#getId() Ids} of the + * destination workbaskets where tasks will be distributed. + * @return BulkResult with {@linkplain Task#getId() ids} and Error for each failed transaction. + * @throws InvalidArgumentException If the input data is invalid. + * @throws WorkbasketNotFoundException If the source or any destination workbasket cannot be + * found. + * @throws NotAuthorizedOnWorkbasketException If the user is not authorized to access or modify + * the specified workbaskets. + * @throws InvalidTaskStateException If any {@linkplain Task} is in an {@linkplain + * TaskState#END_STATES EndState}. + * @throws TaskNotFoundException If any {@linkplain Task} in the list cannot be found. + */ + BulkOperationResults distributeWithDestinations( + String sourceWorkbasketId, List taskIds, List destinationWorkbasketIds) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + TaskNotFoundException, + InvalidTaskStateException; + /** * Update a {@linkplain Task}. * diff --git a/lib/kadai-core/src/main/java/io/kadai/task/internal/TaskDistributor.java b/lib/kadai-core/src/main/java/io/kadai/task/internal/TaskDistributor.java new file mode 100644 index 000000000..cc9c4d78f --- /dev/null +++ b/lib/kadai-core/src/main/java/io/kadai/task/internal/TaskDistributor.java @@ -0,0 +1,297 @@ +package io.kadai.task.internal; + +import io.kadai.common.api.BulkOperationResults; +import io.kadai.common.api.exceptions.InvalidArgumentException; +import io.kadai.common.api.exceptions.KadaiException; +import io.kadai.common.internal.InternalKadaiEngine; +import io.kadai.spi.task.api.TaskDistributionProvider; +import io.kadai.spi.task.internal.TaskDistributionManager; +import io.kadai.task.api.exceptions.TaskNotFoundException; +import io.kadai.task.api.models.Task; +import io.kadai.task.api.models.TaskSummary; +import io.kadai.workbasket.api.WorkbasketPermission; +import io.kadai.workbasket.api.WorkbasketService; +import io.kadai.workbasket.api.exceptions.NotAuthorizedOnWorkbasketException; +import io.kadai.workbasket.api.exceptions.WorkbasketNotFoundException; +import io.kadai.workbasket.api.models.Workbasket; +import io.kadai.workbasket.api.models.WorkbasketSummary; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * This class is responsible for the distribution of {@linkplain Task}s from a {@linkplain + * Workbasket} to another {@linkplain Workbasket} based on a DistributionStrategy implemented by a + * {@linkplain TaskDistributionProvider}. + */ +public class TaskDistributor { + + private final InternalKadaiEngine kadaiEngine; + private final WorkbasketService workbasketService; + private final TaskServiceImpl taskService; + private final TaskDistributionManager taskDistributionManager; + + public TaskDistributor(InternalKadaiEngine kadaiEngine, TaskServiceImpl taskService) { + this.kadaiEngine = kadaiEngine; + this.taskService = taskService; + this.workbasketService = kadaiEngine.getEngine().getWorkbasketService(); + this.taskDistributionManager = kadaiEngine.getTaskDistributionManager(); + } + + public BulkOperationResults distribute( + String sourceWorkbasketId, + List taskIds, + List destinationWorkbasketIds, + String distributionStrategyName, + Map additionalInformation) + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + TaskNotFoundException { + + return distributeTasks( + sourceWorkbasketId, + taskIds, + distributionStrategyName, + destinationWorkbasketIds, + additionalInformation); + } + + public BulkOperationResults distribute( + String sourceWorkbasketId, + List destinationWorkbasketIds, + String distributionStrategyName, + Map additionalInformation) + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + TaskNotFoundException { + + return distributeTasks( + sourceWorkbasketId, + null, + distributionStrategyName, + destinationWorkbasketIds, + additionalInformation); + } + + public BulkOperationResults distribute(String sourceWorkbasketId) + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + TaskNotFoundException { + + return distributeTasks(sourceWorkbasketId, null, null, null, null); + } + + public BulkOperationResults distribute( + String sourceWorkbasketId, List taskIds) + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + TaskNotFoundException { + + return distributeTasks(sourceWorkbasketId, taskIds, null, null, null); + } + + public BulkOperationResults distributeWithStrategy( + String sourceWorkbasketId, + List taskIds, + String distributionStrategyName, + Map additionalInformation) + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + TaskNotFoundException { + + return distributeTasks( + sourceWorkbasketId, taskIds, distributionStrategyName, null, additionalInformation); + } + + public BulkOperationResults distributeWithStrategy( + String sourceWorkbasketId, + String distributionStrategyName, + Map additionalInformation) + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + TaskNotFoundException { + + return distributeTasks( + sourceWorkbasketId, null, distributionStrategyName, null, additionalInformation); + } + + public BulkOperationResults distributeWithDestinations( + String sourceWorkbasketId, List destinationWorkbasketIds) + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + TaskNotFoundException { + + return distributeTasks(sourceWorkbasketId, null, null, destinationWorkbasketIds, null); + } + + public BulkOperationResults distributeWithDestinations( + String sourceWorkbasketId, List taskIds, List destinationWorkbasketIds) + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + TaskNotFoundException { + + return distributeTasks(sourceWorkbasketId, taskIds, null, destinationWorkbasketIds, null); + } + + private BulkOperationResults distributeTasks( + String sourceWorkbasketId, + List taskIds, + String distributionStrategyName, + List destinationWorkbasketIds, + Map additionalInformation) + throws InvalidArgumentException, + NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + TaskNotFoundException { + + BulkOperationResults operationResults = new BulkOperationResults<>(); + + try { + kadaiEngine.openConnection(); + + workbasketService.checkAuthorization(sourceWorkbasketId, WorkbasketPermission.DISTRIBUTE); + + taskIds = resolveTaskIds(sourceWorkbasketId, taskIds); + if (taskIds.isEmpty()) { + return operationResults; + } + + destinationWorkbasketIds = + resolveDestinationWorkbasketIds(sourceWorkbasketId, destinationWorkbasketIds); + + Map> newTaskDistribution = + taskDistributionManager.distributeTasks( + taskIds, destinationWorkbasketIds, additionalInformation, distributionStrategyName); + + Map taskTransferFlags = getTaskTransferFlags(taskIds); + + transferTasks(newTaskDistribution, taskTransferFlags, operationResults); + } finally { + kadaiEngine.returnConnection(); + } + return operationResults; + } + + private List resolveTaskIds(String sourceWorkbasketId, List taskIds) + throws TaskNotFoundException { + if (taskIds == null) { + return getTaskIdsFromWorkbasket(sourceWorkbasketId); + } else { + checkIfTasksInSameWorkbasket(taskIds); + checkIfTaskIdsExist(taskIds); + return taskIds; + } + } + + private List resolveDestinationWorkbasketIds( + String sourceWorkbasketId, List destinationWorkbasketIds) + throws WorkbasketNotFoundException, NotAuthorizedOnWorkbasketException { + if (destinationWorkbasketIds == null) { + return getDestinationWorkbasketIds(sourceWorkbasketId); + } + checkIfDestinationWorkbasketIdsExist(destinationWorkbasketIds); + return destinationWorkbasketIds; + } + + private Map getTaskTransferFlags(List taskIds) { + return taskService.createTaskQuery().idIn(taskIds.toArray(new String[0])).list().stream() + .collect(Collectors.toMap(TaskSummary::getId, TaskSummary::isTransferred)); + } + + private void transferTasks( + Map> newTaskDistribution, + Map taskTransferFlags, + BulkOperationResults operationResults) { + newTaskDistribution.forEach( + (newDestinationWorkbasketId, taskIdsForDestination) -> { + Map> partitionedTaskIds = + taskIdsForDestination.stream() + .collect(Collectors.partitioningBy(taskTransferFlags::get)); + + List transferredTaskIds = partitionedTaskIds.getOrDefault(true, List.of()); + List notTransferredTaskIds = partitionedTaskIds.getOrDefault(false, List.of()); + + try { + if (!transferredTaskIds.isEmpty()) { + BulkOperationResults transferResults = + taskService.transferTasksWithOwner( + newDestinationWorkbasketId, transferredTaskIds, null, true); + transferResults.getErrorMap().forEach(operationResults::addError); + } + if (!notTransferredTaskIds.isEmpty()) { + BulkOperationResults transferResults = + taskService.transferTasksWithOwner( + newDestinationWorkbasketId, notTransferredTaskIds, null, false); + transferResults.getErrorMap().forEach(operationResults::addError); + } + } catch (NotAuthorizedOnWorkbasketException | WorkbasketNotFoundException e) { + taskIdsForDestination.forEach(taskId -> operationResults.addError(taskId, e)); + } + }); + } + + List getTaskIdsFromWorkbasket(String sourceWorkbasketId) { + return kadaiEngine + .getEngine() + .runAsAdmin(() -> taskService.createTaskQuery().workbasketIdIn(sourceWorkbasketId).list()) + .stream() + .map(TaskSummary::getId) + .toList(); + } + + List getDestinationWorkbasketIds(String sourceWorkbasketId) + throws NotAuthorizedOnWorkbasketException, WorkbasketNotFoundException { + return workbasketService.getDistributionTargets(sourceWorkbasketId).stream() + .map(WorkbasketSummary::getId) + .toList(); + } + + private void checkIfTasksInSameWorkbasket(List taskIds) { + List taskSummariesToDistribute = + taskService.createTaskQuery().idIn(taskIds.toArray(new String[0])).list(); + + Set workbasketIds = + taskSummariesToDistribute.stream() + .map(taskSummary -> taskSummary.getWorkbasketSummary().getId()) + .collect(Collectors.toSet()); + + if (workbasketIds.size() > 1) { + throw new InvalidArgumentException("Not all tasks are in the same workbasket."); + } + } + + private void checkIfTaskIdsExist(List taskIds) + throws TaskNotFoundException, InvalidArgumentException { + + List existingTaskIds = + taskService.createTaskQuery().idIn(taskIds.toArray(new String[0])).list().stream() + .map(TaskSummary::getId) + .toList(); + + for (String taskId : taskIds) { + if (!existingTaskIds.contains(taskId)) { + throw new TaskNotFoundException(taskId); + } + } + } + + private void checkIfDestinationWorkbasketIdsExist(List destinationWorkbasketIds) + throws WorkbasketNotFoundException, InvalidArgumentException { + + List existingWorkbasketIds = + workbasketService + .createWorkbasketQuery() + .idIn(destinationWorkbasketIds.toArray(new String[0])) + .list() + .stream() + .map(WorkbasketSummary::getId) + .toList(); + + for (String workbasketId : destinationWorkbasketIds) { + if (!existingWorkbasketIds.contains(workbasketId)) { + throw new WorkbasketNotFoundException(workbasketId); + } + } + } +} diff --git a/lib/kadai-core/src/main/java/io/kadai/task/internal/TaskServiceImpl.java b/lib/kadai-core/src/main/java/io/kadai/task/internal/TaskServiceImpl.java index 311b8fe8b..333dbe422 100644 --- a/lib/kadai-core/src/main/java/io/kadai/task/internal/TaskServiceImpl.java +++ b/lib/kadai-core/src/main/java/io/kadai/task/internal/TaskServiceImpl.java @@ -130,6 +130,7 @@ public class TaskServiceImpl implements TaskService { private final ClassificationService classificationService; private final TaskMapper taskMapper; private final TaskTransferrer taskTransferrer; + private final TaskDistributor taskDistributor; private final TaskCommentServiceImpl taskCommentService; private final ServiceLevelHandler serviceLevelHandler; private final AttachmentHandler attachmentHandler; @@ -171,6 +172,7 @@ public TaskServiceImpl( this.afterRequestChangesManager = kadaiEngine.getAfterRequestChangesManager(); this.taskEndstatePreprocessorManager = kadaiEngine.getTaskEndstatePreprocessorManager(); this.taskTransferrer = new TaskTransferrer(kadaiEngine, taskMapper, this); + this.taskDistributor = new TaskDistributor(kadaiEngine, this); this.taskCommentService = new TaskCommentServiceImpl(kadaiEngine, taskCommentMapper, userMapper, taskMapper, this); this.serviceLevelHandler = @@ -433,8 +435,8 @@ public Task createTask(Task taskToCreate) workbasket = workbasketService.getWorkbasket(task.getWorkbasketKey(), task.getDomain()); } else { RoutingTarget routingTarget = calculateWorkbasketDuringTaskCreation(task); - String owner = routingTarget.getOwner() == null - ? task.getOwner() : routingTarget.getOwner(); + String owner = + routingTarget.getOwner() == null ? task.getOwner() : routingTarget.getOwner(); workbasket = workbasketService.getWorkbasket(routingTarget.getWorkbasketId()); task.setOwner(owner); } @@ -806,6 +808,111 @@ public BulkOperationResults transferTasksWithOwner( taskIds, destinationWorkbasketKey, destinationWorkbasketDomain, owner, setTransferFlag); } + @Override + public BulkOperationResults distribute( + String sourceWorkbasketId, + List taskIds, + List destinationWorkbasketIds, + String distributionStrategyName, + Map additionalInformation) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + TaskNotFoundException, + InvalidTaskStateException { + return taskDistributor.distribute( + sourceWorkbasketId, + taskIds, + destinationWorkbasketIds, + distributionStrategyName, + additionalInformation); + } + + @Override + public BulkOperationResults distribute( + String sourceWorkbasketId, + List destinationWorkbasketIds, + String distributionStrategyName, + Map additionalInformation) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + TaskNotFoundException, + InvalidTaskStateException { + return taskDistributor.distribute( + sourceWorkbasketId, + destinationWorkbasketIds, + distributionStrategyName, + additionalInformation); + } + + @Override + public BulkOperationResults distribute(String sourceWorkbasketId) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + TaskNotFoundException, + InvalidTaskStateException { + return taskDistributor.distribute(sourceWorkbasketId); + } + + @Override + public BulkOperationResults distribute( + String sourceWorkbasketId, List taskIds) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + TaskNotFoundException, + InvalidTaskStateException { + return taskDistributor.distribute(sourceWorkbasketId, taskIds); + } + + @Override + public BulkOperationResults distributeWithStrategy( + String sourceWorkbasketId, + String distributionStrategyName, + Map additionalInformation) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, TaskNotFoundException { + return taskDistributor.distributeWithStrategy( + sourceWorkbasketId, distributionStrategyName, additionalInformation); + } + + @Override + public BulkOperationResults distributeWithStrategy( + String sourceWorkbasketId, + List taskIds, + String distributionStrategyName, + Map additionalInformation) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, TaskNotFoundException { + return taskDistributor.distributeWithStrategy( + sourceWorkbasketId, taskIds, distributionStrategyName, additionalInformation); + } + + @Override + public BulkOperationResults distributeWithDestinations( + String sourceWorkbasketId, List destinationWorkbasketIds) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + TaskNotFoundException { + return taskDistributor.distributeWithDestinations(sourceWorkbasketId, destinationWorkbasketIds); + } + + @Override + public BulkOperationResults distributeWithDestinations( + String sourceWorkbasketId, List taskIds, List destinationWorkbasketIds) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + TaskNotFoundException { + return taskDistributor.distributeWithDestinations( + sourceWorkbasketId, taskIds, destinationWorkbasketIds); + } + @Override public void deleteTask(String taskId) throws TaskNotFoundException, @@ -2031,11 +2138,14 @@ private RoutingTarget calculateWorkbasketDuringTaskCreation(TaskImpl task) { } routingTarget = new RoutingTarget(workbasketId); } else { - routingTarget = kadaiEngine.getTaskRoutingManager() - .determineRoutingTarget(task).orElseThrow( - () -> new InvalidArgumentException( - "Cannot create a Task in an empty RoutingTarget") - ); + routingTarget = + kadaiEngine + .getTaskRoutingManager() + .determineRoutingTarget(task) + .orElseThrow( + () -> + new InvalidArgumentException( + "Cannot create a Task in an empty RoutingTarget")); } return routingTarget; } diff --git a/lib/kadai-test-api/src/main/java/io/kadai/testapi/util/ServiceProviderExtractor.java b/lib/kadai-test-api/src/main/java/io/kadai/testapi/util/ServiceProviderExtractor.java index 5be28581f..f1a045cd0 100644 --- a/lib/kadai-test-api/src/main/java/io/kadai/testapi/util/ServiceProviderExtractor.java +++ b/lib/kadai-test-api/src/main/java/io/kadai/testapi/util/ServiceProviderExtractor.java @@ -29,6 +29,7 @@ import io.kadai.spi.task.api.BeforeRequestReviewProvider; import io.kadai.spi.task.api.CreateTaskPreprocessor; import io.kadai.spi.task.api.ReviewRequiredProvider; +import io.kadai.spi.task.api.TaskDistributionProvider; import io.kadai.spi.task.api.TaskEndstatePreprocessor; import io.kadai.testapi.WithServiceProvider; import java.lang.reflect.Modifier; @@ -48,6 +49,7 @@ public class ServiceProviderExtractor { KadaiHistory.class, PriorityServiceProvider.class, TaskRoutingProvider.class, + TaskDistributionProvider.class, CreateTaskPreprocessor.class, ReviewRequiredProvider.class, BeforeRequestReviewProvider.class, diff --git a/rest/kadai-rest-spring/src/main/java/io/kadai/common/rest/RestEndpoints.java b/rest/kadai-rest-spring/src/main/java/io/kadai/common/rest/RestEndpoints.java index f01d73590..c0867099e 100644 --- a/rest/kadai-rest-spring/src/main/java/io/kadai/common/rest/RestEndpoints.java +++ b/rest/kadai-rest-spring/src/main/java/io/kadai/common/rest/RestEndpoints.java @@ -81,6 +81,8 @@ public final class RestEndpoints { API_V1 + "tasks/{taskId}/transfer/{workbasketId}"; public static final String URL_TRANSFER_WORKBASKET_ID = API_V1 + "tasks/transfer/{workbasketId}"; public static final String URL_TASKS_ID_SET_READ = API_V1 + "tasks/{taskId}/set-read"; + public static final String URL_DISTRIBUTE = + API_V1 + "tasks/distribute/{workbasketId}"; // task comment endpoints public static final String URL_TASK_COMMENTS = API_V1 + "tasks/{taskId}/comments"; diff --git a/rest/kadai-rest-spring/src/main/java/io/kadai/task/rest/TaskApi.java b/rest/kadai-rest-spring/src/main/java/io/kadai/task/rest/TaskApi.java index 9c8bb0253..b348c190b 100644 --- a/rest/kadai-rest-spring/src/main/java/io/kadai/task/rest/TaskApi.java +++ b/rest/kadai-rest-spring/src/main/java/io/kadai/task/rest/TaskApi.java @@ -17,6 +17,7 @@ import io.kadai.task.api.models.TaskSummary; import io.kadai.task.rest.TaskController.TaskQuerySortParameter; import io.kadai.task.rest.models.BulkOperationResultsRepresentationModel; +import io.kadai.task.rest.models.DistributionTasksRepresentationModel; import io.kadai.task.rest.models.IsReadRepresentationModel; import io.kadai.task.rest.models.TaskRepresentationModel; import io.kadai.task.rest.models.TaskSummaryCollectionRepresentationModel; @@ -1183,6 +1184,134 @@ ResponseEntity transferTasks( @RequestBody TransferTaskRepresentationModel transferTaskRepresentationModel) throws NotAuthorizedOnWorkbasketException, WorkbasketNotFoundException; + /** + * Distributes tasks to one or more destination workbaskets based on the provided distribution + * strategy and additional information. The tasks to be distributed are passed in the request + * body. + * + *

This endpoint provides flexible distribution options: + * + *

    + *
  • If destination workbasket IDs are provided, tasks will be distributed to those specific + * workbaskets. + *
  • If a distribution strategy name is provided, the specified strategy will determine how + * tasks are allocated. + *
  • Additional information can be passed to further customize the distribution logic. + *
+ * + *

Tasks that cannot be distributed (e.g., due to errors) will remain in their original + * workbaskets. + * + * @title Distribute Tasks + * @param distributionTasksRepresentationModel a JSON-formatted request body containing: + *

    + *
  • {@code taskIds} ({@code List}): A list of task IDs to be distributed from a + * given sourceworkbasket (optional). + *
  • {@code destinationWorkbasketIds} ({@code List}): A list of destination + * workbasket IDs (optional). + *
  • {@code distributionStrategyName} ({@code String}): The name of the distribution + * strategy to use (optional). + *
  • {@code additionalInformation} ({@code Map}): Additional details to + * customize the distribution logic (optional). + *
+ * + * @param workbasketId the ID of the workbasket from which the tasks are to be distributed. + * @return a {@link ResponseEntity} containing a {@link BulkOperationResultsRepresentationModel} + * that includes: + *
    + *
  • A map of task IDs and corresponding error codes for failed distributions. + *
+ * + * @throws WorkbasketNotFoundException if the destination workbaskets cannot be found. + * @throws NotAuthorizedOnWorkbasketException if the current user lacks permission to perform this + * operation. + * @throws InvalidTaskStateException if one or more tasks are in a state that prevents + * distribution. + * @throws TaskNotFoundException if specific tasks referenced in the operation cannot be found. + * @throws InvalidArgumentException if no task IDs are provided. + */ + @Operation( + summary = "Distribute Tasks to Destination Workbaskets", + description = + """ + This endpoint distributes tasks to one or more destination workbaskets based on the + provided distribution strategy and additional information. + + - If destination workbasket IDs are provided, tasks will be distributed to those specific + workbaskets. + - If a distribution strategy name is provided, the specified strategy will determine how + tasks are allocated. + - Additional information can be passed to further customize the distribution logic. + + Tasks that cannot be distributed (e.g., due to errors) will remain in their original + workbaskets. + """, + requestBody = + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = + "The JSON-formatted request body containing the details for task distribution.", + required = true, + content = + @Content( + schema = @Schema(implementation = DistributionTasksRepresentationModel.class), + examples = + @ExampleObject( + value = + """ + { + "taskIds": ["TKI:abcdef1234567890abcdef1234567890"], + "destinationWorkbasketIds": [ + "WBI:abcdef1234567890abcdef1234567890", + "WBI:1234567890abcdef1234567890abcdef" + ], + "distributionStrategyName": "CustomDistributionStrategy" + } + """))), + responses = { + @ApiResponse( + responseCode = "200", + description = + "The bulk operation result containing details of successfully " + + "distributed tasks or errors.", + content = + @Content( + mediaType = MediaTypes.HAL_JSON_VALUE, + schema = + @Schema(implementation = BulkOperationResultsRepresentationModel.class))), + @ApiResponse( + responseCode = "400", + description = "TASK_INVALID_STATE", + content = { + @Content(schema = @Schema(implementation = InvalidTaskStateException.class)) + }), + @ApiResponse( + responseCode = "403", + description = + "NOT_AUTHORIZED_ON_WORKBASKET_WITH_ID, " + + "NOT_AUTHORIZED_ON_WORKBASKET_WITH_KEY_AND_DOMAIN", + content = { + @Content(schema = @Schema(implementation = NotAuthorizedOnWorkbasketException.class)) + }), + @ApiResponse( + responseCode = "404", + description = + "WORKBASKET_WITH_ID_NOT_FOUND, WORKBASKET_WITH_KEY_NOT_FOUND, TASK_NOT_FOUND", + content = { + @Content( + schema = + @Schema( + anyOf = {WorkbasketNotFoundException.class, TaskNotFoundException.class})) + }) + }) + ResponseEntity distributeTasks( + @RequestBody DistributionTasksRepresentationModel distributionTasksRepresentationModel, + @PathVariable("workbasketId") String workbasketId) + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + InvalidTaskStateException, + TaskNotFoundException, + InvalidArgumentException; + /** * This endpoint updates a requested Task. * diff --git a/rest/kadai-rest-spring/src/main/java/io/kadai/task/rest/TaskController.java b/rest/kadai-rest-spring/src/main/java/io/kadai/task/rest/TaskController.java index d8c263a79..fc9c9e0fd 100644 --- a/rest/kadai-rest-spring/src/main/java/io/kadai/task/rest/TaskController.java +++ b/rest/kadai-rest-spring/src/main/java/io/kadai/task/rest/TaskController.java @@ -48,6 +48,7 @@ import io.kadai.task.rest.assembler.TaskRepresentationModelAssembler; import io.kadai.task.rest.assembler.TaskSummaryRepresentationModelAssembler; import io.kadai.task.rest.models.BulkOperationResultsRepresentationModel; +import io.kadai.task.rest.models.DistributionTasksRepresentationModel; import io.kadai.task.rest.models.IsReadRepresentationModel; import io.kadai.task.rest.models.TaskRepresentationModel; import io.kadai.task.rest.models.TaskSummaryCollectionRepresentationModel; @@ -59,6 +60,7 @@ import java.beans.ConstructorProperties; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -408,6 +410,71 @@ public ResponseEntity transferTasks( return ResponseEntity.ok(repModel); } + @PostMapping(path = RestEndpoints.URL_DISTRIBUTE) + @Transactional(rollbackFor = Exception.class) + public ResponseEntity distributeTasks( + @RequestBody DistributionTasksRepresentationModel distributionTasksRepresentationModel, + @PathVariable("workbasketId") String workbasketId) + throws NotAuthorizedOnWorkbasketException, + WorkbasketNotFoundException, + InvalidTaskStateException, + TaskNotFoundException, + InvalidArgumentException { + + List taskIds = distributionTasksRepresentationModel.getTaskIds(); + List destinationWorkbasketIds = + distributionTasksRepresentationModel.getDestinationWorkbasketIds(); + String distributionStrategyName = + distributionTasksRepresentationModel.getDistributionStrategyName(); + Map additionalInformation = + distributionTasksRepresentationModel.getAdditionalInformation(); + + BulkOperationResults result; + + if (taskIds == null) { + if (destinationWorkbasketIds != null && distributionStrategyName != null) { + result = + taskService.distribute( + workbasketId, + destinationWorkbasketIds, + distributionStrategyName, + additionalInformation); + } else if (destinationWorkbasketIds != null) { + result = taskService.distributeWithDestinations(workbasketId, destinationWorkbasketIds); + } else if (distributionStrategyName != null) { + result = + taskService.distributeWithStrategy( + workbasketId, distributionStrategyName, additionalInformation); + } else { + result = taskService.distribute(workbasketId); + } + } else { + if (destinationWorkbasketIds != null && distributionStrategyName != null) { + result = + taskService.distribute( + workbasketId, + taskIds, + destinationWorkbasketIds, + distributionStrategyName, + additionalInformation); + } else if (destinationWorkbasketIds != null) { + result = + taskService.distributeWithDestinations(workbasketId, taskIds, destinationWorkbasketIds); + } else if (distributionStrategyName != null) { + result = + taskService.distributeWithStrategy( + workbasketId, taskIds, distributionStrategyName, additionalInformation); + } else { + result = taskService.distribute(workbasketId, taskIds); + } + } + + BulkOperationResultsRepresentationModel repModel = + bulkOperationResultsRepresentationModelAssembler.toModel(result); + + return ResponseEntity.ok(repModel); + } + @PutMapping(path = RestEndpoints.URL_TASKS_ID) @Transactional(rollbackFor = Exception.class) public ResponseEntity updateTask( diff --git a/rest/kadai-rest-spring/src/main/java/io/kadai/task/rest/models/DistributionTasksRepresentationModel.java b/rest/kadai-rest-spring/src/main/java/io/kadai/task/rest/models/DistributionTasksRepresentationModel.java new file mode 100644 index 000000000..4668d5124 --- /dev/null +++ b/rest/kadai-rest-spring/src/main/java/io/kadai/task/rest/models/DistributionTasksRepresentationModel.java @@ -0,0 +1,57 @@ +package io.kadai.task.rest.models; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.beans.ConstructorProperties; +import java.util.List; +import java.util.Map; + +public class DistributionTasksRepresentationModel { + /** The list of task IDs to be distributed. */ + @JsonProperty("taskIds") + private final List taskIds; + + /** The list of destination workbasket IDs. */ + @JsonProperty("destinationWorkbasketIds") + private final List destinationWorkbasketIds; + + /** The name of the distribution strategy. */ + @JsonProperty("distributionStrategyName") + private final String distributionStrategyName; + + /** Additional information for the distribution process. */ + @JsonProperty("additionalInformation") + private final Map additionalInformation; + + @ConstructorProperties({ + "taskIds", + "destinationWorkbasketIds", + "distributionStrategyName", + "additionalInformation" + }) + public DistributionTasksRepresentationModel( + List taskIds, + List destinationWorkbasketIds, + String distributionStrategyName, + Map additionalInformation) { + this.taskIds = taskIds; + this.destinationWorkbasketIds = destinationWorkbasketIds; + this.distributionStrategyName = distributionStrategyName; + this.additionalInformation = additionalInformation; + } + + public List getTaskIds() { + return taskIds; + } + + public List getDestinationWorkbasketIds() { + return destinationWorkbasketIds; + } + + public String getDistributionStrategyName() { + return distributionStrategyName; + } + + public Map getAdditionalInformation() { + return additionalInformation; + } +} diff --git a/rest/kadai-rest-spring/src/test/java/io/kadai/task/rest/TaskControllerIntTest.java b/rest/kadai-rest-spring/src/test/java/io/kadai/task/rest/TaskControllerIntTest.java index e8a208c39..b8642f933 100644 --- a/rest/kadai-rest-spring/src/test/java/io/kadai/task/rest/TaskControllerIntTest.java +++ b/rest/kadai-rest-spring/src/test/java/io/kadai/task/rest/TaskControllerIntTest.java @@ -31,6 +31,8 @@ import io.kadai.rest.test.RestHelper; import io.kadai.task.api.TaskState; import io.kadai.task.rest.models.AttachmentRepresentationModel; +import io.kadai.task.rest.models.BulkOperationResultsRepresentationModel; +import io.kadai.task.rest.models.DistributionTasksRepresentationModel; import io.kadai.task.rest.models.IsReadRepresentationModel; import io.kadai.task.rest.models.ObjectReferenceRepresentationModel; import io.kadai.task.rest.models.TaskRepresentationModel; @@ -69,9 +71,12 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.hateoas.IanaLinkRelations; import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpStatusCodeException; import org.testcontainers.shaded.com.google.common.collect.Lists; @@ -2109,6 +2114,175 @@ Stream should_ReturnFailedTasks_When_TransferringTasks() { } } + @Nested + @TestInstance(Lifecycle.PER_CLASS) + class DistributeTasks { + + @Test + void should_ThrowException_When_SourceWorkbasketIdIsMissing() { + DistributionTasksRepresentationModel requestBody = + new DistributionTasksRepresentationModel(null, null, null, null); + + String url = + restHelper.toUrl(RestEndpoints.URL_DISTRIBUTE.replace("{workbasketId}", "dummyId")); + + HttpEntity requestEntity = + new HttpEntity<>(requestBody, RestHelper.generateHeadersForUser("admin")); + + assertThatThrownBy( + () -> + TEMPLATE.exchange( + url, HttpMethod.POST, requestEntity, TaskRepresentationModel.class)) + .isInstanceOf(HttpClientErrorException.NotFound.class) + .hasMessageContaining("Workbasket with id 'dummyId' was not found."); + } + + @Test + void should_CallDistributeWithTaskIdsAndWithDestinationWorkbasketIds_When_Provided() { + List taskIds = List.of("TKI:000000000000000000000000000000000039"); + List destinationWorkbasketIds = List.of("WBI:100000000000000000000000000000000006"); + + DistributionTasksRepresentationModel requestBody = + new DistributionTasksRepresentationModel(taskIds, destinationWorkbasketIds, null, null); + + String url = + restHelper.toUrl( + RestEndpoints.URL_DISTRIBUTE, "WBI:100000000000000000000000000000000006"); + + HttpEntity auth = + new HttpEntity<>(requestBody, RestHelper.generateHeadersForUser("admin")); + + ResponseEntity> response = + TEMPLATE.exchange(url, HttpMethod.POST, auth, BULK_RESULT_TASKS_MODEL_TYPE); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + + Map result = response.getBody(); + assertThat(result).isNotNull().containsKey("tasksWithErrors"); + + Map tasksWithErrors = (Map) result.get("tasksWithErrors"); + + assertThat(tasksWithErrors) + .hasSize(1) + .containsKey("TKI:000000000000000000000000000000000039"); + + Map errorDetails = + (Map) tasksWithErrors.get("TKI:000000000000000000000000000000000039"); + + assertThat(errorDetails) + .containsEntry("key", "TASK_INVALID_STATE") + .containsKey("messageVariables"); + + Map messageVariables = + (Map) errorDetails.get("messageVariables"); + + // **Überprüfen, ob die `requiredTaskStates` korrekt sind** + assertThat(messageVariables).containsKey("requiredTaskStates"); + assertThat((List) messageVariables.get("requiredTaskStates")) + .containsExactlyInAnyOrder("READY", "CLAIMED", "READY_FOR_REVIEW", "IN_REVIEW"); + + // **Zusätzliche Variablen prüfen** + assertThat(messageVariables) + .containsEntry("taskState", "COMPLETED") + .containsEntry("taskId", "TKI:000000000000000000000000000000000039"); + } + + @Test + void should_ThrowException_When_InvalidDistributionStrategyProvided() { + List taskIds = List.of("TKI:000000000000000000000000000000000039"); + String invalidDistributionStrategyName = "ROUND_ROBIN"; + DistributionTasksRepresentationModel requestBody = + new DistributionTasksRepresentationModel( + taskIds, null, invalidDistributionStrategyName, null); + + String url = + restHelper.toUrl( + RestEndpoints.URL_DISTRIBUTE, "WBI:100000000000000000000000000000000006"); + HttpEntity auth = + new HttpEntity<>(requestBody, RestHelper.generateHeadersForUser("admin")); + + assertThatThrownBy( + () -> + TEMPLATE.exchange( + url, HttpMethod.POST, auth, BulkOperationResultsRepresentationModel.class)) + .isInstanceOf(HttpClientErrorException.class) + .extracting(HttpClientErrorException.class::cast) + .extracting(HttpClientErrorException::getStatusCode) + .isEqualTo(HttpStatus.BAD_REQUEST); + + assertThatThrownBy( + () -> + TEMPLATE.exchange( + url, HttpMethod.POST, auth, BulkOperationResultsRepresentationModel.class)) + .isInstanceOf(HttpClientErrorException.class) + .hasMessageContaining("The distribution strategy 'ROUND_ROBIN' does not exist."); + } + + @Test + void should_ThrowNotAuthorizedOnWorkbasketException() { + HttpHeaders headers = RestHelper.generateHeadersForUser("user-1-1"); + headers.setContentType(MediaType.APPLICATION_JSON); + + String sourceWorkbasketId = "WBI:100000000000000000000000000000000001"; + DistributionTasksRepresentationModel requestBody = + new DistributionTasksRepresentationModel(null, null, null, null); + + HttpEntity requestEntity = + new HttpEntity<>(requestBody, headers); + + String url = restHelper.toUrl(RestEndpoints.URL_DISTRIBUTE, sourceWorkbasketId); + + ThrowingCallable response = + () -> + TEMPLATE.exchange( + url, + HttpMethod.POST, + requestEntity, + BulkOperationResultsRepresentationModel.class); + + assertThatThrownBy(response).isInstanceOf(HttpClientErrorException.class); + } + + @Test + void should_CallDistributeWithWIdWithAdditionalInformation_When_OnlySourceWtIdProvided() { + String sourceWorkbasketId = "WBI:100000000000000000000000000000000006"; + DistributionTasksRepresentationModel requestBody = + new DistributionTasksRepresentationModel(null, null, null, Map.of("priority", "high")); + + String url = restHelper.toUrl(RestEndpoints.URL_DISTRIBUTE, sourceWorkbasketId); + HttpEntity auth = + new HttpEntity<>(requestBody, RestHelper.generateHeadersForUser("admin")); + + ResponseEntity response = + TEMPLATE.exchange( + url, HttpMethod.POST, auth, BulkOperationResultsRepresentationModel.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + } + + @Test + void should_CallDistributeWithWIdAndWithDestinationWorkbasketIds_When_Provided() { + String sourceWorkbasketId = "WBI:100000000000000000000000000000000006"; + List destinationWorkbasketIds = List.of("WBI:100000000000000000000000000000000005"); + DistributionTasksRepresentationModel requestBody = + new DistributionTasksRepresentationModel( + null, destinationWorkbasketIds, null, Map.of("priority", "high")); + + String url = restHelper.toUrl(RestEndpoints.URL_DISTRIBUTE, sourceWorkbasketId); + HttpEntity auth = + new HttpEntity<>(requestBody, RestHelper.generateHeadersForUser("admin")); + + ResponseEntity response = + TEMPLATE.exchange( + url, HttpMethod.POST, auth, BulkOperationResultsRepresentationModel.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + } + } + @Nested @TestInstance(Lifecycle.PER_CLASS) class RequestChangesOnTasks {