NOTE: This example has not been updated to work with jBPM versions 3.2 or greater. See below the example for 3.2.1.
Suppose you receive an order on 2006/10/05 and some related task is created on 10/7; the task needs to be done within 5 days of recieving the order. How do we do that?
jBPM's definition language, JPDL, does not provide a packaged mechanism for dynamically setting Due Dates on Timers and Tasks based on arbitrary data. However, it is not too difficult to work up a solution that brings us the flexibility we need. Here is an example that takes advantage of an ActionHandler to set these due dates dynamically based on a process instance variable:
<?xml version="1.0" encoding="UTF-8"?>
<process-definition name="Due date Test">
<start-state name='START' >
<transition name='done' to='NODE1'></transition>
</start-state>
<task-node name='NODE1'>
<task name='task1'></task>
<event type='node-enter' >
<create-timer name='myTimeout' duedate='2000 days' >
<script>System.out.println("I reset my timer!");</script>
</create-timer>
<!-- Dynamically set due date of the timer created in the line above.
(This ActionHandler can also be used to dynamically set a Task's due date
by substituting the <timerName> tag with a <taskName> tag) -->
<action name='setThisTimer' class='com.???.UpdateDueDateAH'>
<timerName>myTimeout</timerName>
<!-- Process instance variable containing a java.util.Date. This value will
be used as a base value for calculating the Timer's new due date. If not
provided, the current time will be used. -->
<baseTimeVar>testDate</baseTimeVar>
<!-- The 'baseTimeVar' Date can be modified by optionally adding a period of
time. The value is a valid jBPM Duration styled string -->
<addDuration>2 minutes</addDuration>
</action>
</event>
<event type='node-leave' >
<!-- To mimic the 'node context' of the short hand timer syntax <timer>, we
would need to ensure that our timer is cancelled on node exit. -->
<cancel-timer name='timeout' ></cancel-timer>
</event>
<event type='task-create'>
<!-- Use UpdateDueDateAH here to dynamically set a task's due date -->
<action name='setDueDate' class='com.???.UpdateDueDateAH'>
<taskName>task1</taskName>
<baseTimeVar>testDate</baseTimeVar>
<addDuration>5 minutes</addDuration>
</action>
</event>
<transition name='done' to='NODE2'></transition>
<transition name='_sys_redoNode' to='NODE1'></transition>
</task-node>
<state name='NODE2'>
<!-- Timers with this syntax are created on node-enter and cancelled on node-leave.
If timer creation is needed from within an event element or the timer should continue
after the node has exited, then use <create-timer> instead -->
<timer name='thisNodeOnlyTimeout' duedate='5 minutes' transition='done' ></timer>
<transition name='done' to='END'></transition>
</state>
<end-state name="END" ></end-state>
</process-definition>
...and here is the UpdateDueDateAH class:
/*
* UpdateDueDateAH.java
*
* Created on September 11, 2006, 9:20 AM
*/
package com.???;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jbpm.graph.def.ActionHandler;
import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.graph.exe.Token;
import org.jbpm.taskmgmt.exe.*;
import org.jbpm.calendar.BusinessCalendar;
import org.jbpm.calendar.Duration;
import org.jbpm.svc.Services;
import org.jbpm.scheduler.SchedulerService;
import org.jbpm.db.SchedulerSession;
import org.jbpm.scheduler.exe.Timer;
import java.io.*;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
/**
* @author brittm
*/
public class UpdateDueDateAH implements ActionHandler{
//-- variables set by process def action parameter elements --
private String baseTimeVar = null;
private String addDuration = null; //a jbpm Duration styled string
private String timerName = null; //either this OR taskName should be set
private String taskName = null; //either this OR timerName should be set
private ExecutionContext ec = null;
public Log log = LogFactory.getLog(this.getClass());
public UpdateDueDateAH() {
}
/*Execute the action handler*/
public void execute(ExecutionContext executionContext){
ec = executionContext;
try{
Token token = ec.getToken();
Date newDueDate = null;
if(timerName != null) {
SchedulerSession schedulerSession = ec.getJbpmContext().getSchedulerSession();
//apparently throws an exception if no timer is found...
List<Timer> timers = (List<Timer>)schedulerSession.findTimersByName(timerName, token);
for(Timer timer : timers) {
try{
newDueDate = this.calculateDueDate();
timer.setDueDate(newDueDate);
}catch(Exception e) {
throw new Exception("Timer '" + timer.getName() + "' due date was not updated to " +
newDueDate + "': " + e);
}
schedulerSession.saveTimer(timer);
log.info("Timer '" + timer.getName() + "' due date updated to " + timer.getDueDate());
}
}else if(taskName != null) {
TaskMgmtInstance tmi = ec.getTaskMgmtInstance();
for(TaskInstance task : (Collection<TaskInstance>)tmi.getTaskInstances()) {
//if wildcard is specified, update all tasks...
if(taskName.equals("*")) {
try{
newDueDate = this.calculateDueDate();
task.setDueDate(newDueDate);
}catch(Exception e) {
throw new Exception("Task '" + task.getName() + "' due date was not updated to " +
newDueDate + "': " + e);
}
log.info("Task '" + task.getName() + "' due date updated to " + task.getDueDate());
}else{
if(taskName.equals(task.getName())) {
try{
newDueDate = this.calculateDueDate();
task.setDueDate(newDueDate);
}catch(Exception e) {
throw new Exception("Task '" + task.getName() + "' due date was not updated to '" +
newDueDate + "': " + e);
}
log.info("Task '" + task.getName() + "' due date updated to "+task.getDueDate());
}
}
}
}
}catch(Exception e){
log.error(e.getMessage(), e);
}
}
private Date calculateDueDate() throws Exception {
Date dueDate = null;
Calendar cal = Calendar.getInstance();
//if a baseTime is specified, we'll use that; otherwise, we'll just use the current time
if(baseTimeVar != null && baseTimeVar.length() > 0) {
Object baseTime = ec.getContextInstance().getVariable(baseTimeVar);
if(baseTime != null) {
if(baseTime instanceof String) {
throw new Exception("Could not calculate Due Date: the variable '" + baseTimeVar + "' should be a type of java.util.Date.");
}else if(baseTime instanceof Date) {
cal.setTime((Date)baseTime);
}else{
throw new Exception("Could not calculate Due Date: baseTimeVar '" + baseTimeVar +
"' was specified but no valid date/time data was found.");
}
}else{
throw new Exception("Could not calculate Due Date: baseTimeVar was specified but no data was found.");
}
}
if(addDuration != null) {
BusinessCalendar businessCalendar = new BusinessCalendar();
Duration duration = new Duration(addDuration);
dueDate = businessCalendar.add( cal.getTime(), duration );
}
return dueDate;
}
public void setTimerName(String timerName) {
if(timerName != null && timerName.trim().length() > 0) this.timerName = timerName;
}
public void setTaskName(String taskName) {
if(taskName != null && taskName.trim().length() > 0) this.taskName = taskName;
}
public void setBaseTimeVar(String baseTimeVar) {
this.baseTimeVar = baseTimeVar;
}
/*
* Takes a jbpm Duration styled string
*/
public void setAddDuration(String addDuration) {
this.addDuration = addDuration;
}
}
-
An example for jbpm v.3.2.1.
This is a snippet from a process definition:
...
<state name="state1">
<event type="timer-create">
<action name="timerCreated" class="org.sample.jbpm.handler.ChangeDueDateActionHandler">
<timerName>varTimer</timerName>
<delay>#{newDelay}</delay>
</action>
</event>
<timer duedate="10 years" name="varTimer" transition="fired">
<action name="message" class="..."></action>
</timer>
<transition to="node1" name="fired"></transition>
<transition to="end-state1" name="notFired"></transition>
</state>
...
Below is a handler which changes the due date of a timer.
package org.sample.jbpm.handler;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jbpm.calendar.BusinessCalendar;
import org.jbpm.calendar.Duration;
import org.jbpm.graph.def.ActionHandler;
import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.job.Timer;
import org.jbpm.scheduler.SchedulerService;
import org.jbpm.svc.Services;
public class ChangeDueDateActionHandler implements ActionHandler {
private static final long serialVersionUID = 1705L;
private static final Log log = LogFactory.getLog(ChangeDueDateActionHandler.class);
static BusinessCalendar businessCalendar = new BusinessCalendar();
String timerName;
String delay;
public void execute(ExecutionContext executionContext) throws Exception {
try {
Timer timer = executionContext.getTimer();
if (timer != null && timerName.equals(timer.getName())) {
String dueDate = delay;
// get the value of a variable
if (delay.startsWith("#{")) {
dueDate = (String) executionContext.getVariable(delay.substring(2, delay.length() -1));
}
log.info("Changing the timer: " + timer + " to fire in " + dueDate);
SchedulerService schedulerService = (SchedulerService) Services.getCurrentService(Services.SERVICENAME_SCHEDULER);
// delete the existing timer
schedulerService.deleteTimersByName(timer.getName(), executionContext.getToken());
// create a new one with the right delay
Duration duration = new Duration(dueDate);
Date dueDateDate = businessCalendar.add(new Date(), duration);
timer.setDueDate(dueDateDate);
schedulerService.createTimer(timer);
} else {
log.debug("Doesn't match: " + timer);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
public String getDelay() {
return delay;
}
public void setDelay(String delay) {
this.delay = delay;
}
public String getTimerName() {
return timerName;
}
public void setTimerName(String timerName) {
this.timerName = timerName;
}
}
Comments