From 32c27e323c1652bb7039b8ef261e9a531f2901e6 Mon Sep 17 00:00:00 2001 From: BerndBreier <33351391+BerndBreier@users.noreply.github.com> Date: Tue, 10 Dec 2019 12:41:22 +0100 Subject: [PATCH] TSK-972 initial commit - add task routing SPI to taskana --- .../main/java/pro/taskana/TaskService.java | 12 +++ .../taskana/impl/InternalTaskanaEngine.java | 8 ++ .../pro/taskana/impl/TaskServiceImpl.java | 15 +++- .../pro/taskana/impl/TaskanaEngineImpl.java | 8 ++ .../impl/WorkbasketAccessItemQueryImpl.java | 2 +- .../taskrouting/TaskRoutingProducer.java | 82 +++++++++++++++++++ .../taskana/taskrouting/api/TaskRouter.java | 28 +++++++ .../taskrouting/TaskRoutingAccTest.java | 53 ++++++++++++ .../taskrouting/TestTaskRouterForDomainA.java | 28 +++++++ .../taskrouting/TestTaskRouterForDomainB.java | 28 +++++++ .../pro.taskana.taskrouting.api.TaskRouter | 2 + 11 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 lib/taskana-core/src/main/java/pro/taskana/taskrouting/TaskRoutingProducer.java create mode 100644 lib/taskana-core/src/main/java/pro/taskana/taskrouting/api/TaskRouter.java create mode 100644 lib/taskana-core/src/test/java/acceptance/taskrouting/TaskRoutingAccTest.java create mode 100644 lib/taskana-core/src/test/java/acceptance/taskrouting/TestTaskRouterForDomainA.java create mode 100644 lib/taskana-core/src/test/java/acceptance/taskrouting/TestTaskRouterForDomainB.java create mode 100644 lib/taskana-core/src/test/resources/META-INF/services/pro.taskana.taskrouting.api.TaskRouter diff --git a/lib/taskana-core/src/main/java/pro/taskana/TaskService.java b/lib/taskana-core/src/main/java/pro/taskana/TaskService.java index f59e3d77b..79fbe42d5 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/TaskService.java +++ b/lib/taskana-core/src/main/java/pro/taskana/TaskService.java @@ -229,6 +229,18 @@ public interface TaskService { */ TaskQuery createTaskQuery(); + /** + * Returns a not persisted instance of {@link Task}. + * The returned task has no workbasket Id set. When createTask() is + * invoked for this task, TaskService will call the TaskRouting SPI to + * determine a workbasket for the task. If the TaskRouting API is not active, + * e.g. because no TaskRouter is registered, or the TaskRouter(s) don't find a workbasket, + * the task will not be persisted. + * + * @return an empty new Task + */ + Task newTask(); + /** * Returns a not persisted instance of {@link Task}. * diff --git a/lib/taskana-core/src/main/java/pro/taskana/impl/InternalTaskanaEngine.java b/lib/taskana-core/src/main/java/pro/taskana/impl/InternalTaskanaEngine.java index da57a7c6d..e564b40f2 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/impl/InternalTaskanaEngine.java +++ b/lib/taskana-core/src/main/java/pro/taskana/impl/InternalTaskanaEngine.java @@ -6,6 +6,7 @@ import org.apache.ibatis.session.SqlSession; import pro.taskana.TaskanaEngine; import pro.taskana.history.HistoryEventProducer; +import pro.taskana.taskrouting.TaskRoutingProducer; /** * FOR INTERNAL USE ONLY. @@ -69,4 +70,11 @@ public interface InternalTaskanaEngine { */ HistoryEventProducer getHistoryEventProducer(); + /** + * Retrieve TaskRoutingProducer. + * + * @return the TaskRoutingProducer instance. + */ + TaskRoutingProducer getTaskRoutingProducer(); + } diff --git a/lib/taskana-core/src/main/java/pro/taskana/impl/TaskServiceImpl.java b/lib/taskana-core/src/main/java/pro/taskana/impl/TaskServiceImpl.java index c59c8ac86..25a574fbe 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/impl/TaskServiceImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/impl/TaskServiceImpl.java @@ -193,7 +193,13 @@ public class TaskServiceImpl implements TaskService { } else if (task.getWorkbasketKey() != null) { workbasket = workbasketService.getWorkbasket(task.getWorkbasketKey(), task.getDomain()); } else { - throw new InvalidArgumentException("Cannot create a task outside a workbasket"); + String workbasketId = taskanaEngine.getTaskRoutingProducer().routeToWorkbasketId(task); + if (workbasketId != null) { + workbasket = workbasketService.getWorkbasket(workbasketId); + task.setWorkbasketSummary(workbasket.asSummary()); + } else { + throw new InvalidArgumentException("Cannot create a task outside a workbasket"); + } } if (workbasket.isMarkedForDeletion()) { @@ -357,6 +363,13 @@ public class TaskServiceImpl implements TaskService { return new TaskQueryImpl(taskanaEngine); } + @Override + public Task newTask() { + TaskImpl task = new TaskImpl(); + task.setCallbackState(CallbackState.NONE); + return task; + } + @Override public Task newTask(String workbasketId) { TaskImpl task = new TaskImpl(); diff --git a/lib/taskana-core/src/main/java/pro/taskana/impl/TaskanaEngineImpl.java b/lib/taskana-core/src/main/java/pro/taskana/impl/TaskanaEngineImpl.java index cc47f39fa..a60307411 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/impl/TaskanaEngineImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/impl/TaskanaEngineImpl.java @@ -49,6 +49,7 @@ import pro.taskana.mappings.TaskMonitorMapper; import pro.taskana.mappings.WorkbasketAccessMapper; import pro.taskana.mappings.WorkbasketMapper; import pro.taskana.security.CurrentUserContext; +import pro.taskana.taskrouting.TaskRoutingProducer; /** * This is the implementation of TaskanaEngine. @@ -64,6 +65,7 @@ public class TaskanaEngineImpl implements TaskanaEngine { protected ConnectionManagementMode mode = ConnectionManagementMode.PARTICIPATE; protected java.sql.Connection connection = null; private HistoryEventProducer historyEventProducer; + private TaskRoutingProducer taskRoutingProducer; private InternalTaskanaEngineImpl internalTaskanaEngineImpl; protected TaskanaEngineImpl(TaskanaEngineConfiguration taskanaEngineConfiguration) { @@ -71,6 +73,7 @@ public class TaskanaEngineImpl implements TaskanaEngine { createTransactionFactory(taskanaEngineConfiguration.getUseManagedTransactions()); this.sessionManager = createSqlSessionManager(); historyEventProducer = HistoryEventProducer.getInstance(taskanaEngineConfiguration); + taskRoutingProducer = TaskRoutingProducer.getInstance(this); this.internalTaskanaEngineImpl = new InternalTaskanaEngineImpl(); } @@ -385,5 +388,10 @@ public class TaskanaEngineImpl implements TaskanaEngine { return historyEventProducer; } + @Override + public TaskRoutingProducer getTaskRoutingProducer() { + return taskRoutingProducer; + } + } } diff --git a/lib/taskana-core/src/main/java/pro/taskana/impl/WorkbasketAccessItemQueryImpl.java b/lib/taskana-core/src/main/java/pro/taskana/impl/WorkbasketAccessItemQueryImpl.java index 6c3e95fb1..e4114e50f 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/impl/WorkbasketAccessItemQueryImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/impl/WorkbasketAccessItemQueryImpl.java @@ -103,7 +103,7 @@ public class WorkbasketAccessItemQueryImpl implements WorkbasketAccessItemQuery public List list() { LOGGER.debug("entry to list(), this = {}", this); List result = taskanaEngine.openAndReturnConnection( - () -> new ArrayList<>(taskanaEngine.getSqlSession().selectList(LINK_TO_MAPPER, this))); + () -> new ArrayList(taskanaEngine.getSqlSession().selectList(LINK_TO_MAPPER, this))); if (LOGGER.isDebugEnabled()) { LOGGER.debug("exit from list(). Returning {} resulting Objects: {} ", result.size(), LoggerUtils.listToString(result)); diff --git a/lib/taskana-core/src/main/java/pro/taskana/taskrouting/TaskRoutingProducer.java b/lib/taskana-core/src/main/java/pro/taskana/taskrouting/TaskRoutingProducer.java new file mode 100644 index 000000000..1ca79d8c8 --- /dev/null +++ b/lib/taskana-core/src/main/java/pro/taskana/taskrouting/TaskRoutingProducer.java @@ -0,0 +1,82 @@ +package pro.taskana.taskrouting; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import pro.taskana.Task; +import pro.taskana.TaskanaEngine; +import pro.taskana.taskrouting.api.TaskRouter; + +/** + * Loads TaskRouter SPI implementation(s) and passes requests route tasks to workbaskets to the router(s). + */ +public final class TaskRoutingProducer { + + private static final Logger LOGGER = LoggerFactory.getLogger(TaskRoutingProducer.class); + private static TaskRoutingProducer singleton; + private static boolean enabled = false; + private ServiceLoader serviceLoader; + private static List theTaskRouters = new ArrayList<>(); + + private TaskRoutingProducer(TaskanaEngine taskanaEngine) { + serviceLoader = ServiceLoader.load(TaskRouter.class); + for (TaskRouter router : serviceLoader) { + router.initialize(taskanaEngine); + theTaskRouters.add(router); + LOGGER.info("Registered TaskRouter provider: {}", router.getClass().getName()); + } + + if (theTaskRouters.isEmpty()) { + LOGGER.info("No TaskRouter provider found. Running without Task Routing."); + } else { + enabled = true; + } + } + + public static synchronized TaskRoutingProducer getInstance(TaskanaEngine taskanaEngine) { + if (singleton == null) { + singleton = new TaskRoutingProducer(taskanaEngine); + } + return singleton; + } + + public static boolean isTaskRoutingEnabled() { + return enabled; + } + + /** + * routes tasks to Workbaskets. + * The task that is to be routed is passed to all registered TaskRouters. If they return no or more than one + * workbasketId, null is returned, otherwise we return the workbasketId that was returned from the TaskRouters. + * @param task the task for which a workbasketId is to be determined. + * @return the id of the workbasket in which the task is to be created. + */ + public String routeToWorkbasketId(Task task) { + LOGGER.debug("entry to routeToWorkbasket. TaskRouterr is enabled {}, task = {}", isTaskRoutingEnabled(), task); + String workbasketId = null; + if (isTaskRoutingEnabled()) { + // route to all task routers + // collect in a set to see whether different workbasket ids are returned + Set workbasketIds = theTaskRouters.stream() + .map(rtr -> rtr.routeToWorkbasketId(task)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + if (workbasketIds.isEmpty()) { + LOGGER.error("No TaskRouter determined a workbasket for task {}.", task); + } else if (workbasketIds.size() > 1) { + LOGGER.error("The TaskRouters determined more than one workbasket for task{}", task); + } else { + workbasketId = workbasketIds.stream().findFirst().orElse(null); + } + } + LOGGER.debug("exit from routeToWorkbasketId. Destination WorkbasketId = {}", workbasketId); + return workbasketId; + } +} diff --git a/lib/taskana-core/src/main/java/pro/taskana/taskrouting/api/TaskRouter.java b/lib/taskana-core/src/main/java/pro/taskana/taskrouting/api/TaskRouter.java new file mode 100644 index 000000000..a29af7cbe --- /dev/null +++ b/lib/taskana-core/src/main/java/pro/taskana/taskrouting/api/TaskRouter.java @@ -0,0 +1,28 @@ +package pro.taskana.taskrouting.api; + +import pro.taskana.Task; +import pro.taskana.TaskanaEngine; + +/** + * Interface for TASKANA TaskRouter SPI. + */ +public interface TaskRouter { + + /** + * Initialize TaskRouter service. + * + * @param taskanaEngine + * {@link TaskanaEngine} The Taskana engine for needed initialization. + */ + void initialize(TaskanaEngine taskanaEngine); + + /** + * Determines a WorkbasketId for a given task. + * + * @param task + * {@link Task} The task for which a workbasket must be determined. + * @return the id of the workbasket in which the task is to be created. + */ + String routeToWorkbasketId(Task task); + +} diff --git a/lib/taskana-core/src/test/java/acceptance/taskrouting/TaskRoutingAccTest.java b/lib/taskana-core/src/test/java/acceptance/taskrouting/TaskRoutingAccTest.java new file mode 100644 index 000000000..903aeefea --- /dev/null +++ b/lib/taskana-core/src/test/java/acceptance/taskrouting/TaskRoutingAccTest.java @@ -0,0 +1,53 @@ +package acceptance.taskrouting; + +import static org.junit.Assert.assertEquals; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import acceptance.AbstractAccTest; +import pro.taskana.Task; +import pro.taskana.TaskService; +import pro.taskana.exceptions.ClassificationNotFoundException; +import pro.taskana.exceptions.InvalidArgumentException; +import pro.taskana.exceptions.NotAuthorizedException; +import pro.taskana.exceptions.TaskAlreadyExistException; +import pro.taskana.exceptions.TaskNotFoundException; +import pro.taskana.exceptions.WorkbasketNotFoundException; +import pro.taskana.impl.TaskImpl; +import pro.taskana.security.JAASExtension; +import pro.taskana.security.WithAccessId; + +/** + * Acceptance test for all "create task" scenarios. + */ +@ExtendWith(JAASExtension.class) +class TaskRoutingAccTest extends AbstractAccTest { + + @WithAccessId(userName = "admin", groupNames = {"group_1"}) + @Test + void testCreateTaskWithNullWorkbasket() + throws WorkbasketNotFoundException, ClassificationNotFoundException, NotAuthorizedException, + TaskAlreadyExistException, InvalidArgumentException, TaskNotFoundException { + TaskImpl createdTaskA = createTask("DOMAIN_A", "L12010"); + assertEquals("WBI:100000000000000000000000000000000001", createdTaskA.getWorkbasketSummary().getId()); + TaskImpl createdTaskB = createTask("DOMAIN_B", "T21001"); + assertEquals("WBI:100000000000000000000000000000000011", createdTaskB.getWorkbasketSummary().getId()); + Assertions.assertThrows(InvalidArgumentException.class, () -> createTask(null, "L12010")); + } + + private TaskImpl createTask(String domain, String classificationKey) + throws WorkbasketNotFoundException, ClassificationNotFoundException, NotAuthorizedException, + TaskAlreadyExistException, InvalidArgumentException { + TaskService taskService = taskanaEngine.getTaskService(); + + Task newTask = taskService.newTask(null, domain); + newTask.setClassificationKey(classificationKey); + + newTask.setPrimaryObjRef(createObjectReference("COMPANY_A", "SYSTEM_A", "INSTANCE_A", "VNR", "1234567")); + TaskImpl createdTask = (TaskImpl) taskService.createTask(newTask); + return createdTask; + } + +} diff --git a/lib/taskana-core/src/test/java/acceptance/taskrouting/TestTaskRouterForDomainA.java b/lib/taskana-core/src/test/java/acceptance/taskrouting/TestTaskRouterForDomainA.java new file mode 100644 index 000000000..df5a3e8f3 --- /dev/null +++ b/lib/taskana-core/src/test/java/acceptance/taskrouting/TestTaskRouterForDomainA.java @@ -0,0 +1,28 @@ +package acceptance.taskrouting; + +import pro.taskana.Task; +import pro.taskana.TaskanaEngine; +import pro.taskana.taskrouting.api.TaskRouter; + +/** + * This is a sample implementation of TaskRouter. + */ +public class TestTaskRouterForDomainA implements TaskRouter { + + TaskanaEngine theEngine; + + @Override + public void initialize(TaskanaEngine taskanaEngine) { + theEngine = taskanaEngine; + } + + @Override + public String routeToWorkbasketId(Task task) { + if ("DOMAIN_A".equals(task.getDomain())) { + return "WBI:100000000000000000000000000000000001"; + } else { + return null; + } + } + +} diff --git a/lib/taskana-core/src/test/java/acceptance/taskrouting/TestTaskRouterForDomainB.java b/lib/taskana-core/src/test/java/acceptance/taskrouting/TestTaskRouterForDomainB.java new file mode 100644 index 000000000..3cbde02b8 --- /dev/null +++ b/lib/taskana-core/src/test/java/acceptance/taskrouting/TestTaskRouterForDomainB.java @@ -0,0 +1,28 @@ +package acceptance.taskrouting; + +import pro.taskana.Task; +import pro.taskana.TaskanaEngine; +import pro.taskana.taskrouting.api.TaskRouter; + +/** + * This is a sample implementation of TaskRouter. + */ +public class TestTaskRouterForDomainB implements TaskRouter { + + TaskanaEngine theEngine; + + @Override + public void initialize(TaskanaEngine taskanaEngine) { + theEngine = taskanaEngine; + } + + @Override + public String routeToWorkbasketId(Task task) { + if ("DOMAIN_B".equals(task.getDomain())) { + return "WBI:100000000000000000000000000000000011"; + } else { + return null; + } + } + +} diff --git a/lib/taskana-core/src/test/resources/META-INF/services/pro.taskana.taskrouting.api.TaskRouter b/lib/taskana-core/src/test/resources/META-INF/services/pro.taskana.taskrouting.api.TaskRouter new file mode 100644 index 000000000..35c4aa24a --- /dev/null +++ b/lib/taskana-core/src/test/resources/META-INF/services/pro.taskana.taskrouting.api.TaskRouter @@ -0,0 +1,2 @@ +acceptance.taskrouting.TestTaskRouterForDomainA +acceptance.taskrouting.TestTaskRouterForDomainB