TSK-972 initial commit - add task routing SPI to taskana

This commit is contained in:
BerndBreier 2019-12-10 12:41:22 +01:00 committed by Holger Hagen
parent b0ee202402
commit 32c27e323c
11 changed files with 264 additions and 2 deletions

View File

@ -229,6 +229,18 @@ public interface TaskService {
*/ */
TaskQuery createTaskQuery(); 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}. * Returns a not persisted instance of {@link Task}.
* *

View File

@ -6,6 +6,7 @@ import org.apache.ibatis.session.SqlSession;
import pro.taskana.TaskanaEngine; import pro.taskana.TaskanaEngine;
import pro.taskana.history.HistoryEventProducer; import pro.taskana.history.HistoryEventProducer;
import pro.taskana.taskrouting.TaskRoutingProducer;
/** /**
* FOR INTERNAL USE ONLY. * FOR INTERNAL USE ONLY.
@ -69,4 +70,11 @@ public interface InternalTaskanaEngine {
*/ */
HistoryEventProducer getHistoryEventProducer(); HistoryEventProducer getHistoryEventProducer();
/**
* Retrieve TaskRoutingProducer.
*
* @return the TaskRoutingProducer instance.
*/
TaskRoutingProducer getTaskRoutingProducer();
} }

View File

@ -193,7 +193,13 @@ public class TaskServiceImpl implements TaskService {
} else if (task.getWorkbasketKey() != null) { } else if (task.getWorkbasketKey() != null) {
workbasket = workbasketService.getWorkbasket(task.getWorkbasketKey(), task.getDomain()); workbasket = workbasketService.getWorkbasket(task.getWorkbasketKey(), task.getDomain());
} else { } 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()) { if (workbasket.isMarkedForDeletion()) {
@ -357,6 +363,13 @@ public class TaskServiceImpl implements TaskService {
return new TaskQueryImpl(taskanaEngine); return new TaskQueryImpl(taskanaEngine);
} }
@Override
public Task newTask() {
TaskImpl task = new TaskImpl();
task.setCallbackState(CallbackState.NONE);
return task;
}
@Override @Override
public Task newTask(String workbasketId) { public Task newTask(String workbasketId) {
TaskImpl task = new TaskImpl(); TaskImpl task = new TaskImpl();

View File

@ -49,6 +49,7 @@ import pro.taskana.mappings.TaskMonitorMapper;
import pro.taskana.mappings.WorkbasketAccessMapper; import pro.taskana.mappings.WorkbasketAccessMapper;
import pro.taskana.mappings.WorkbasketMapper; import pro.taskana.mappings.WorkbasketMapper;
import pro.taskana.security.CurrentUserContext; import pro.taskana.security.CurrentUserContext;
import pro.taskana.taskrouting.TaskRoutingProducer;
/** /**
* This is the implementation of TaskanaEngine. * This is the implementation of TaskanaEngine.
@ -64,6 +65,7 @@ public class TaskanaEngineImpl implements TaskanaEngine {
protected ConnectionManagementMode mode = ConnectionManagementMode.PARTICIPATE; protected ConnectionManagementMode mode = ConnectionManagementMode.PARTICIPATE;
protected java.sql.Connection connection = null; protected java.sql.Connection connection = null;
private HistoryEventProducer historyEventProducer; private HistoryEventProducer historyEventProducer;
private TaskRoutingProducer taskRoutingProducer;
private InternalTaskanaEngineImpl internalTaskanaEngineImpl; private InternalTaskanaEngineImpl internalTaskanaEngineImpl;
protected TaskanaEngineImpl(TaskanaEngineConfiguration taskanaEngineConfiguration) { protected TaskanaEngineImpl(TaskanaEngineConfiguration taskanaEngineConfiguration) {
@ -71,6 +73,7 @@ public class TaskanaEngineImpl implements TaskanaEngine {
createTransactionFactory(taskanaEngineConfiguration.getUseManagedTransactions()); createTransactionFactory(taskanaEngineConfiguration.getUseManagedTransactions());
this.sessionManager = createSqlSessionManager(); this.sessionManager = createSqlSessionManager();
historyEventProducer = HistoryEventProducer.getInstance(taskanaEngineConfiguration); historyEventProducer = HistoryEventProducer.getInstance(taskanaEngineConfiguration);
taskRoutingProducer = TaskRoutingProducer.getInstance(this);
this.internalTaskanaEngineImpl = new InternalTaskanaEngineImpl(); this.internalTaskanaEngineImpl = new InternalTaskanaEngineImpl();
} }
@ -385,5 +388,10 @@ public class TaskanaEngineImpl implements TaskanaEngine {
return historyEventProducer; return historyEventProducer;
} }
@Override
public TaskRoutingProducer getTaskRoutingProducer() {
return taskRoutingProducer;
}
} }
} }

View File

@ -103,7 +103,7 @@ public class WorkbasketAccessItemQueryImpl implements WorkbasketAccessItemQuery
public List<WorkbasketAccessItem> list() { public List<WorkbasketAccessItem> list() {
LOGGER.debug("entry to list(), this = {}", this); LOGGER.debug("entry to list(), this = {}", this);
List<WorkbasketAccessItem> result = taskanaEngine.openAndReturnConnection( List<WorkbasketAccessItem> result = taskanaEngine.openAndReturnConnection(
() -> new ArrayList<>(taskanaEngine.getSqlSession().selectList(LINK_TO_MAPPER, this))); () -> new ArrayList<WorkbasketAccessItem>(taskanaEngine.getSqlSession().selectList(LINK_TO_MAPPER, this)));
if (LOGGER.isDebugEnabled()) { if (LOGGER.isDebugEnabled()) {
LOGGER.debug("exit from list(). Returning {} resulting Objects: {} ", result.size(), LOGGER.debug("exit from list(). Returning {} resulting Objects: {} ", result.size(),
LoggerUtils.listToString(result)); LoggerUtils.listToString(result));

View File

@ -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<TaskRouter> serviceLoader;
private static List<TaskRouter> 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<String> 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;
}
}

View File

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

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,2 @@
acceptance.taskrouting.TestTaskRouterForDomainA
acceptance.taskrouting.TestTaskRouterForDomainB