diff --git a/lib/taskana-core-test/src/test/java/acceptance/task/complete/CancelTaskAccTest.java b/lib/taskana-core-test/src/test/java/acceptance/task/complete/CancelTaskAccTest.java index eab92363c..30e8780a0 100644 --- a/lib/taskana-core-test/src/test/java/acceptance/task/complete/CancelTaskAccTest.java +++ b/lib/taskana-core-test/src/test/java/acceptance/task/complete/CancelTaskAccTest.java @@ -6,18 +6,23 @@ import static pro.taskana.testapi.DefaultTestEntities.defaultTestClassification; import static pro.taskana.testapi.DefaultTestEntities.defaultTestObjectReference; import static pro.taskana.testapi.DefaultTestEntities.defaultTestWorkbasket; +import acceptance.task.complete.CompleteTaskWithSpiAccTest.SetCustomAttributeToEndstate; import java.util.List; import java.util.stream.Stream; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.function.ThrowingConsumer; import pro.taskana.classification.api.ClassificationService; import pro.taskana.classification.api.models.ClassificationSummary; import pro.taskana.common.internal.util.Triplet; +import pro.taskana.spi.task.api.TaskEndstatePreprocessor; import pro.taskana.task.api.TaskService; import pro.taskana.task.api.TaskState; import pro.taskana.task.api.exceptions.InvalidTaskStateException; @@ -26,6 +31,7 @@ import pro.taskana.task.api.models.Task; import pro.taskana.testapi.DefaultTestEntities; import pro.taskana.testapi.TaskanaInject; import pro.taskana.testapi.TaskanaIntegrationTest; +import pro.taskana.testapi.WithServiceProvider; import pro.taskana.testapi.builder.TaskBuilder; import pro.taskana.testapi.builder.WorkbasketAccessItemBuilder; import pro.taskana.testapi.security.WithAccessId; @@ -182,4 +188,33 @@ class CancelTaskAccTest { }; return DynamicTest.stream(list.iterator(), Triplet::getLeft, testCancelTask); } + + @Nested + @TestInstance(Lifecycle.PER_CLASS) + @WithServiceProvider( + serviceProviderInterface = TaskEndstatePreprocessor.class, + serviceProviders = SetCustomAttributeToEndstate.class) + class ServiceProviderSetsCustomAttributeToCancelled { + + @TaskanaInject TaskService taskService; + + @WithAccessId(user = "user-1-2") + @Test + void should_SetCustomAttribute_When_UserCancelsTask() throws Exception { + Task task = + TaskBuilder.newTask() + .classificationSummary(defaultClassificationSummary) + .workbasketSummary(defaultWorkbasketSummary) + .state(TaskState.CLAIMED) + .primaryObjRef(DefaultTestEntities.defaultTestObjectReference().build()) + .buildAndStore(taskService); + Task processedTask = taskService.cancelTask(task.getId()); + assertThat(processedTask.getState()).isEqualTo(TaskState.CANCELLED); + assertThat(processedTask.getCustomAttributeMap()) + .containsEntry( + "camunda:attribute1", + "{\"valueInfo\":{\"objectTypeName\":\"java.lang.String\"}," + + "\"type\":\"String\",\"value\":\"CANCELLED\"}"); + } + } } diff --git a/lib/taskana-core-test/src/test/java/acceptance/task/complete/CompleteTaskWithSpiAccTest.java b/lib/taskana-core-test/src/test/java/acceptance/task/complete/CompleteTaskWithSpiAccTest.java index 864b78f25..740811c34 100644 --- a/lib/taskana-core-test/src/test/java/acceptance/task/complete/CompleteTaskWithSpiAccTest.java +++ b/lib/taskana-core-test/src/test/java/acceptance/task/complete/CompleteTaskWithSpiAccTest.java @@ -10,6 +10,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; @@ -17,6 +18,7 @@ import org.junit.jupiter.api.function.ThrowingConsumer; import pro.taskana.classification.api.ClassificationService; import pro.taskana.classification.api.models.ClassificationSummary; import pro.taskana.spi.task.api.ReviewRequiredProvider; +import pro.taskana.spi.task.api.TaskEndstatePreprocessor; import pro.taskana.task.api.TaskService; import pro.taskana.task.api.TaskState; import pro.taskana.task.api.models.ObjectReference; @@ -91,6 +93,21 @@ class CompleteTaskWithSpiAccTest { } } + static class SetCustomAttributeToEndstate implements TaskEndstatePreprocessor { + @Override + public Task processTaskBeforeEndstate(Task task) { + String endstate = task.getState().toString(); + task.getCustomAttributeMap() + .put( + "camunda:attribute1", + "{\"valueInfo\":{\"objectTypeName\":\"java.lang.String\"}," + + "\"type\":\"String\",\"value\":\"" + + endstate + + "\"}"); + return task; + } + } + @Nested @TestInstance(Lifecycle.PER_CLASS) @WithServiceProvider( @@ -171,4 +188,27 @@ class CompleteTaskWithSpiAccTest { tasks.iterator(), t -> "Try to complete " + t.getState().name() + " Task", test); } } + + @Nested + @TestInstance(Lifecycle.PER_CLASS) + @WithServiceProvider( + serviceProviderInterface = TaskEndstatePreprocessor.class, + serviceProviders = SetCustomAttributeToEndstate.class) + class ServiceProviderSetsCustomAttributeToCompleted { + + @TaskanaInject TaskService taskService; + + @WithAccessId(user = "user-1-1") + @Test + void should_SetCustomAttribute_When_UserCompletesTask() throws Exception { + Task task = createTaskClaimedByUser("user-1-1").buildAndStore(taskService); + Task processedTask = taskService.completeTask(task.getId()); + assertThat(processedTask.getState()).isEqualTo(TaskState.COMPLETED); + assertThat(processedTask.getCustomAttributeMap()) + .containsEntry( + "camunda:attribute1", + "{\"valueInfo\":{\"objectTypeName\":\"java.lang.String\"}," + + "\"type\":\"String\",\"value\":\"COMPLETED\"}"); + } + } } diff --git a/lib/taskana-core-test/src/test/java/acceptance/task/complete/TerminateTaskAccTest.java b/lib/taskana-core-test/src/test/java/acceptance/task/complete/TerminateTaskAccTest.java index c951a85e1..8895e8078 100644 --- a/lib/taskana-core-test/src/test/java/acceptance/task/complete/TerminateTaskAccTest.java +++ b/lib/taskana-core-test/src/test/java/acceptance/task/complete/TerminateTaskAccTest.java @@ -5,12 +5,17 @@ import static org.assertj.core.api.Assertions.catchThrowableOfType; import static pro.taskana.testapi.DefaultTestEntities.defaultTestClassification; import static pro.taskana.testapi.DefaultTestEntities.defaultTestWorkbasket; +import acceptance.task.complete.CompleteTaskWithSpiAccTest.SetCustomAttributeToEndstate; import java.util.List; import java.util.stream.Stream; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.function.ThrowingConsumer; import pro.taskana.classification.api.ClassificationService; @@ -19,6 +24,7 @@ import pro.taskana.common.api.TaskanaRole; import pro.taskana.common.api.exceptions.NotAuthorizedException; import pro.taskana.common.api.security.CurrentUserContext; import pro.taskana.common.internal.util.Triplet; +import pro.taskana.spi.task.api.TaskEndstatePreprocessor; import pro.taskana.task.api.TaskService; import pro.taskana.task.api.TaskState; import pro.taskana.task.api.exceptions.InvalidTaskStateException; @@ -26,6 +32,7 @@ import pro.taskana.task.api.models.Task; import pro.taskana.testapi.DefaultTestEntities; import pro.taskana.testapi.TaskanaInject; import pro.taskana.testapi.TaskanaIntegrationTest; +import pro.taskana.testapi.WithServiceProvider; import pro.taskana.testapi.builder.TaskBuilder; import pro.taskana.testapi.builder.WorkbasketAccessItemBuilder; import pro.taskana.testapi.security.WithAccessId; @@ -157,4 +164,33 @@ class TerminateTaskAccTest { return DynamicTest.stream(testValues.iterator(), Triplet::getLeft, test); } + + @Nested + @TestInstance(Lifecycle.PER_CLASS) + @WithServiceProvider( + serviceProviderInterface = TaskEndstatePreprocessor.class, + serviceProviders = SetCustomAttributeToEndstate.class) + class ServiceProviderSetsCustomAttributeToTerminated { + + @TaskanaInject TaskService taskService; + + @WithAccessId(user = "admin") + @Test + void should_SetCustomAttribute_When_UserTerminatesTask() throws Exception { + Task task = + TaskBuilder.newTask() + .classificationSummary(defaultClassificationSummary) + .workbasketSummary(defaultWorkbasketSummary) + .state(TaskState.READY) + .primaryObjRef(DefaultTestEntities.defaultTestObjectReference().build()) + .buildAndStore(taskService); + Task processedTask = taskService.terminateTask(task.getId()); + assertThat(processedTask.getState()).isEqualTo(TaskState.TERMINATED); + assertThat(processedTask.getCustomAttributeMap()) + .containsEntry( + "camunda:attribute1", + "{\"valueInfo\":{\"objectTypeName\":\"java.lang.String\"}," + + "\"type\":\"String\",\"value\":\"TERMINATED\"}"); + } + } } diff --git a/lib/taskana-core/src/main/java/pro/taskana/common/internal/InternalTaskanaEngine.java b/lib/taskana-core/src/main/java/pro/taskana/common/internal/InternalTaskanaEngine.java index 3f67f91d5..dda149d16 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/common/internal/InternalTaskanaEngine.java +++ b/lib/taskana-core/src/main/java/pro/taskana/common/internal/InternalTaskanaEngine.java @@ -12,6 +12,7 @@ import pro.taskana.spi.task.internal.BeforeRequestChangesManager; import pro.taskana.spi.task.internal.BeforeRequestReviewManager; import pro.taskana.spi.task.internal.CreateTaskPreprocessorManager; import pro.taskana.spi.task.internal.ReviewRequiredManager; +import pro.taskana.spi.task.internal.TaskEndstatePreprocessorManager; /** * FOR INTERNAL USE ONLY. @@ -144,4 +145,11 @@ public interface InternalTaskanaEngine { * @return the {@linkplain AfterRequestChangesManager} instance */ AfterRequestChangesManager getAfterRequestChangesManager(); + + /** + * Retrieves the {@linkplain pro.taskana.spi.task.internal.TaskEndstatePreprocessorManager}. + * + * @return the {@linkplain pro.taskana.spi.task.internal.TaskEndstatePreprocessorManager} instance + */ + TaskEndstatePreprocessorManager getTaskEndstatePreprocessorManager(); } diff --git a/lib/taskana-core/src/main/java/pro/taskana/common/internal/TaskanaEngineImpl.java b/lib/taskana-core/src/main/java/pro/taskana/common/internal/TaskanaEngineImpl.java index f59863ad6..73d5da160 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/common/internal/TaskanaEngineImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/common/internal/TaskanaEngineImpl.java @@ -65,6 +65,7 @@ import pro.taskana.spi.task.internal.BeforeRequestChangesManager; import pro.taskana.spi.task.internal.BeforeRequestReviewManager; import pro.taskana.spi.task.internal.CreateTaskPreprocessorManager; import pro.taskana.spi.task.internal.ReviewRequiredManager; +import pro.taskana.spi.task.internal.TaskEndstatePreprocessorManager; import pro.taskana.task.api.TaskService; import pro.taskana.task.internal.AttachmentMapper; import pro.taskana.task.internal.ObjectReferenceMapper; @@ -98,6 +99,8 @@ public class TaskanaEngineImpl implements TaskanaEngine { private final AfterRequestReviewManager afterRequestReviewManager; private final BeforeRequestChangesManager beforeRequestChangesManager; private final AfterRequestChangesManager afterRequestChangesManager; + private final TaskEndstatePreprocessorManager taskEndstatePreprocessorManager; + private final InternalTaskanaEngineImpl internalTaskanaEngineImpl; private final WorkingTimeCalculator workingTimeCalculator; private final HistoryEventManager historyEventManager; @@ -176,6 +179,7 @@ public class TaskanaEngineImpl implements TaskanaEngine { afterRequestReviewManager = new AfterRequestReviewManager(this); beforeRequestChangesManager = new BeforeRequestChangesManager(this); afterRequestChangesManager = new AfterRequestChangesManager(this); + taskEndstatePreprocessorManager = new TaskEndstatePreprocessorManager(); // don't remove, to reset possible explicit mode this.mode = connectionManagementMode; @@ -624,5 +628,10 @@ public class TaskanaEngineImpl implements TaskanaEngine { public AfterRequestChangesManager getAfterRequestChangesManager() { return afterRequestChangesManager; } + + @Override + public TaskEndstatePreprocessorManager getTaskEndstatePreprocessorManager() { + return taskEndstatePreprocessorManager; + } } } diff --git a/lib/taskana-core/src/main/java/pro/taskana/spi/task/api/TaskEndstatePreprocessor.java b/lib/taskana-core/src/main/java/pro/taskana/spi/task/api/TaskEndstatePreprocessor.java new file mode 100644 index 000000000..66657dc4b --- /dev/null +++ b/lib/taskana-core/src/main/java/pro/taskana/spi/task/api/TaskEndstatePreprocessor.java @@ -0,0 +1,34 @@ +package pro.taskana.spi.task.api; + +import pro.taskana.task.api.models.Task; + +/** + * The TaskEndstatePreprocessor allows to implement customized behaviour before the given + * {@linkplain Task} goes into an {@linkplain pro.taskana.task.api.TaskState#END_STATES end state} + * (cancelled, terminated or completed). + */ +public interface TaskEndstatePreprocessor { + + /** + * Perform any action before a {@linkplain Task} goes into an {@linkplain + * pro.taskana.task.api.TaskState#END_STATES end state}. A {@linkplain Task} goes into an end + * state at the end of the following methods: {@linkplain + * pro.taskana.task.api.TaskService#completeTask(String)}, {@linkplain + * pro.taskana.task.api.TaskService#cancelTask(String)}, {@linkplain + * pro.taskana.task.api.TaskService#terminateTask(String)}. + * + *
This SPI is executed within the same transaction staple as {@linkplain + * pro.taskana.task.api.TaskService#completeTask(String)}, {@linkplain + * pro.taskana.task.api.TaskService#cancelTask(String)}, {@linkplain + * pro.taskana.task.api.TaskService#terminateTask(String)}. + * + *
This SPI is executed with the same {@linkplain
+ * pro.taskana.common.api.security.UserPrincipal} and {@linkplain
+ * pro.taskana.common.api.security.GroupPrincipal} as in the methods mentioned above.
+ *
+ * @param taskToProcess the {@linkplain Task} to preprocess
+ * @return the modified {@linkplain Task}. IMPORTANT: persistent changes to the {@linkplain
+ * Task} have to be managed by the service provider
+ */
+ Task processTaskBeforeEndstate(Task taskToProcess);
+}
diff --git a/lib/taskana-core/src/main/java/pro/taskana/spi/task/internal/TaskEndstatePreprocessorManager.java b/lib/taskana-core/src/main/java/pro/taskana/spi/task/internal/TaskEndstatePreprocessorManager.java
new file mode 100644
index 000000000..5873e68ba
--- /dev/null
+++ b/lib/taskana-core/src/main/java/pro/taskana/spi/task/internal/TaskEndstatePreprocessorManager.java
@@ -0,0 +1,39 @@
+package pro.taskana.spi.task.internal;
+
+import static pro.taskana.common.internal.util.CheckedConsumer.wrap;
+
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import pro.taskana.common.internal.util.SpiLoader;
+import pro.taskana.spi.task.api.TaskEndstatePreprocessor;
+import pro.taskana.task.api.models.Task;
+
+public class TaskEndstatePreprocessorManager {
+
+ private static final Logger LOGGER =
+ LoggerFactory.getLogger(TaskEndstatePreprocessorManager.class);
+ private final List