From dc9e3a25ce9aa317ace656ebe58bdcfefaa8c4b5 Mon Sep 17 00:00:00 2001 From: Tim Gerversmann <72377965+tge20@users.noreply.github.com> Date: Mon, 16 Aug 2021 17:19:52 +0200 Subject: [PATCH] TSK-1686: Introduced an SPI for the individual calculation of priorities --- .../internal/ClassificationServiceImpl.java | 6 +- .../internal/InternalTaskanaEngine.java | 8 ++ .../common/internal/TaskanaEngineImpl.java | 8 ++ .../priority/api/PriorityServiceProvider.java | 25 +++++ .../internal/PriorityServiceManager.java | 84 ++++++++++++++++ .../task/internal/TaskServiceImpl.java | 18 ++++ .../java/acceptance/ArchitectureTest.java | 4 +- .../PriorityServiceAccTest.java | 97 +++++++++++++++++++ .../TestPriorityServiceProvider.java | 27 ++++++ 9 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 lib/taskana-core/src/main/java/pro/taskana/spi/priority/api/PriorityServiceProvider.java create mode 100644 lib/taskana-core/src/main/java/pro/taskana/spi/priority/internal/PriorityServiceManager.java create mode 100644 lib/taskana-core/src/test/java/acceptance/priorityservice/PriorityServiceAccTest.java create mode 100644 lib/taskana-core/src/test/java/acceptance/priorityservice/TestPriorityServiceProvider.java diff --git a/lib/taskana-core/src/main/java/pro/taskana/classification/internal/ClassificationServiceImpl.java b/lib/taskana-core/src/main/java/pro/taskana/classification/internal/ClassificationServiceImpl.java index 75a7984d7..d034d6622 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/classification/internal/ClassificationServiceImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/classification/internal/ClassificationServiceImpl.java @@ -37,6 +37,7 @@ import pro.taskana.spi.history.api.events.classification.ClassificationCreatedEv import pro.taskana.spi.history.api.events.classification.ClassificationDeletedEvent; import pro.taskana.spi.history.api.events.classification.ClassificationUpdatedEvent; import pro.taskana.spi.history.internal.HistoryEventManager; +import pro.taskana.spi.priority.internal.PriorityServiceManager; import pro.taskana.task.api.models.TaskSummary; import pro.taskana.task.internal.TaskMapper; @@ -253,7 +254,10 @@ public class ClassificationServiceImpl implements ClassificationService { this.checkExistenceOfParentClassification(oldClassification, classificationImpl); classificationMapper.update(classificationImpl); - this.createJobIfPriorityOrServiceLevelHasChanged(oldClassification, classificationImpl); + + if (!PriorityServiceManager.isPriorityServiceEnabled()) { + this.createJobIfPriorityOrServiceLevelHasChanged(oldClassification, classificationImpl); + } if (HistoryEventManager.isHistoryEnabled()) { String details = 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 9ff908bbb..e561a2b80 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.priority.internal.PriorityServiceManager; import pro.taskana.spi.routing.internal.TaskRoutingManager; import pro.taskana.spi.task.internal.CreateTaskPreprocessorManager; @@ -97,4 +98,11 @@ public interface InternalTaskanaEngine { * @return the CreateTaskPreprocessorManager instance. */ CreateTaskPreprocessorManager getCreateTaskPreprocessorManager(); + + /** + * Retrieves the {@linkplain PriorityServiceManager}. + * + * @return the {@linkplain PriorityServiceManager} instance + */ + PriorityServiceManager getPriorityServiceManager(); } 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 34be9bde0..a40b1edb0 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 @@ -51,6 +51,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.priority.internal.PriorityServiceManager; import pro.taskana.spi.routing.internal.TaskRoutingManager; import pro.taskana.spi.task.internal.CreateTaskPreprocessorManager; import pro.taskana.task.api.TaskService; @@ -76,6 +77,7 @@ public class TaskanaEngineImpl implements TaskanaEngine { private static final SessionStack SESSION_STACK = new SessionStack(); private final TaskRoutingManager taskRoutingManager; private final CreateTaskPreprocessorManager createTaskPreprocessorManager; + private final PriorityServiceManager priorityServiceManager; private final InternalTaskanaEngineImpl internalTaskanaEngineImpl; private final WorkingDaysToDaysConverter workingDaysToDaysConverter; private final HistoryEventManager historyEventManager; @@ -106,6 +108,7 @@ public class TaskanaEngineImpl implements TaskanaEngine { // to provide a fully initialized TaskanaEngine instance during the SPI initialization! historyEventManager = HistoryEventManager.getInstance(this); taskRoutingManager = TaskRoutingManager.getInstance(this); + priorityServiceManager = PriorityServiceManager.getInstance(); } public static TaskanaEngine createTaskanaEngine( @@ -472,5 +475,10 @@ public class TaskanaEngineImpl implements TaskanaEngine { public CreateTaskPreprocessorManager getCreateTaskPreprocessorManager() { return createTaskPreprocessorManager; } + + @Override + public PriorityServiceManager getPriorityServiceManager() { + return priorityServiceManager; + } } } diff --git a/lib/taskana-core/src/main/java/pro/taskana/spi/priority/api/PriorityServiceProvider.java b/lib/taskana-core/src/main/java/pro/taskana/spi/priority/api/PriorityServiceProvider.java new file mode 100644 index 000000000..2297f759e --- /dev/null +++ b/lib/taskana-core/src/main/java/pro/taskana/spi/priority/api/PriorityServiceProvider.java @@ -0,0 +1,25 @@ +package pro.taskana.spi.priority.api; + +import java.util.Optional; + +import pro.taskana.task.api.models.Task; +import pro.taskana.task.api.models.TaskSummary; + +/** + * This SPI enables the computation of {@linkplain Task} priorities depending on individual + * preferences. + */ +public interface PriorityServiceProvider { + + /** + * Calculates the {@linkplain Task#getPriority() priority} of a certain {@linkplain Task}. + * + *

The implemented method must calculate the {@linkplain Task#getPriority() priority} + * efficiently. There can be a huge amount of {@linkplain Task Tasks} the SPI has to handle. + * + * @param taskSummary the {@linkplain TaskSummary} to compute the {@linkplain Task#getPriority() + * priority} for + * @return the computed {@linkplain Task#getPriority() priority} + */ + Optional calculatePriority(TaskSummary taskSummary); +} diff --git a/lib/taskana-core/src/main/java/pro/taskana/spi/priority/internal/PriorityServiceManager.java b/lib/taskana-core/src/main/java/pro/taskana/spi/priority/internal/PriorityServiceManager.java new file mode 100644 index 000000000..179d4e0e3 --- /dev/null +++ b/lib/taskana-core/src/main/java/pro/taskana/spi/priority/internal/PriorityServiceManager.java @@ -0,0 +1,84 @@ +package pro.taskana.spi.priority.internal; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import pro.taskana.common.api.exceptions.SystemException; +import pro.taskana.common.internal.util.LogSanitizer; +import pro.taskana.spi.priority.api.PriorityServiceProvider; +import pro.taskana.task.api.models.TaskSummary; + +public class PriorityServiceManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(PriorityServiceManager.class); + private static PriorityServiceManager singleton; + private final ServiceLoader serviceLoader; + private boolean enabled = false; + + private PriorityServiceManager() { + serviceLoader = ServiceLoader.load(PriorityServiceProvider.class); + for (PriorityServiceProvider priorityProvider : serviceLoader) { + LOGGER.info("Registered PriorityServiceProvider: {}", priorityProvider.getClass().getName()); + enabled = true; + } + if (!enabled) { + LOGGER.info("No PriorityServiceProvider found. Running without PriorityServiceProvider."); + } + } + + public static synchronized PriorityServiceManager getInstance() { + if (singleton == null) { + singleton = new PriorityServiceManager(); + } + return singleton; + } + + public static boolean isPriorityServiceEnabled() { + return Objects.nonNull(singleton) && singleton.enabled; + } + + public Optional calculatePriorityOfTask(TaskSummary task) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Sending task to PriorityServiceProviders: {}", task); + } + + // ServiceLoader.stream() is only available in Java11. + List priorities = + StreamSupport.stream(serviceLoader.spliterator(), false) + .map(provider -> getPriorityByProvider(task, provider)) + .filter(Optional::isPresent) + .map(Optional::get) + .distinct() + .collect(Collectors.toList()); + + if (priorities.size() <= 1) { + return priorities.stream().findFirst(); + } + + if (LOGGER.isErrorEnabled()) { + LOGGER.error( + "The PriorityServiceProviders determined more than one priority for Task {}.", + LogSanitizer.stripLineBreakingChars(task)); + } + return Optional.empty(); + } + + private Optional getPriorityByProvider( + TaskSummary task, PriorityServiceProvider provider) { + try { + return provider.calculatePriority(task); + } catch (Exception e) { + throw new SystemException( + String.format( + "Caught exception while calculating priority of Task in provider %s.", + provider.getClass().getName()), + e); + } + } +} 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 5a85d2eff..05b64fb2b 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 @@ -47,6 +47,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.priority.internal.PriorityServiceManager; import pro.taskana.spi.task.internal.CreateTaskPreprocessorManager; import pro.taskana.task.api.CallbackState; import pro.taskana.task.api.TaskCustomField; @@ -99,6 +100,7 @@ public class TaskServiceImpl implements TaskService { private final AttachmentMapper attachmentMapper; private final HistoryEventManager historyEventManager; private final CreateTaskPreprocessorManager createTaskPreprocessorManager; + private final PriorityServiceManager priorityServiceManager; public TaskServiceImpl( InternalTaskanaEngine taskanaEngine, @@ -112,6 +114,7 @@ public class TaskServiceImpl implements TaskService { this.classificationService = taskanaEngine.getEngine().getClassificationService(); this.historyEventManager = taskanaEngine.getHistoryEventManager(); this.createTaskPreprocessorManager = taskanaEngine.getCreateTaskPreprocessorManager(); + this.priorityServiceManager = taskanaEngine.getPriorityServiceManager(); this.taskTransferrer = new TaskTransferrer(taskanaEngine, taskMapper, this); this.taskCommentService = new TaskCommentServiceImpl(taskanaEngine, taskCommentMapper, this); this.serviceLevelHandler = @@ -222,6 +225,14 @@ public class TaskServiceImpl implements TaskService { ObjectReference.validate(task.getPrimaryObjRef(), "primary ObjectReference", "Task"); standardSettingsOnTaskCreation(task, classification); setCallbackStateOnTaskCreation(task); + + if (PriorityServiceManager.isPriorityServiceEnabled()) { + Optional newPriority = priorityServiceManager.calculatePriorityOfTask(task); + if (newPriority.isPresent()) { + task.setPriority(newPriority.get()); + } + } + try { this.taskMapper.insert(task); if (LOGGER.isDebugEnabled()) { @@ -419,6 +430,13 @@ public class TaskServiceImpl implements TaskService { standardUpdateActions(oldTaskImpl, newTaskImpl); + if (PriorityServiceManager.isPriorityServiceEnabled()) { + Optional newPriority = priorityServiceManager.calculatePriorityOfTask(newTaskImpl); + if (newPriority.isPresent()) { + newTaskImpl.setPriority(newPriority.get()); + } + } + taskMapper.update(newTaskImpl); if (LOGGER.isDebugEnabled()) { diff --git a/lib/taskana-core/src/test/java/acceptance/ArchitectureTest.java b/lib/taskana-core/src/test/java/acceptance/ArchitectureTest.java index 74a076424..5637a6366 100644 --- a/lib/taskana-core/src/test/java/acceptance/ArchitectureTest.java +++ b/lib/taskana-core/src/test/java/acceptance/ArchitectureTest.java @@ -71,7 +71,9 @@ class ArchitectureTest { "pro.taskana.spi.routing.api", "pro.taskana.spi.routing.internal", "pro.taskana.spi.task.api", - "pro.taskana.spi.task.internal"); + "pro.taskana.spi.task.internal", + "pro.taskana.spi.priority.api", + "pro.taskana.spi.priority.internal"); private static JavaClasses importedClasses; @BeforeAll diff --git a/lib/taskana-core/src/test/java/acceptance/priorityservice/PriorityServiceAccTest.java b/lib/taskana-core/src/test/java/acceptance/priorityservice/PriorityServiceAccTest.java new file mode 100644 index 000000000..738566ebf --- /dev/null +++ b/lib/taskana-core/src/test/java/acceptance/priorityservice/PriorityServiceAccTest.java @@ -0,0 +1,97 @@ +package acceptance.priorityservice; + +import static org.assertj.core.api.Assertions.assertThat; + +import acceptance.AbstractAccTest; +import java.sql.Date; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.function.ThrowingConsumer; + +import pro.taskana.classification.api.ClassificationService; +import pro.taskana.classification.api.models.Classification; +import pro.taskana.common.api.ScheduledJob; +import pro.taskana.common.internal.util.Pair; +import pro.taskana.common.test.security.JaasExtension; +import pro.taskana.common.test.security.WithAccessId; +import pro.taskana.task.api.TaskCustomField; +import pro.taskana.task.api.TaskService; +import pro.taskana.task.api.models.ObjectReference; +import pro.taskana.task.api.models.Task; + +/** Acceptance test for all priority computation scenarios. */ +@Disabled("Until we enable the use of Test-SPI's in only specific tests") +@ExtendWith(JaasExtension.class) +class PriorityServiceAccTest extends AbstractAccTest { + + private static final TaskService TASK_SERVICE = taskanaEngine.getTaskService(); + + @WithAccessId(user = "user-1-1") + @TestFactory + Stream should_SetThePriorityAccordingToTestProvider_When_CreatingTask() { + List> testCases = List.of(Pair.of("false", 1), Pair.of("true", 10)); + + ThrowingConsumer> test = + x -> { + Task task = TASK_SERVICE.newTask("USER-1-1", "DOMAIN_A"); + task.setCustomAttribute(TaskCustomField.CUSTOM_6, x.getLeft()); + task.setClassificationKey("T2100"); + ObjectReference objectReference = + createObjectReference("COMPANY_A", "SYSTEM_A", "INSTANCE_A", "VNR", "1234567"); + task.setPrimaryObjRef(objectReference); + + Task createdTask = TASK_SERVICE.createTask(task); + assertThat(createdTask.getPriority()).isEqualTo(x.getRight()); + }; + + return DynamicTest.stream(testCases.iterator(), x -> "entry in custom6: " + x.getLeft(), test); + } + + @WithAccessId(user = "user-1-1") + @TestFactory + Stream should_SetThePriorityAccordingToTestProvider_When_UpdatingTask() + throws Exception { + List> testCases = List.of(Pair.of("false", 1), Pair.of("true", 10)); + Task task = TASK_SERVICE.getTask("TKI:000000000000000000000000000000000000"); + int daysSinceCreated = + Math.toIntExact( + TimeUnit.DAYS.convert( + Date.from(Instant.now()).getTime() - Date.from(task.getCreated()).getTime(), + TimeUnit.MILLISECONDS)); + + ThrowingConsumer> test = + x -> { + task.setCustomAttribute(TaskCustomField.CUSTOM_6, x.getLeft()); + + Task updatedTask = TASK_SERVICE.updateTask(task); + assertThat(updatedTask.getPriority()).isEqualTo(daysSinceCreated * x.getRight()); + }; + + return DynamicTest.stream(testCases.iterator(), x -> "entry in custom6: " + x.getLeft(), test); + } + + @WithAccessId(user = "admin") + @Test + void should_NotCreateClassificationChangedJob_When_PriorityProviderExisting() throws Exception { + ClassificationService classificationService = taskanaEngine.getClassificationService(); + Classification classification = + classificationService.getClassification("CLI:000000000000000000000000000000000001"); + classification.setPriority(10); + + classificationService.updateClassification(classification); + List jobsToRun = getJobMapper().findJobsToRun(Instant.now()); + assertThat(jobsToRun).isEmpty(); + + classification.setServiceLevel("P4D"); + classificationService.updateClassification(classification); + jobsToRun = getJobMapper().findJobsToRun(Instant.now()); + assertThat(jobsToRun).isEmpty(); + } +} diff --git a/lib/taskana-core/src/test/java/acceptance/priorityservice/TestPriorityServiceProvider.java b/lib/taskana-core/src/test/java/acceptance/priorityservice/TestPriorityServiceProvider.java new file mode 100644 index 000000000..df03b8782 --- /dev/null +++ b/lib/taskana-core/src/test/java/acceptance/priorityservice/TestPriorityServiceProvider.java @@ -0,0 +1,27 @@ +package acceptance.priorityservice; + +import java.sql.Date; +import java.time.Instant; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import pro.taskana.spi.priority.api.PriorityServiceProvider; +import pro.taskana.task.api.TaskCustomField; +import pro.taskana.task.api.models.TaskSummary; + +public class TestPriorityServiceProvider implements PriorityServiceProvider { + private static final int MULTIPLIER = 10; + + @Override + public Optional calculatePriority(TaskSummary taskSummary) { + long diffInMillies = + Date.from(Instant.now()).getTime() - Date.from(taskSummary.getCreated()).getTime(); + long diffInDays = TimeUnit.DAYS.convert(diffInMillies, TimeUnit.MILLISECONDS); + int priority = diffInDays >= 1 ? Math.toIntExact(diffInDays) : 1; + + if (taskSummary.getCustomAttribute(TaskCustomField.CUSTOM_6) == "true") { + priority *= MULTIPLIER; + } + return Optional.of(Integer.valueOf(priority)); + } +}