Chapter 3. Examples

3.1. Graph based control flow activities

3.1.1. Automatic decision

This example shows how to implement automatic conditional branching. This is mostly called a decision or an or-split. It selects one path of execution from many alternatives. A decision node should have multiple outgoing transitions.

In a decision, information is collected from somewhere. Usually that is the process variables. But it can also collect information from a database, a file, any other form of input or a combination of these. In this example, a variable creditRate is used. It contains an integer. The higher the integer, the better the credit rating. Let's look at the example implementation:

Then based on the obtained information, in our case that is the creditRate, an outgoing transition has to be selected. In the example, transition good will be selected when the creditRate is above 5, transition bad will be selected when creditRate is below -5 and otherwise transition average will be selected.

Once the selection is done, the transition is taken with execution.take(String) or the execution.take(Transition) method.

public class AutomaticCreditRating implements Activity {
    public void execute(Execution execution) {
      int creditRate = (Integer) execution.getVariable("creditRate");
      
      if (creditRate > 5) {
        execution.take("good");
  
      } else if (creditRate < -5) {
        execution.take("bad");
        
      } else {
        execution.take("average");
      }
    }
  }

We'll demonstrate the AutomaticCreditRating in the following process:

The decision process

Figure 3.1. The decision process

ProcessDefinition processDefinition = ProcessFactory.build()
    .node("initial").initial().behaviour(new WaitState())
      .transition().to("creditRate?")
    .node("creditRate?").behaviour(new AutomaticCreditRating())
      .transition("good").to("a")
      .transition("average").to("b")
      .transition("bad").to("c")
    .node("a").behaviour(new WaitState())
    .node("b").behaviour(new WaitState())
    .node("c").behaviour(new WaitState())
.done();

Executing this process goes like this:

Execution execution = processDefinition.startExecution();

startExecution() will bring the execution into the initial node. That's a wait state so the execution will point to that node when the startExecution() returns.

Then we have a chance to set the creditRate to a specific value like e.g. 13.

execution.setVariable("creditRate", 13);

Next, we provide a signal so that the execution takes the default transition to the creditRate? node. Since process variable creditRate is set to 13, the AutomaticCreditRating activity will take transition good to node a. Node a is a wait state so them the invocation of signal will return.

Similarly, a decision can be implemented making use of the transition's guard condition. For each outgoing transition, the guard condition expression can be evaluated. The first transition for which its guard condition evaluates to true is taken.

This example showed automatic conditional branching. Meaning that all information is available when the execution arrives in the decision node, even if it may have to be collected from different sources. In the next example, we show how a decision is implemented for which an external entity needs to supply the information, which results into a wait state.

3.1.2. External decision

This example shows an activity that again selects one path of execution out of many alternatives. But this time, the information on which the decision is based is not yet available when the execution arrives at the decision. In other words, the execution will have to wait in the decision until the information is provided from externally.

public class ExternalSelection implements ExternalActivity {
  
  public void execute(Execution execution) {
    execution.waitForSignal();
  }

  public void signal(Execution execution, String signalName, Map<String, Object> parameters) throws Exception {
    execution.take(signalName);
  }
  
  public Set<SignalDefinition> getSignals(Execution execution) throws Exception {
    return null;
  }
}

The diagram for this external decision will be the same as for the automatic decision:

A decision

Figure 3.2. A decision

ProcessDefinition processDefinition = ProcessFactory.build()
    .node("initial").initial().behaviour(new WaitState())
      .transition().to("creditRate?")
    .node("creditRate?").behaviour(new ExternalSelection())
      .transition("good").to("a")
      .transition("average").to("b")
      .transition("bad").to("c")
    .node("a").behaviour(new WaitState())
    .node("b").behaviour(new WaitState())
    .node("c").behaviour(new WaitState())
.done();

The execution starts the same as in the automatic example. After starting a new execution, it will be pointing to the initial wait state.

Execution execution = processDefinition.startExecution();

But the next signal will cause the execution to take the default transition out of the initial node and arrive in the creditRate? node. Then the ExternalSelection is executed, which will result into a wait state. So when the invocation of signal() returns, the execution will be pointing to the creditRate? node and it expects an external trigger.

Next we'll give an external trigger with good as the signalName. So supplying the external trigger is done together with feeding the information needed by the decision.

execution.signal("good");

That external trigger will be translated by the ExternalSelection activity into taking the transition with name good. That way the execution will have arrived in node a when signal("good") returns.

Note that both parameters signalName and parameters can be used by external activities as they want. In the example here, we used the signalName to specify the result. But another variation might expect an integer value under the creditRate key of the parameters.

But leveraging the execution API like that is not done very often in practice. The reason is that for most external functions, typically activity instances are created. Think about Task as an instance of a TaskActivity (see later) or analogue, a ServiceInvocation could be imagined as an instance of a ServiceInvocationActivity. In those cases, those activity instances make the link between the external activity and the execution. And these instances also can make sure that an execution is not signalled inappropriately. Inappropriate signalling could happen when for instance a service response message would arrive twice. If in such a scenario, the message receiver would just signal the execution, it would not notice that the second time, the execution is not positioned in the service invocation node any more.

3.2. Composite based control flow activities

3.2.1. Composite sequence

Block structured languages like BPEL are completely based on composite nodes. Such languages don't have transitions. The composite node structure of the Process Virtual Machine allows to build a process with a structure that exactly matches the block structured languages. There is no need for a conversion to a transition based model. We have already discussed some examples of composite nodes. The following example will show howw to implement a sequence, one of the most common composite node types.

A sequence has a list of nested activities that need to be executed in sequence.

This is how a sequence can be implemented:

public class Sequence implements ExternalActivity {

  public void execute(Execution execution) {
    List<Node> nodes = execution.getNode().getNodes();
    execution.execute(nodes.get(0));
  }

  public void signal(Execution execution, String signal, Map<String, Object> parameters) {
    Node previous = execution.getPreviousNode();
    List<Node> nodes = execution.getNode().getNodes();
    int previousIndex = nodes.indexOf(previous);
    int nextIndex = previousIndex+1;
    if (nextIndex < nodes.size()) {
      Node next = nodes.get(nextIndex);
      execution.execute(next);
    } else {
      execution.proceed();
    }
  }

  public Set<SignalDefinition> getSignals(Execution execution) {
    return null;
  }
}

When an execution arrives in this sequence, the execute method will execute the first node in the list of child nodes (aka composite nodes or nested nodes). The sequence assumes that the child node's behaviour doesn't have outgoing transitions and will end with an execution.proceed(). That proceed will cause the execution to be propagated back to the parent (the sequence) with a signal.

The signal method will look up the previous node from the execution, determine its index in the list of child nodes and increments it. If there is a next node in the list it is executed. If the previous node was the last one in the list, the proceed is called, which will propagate the execution to the parent of the sequence in case there are no outgoing transitions.

To optimize persistence of executions, the previous node of an execution is normally not maintained and will be to null. If a node requires the previous node or the previous transition like in this Sequence, the property isPreviousNeeded must be set on the node.

Let's look at how that translates to a process and an execution:

A sequence.

Figure 3.3. A sequence.

ProcessDefinition processDefinition = ProcessFactory.build("sequence")
    .compositeNode("sequence").initial().behaviour(new Sequence())
      .needsPrevious()
      .node("one").behaviour(new Display("one"))
      .node("wait").behaviour(new WaitState())
      .node("two").behaviour(new Display("two"))
    .compositeEnd()
.done();

The three numbered nodes will now be executed in sequence. Nodes 1 and 2 are automatic Display activities, while node wait is a wait state.

Execution execution = processDefinition.startExecution();

The startExecution will execute the Sequence activity. The execute method of the sequence will immediately execute node 1, which will print message one on the console. Then the execution is automatically proceeded back to the sequence. The sequence will have access to the previous node. It will look up the index and execute the next. That will bring the execution to node wait, which is a wait state. At that point, the startExecution() will return. A new external trigger is needed to complete the wait state.

execution.signal();

That signal will delegate to the WaitState's signal method. That method is empty so the execution will proceed in a default way. Since there are no outgoing transitions, the execution will be propagated back to the sequence node, which will be signalled. Then node 2 is executed. When the execution comes back into the sequence it will detect that the previously executed node was the last child node, therefore, no propagation method will be invoked, causing the default proceed to end the execution. The console will show:

one
two

3.2.2. Composite decision

In a composite model, the node behaviour can use the execution.execute(Node) method to execute one of the child nodes.

A decision based on node composition

Figure 3.4. A decision based on node composition

ProcessDefinition processDefinition = ProcessFactory.build()
    .compositeNode("creditRate?").initial().behaviour(new CompositeCreditRating())
      .node("good").behaviour(new ExternalSelection())
      .node("average").behaviour(new ExternalSelection())
      .node("bad").behaviour(new ExternalSelection())
    .compositeEnd()
.done();

The CompositeCreditRating is an automatic decision, implemented like this:

public class CompositeCreditRating implements Activity {
  
  public void execute(Execution execution) {
    int creditRate = (Integer) execution.getVariable("creditRate");
    
    if (creditRate > 5) {
      execution.execute("good");

    } else if (creditRate < -5) {
      execution.execute("bad");
      
    } else {
      execution.execute("average");
    }
  }
}

So when we start a new execution with

Map<String, Object> variables = new HashMap<String, Object>();
variables.put("creditRate", 13);
Execution execution = processDefinition.startExecution(variables);

The execution will execute the CompositeCreditRating. The CompositeCreditRating will execute node good cause the process variable creditRate is 13. When the startExecution() returns, the execution will be positioned in the good state. The other scenarios are very similar.

3.3. Human tasks

This section will demonstrate how support for human tasks can be build on top of the Process Virtual Machine.

As we indicated in Section 4.4, “Execution and threads”, for each step in the process the most important characteristic is whether responsibility for an activity lies within the process system or outside. In case of a human task, it should be clear that the responsibility is outside of the process system. This means that for the process, a human task is a wait state. The execution will have to wait until the person provides the external trigger that the task is completed or submitted.

Overview of the link between processes and tasks.

Figure 3.5. Overview of the link between processes and tasks.

In the picture above, the typical link between process execution and tasks is represented. When an execution arrives in a task node, a task is created in a task component. Typically such a task will end up in a task table somewhere in the task component's database. Then users can look at their task lists. A task list is then a filter on the complete task list based on the task's assigned user column. When the user completes the task, the execution is signalled and typically leaves the node in the process.

A task management component keeps track of tasks for people. To integrate human tasks into a process, we need an API to create new tasks and to get notifications of task completions. The following example might have only a rudimentary integration between between process execution and the task management component, but the goal is to show the interactions as clearly as possible. Real process languages like jPDL have a much better integration between process execution and tasks, resulting in more complexity.

For this example we'll first define a simplest task component with classes Task and TaskComponent:

public class Task {
  public String userId;
  public String taskName;
  public Execution execution;
  
  public Task(String userId, String taskName, Execution execution) {
    this.userId = userId;
    this.taskName = taskName;
    this.execution = execution;
  }
  
  public void complete() {
    execution.signal();
  }
}

This task has public fields to avoid the getters and setters. The taskName property is the short description of the task. The userId is a reference to the user that is assigned to this task. And the execution is a reference to the execution to which this task relates. When a task completes it signals the execution.

The next task component manages a set of tasks.

public class TaskComponent {
  
  static List<Task> tasks = new ArrayList<Task>();
  
  public static void createTask(String taskName, Execution execution) {
    String userId = assign(taskName, execution);
    tasks.add(new Task(userId, taskName, execution));
  }
  
  private static String assign(String taskName, Execution execution) {
    return "johndoe";
  }
  
  public static List<Task> getTaskList(String userId) {
    List<Task> taskList = new ArrayList<Task>();
    for (Task task : tasks) {
      if (task.userId.equals(userId)) {
        taskList.add(task);
      }
    }
    return taskList;
  }
}

To keep this example short, this task component is to be accessed through static methods. The assigning tasks is done hard coded to "johndoe". Tasks can be created and tasklists can be extracted by userId. Next we can look at the node behaviour implementation of a TaskActivity.

public class TaskActivity implements ExternalActivity {
  
  public void execute(Execution execution) {
    // let's use the node name as the task id
    String taskName = execution.getNode().getName();
    TaskComponent.createTask(taskName, execution);
  }
  
  public void signal(Execution execution, String signal, Map<String, Object> parameters) {
    execution.takeDefaultTransition();
  }
  
  public Set<SignalDefinition> getSignals(Execution execution) {
    return null;
  }
}

The task node works as follows. When an execution arrives in a task node, the execute method of the TaskActivity is invoked. The execute method will then take the node name and use it as the task name. Alternatively, 'taskName' could be a configuration property on the TaskActivity class. The task name is then used to create a task in the task component. Once the task is created, the execution is not propagated which means that the execution will wait in this node till a signal comes in.

When the task is completed with the Task.complete() method, it will signal the execution. The TaskActivity's signal implementation will take the default transition.

This is how a process can be build with a task node:

ProcessDefinition processDefinition = ProcessFactory.build("task")
    .node("initial").initial().behaviour(new AutomaticActivity())
      .transition().to("shred evidence")
    .node("shred evidence").behaviour(new TaskActivity())
      .transition().to("next")
    .node("next").behaviour(new WaitState())
.done();

When a new execution is started, the initial node is an automatic activity. So it will immediately propagate to the task node the task will be created and the execution will stop in the 'shred evidence' node.

Execution execution = processDefinition.startExecution();

assertEquals("shred evidence", execution.getNode().getName());

Task task = TaskComponent.getTaskList("johndoe").get(0);

Next, time can elapse until the human user is ready to complete the task. In other words, the thread of control is now with 'johndoe'. When John completes his task e.g. through a web UI, then this should result into an invocation of the complete method on the task.

task.complete();
  
assertEquals("next", execution.getNode().getName());

The invocation of the complete method cause the execution to take the default transition to the 'next' node.