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));
+ }
+}