Enterprise Java

How to use transient variables in Activiti

A feature that has been requested quite a bit – transient variables – has landed in the Beta3 of Activiti v6 we’ve released yesterday. In this post, I’ll show you an example on how transient variables can be used to cover some advanced use cases that weren’t possible (or optimal) before.

So far, all variables in Activiti were persistent. This means the variable and value are stored in the data store and historical audit data is kept. Transient variables on the other hand act and behave like a regular variable, but they are not persisted. Beyond not being persisted, the following is special to transient variables:

  • a transient variable only survives until the next ‘wait state’, when the state of the process instance is persisted to the database.
  • a transient variable shadows a persistent variable with the same name.

More detailed information about transient variables and the API’s can be found in the documentation.

Example

The process definition that we’ll use to demo some bits of the transient variables is shown below. It’s a fairly simple process: the idea is that we’ll ask some things like keyword and language from the user and use it to do a GitHub API call. If successful, the results are shown to the user. It’s easy to write a UI for this (or use the new forms in the Beta3 angularJS app), but in this post we’ll focus on the code only.

The BPMN 2.0 xml and code can be found on this Github repo: https://github.com/jbarrez/transient-vars-example

Screenshot-from-2016-09-01-114450

Let’s walk through the process together. The process starts by providing some input from the user about what should be searched on (usually this would be done using a start form).

repositoryService.createDeployment().addClasspathResource("process.bpmn20.xml").deploy();

Map<String, Object> variables = new HashMap<String, Object>();
variables.put("keyWord", "workflow");
variables.put("language", "java");
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("githubsearch", variables);

The variables we pass when starting the process instance are regular variables. They are persisted and audit history will be kept, as there is no reason why this shouldn’t be the case.

The first step that is executed is the ‘execute HTTP call’ step, which is a Service Task with a Java delegate:

<serviceTask name="Execute HTTP call" activiti:class="org.activiti.ExecuteHttpCallDelegate"></serviceTask>

Java code:

public class ExecuteHttpCallDelegate implements JavaDelegate {

    public void execute(DelegateExecution execution) {

        String keyword = (String) execution.getVariable("keyWord");
        String language = (String) execution.getVariable("language");

        String url = "https://api.github.com/search/repositories?q=%s+language:%s&sort=starsℴ=desc";
        url = String.format(url, keyword, language);
        HttpGet httpget = new HttpGet(url);

        CloseableHttpClient httpclient = HttpClients.createDefault();
        try {
            CloseableHttpResponse response = httpclient.execute(httpget);

            execution.setTransientVariable("response", IOUtils.toString(response.getEntity().getContent(), "UTF-8"));
            execution.setTransientVariable("responseStatus", response.getStatusLine().getStatusCode());

            response.close();

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}

Here, we’re doing a simple HTTP get against the GitHub API, using the ‘keyword’ and ‘language’ variables we’ve passed on process instance start. Special here is on line 16 and 17 that we’re storing the response and response status in transient variables (that’s the setTransientVariable() call). The reasons for choosing transient variables here are

  • The json response from the Github API is very large. It can be stored in a persistent way of course, but this won’t be good for performance.
  • From an audit point of view, the whole response matters very little. We’ll extract the important bits later from that response, and those will be stored in the historical data.

After getting the response and storing it in a transient variable, we pass the exclusive gateway. The sequenceflow looks like this:

<sequenceFlow ... >
  <extensionElements>
    <activiti:executionListener event="take" class="org.activiti.ProcessResponseExecutionListener"></activiti:executionListener>
  </extensionElements>
  <conditionExpression xsi:type="tFormalExpression"><![CDATA[${responseStatus == 200}]]></conditionExpression>
</sequenceFlow>

Note that for the sequence flow condition there is no difference when it comes to using a transient or non-transient variable. A regular getVariable will also return the transient variable with the name, if set (this is the shadowing part in the docs mentioned above). A getTransientVariable also exists, when only the transient set of variables should be consulted. Anyway: for the condition: no difference at all.

You can also see that the sequence flow has a (hidden in the diagram) execution listener. The execution listener will parse the json response, select the relevant bits and store these in a transient array list. This is important, as you’ll read below the code.

public class ProcessResponseExecutionListener implements ExecutionListener {

    private ObjectMapper objectMapper = new ObjectMapper();

    public void notify(DelegateExecution execution) {

        List<String> searchResults = new ArrayList<String>();

        String response = (String) execution.getVariable("response");
        try {
            JsonNode jsonNode = objectMapper.readTree(response);
            JsonNode itemsNode = jsonNode.get("items");
            if (itemsNode != null && itemsNode.isArray()) {
                for (JsonNode itemNode : (ArrayNode) itemsNode) {
                    String url = itemNode.get("html_url").asText();
                    searchResults.add(url);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        execution.setTransientVariable("searchResults", searchResults);
    }

}

The reason for storing the list as a transient variable is an important one. As you can see in the diagram, a multi instance subprocess follows. A subprocess often takes a collection variable to create the instances. So far, this was a persistent variable, in the form of a java-serialized ArrayList. I’ve never liked that (I’ve always used delegateExpressions with a bean if I had to do it before). This has always bothered me a bit. Now, the arraylist is transient and won’t be stored in the data store:

<subProcess name="subProcess">
    <multiInstanceLoopCharacteristics isSequential="false" 
          activiti:collection="searchResults" activiti:elementVariable="searchResult" />

Do note that the ‘searchResult’ variable above will be a persistent variable.

Note that the transient variables will be there until the user task is reached and the state is stored in the data store. It’s also possible to pass transient variables when starting a process instance (which could have been an option here, but I think storing user input is a thing you’d want in your audit data).

If you’d run the process instance like this for example:

Map<String, Object> variables = new HashMap<String, Object>();
variables.put("keyWord", "workflow");
variables.put("language", "java");
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("githubsearch", variables);

List<Task> tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list();
for (Task task : tasks) {
  System.out.println("Current task : " + task.getName());
}

Which gives as example output (limited to 5 results):

Current task : Review result https://github.com/Activiti/Activiti
Current task : Review result https://github.com/twitter/ambrose
Current task : Review result https://github.com/azkaban/azkaban
Current task : Review result https://github.com/romannurik/AndroidDesignPreview
Current task : Review result https://github.com/spring-projects/spring-xd

The user could now look into the details of each of the results and continue the process.

Last Words

As you can imagine, there are quite a few use cases for transient variables. I know that for many people this was an important feature, so I’m glad it’s out there now. Feedback and comments of course, as usual, always welcome!

Joram Barrez

Joram is an all-around software engineer with a keen interest in anything that is slightly related to software development. After working with Ruby on Rails at the university, he became a java consultant specializing in jBPM which later led him to joining the jBPM project as JBoss employee. He left JBoss for starting the Activiti project together with Tom Baeyens (also ex-JBoss) at Alfresco .Besides Activiti, he is also heavily involved with iOS development and the workflow features for the Alfresco Mobile app.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button