Advanced scripting in Activiti: Custom Configuration Injection

The scripting task is probably one of the ‘oldest’ classes in the Activiti code base, but I think it is still underused by many. The (perceived?) downsides are of course performance (interpretation vs compilation) and less support from IDE perspective.

However, the benefits (imo) outweigh this:

  • Scripts are defined in the process xml itself. No more worries about versioning and having to juggle with libs on the classpath.
  • What we’ve seen in the past is that less technical skilled people dare to try scripts. But never Java.

Anyway, what few people know or have realized is that you can do really awesome and advance stuff in scripts in Activiti. Since such a script is executed within the process engine, you have access to everything the engine is capable of. Yes … everything… which makes it both a very powerful but also (potential) dangerous thing (if you don’t know what you’re doing).

Let me walk you through such an example. I like to call it ‘custom configuration injection’ as a concept, because it effectively allows you to add custom logic at runtime which alters process execution significantly. If you have a cooler name for it, please let me know.

All code can be found on my Github Page: https://github.com/jbarrez/activiti-advanced-scripting

awesome-code-648x303

The use case

Now what is this thing I want to do. Well, I want to have a process that, when executed

  • Adds a ‘task completion event handler’ to every user task that is executed
  • This event handler must fire a custom event off to a remote URL, where potentially a event processor is doing its stuff

So basically, we want to fire off custom events to some remote URL whenever a task gets completed. A good use case for this could be Business Intelligence reporting/Complex event processing, eg with something like Esper.

Screen Shot 2013-07-23 at 10.03.11

The first version

The first cut of this functionality can be found at https://github.com/jbarrez/activit-advanced-scripting/blob/master/src/test/resources/org/activiti/test/my-process.bpmn20.xml. When this process is executed, the following happens:

var config = Context.getProcessEngineConfiguration();
var bpmnParser = config.getBpmnParser();

We simply fetch the current ProcessEngineConfiguration instance. We fetch the BpmnParser instance from this configuration, as we will want to change the general user task parsing for the whole engine.

Next, we build the script:

var script = "";
script = script + "importPackage(java.net);";
script = script + "importPackage(java.io);";
script = script + "var url = new URL('http://localhost:8182/echo');";
script = script + "var connection = url.openConnection();";
script = script + "connection.setRequestMethod('POST');";
script = script + "connection.setDoOutput(true);";
script = script + "var outputStream = new BufferedOutputStream(connection.getOutputStream());";
script = script + "outputStream.write(new java.lang.String(\"{'eventType':'task-complete'}\").bytes);";
script = script + "outputStream.flush();";
script = script + "connection.connect();";
script = script + "var respCode = connection.getResponseCode();";
script = script + "if (respCode != 200) ";
script = script + "println('Response code : ' + respCode);";
script = script + "outputStream.close();";
script = script + "connection.disconnect();";

This is obviously not the most efficient way to do this, but it sure shows the details of what happens. The message ‘eventType:task-complete’ is send to the localhost:8182 url through standard java.net and java.io classes.

The tricky part comes next:

var handler = new ExecuteScriptOnTaskCompleteBpmnParseHandler("JavaScript");
handler.setUserTaskCompleteScript(script);
bpmnParser.getBpmnParserHandlers().addHandler(handler);

// reset the deployment cache such that the new listener gets picked up on a new redeploy
config.getProcessDefinitionCache().clear();

Here we add a BpmnParseHandler class to the engine configuration. The parse handler will add the execution of the script defined above to every receival of the ‘task complete event’ send out by the engine. This parse handler kicks in every time a user task is parsed, which effectively adds our ‘send-event-to-remote-service’ to every user task now happening in your Activiti environment!

There is a unit test to see how this works: https://github.com/jbarrez/activiti-advanced-scripting/blob/master/src/test/java/org/activiti/test/ExecuteScriptInProcessTest.java. In the test, we setup a very simple ‘echo service’ which simply prints out whenever such an event is received. If you run it in your IDE, you’ll see something like this:

Screen Shot 2013-07-23 at 09.53.00

 

But we can do better

But we can do better. Check the following code.

var handler = new ExecuteScriptOnTaskCompleteBpmnParseHandler("JavaScript");
handler.setUserTaskCompleteScript("http://localhost:8182/scripts/task-complete.js");
handler.setExecuteScriptInJob(true);
bpmnParser.getBpmnParserHandlers().addHandler(handler);

// Update the configuration to use the correct job handler
var jobHandler = new ExecuteScriptJobHandler();
config.getJobHandlers().put(jobHandler.type,jobHandler);

This code does the same as in the previous section, ie. attaching a listener for ‘complete’ events to every user task. However, this implementation:

  • Executes the script asynchronously
  • Does not define the script in the process xml, but it is fetched from a remote url
  • Updates the job handler configuration

If you ask me, that’s pretty awesome! So this means that the actual sending of a message to the remote service is not impacting the execution performance of your process instance. Obviously, from here you can go crazy and add persistent queues and all that fanciness. And on top of that, the script is always fetched from a remote server. If you want to update the logic that is executed, simply change the script that is returned. This means you can impact process execution AT RUNTIME without touching the actual process.

There is a unit test for this at https://github.com/jbarrez/activiti-advanced-scripting/blob/master/src/test/java/org/activiti/test/ExecuteScriptWithJobTest.java

If you run this test,  you’ll see the following. Note that we host the completion script as static file called ‘task-complete.js’ on the test server.

Screen Shot 2013-07-23 at 09.50.36

In the test, you can see we have to execute the async job specifically to see the output of the test.

Caveat

On small caveat here: when the process engine reboots, the configuration will be reloaded from config file. Hence, the process from above that injects custom logic is not added. However, this can easily be done by using a ProcessEngineLifeCycleListener implementation that executes a process definition of a certain category after the engine has booted up. If you for example give all these processes ‘config-processes’ as category, they can easily be executed on bootup.

Conclusion

Scripting in BPMN 2.0 processes is a very powerful feature. It allows you to change process execution engine-wide in a matter of a few lines. Of course, all the code above can be done with Java. But the examples above use nothing more than standard BPMN 2.0 and the javascript engine that is bundled with every JDK install.

Thanks for reading. Happy coding!

5 Comments

  1. jackyrong August 18, 2013

    HI,I download the code of this article and run it,but i got the error message,why?
    org.activiti.engine.ActivitiException: problem evaluating script: sun.org.mozilla.javascript.internal.EvaluatorException: The choice of Java constructor write matching JavaScript argument types ([B) is ambiguous; candidate constructors are:
    void write(int)
    void write(byte[]) (#1) in at line number 1
    at org.activiti.ExecuteScriptJobHandler.execute(ExecuteScriptJobHandler.java:37)
    at org.activiti.engine.impl.persistence.entity.JobEntity.execute(JobEntity.java:79)
    at org.activiti.engine.impl.persistence.entity.MessageEntity.execute(MessageEntity.java:29)
    at org.activiti.engine.impl.cmd.ExecuteJobsCmd.execute(ExecuteJobsCmd.java:70)
    at org.activiti.engine.impl.interceptor.CommandExecutorImpl.execute(CommandExecutorImpl.java:24)
    at org.activiti.engine.impl.interceptor.CommandContextInterceptor.execute(CommandContextInterceptor.java:61)
    at org.activiti.engine.impl.interceptor.LogInterceptor.execute(LogInterceptor.java:31)
    at org.activiti.engine.impl.ManagementServiceImpl.executeJob(ManagementServiceImpl.java:57)
    at org.activiti.test.ExecuteScriptWithJobTest.test(ExecuteScriptWithJobTest.java:59)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.rules.TestWatchman$1.evaluate(TestWatchman.java:53)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
    Caused by: javax.script.ScriptException: sun.org.mozilla.javascript.internal.EvaluatorException: The choice of Java constructor write matching JavaScript argument types ([B) is ambiguous; candidate constructors are:
    void write(int)
    void write(byte[]) (#1) in at line number 1
    at com.sun.script.javascript.RhinoScriptEngine.eval(RhinoScriptEngine.java:110)
    at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:232)
    at org.activiti.ExecuteScriptJobHandler.execute(ExecuteScriptJobHandler.java:35)
    … 34 more

  2. Joram Barrez August 18, 2013

    Which JDK are you running on?

    Also, you’re sure you don’t have Rhino on your classpath?

  3. jackyrong August 19, 2013

    HI,I AM USING JDK 6,and rhino?why must use rhino?i never use it before,

  4. Joram Barrez August 21, 2013

    Could you try running it with jdk 7? To be honest, I don’t think I tested it with JDK 6.

    You should *not* have Rhino on the classpath. I was just asking, because if you have I’ve seen it clash with the default javascripting in the JDK

  5. jackyrong August 21, 2013

    thank you very much!and according to what your introudction about rhino,i begin to know this cool rhino!

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>