Jbpm Plugin

Jbpm is an open source workflow / BPM solution. It helps coordinate business processes involving multiple people or services. The goal of this plugin is to make it easier to use Jbpm in a grails application

Features

  • Easy integration with jbpm (handles all the configuration changes required)
  • Manage processes - deploy, update process definitions
  • Worklist application to quickly prototype new business processes

Usage

The plugin can be used in two modes. The basic mode provides the integration support and support for automatically deploying process definitions. It also makes the jbpmTemplate (provided by spring modules) as a spring bean that can be used from within a grails application (Controller / Service etc) to interact with the jbpm.

On top of this the plugin also provides a worklist application.

Basic Mode

Create a grails application called "Expense" and install the jbpm plugin

grails install-plugin jbpm

The process definitions need to placed under

grails-app/conf/jbpm/processes
. Each process definition needs to be created under a separate folder and file name should be processdefinition.xml (this is the format used by the eclipe jpdl plugin) Create a folder called "Expense Reimbursement" and add the following contents to a file named processdefinition.xml under that folder.

Download

<?xml version="1.0" encoding="UTF-8"?>
<process-definition  xmlns=""  name="Expense Reimbursement">
    <swimlane name="ROLE_EMPLOYEE"><assignment actor-id="employee"/></swimlane>
    <swimlane name="ROLE_MANAGER"><assignment actor-id="manager"/></swimlane>
    <swimlane name="ROLE_ACCOUNTANT"><assignment actor-id="accountant"/></swimlane>

<start-state name="New Expense Claim"> <task swimlane="ROLE_EMPLOYEE"/> <transition to="Manager Review" name="Submit"/> <transition to="Draft" name="Save As Draft"></transition> </start-state>

<task-node name="Manager Review"> <task swimlane="ROLE_MANAGER"/> <transition to="Settle Claims" name="Approve"/> <transition to="Amend Claim" name="Amend"/> <transition to="Rejected" name="Reject"/> </task-node>

<task-node name="Amend Claim"> <task swimlane="ROLE_EMPLOYEE"/> <transition to="Manager Review" name="Re Submit"/> </task-node>

<task-node name="Settle Claims"> <task swimlane="ROLE_ACCOUNTANT"/> <transition to="Settled" name="Pay"/> <transition to="Rejected" name="Reject"/> </task-node>

<task-node name="Draft"> <task swimlane="ROLE_EMPLOYEE"/> <transition to="Manager Review" name="Submit"/> <transition to="Deleted" name="Delete"/> </task-node>

<end-state name="Settled"/> <end-state name="Rejected"/> <end-state name="Deleted"/> </process-definition>

Run the app and navigate to, you should the "Expense Reimbursement" process listed.

http://localhost:8080/Expense/process/list

You can then use the jbpmTemplate to hook it up with the rest of the grails app. For example the following code will create new instance of the process

class MyController {
    def jbpmTemplate

def instantiate = { jbpmTemplate.execute() { ctx -> def processInstance = ctx.newProcessInstanceForUpdate("Expense Reimbursement") // write code here to associate process variables

processInstance.signal() } } }

The jbpmTemplate's execute method takes a closure as input. The closure is invoked with one argument - the JbpmContext. The cotext can be used to perform various operations (deploy process definitions, create a new instance of a process, find pending tasks assigned to an user etc)

Using the Worklist Application

The plugin also provides a worklist application that can be used to quickly prototype the business process. To make use of the worklist application you will need to use the Acegi plugin for authentication and adhere to the following conventions.

  • The actor-id used in the jbpm process definition should be same as the user name
  • The swmilane name should be same as the role name
  • Create GSP templates for the list, detail view and form for instatiating a process
The following instructions will guide you through the setup required to use the worklist application.

Install the acegi plugin.

grails install-plugin acegi

and then create the auth domain classes using.

grails create-auth-domains

Add the following code to the BootStrap for setting up the users & Roles. We are setting up three roles (ROLE_EMPLOYEE, ROLE_MANAGER and ROLE_ACCOUNTANT) and one user per role (employee, manager and accountant). Please note that these should match with the swimlane and the actor-id specified in the processdefinition.

Donwload

class BootStrap {
     def authenticateService
     def init = { servletContext ->
     	createDefaultRoles()
     	createDefaultUsers()
        createRequestMap()
     }

private def createDefaultRoles() { if (!Authority.findByAuthority('ROLE_EMPLOYEE')) new Authority(authority:'ROLE_EMPLOYEE',description:"Employee").save()

if (!Authority.findByAuthority('ROLE_MANAGER')) new Authority(authority:'ROLE_MANAGER',description:"Manager").save()

if (!Authority.findByAuthority('ROLE_ACCOUNTANT')) new Authority(authority:'ROLE_ACCOUNTANT',description:"Accountant").save() }

private def createDefaultUsers() { def employee = Person.findByUsername('employee') if (!employee) { employee = new Person(username:'employee',userRealName:'Employee', passwd : authenticateService.passwordEncoder('welcome'), email:'employee@lxisoft.com',enabled:true) employee.save() }

def manager = Person.findByUsername('manager') if (manager == null) { manager = new Person(username:'manager',userRealName:'Manager', passwd : authenticateService.passwordEncoder('welcome'), email:'manager@lxisoft.com',enabled:true) manager.save() }

def accountant = Person.findByUsername('accountant') if (accountant == null) { accountant = new Person(username:'accountant',userRealName:'Accountant', passwd : authenticateService.passwordEncoder('welcome'), email:'accountant@lxisoft.com',enabled:true) accountant.save() }

Authority.findByAuthority('ROLE_EMPLOYEE').addToPeople(employee) Authority.findByAuthority('ROLE_MANAGER').addToPeople(manager) Authority.findByAuthority('ROLE_ACCOUNTANT').addToPeople(accountant) }

private def createRequestMap() { new Requestmap(url: '/login/**', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').save() new Requestmap(url: '/task/**', configAttribute: 'IS_AUTHENTICATED_FULLY').save() new Requestmap(url: '/process/**', configAttribute: 'IS_AUTHENTICATED_FULLY').save() new Requestmap(url: '/**', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').save() }

def destroy = { } }

Create a domain class "ExpenseClaim" for capturing the details of the expense claim.

Download

class ExpenseClaim {
    def String purpose
    def Date date
    def double amount
}

We now need to create a form to enable an employee to submit a new expense claim and kick start the process. For doing that we need to create a GSP template. Create the file "_New Expense Claim_create.gsp" (the name is derived from the name given to the start-state in the process definition) under the folder "grails-app/views/jbpm/templates/Expense Reimbursement" with the following contents

Download

<table>
    <tr class="prop">
        <td class="name" valign="top">
            <label for="customer">Purpose: </label>
        </td>
        <td clas="value" valign="top">
            <input type="text" id="purpose"
                   name="${jbpm.ctxVariable('expenseClaim.purpose')}"
                    value="${expenseClaim?.purpose?.encodeAsHTML()}"/>
        </td>
    </tr>
    <tr class="prop">
        <td class="name" valign="top">
            <label for="product">Date:</label>
        </td>
        <td clas="value" valign="top">
            <g:datePicker name="${jbpm.ctxVariable('expenseClaim.date')}" value="${expenseClaim?.date}" precision="day"/>
        </td>
    </tr>
    <tr class="prop"> 
        <td class="name" valign="top"><label for="product">Amount:</label> </td> 
        <td clas="value" valign="top"> 
            <input type="text" id="amount" name="${jbpm.ctxVariable('expenseClaim.amount')}"   
                    value="${expenseClaim?.amount?.encodeAsHTML()}"/> 
         </td> 
    </tr> 
</table>

Also create the process wide templates for the list and details views. Create the file "_list_header.gsp" under the folder "grails-app/views/jbpm/templates/Expense Reimbursement" with the following contents

Download

<tr>
      <th>ID</th>
      <th>Purpose</th>
      <th>Date</th>
      <th>Amount</th>
      <th>Filed Date</th>
  </tr>

Create the file "_list_body.gsp" under the folder "grails-app/views/jbpm/templates/Expense Reimbursement" with the following contents

Download

<tr>
    <td><g:link action="details" id="${taskInstance.id}">${taskInstance.id}</g:link></td>
    <td>${taskInstance.variables['expenseClaim']?.purpose}</td>
    <td>${taskInstance.variables['expenseClaim']?.date}</td>
    <td>${taskInstance.variables['expenseClaim']?.amount}</td>
    <td>${taskInstance.create}</td>
</tr>

Create the file "_details.gsp" under the folder "grails-app/views/jbpm/templates/Expense Reimbursement" with the following contents

Donwload

<table>
    <tr class="prop">
        <td class="name" valign="top">
            <label for="customer">Purpose: </label>
        </td>
        <td clas="value" valign="top">
            <input type="text" id="purpose"
                   name="${jbpm.ctxVariable('expenseClaim.purpose')}"
                    value="${expenseClaim?.purpose?.encodeAsHTML()}"/>
        </td>
    </tr>
    <tr class="prop">
        <td class="name" valign="top">
            <label for="product">Date:</label>
        </td>
        <td clas="value" valign="top">
            <g:datePicker name="${jbpm.ctxVariable('expenseClaim.date')}" value="${expenseClaim?.date}" precision="day"/>
        </td>
    </tr>
    <tr class="prop"> 
        <td class="name" valign="top"><label for="product">Amount:</label> </td> 
        <td clas="value" valign="top"> 
            <input type="text" id="amount" name="${jbpm.ctxVariable('expenseClaim.amount')}"   
                    value="${expenseClaim?.amount?.encodeAsHTML()}"/> 
         </td> 
    </tr> 
</table>

You can test drive the process now. Login as employee and navigate to the following url

http://localhost:8080/Expense/task/list

Click on the "New Expense Claim" link, fill out the details, choose the action as "Save As Draft" and click done. This would instantiate a new process. The newly created process would create a new task entry for "Draft". Click on the "Draft" link on the left pane. The newly created claim will be shown in the list. You can click on the ID to bring up the details column, which will bring up the Expense claim details form. Choose the action as "Submit" and click on done. This would submit the claim for manager review. You can now login as manager and see this expense claim listed under the "Manager Review" task.

The list and detail page can be customized per task basis by creating a folder with the task name and creating the files _list_header.gsp, _list_body.gsp and _details.gsp under that folder. This is required for tasks where the fields to be shown are different or some of the fields need to be made non-editable.

TODO - include more details on the convention used to locate the templates.

Donwload Sample Application

Roadmap

  • Add pagination & sorting support for the task list
  • Validation support for create & details forms
  • Add support for implementing action, decision and assignment handlers as grails artifacts
  • Redeploy process only if it has changed
Author(s):
  • Kamal Govindraj (kamal.gs@lxisoft.com)
  • Chetan Mehrotra (chetan.mehrotra@gmail.com)
  • and other developers at LXI Technologies

3 Comments

  • Gravatar
    Hi, I´m trying yout plugin. Congratulations, it´s very nice.

    But I´m trying to write some action code and use with it. Both in Java and in Groovy the jbpm classloader doesn´t find my class. Any clue in this?

    The error is:

    009-03-19 17:38:24,421 8409752@qtp0-0 (+) ERROR instantiation.Delegation - couldn't load delegation class 'MyAction2' java.lang.ClassNotFoundException: class 'MyAction2' could not be found by the process classloader at org.jbpm.instantiation.ProcessClassLoader.findClass(ProcessClassLoader.java:118) at java.lang.ClassLoader.loadClass(Unknown Source) at java.lang.ClassLoader.loadClass(Unknown Source) at org.jbpm.instantiation.Delegation.instantiate(Delegation.java:140) at org.jbpm.instantiation.Delegation.getInstance(Delegation.java:125)

    Mar 19, 2009 15:03 PM joaocm
  • Gravatar
    Hi, I had the same problem with Jbpm classloader not finding my class. (BTW: It works if deployed as WAR -> @grails run-war@) I intended to use a Grails service as action handler.

    I'm not sure if it's the right way to do it, but it works for me. I wrote a JbpmHandlerProxy class(code is derived from @spring-modules-jbpm31 org.springmodules.workflow.jbpm31.JbpmHandlerProxy.java@) created a JAR archive and but it in Jbpm Plugins LIB directory (@.../jbpm-0.1/lib@). Now I can use Grails services as ActionHandlers using:

    …
    <action name="gendoc" config-type="bean" class="com.grintec.jbpm.JbpmHandlerProxy">
        <targetBean>generateDocumentsService</targetBean>
    </action>
    ...

    JbpmHandlerProxy.java:

    package jbpm;

    import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.codehaus.groovy.grails.web.context.ServletContextHolder; import org.codehaus.groovy.grails.web.servlet.GrailsApplicationAttributes; import org.jbpm.context.exe.ContextInstance; import org.jbpm.graph.def.ActionHandler; import org.jbpm.graph.exe.ExecutionContext; import org.jbpm.graph.exe.Token; import org.jbpm.graph.node.DecisionHandler; import org.jbpm.taskmgmt.def.AssignmentHandler; import org.jbpm.taskmgmt.def.TaskControllerHandler; import org.jbpm.taskmgmt.exe.Assignable; import org.jbpm.taskmgmt.exe.TaskInstance; import org.springframework.context.ApplicationContext;

    /** * Handler that allow retrieval of Grails services from within jBPM. * The class implements the family of jBPM handler interfaces * namely: ActionHandler, AssignmentHandler, DecisionHandler, * TaskControllerHandler. The class can be used through jBPM delegation * facilities, for example: * * <pre> * &lt;action config-type=&quot;bean&quot; class=&quot;com.grintec.jbpm.JbpmHandlerProxy&quot;&gt; * &lt;beanName&gt;GrailsTestService&lt;/beanName&gt; * &lt;/action&gt; * </pre> * * where beanName represents a jBPM actionHandler defined inside Grails * container using it's capabilities (IoC, AOP, etc). * */ public class JbpmHandlerProxy implements ActionHandler, AssignmentHandler, DecisionHandler, TaskControllerHandler {

    /** * */ private static final long serialVersionUID = 1L;

    private static final Log logger = LogFactory.getLog(JbpmHandlerProxy.class);

    /** * Grails beanName name. */ private String targetBean;

    /** * @return Returns the beanName. */ public String getTargetBean() { return targetBean; }

    /** * @param targetBean The beanName to set. */ public void setTargetBean(String bean) { this.targetBean = bean; }

    /** * Retrieves the ApplicationContext. * * @return */ protected ApplicationContext retrieveApplicationContext() { ApplicationContext ctx = (ApplicationContext) ServletContextHolder.getServletContext().getAttribute( GrailsApplicationAttributes.APPLICATION_CONTEXT); return ctx; }

    /** * Find the beanName inside the Grails container. * * @return */ protected Object lookupBean(Class type) { return retrieveApplicationContext().getBean(getTargetBean(), type); }

    /** * @see org.jbpm.graph.node.DecisionHandler#decide(org.jbpm.graph.exe.ExecutionContext) */ public String decide(ExecutionContext executionContext) throws Exception { DecisionHandler handler = (DecisionHandler) lookupBean(DecisionHandler.class); if (logger.isDebugEnabled()) logger.debug("using Grails-managed decisionHandler=" + handler); return handler.decide(executionContext); }

    /** * @see org.jbpm.graph.def.ActionHandler#execute(org.jbpm.graph.exe.ExecutionContext) */ public void execute(ExecutionContext executionContext) throws Exception { ActionHandler action = (ActionHandler) lookupBean(ActionHandler.class);

    if (logger.isDebugEnabled()) logger.debug("using Grails-managed actionHandler=" + action);

    action.execute(executionContext); }

    /** * @see org.jbpm.taskmgmt.def.AssignmentHandler#assign(org.jbpm.taskmgmt.exe.Assignable, * org.jbpm.graph.exe.ExecutionContext) */ public void assign(Assignable assignable, ExecutionContext executionContext) throws Exception { AssignmentHandler handler = (AssignmentHandler) lookupBean(AssignmentHandler.class); if (logger.isDebugEnabled()) logger.debug("using Grails-managed assignmentHandler=" + handler);

    handler.assign(assignable, executionContext); }

    /* * (non-Javadoc) * @see org.jbpm.taskmgmt.def.TaskControllerHandler#initializeTaskVariables(org.jbpm.taskmgmt.exe.TaskInstance, * org.jbpm.context.exe.ContextInstance, org.jbpm.graph.exe.Token) */ public void initializeTaskVariables(TaskInstance taskInstance, ContextInstance contextInstance, Token token) { TaskControllerHandler handler = (TaskControllerHandler) lookupBean(TaskControllerHandler.class); if (logger.isDebugEnabled()) logger.debug("using Grails-managed taskControllerHandler=" + handler); handler.initializeTaskVariables(taskInstance, contextInstance, token); }

    /* * (non-Javadoc) * @see org.jbpm.taskmgmt.def.TaskControllerHandler#submitTaskVariables(org.jbpm.taskmgmt.exe.TaskInstance, * org.jbpm.context.exe.ContextInstance, org.jbpm.graph.exe.Token) */ public void submitTaskVariables(TaskInstance taskInstance, ContextInstance contextInstance, Token token) { TaskControllerHandler handler = (TaskControllerHandler) lookupBean(TaskControllerHandler.class); if (logger.isDebugEnabled()) logger.debug("using Grails-managed taskControllerHandler=" + handler); handler.submitTaskVariables(taskInstance, contextInstance, token); }

    }

    Jun 04, 2009 10:06 AM martins
  • Gravatar
    Hi,I have been being using your jbpm plugin.I would like to represent the visual status of each tasknode on the gsp pages,could you please show me an example? Thank you!
    Jun 04, 2009 23:06 PM crazyboy

Post a Comment