Add Task Listeners Automatically
Target audience: Developers
Overview
Task listeners are reusable snippets of logic (through a class, expression or delegate expression) that are automatically invoked by the engines on certain lifecycle events of the task (e.g. on task assignment). They are not visible in the diagram and thus can be used to add technical logic to a process or case without exposing it to the business user in the diagram.
Task listeners can be set through Flowable Design. However, in this how to guide we want to explore the possibility on how to add task listeners to a selection of user tasks automatically through a BPMN parse handler.
Note that there are also execution listeners (for BPMN models) and plan item lifecycle listeners (for CMMN models) that work in a similar way, but allow to hook into lifecycle events of other types of entities. The concepts in this how-to are similar though.
Example Use Case
Assume we want to build a task SLA management where we initialize a task according its category by calculating and setting its due time as well as initializing some measures for monitoring the task during its lifecycle. Maybe we also want to add some actions exposed to the task UI depending on the underlying task SLA model.
The easiest way to achieve that is by using a task listener (org.flowable.task.service.delegate.TaskListener
). This would mean that we need to
add such a task listener for the Create
event on each task we need to monitor in Design. This is quite a lot of additional extra work, and we don't
even get the guarantee that it will be on all the tasks we need to monitor. What if it will be forgotten by the modeler?
CMMN and BPMN Parse Handlers
Parse handlers are a great way to hook yourself into the parsing process of case and process definitions (deployed models) at runtime and even extend
existing elements. You can find more detailed information in the documentation: Hooking into process parsing.
The same way you can hook yourself into process parsing is also possible with CMMN, you just need the org.flowable.cmmn.engine.impl.parser.CmmnParseHandler
instead
of the org.flowable.engine.parse.BpmnParseHandler
.
Parsing the process and case models and looking for specific elements is providing you the opportunity to hook into the parsing process where we transform the CMMN or BPMN XML into a Java object model we work with on the engine level. This means, we have the possibility to manipulate the elements as they are added to the internal model as in our case, adding a task listener to those user tasks we want to extend.
Writing the parse handler
For our example, we first need a parse handler in order to add our task listener to a selection of user tasks we want to initialize and monitor for task SLAs. Here is a simple example of such a parse handler:
package com.flowable.external.task.sla;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.flowable.bpmn.model.BaseElement;
import org.flowable.bpmn.model.FlowableListener;
import org.flowable.bpmn.model.ImplementationType;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.delegate.TaskListener;
import org.flowable.engine.impl.bpmn.parser.BpmnParse;
import org.flowable.engine.parse.BpmnParseHandler;
public class TaskSLAParseHandler implements BpmnParseHandler {
@Override
public Collection<Class<? extends BaseElement>> getHandledTypes() {
Set<Class<? extends BaseElement>> types = new HashSet<>();
types.add(UserTask.class);
return types;
}
@Override
public void parse(BpmnParse bpmnParse, BaseElement element) {
// the element must be a user task as we filtered this handler for user tasks only
UserTask userTask = (UserTask) element;
String category = userTask.getCategory();
if (StringUtils.isNotEmpty(category)) {
// if this user task has a mapped category, we install a task listener kicking off whenever a new instance of that task is created to
// initialize the tasks SLA according its category and mapped SLA definition
FlowableListener createListener = new FlowableListener();
createListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
createListener.setImplementation("${taskSLAInitializationListener}");
createListener.setEvent(TaskListener.EVENTNAME_CREATE);
userTask.getTaskListeners().add(createListener);
}
}
}
When you use the CmmnParseHandler
you can access the HumanTask inside the parse handler through the PlanItemDefinition
:
PlanItem planItem = (PlanItem) baseElement;
PlanItemDefinition planItemDefinition = planItem.getPlanItemDefinition();
HumanTask humanTask = (HumanTask) planItemDefinition;
The first part is the getHandledTypes
method where we can return a set of BPMN element types we want to hook ourselves into. We are only interested into
user tasks, so we only return a set of UserTask
element class.
The next part is the parsing itself, as we filtered only for user tasks, we can safely cast to UserTask
as we only will be notified if parsing reaches
a user task element.
We have full access to all parsed attributes of the user task model and just as an example, we get the category property and if it is not empty, we want to add our task listener in there to be invoked at runtime whenever a task instance of that particular task model is created. You can find some more details about task listeners in general in the documentation here.
We add our task listener as a delegate expression, this way we can just return an instance of our task listener as a bean, implementing the task listener
interface as we will see later. The event we are interested in is the create
one so our listener will be notified whenever a new task of that specific
task model is instantiated at runtime.
Registering our Parse Handler
Now we need to register our parse handler to the engine, so it gets picked up and will be invoked during the parsing of BPMN process models.
We are going to use an engine configurer to add our parse handler to the engine. You can read about it here.
If you are using a Java based Spring configuration, this is how it could look like:
@Bean
public EngineConfigurationConfigurer<SpringProcessEngineConfiguration> customTaskSLAProcessEngineConfigurationConfigurer() {
return processEngineConfiguration -> {
List<BpmnParseHandler> parseHandlers = processEngineConfiguration.getPostBpmnParseHandlers();
if (parseHandlers == null) {
parseHandlers = new ArrayList<>();
}
parseHandlers.add(new TaskSLAParseHandler());
processEngineConfiguration.setPostBpmnParseHandlers(parseHandlers);
};
}
We simply add our parse handler to the list of post parse handlers directly to the engine using a configurer.
Writing the Task Listener
The task listener is the final piece in our exercise, let's have a closer look at it:
package com.flowable.external.task.sla;
import org.flowable.task.service.delegate.DelegateTask;
import org.flowable.task.service.delegate.TaskListener;
import org.flowable.task.service.impl.persistence.entity.TaskEntity;
import com.flowable.action.api.runtime.ActionRuntimeService;
public class TaskSLAInitializationListener implements TaskListener {
protected final TaskSLAService taskSLAService;
protected final ActionRuntimeService actionService;
public TaskSLAInitializationListener(TaskSLAService taskSLAService, ActionRuntimeService actionService) {
this.taskSLAService = taskSLAService;
this.actionService = actionService;
}
@Override
public void notify(DelegateTask delegateTask) {
// load task definition according category and initialize task accordingly
TaskSLAModel taskSLAModel = taskSLAService.initializeNewTask(delegateTask);
// check, if we have a reaction time and if yes, we need to create an action instance on the task to react on it
if (taskSLAModel != null && taskSLAModel.hasReactionTime()) {
actionService.createActionInstanceBuilder()
.actionDefinitionKey("setReactionOnTask")
.scopeId(delegateTask.getId())
.scopeType("task")
.name("React on task")
.start();
}
}
}
We are not going into details about the task SLA service, it contains all the logic to initialize and handle the task SLA metrics (this how-to is about how to make use of a parser to hook ourselves into that mechanism to automatically add our task listener to selected tasks we want).
We only need to overwrite the notify
method as it will be invoked on all the task instances after creation we added our listener to.
As you might guess, you can now do whatever is needed within this task listener to initialize it, send notifications about it or whatever is necessary when such a task gets created.
There is yet another interesting aspect which is how to create and attach an action to such a task, so it will show up in the default UI. In our example, we check the task SLA model if there is a reaction time specified which will be monitored and if this is the case, we want to add an action to the task to be used to flag the task being worked on as the reaction to it. We use the action service with an instance builder to setup the action and create it later, so it will show up whenever we navigate to this task.