Skip to content

Commit

Permalink
Closes #2366 - add TaskEndstatePreprocessor
Browse files Browse the repository at this point in the history
  • Loading branch information
ryzheboka committed Sep 7, 2023
1 parent 24fb36a commit 948fe21
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,23 @@
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;
Expand All @@ -26,6 +31,7 @@
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;
Expand Down Expand Up @@ -182,4 +188,33 @@ Stream<DynamicTest> should_ThrowException_When_CancellingATaskInEndState() throw
};
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\"}");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
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.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;
Expand Down Expand Up @@ -91,6 +93,21 @@ public boolean reviewRequired(Task task) {
}
}

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(
Expand Down Expand Up @@ -171,4 +188,27 @@ Stream<DynamicTest> should_RequestReview_When_UserTriesToCompleteTask() throws E
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\"}");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@
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;
Expand All @@ -19,13 +24,15 @@
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;
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;
Expand Down Expand Up @@ -157,4 +164,33 @@ Stream<DynamicTest> should_ThrowException_When_TerminateTaskInEndState() throws

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\"}");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
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.
Expand Down Expand Up @@ -144,4 +145,11 @@ default void executeInDatabaseConnection(Runnable runnable) {
* @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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -176,6 +179,7 @@ protected TaskanaEngineImpl(
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;
Expand Down Expand Up @@ -624,5 +628,10 @@ public BeforeRequestChangesManager getBeforeRequestChangesManager() {
public AfterRequestChangesManager getAfterRequestChangesManager() {
return afterRequestChangesManager;
}

@Override
public TaskEndstatePreprocessorManager getTaskEndstatePreprocessorManager() {
return taskEndstatePreprocessorManager;
}
}
}
Original file line number Diff line number Diff line change
@@ -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)}.
*
* <p>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)}.
*
* <p>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}. <b>IMPORTANT:</b> persistent changes to the {@linkplain
* Task} have to be managed by the service provider
*/
Task processTaskBeforeEndstate(Task taskToProcess);
}
Original file line number Diff line number Diff line change
@@ -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<TaskEndstatePreprocessor> taskEndstatePreprocessors;

public TaskEndstatePreprocessorManager() {
taskEndstatePreprocessors = SpiLoader.load(TaskEndstatePreprocessor.class);
for (TaskEndstatePreprocessor preprocessor : taskEndstatePreprocessors) {
LOGGER.info(
"Registered TaskEndstatePreprocessor provider: {}", preprocessor.getClass().getName());
}
if (taskEndstatePreprocessors.isEmpty()) {
LOGGER.info("No TaskEndstatePreprocessor found. Running without TaskEndstatePreprocessor.");
}
}

public Task processTaskBeforeEndstate(Task taskToProcess) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Sending task to TaskEndstatePreprocessor providers: {}", taskToProcess);
}
taskEndstatePreprocessors.forEach(
wrap(
taskEndstatePreprocessor ->
taskEndstatePreprocessor.processTaskBeforeEndstate(taskToProcess)));
return taskToProcess;
}
}
Loading

0 comments on commit 948fe21

Please sign in to comment.