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 d5f03f9c4..23860f97c 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 @@ -5,6 +5,7 @@ import org.apache.ibatis.session.SqlSession; import pro.taskana.common.api.TaskanaEngine; import pro.taskana.spi.history.internal.HistoryEventManager; +import pro.taskana.spi.taskpreprocessing.internal.CreateTaskPreprocessorManager; import pro.taskana.task.internal.TaskRoutingManager; /** @@ -76,6 +77,13 @@ public interface InternalTaskanaEngine { */ TaskRoutingManager getTaskRoutingManager(); + /** + * Retrieve CreateTaskPreprocessorManager. + * + * @return the CreateTaskPreprocessorManager instance. + */ + CreateTaskPreprocessorManager getCreateTaskPreprocessorManager(); + /** * This method is supposed to skip further permission checks if we are already in a secured * environment. With great power comes great responsibility. 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 3beaf98c1..67e4f5a81 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 @@ -50,6 +50,7 @@ import pro.taskana.monitor.api.MonitorService; import pro.taskana.monitor.internal.MonitorMapper; import pro.taskana.monitor.internal.MonitorServiceImpl; import pro.taskana.spi.history.internal.HistoryEventManager; +import pro.taskana.spi.taskpreprocessing.internal.CreateTaskPreprocessorManager; import pro.taskana.task.api.TaskService; import pro.taskana.task.internal.AttachmentMapper; import pro.taskana.task.internal.ObjectReferenceMapper; @@ -71,8 +72,8 @@ public class TaskanaEngineImpl implements TaskanaEngine { private static final String DEFAULT = "default"; private static final Logger LOGGER = LoggerFactory.getLogger(TaskanaEngineImpl.class); private static final SessionStack SESSION_STACK = new SessionStack(); - private HistoryEventManager historyEventManager; private final TaskRoutingManager taskRoutingManager; + private final CreateTaskPreprocessorManager createTaskPreprocessorManager; private final InternalTaskanaEngineImpl internalTaskanaEngineImpl; private final WorkingDaysToDaysConverter workingDaysToDaysConverter; protected TaskanaEngineConfiguration taskanaEngineConfiguration; @@ -80,6 +81,7 @@ public class TaskanaEngineImpl implements TaskanaEngine { protected SqlSessionManager sessionManager; protected ConnectionManagementMode mode = ConnectionManagementMode.PARTICIPATE; protected Connection connection = null; + private HistoryEventManager historyEventManager; protected TaskanaEngineImpl(TaskanaEngineConfiguration taskanaEngineConfiguration) { this.taskanaEngineConfiguration = taskanaEngineConfiguration; @@ -87,6 +89,7 @@ public class TaskanaEngineImpl implements TaskanaEngine { this.sessionManager = createSqlSessionManager(); historyEventManager = HistoryEventManager.getInstance(this); taskRoutingManager = TaskRoutingManager.getInstance(this); + createTaskPreprocessorManager = CreateTaskPreprocessorManager.getInstance(); this.internalTaskanaEngineImpl = new InternalTaskanaEngineImpl(); workingDaysToDaysConverter = new WorkingDaysToDaysConverter( @@ -419,6 +422,11 @@ public class TaskanaEngineImpl implements TaskanaEngine { return taskRoutingManager; } + @Override + public CreateTaskPreprocessorManager getCreateTaskPreprocessorManager() { + return createTaskPreprocessorManager; + } + @Override public T runAsAdmin(Supplier supplier) { diff --git a/lib/taskana-core/src/main/java/pro/taskana/spi/taskpreprocessing/api/CreateTaskPreprocessor.java b/lib/taskana-core/src/main/java/pro/taskana/spi/taskpreprocessing/api/CreateTaskPreprocessor.java new file mode 100644 index 000000000..e098d6192 --- /dev/null +++ b/lib/taskana-core/src/main/java/pro/taskana/spi/taskpreprocessing/api/CreateTaskPreprocessor.java @@ -0,0 +1,13 @@ +package pro.taskana.spi.taskpreprocessing.api; + +import pro.taskana.task.api.models.Task; + +public interface CreateTaskPreprocessor { + + /** + * Processes a task before its creation. + * + * @param taskToProcess {@link Task} The Task to preprocess. + */ + void processTaskBeforeCreation(Task taskToProcess); +} diff --git a/lib/taskana-core/src/main/java/pro/taskana/spi/taskpreprocessing/internal/CreateTaskPreprocessorManager.java b/lib/taskana-core/src/main/java/pro/taskana/spi/taskpreprocessing/internal/CreateTaskPreprocessorManager.java new file mode 100644 index 000000000..b8f1f94fd --- /dev/null +++ b/lib/taskana-core/src/main/java/pro/taskana/spi/taskpreprocessing/internal/CreateTaskPreprocessorManager.java @@ -0,0 +1,48 @@ +package pro.taskana.spi.taskpreprocessing.internal; + +import java.util.Objects; +import java.util.ServiceLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import pro.taskana.spi.taskpreprocessing.api.CreateTaskPreprocessor; +import pro.taskana.task.api.models.Task; + +public class CreateTaskPreprocessorManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(CreateTaskPreprocessorManager.class); + private static CreateTaskPreprocessorManager singleton; + private boolean enabled = false; + private ServiceLoader serviceLoader; + + private CreateTaskPreprocessorManager() { + serviceLoader = ServiceLoader.load(CreateTaskPreprocessor.class); + for (CreateTaskPreprocessor preprocessor : serviceLoader) { + LOGGER.info( + "Registered CreateTaskPreprocessor provider: {}", preprocessor.getClass().getName()); + enabled = true; + } + if (!enabled) { + LOGGER.info("No CreateTaskPreprocessor found. Running without CreateTaskPreprocessor."); + } + } + + public static synchronized CreateTaskPreprocessorManager getInstance() { + if (singleton == null) { + singleton = new CreateTaskPreprocessorManager(); + } + return singleton; + } + + public static boolean isCreateTaskPreprocessorEnabled() { + return Objects.nonNull(singleton) && singleton.enabled; + } + + public Task processTaskBeforeCreation(Task taskToProcess) { + LOGGER.debug("Sending task to CreateTaskPreprocessor providers: {}", taskToProcess); + serviceLoader.forEach( + createTaskPreprocessorProvider -> + createTaskPreprocessorProvider.processTaskBeforeCreation(taskToProcess)); + return taskToProcess; + } +} diff --git a/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskServiceImpl.java b/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskServiceImpl.java index e11e382dc..8fae59f6f 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskServiceImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskServiceImpl.java @@ -42,6 +42,7 @@ import pro.taskana.spi.history.api.events.task.TaskCreatedEvent; import pro.taskana.spi.history.api.events.task.TaskTerminatedEvent; import pro.taskana.spi.history.api.events.task.TaskUpdatedEvent; import pro.taskana.spi.history.internal.HistoryEventManager; +import pro.taskana.spi.taskpreprocessing.internal.CreateTaskPreprocessorManager; import pro.taskana.task.api.CallbackState; import pro.taskana.task.api.TaskCustomField; import pro.taskana.task.api.TaskQuery; @@ -108,6 +109,7 @@ public class TaskServiceImpl implements TaskService { private final AttachmentHandler attachmentHandler; private final AttachmentMapper attachmentMapper; private final HistoryEventManager historyEventManager; + private final CreateTaskPreprocessorManager createTaskPreprocessorManager; public TaskServiceImpl( InternalTaskanaEngine taskanaEngine, @@ -120,6 +122,7 @@ public class TaskServiceImpl implements TaskService { this.attachmentMapper = attachmentMapper; this.classificationService = taskanaEngine.getEngine().getClassificationService(); this.historyEventManager = taskanaEngine.getHistoryEventManager(); + this.createTaskPreprocessorManager = taskanaEngine.getCreateTaskPreprocessorManager(); this.taskTransferrer = new TaskTransferrer(taskanaEngine, taskMapper, this); this.taskCommentService = new TaskCommentServiceImpl(taskanaEngine, taskCommentMapper, this); this.serviceLevelHandler = new ServiceLevelHandler(taskanaEngine, taskMapper, attachmentMapper); @@ -209,6 +212,10 @@ public class TaskServiceImpl implements TaskService { workbasketService.checkAuthorization( task.getWorkbasketSummary().getId(), WorkbasketPermission.APPEND); + if (CreateTaskPreprocessorManager.isCreateTaskPreprocessorEnabled()) { + task = (TaskImpl) createTaskPreprocessorManager.processTaskBeforeCreation(task); + } + // we do use the key and not the ID to make sure that we use the classification from the right // domain. // otherwise we would have to check the classification and its domain for validity. diff --git a/lib/taskana-core/src/test/java/acceptance/taskpreprocessing/CreateTaskPreprocessingAccTest.java b/lib/taskana-core/src/test/java/acceptance/taskpreprocessing/CreateTaskPreprocessingAccTest.java new file mode 100644 index 000000000..e4e43ddc7 --- /dev/null +++ b/lib/taskana-core/src/test/java/acceptance/taskpreprocessing/CreateTaskPreprocessingAccTest.java @@ -0,0 +1,42 @@ +package acceptance.taskpreprocessing; + +import static org.assertj.core.api.Assertions.assertThat; + +import acceptance.AbstractAccTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import pro.taskana.common.internal.security.JaasExtension; +import pro.taskana.common.internal.security.WithAccessId; +import pro.taskana.task.api.TaskCustomField; +import pro.taskana.task.api.TaskService; +import pro.taskana.task.api.models.Task; +import pro.taskana.task.internal.models.TaskImpl; + +/** Acceptance test for "task preprocessing" scenario. */ +@ExtendWith(JaasExtension.class) +class CreateTaskPreprocessingAccTest extends AbstractAccTest { + + private final TaskService taskService = taskanaEngine.getTaskService(); + + @WithAccessId(user = "admin") + @Test + void should_processTaskBeforeCreation_When_CreateTaskPreprocessorEnabled() throws Exception { + + TaskImpl newTaskToCreate = (TaskImpl) taskService.newTask(); + + newTaskToCreate.setClassificationKey("L10303"); + + newTaskToCreate.setPrimaryObjRef( + createObjectReference("COMPANY_A", "SYSTEM_A", "INSTANCE_A", "VNR", "1234567")); + + newTaskToCreate.setWorkbasketKey("GPK_KSC"); + + newTaskToCreate.setDomain("DOMAIN_A"); + + Task createdTask = taskService.createTask(newTaskToCreate); + + assertThat(createdTask.getCustomAttribute(TaskCustomField.CUSTOM_1)) + .isEqualTo("preprocessedCustomField"); + } +} diff --git a/lib/taskana-core/src/test/java/acceptance/taskpreprocessing/TestCreateTaskPreprocessorProvider.java b/lib/taskana-core/src/test/java/acceptance/taskpreprocessing/TestCreateTaskPreprocessorProvider.java new file mode 100644 index 000000000..f82ca4a1f --- /dev/null +++ b/lib/taskana-core/src/test/java/acceptance/taskpreprocessing/TestCreateTaskPreprocessorProvider.java @@ -0,0 +1,14 @@ +package acceptance.taskpreprocessing; + +import pro.taskana.spi.taskpreprocessing.api.CreateTaskPreprocessor; +import pro.taskana.task.api.TaskCustomField; +import pro.taskana.task.api.models.Task; + +public class TestCreateTaskPreprocessorProvider implements CreateTaskPreprocessor { + + @Override + public void processTaskBeforeCreation(Task taskToProcess) { + taskToProcess + .setCustomAttribute(TaskCustomField.CUSTOM_1, "preprocessedCustomField"); + } +} diff --git a/lib/taskana-core/src/test/java/pro/taskana/ArchitectureTest.java b/lib/taskana-core/src/test/java/pro/taskana/ArchitectureTest.java index da49dc9e9..2d1388b61 100644 --- a/lib/taskana-core/src/test/java/pro/taskana/ArchitectureTest.java +++ b/lib/taskana-core/src/test/java/pro/taskana/ArchitectureTest.java @@ -45,7 +45,10 @@ class ArchitectureTest { "pro.taskana.task.internal", "pro.taskana.workbasket.api", "pro.taskana.workbasket.internal", - "pro.taskana.spi.routing.api"); + "pro.taskana.spi.routing.api", + "pro.taskana.spi.taskpreprocessing.api", + "pro.taskana.spi.taskpreprocessing.internal" + ); private static JavaClasses importedClasses; @BeforeAll diff --git a/lib/taskana-core/src/test/resources/META-INF/services/pro.taskana.spi.taskpreprocessing.api.CreateTaskPreprocessor b/lib/taskana-core/src/test/resources/META-INF/services/pro.taskana.spi.taskpreprocessing.api.CreateTaskPreprocessor new file mode 100644 index 000000000..43f0e9a10 --- /dev/null +++ b/lib/taskana-core/src/test/resources/META-INF/services/pro.taskana.spi.taskpreprocessing.api.CreateTaskPreprocessor @@ -0,0 +1 @@ +acceptance.taskpreprocessing.TestCreateTaskPreprocessorProvider