SOA-P Creating Custom GUIs for jBPM

Version 3

    jBPM is a BPM capability included with SOA Platform.  At its core, it is a simple Java API wrapping Hibernate.  It can be used to orchestrate ESB services, orchestrate Java actions, create user tasks, submit user tasks, and a host of other activities.

     

    One common question is how users can create GUIs to interact with the jBPM processes.  There are several ways to do this.  Developers could utilize Seam which has built in annotations for interacting with jBPM.  Developers could create a custom web application (JSP, Servlet, Spring, Struts, Seam, etc.) that uses the jBPM Java API directly.  Developers could modify an existing Java client of any type to add interaction with the jBPM processes via the jBPM Java APIs.

     

    In this wiki, we will see how to create a very simple web based GUI that interacts with jBPM.  This GUI will be a single, simple JSP file in a WAR.  The JSP file will provide the capability to:

     

    1.)  Create process definitions

    2.)  Delete process definitions

    3.)  Create process instances

    4.)  Signal process instances

    5.)  Show tasks for a given user

    6.)  Submit tasks

    7.)  Add new variables when submitting a task

    8.)  List variables associated with a process

     

    If a single JSP file has all this functionality, it must be huge right?  Actually, it's less than 200 total lines of code (including all the HTML, comments, and whitespace).  I would not recommend writing a single JSP with everything in it (GUI, buisiness logic, control logic, etc.).  I only did it this way for sake of simplicity for this example.

     

    First, note the attached jBPMProject.war.  This can be directly deployed to SOA-P 4.3 or 5.0 in the "deploy" directory of your server configuration and accessed via "http://localhost:8080/jBPMProject".

     

    You should see a screen like this below.  If you have other processes already deployed with jBPM, you will see them as well and can interact with them also.  So on this screen, click "DeployDefaultProcessDefinition" to deploy an example jBPM process.

     

    1.png

     

    Now we should see the process definition deployed below.  Click the "CreateInstance 101" button to create an instance of this process.

     

    2.png

     

    Now we see the instance we created and that it has 5 nodes:  Start, three tasks, and End.  Also note that there are currently no tasks assigned to "Aaron" as shown at the top of the screen.  Now, we will click the "Signal 122" button to move this process to the "Submit Order Node".

     

    4.png

     

    Now we are in the "Submit Order Node", and we see that there is now a task for Aaron.  The text box below the task is a list of comma separated properties that can be associated with the process as part of submitting the task.  The JSP error handling is not very robust, so make sure you have at least one variable defined, then click the "SubmitTask 149" button to submit Aaron's task.

     

    5.png

     

    Note that the process has moved to the next task which is "Approve Order Node".  Also note that there are no more tasks assigned to Aaron.  So who is the "Approve Order Node" task assigned to?  Well, if you click the "Aaron" dropdown, you'll see there is also a "Bob" user - so select him.

     

    6.png

     

    Sure enough, the "Approve Order Task" is assigned to Bob.  You can add new variable (see Hello World below) and submit this task as well.

     

    10.png

     

    Now we are in the last node of the process, and it is also a task node assigned to Bob.  Note that on the process instance, there are three variables currently associated with it - the two default ones and the new Hello World variable that we added.  Now, click "SubmitTask 151" to finish the last task of this process.

     

    11.png

     

    Now we see that there are no more tasks and the process is in the end state.

     

    12.png

     

    Congratulations on using an ~200 line JSP to create process definitions, proces instances, search tasks, submit tasks, add variables, and signal processes!

     

    Next we'll show the content of the JSP file that is embedded in the attached WAR.  Note that the JSP has basically three sections.

     

    The first section is a set of helper functions to deploy process definitions, signal processes, get user tasks, etc.  These are functions that would likely be reused in any GUI - and note how simple they are.  Note that the deploy process definition actually reads the process definition from an XML file also in the WAR.  Alternatively, this process definition could be deployed:  1.)  From JBDS, 2.)  From Ant, 3.)  From the jBPM admin console.

     

    The second section is the controller logic that makes sure that when the user clicks something on the GUI, it does what the user wanted it to do.

     

    The third section is what actually renders the GUI that we have seen above.

     

    <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8" import="java.util.*,org.jbpm.graph.exe.*,org.jbpm.graph.def.*,org.jbpm.*,org.jbpm.db.*,org.jbpm.taskmgmt.exe.*"%>
    <%!

    //////////////////////////////////////////////////////////////////////
    //
    // Section 1:  Helper functions
    //
    //////////////////////////////////////////////////////////////////////

    public void deployDefaultProcessDefinition(JbpmContext context) {
        
        //You probably would never deploy processes this way, instead would deploy them from JBoss
        //Developer Studio or Ant.  However, this mechanism allows this JSP to create
        //process definitions if none have been deployed.
        context.deployProcessDefinition(ProcessDefinition.parseXmlInputStream(
                this.getClass().getResourceAsStream("/processDefinition.xml")));
    }

    public static List<ProcessDefinition> GetProcessDefinitions(JbpmContext context) {
        GraphSession session = context.getGraphSession();
        return session.findAllProcessDefinitions();
    }

    public static void DeleteProcessDefinition(JbpmContext context, long definitionId) {
        GraphSession session = context.getGraphSession();
        session.deleteProcessDefinition(definitionId);
    }

    public static List<ProcessInstance> GetProcessInstances(JbpmContext context, long definitionId) {
        GraphSession session = context.getGraphSession();
        return session.findProcessInstances(definitionId);
    }

    public static void CreateProcessInstance(JbpmContext context, long definitionId) {
        GraphSession session = context.getGraphSession();
        ProcessInstance process = session.getProcessDefinition(definitionId).createProcessInstance();
        context.save(process);
    }

    public static void DeleteProcessInstance(JbpmContext context, long instanceId) {
        GraphSession session = context.getGraphSession();
        session.deleteProcessInstance(instanceId);
    }

    public static List<TaskInstance> GetUserTasks(JbpmContext context, String user) {
        return (List<TaskInstance>)context.getTaskList(user);
    }

    public static void SubmitTask(JbpmContext context, long taskId, String properties) {
        Map<String,String> propMap = new TreeMap<String,String>();
        String[] propArray = properties.split(",");
        for(String prop : propArray) {
            String[] propPieces = prop.split("=");
            propMap.put(propPieces[0].trim(), propPieces[1].trim());
        }
       
        TaskInstance task = context.getTaskInstance(taskId);
        task.start();
        task.addVariables(propMap);   
        task.end();
    }

    public static void SignalToken(JbpmContext context, long tokenId) {
        Token token = context.getTokenForUpdate(tokenId);
        token.signal();
    }
    public static void MoveToken(JbpmContext context, long tokenId) {
        Token token = context.getTokenForUpdate(tokenId);
        //Should call signal, but can't do this because some processes
        //require real data to be there...
        //token.signal();

        //Instead, we'll find all the nodes and move to the next or end the process.
        //This is not reality, but for demonstration
        Node currentNode = token.getNode();
        ProcessInstance processInstance = token.getProcessInstance();
        List<Node> nodes = processInstance.getProcessDefinition().getNodes();
        for(int i=0; i<nodes.size(); i++) {
           
            //If we found the current node and there are more available...
            if(nodes.get(i).getId() == currentNode.getId() && (i+1) < nodes.size()) {
                token.setNode(nodes.get(i+1));
                continue;
            }
        }
    }
    %>
    <%

    //////////////////////////////////////////////////////////////////////
    //
    // Section 2:  Controller logic
    //
    //////////////////////////////////////////////////////////////////////

    JbpmContext context = JbpmConfiguration.getInstance().createJbpmContext();
    String action = request.getParameter("action");
    if(action != null) {
        if(action.startsWith("CreateInstance")) {
            long definitionId = Long.parseLong(action.split(" ")[1]);
            CreateProcessInstance(context, definitionId);
        }
        else if(action.startsWith("DeleteInstance")) {
            long definitionId = Long.parseLong(action.split(" ")[1]);
            DeleteProcessInstance(context, definitionId);
        }
        else if(action.startsWith("Signal")) {
            long definitionId = Long.parseLong(action.split(" ")[1]);
            SignalToken(context, definitionId);
        }
        else if(action.startsWith("Move")) {
            long definitionId = Long.parseLong(action.split(" ")[1]);
            MoveToken(context, definitionId);
        }
        else if(action.startsWith("DeleteDefinition")) {
            long definitionId = Long.parseLong(action.split(" ")[1]);
            DeleteProcessDefinition(context, definitionId);
        }
        else if(action.startsWith("DeployDefaultProcessDefinition")) {
            deployDefaultProcessDefinition(context);
        }
        else if(action.startsWith("SubmitTask")) {
            SubmitTask(context, Long.parseLong(action.split(" ")[1]), request.getParameter("properties"));
        }
    }

    //////////////////////////////////////////////////////////////////////
    //
    // Section 3:  Rendering the GUI
    //
    //////////////////////////////////////////////////////////////////////

    %>
    <html>
    <body>
    <form method="POST">
    <table border="1" cellpadding="5" cellspacing="1">
    <tr bgcolor="#aaaaff"><td>Tasks for user
       <select name="username" onchange="form.submit()">
          <option value="Aaron" <%=("Aaron".equals(request.getParameter("username"))?"SELECTED=\"SELECTED\"":"")%>>Aaron</option>
          <option value="Bob" <%=("Bob".equals(request.getParameter("username"))?"SELECTED=\"SELECTED\"":"")%>>Bob</option>
       </select>
       <input type="submit" value="Refresh"/></td></tr>

    <%
    for(TaskInstance task : GetUserTasks(context, request.getParameter("username"))) {
    %>
    <tr bgcolor="#eeeeff"><td>Task: <%=task.getName()%>
    <input type="submit" name="action" value="SubmitTask <%=task.getId()%>"/>
    Add Properties to Process: <input type="text" name="properties" value="key1=val1, key2=val2"/>
    </td></tr>
    <% } %>

    <tr><td></td></tr>
    <tr bgcolor="#0000ff"><td align="center">
    <input type="submit" name="action" value="DeployDefaultProcessDefinition"/>
    </td></tr>
    <%
    List<ProcessDefinition> processDefinitions = GetProcessDefinitions(context);

    for(ProcessDefinition definition : processDefinitions) {
        out.println("<tr bgcolor=\"#aaaaff\"><td nowrap><b><font size=\"+1\">");
        out.println(definition.getName());
        out.println("(version " + definition.getVersion() + ")</font></b>");
        out.println("<input type=\"submit\" name=\"action\" value=\"CreateInstance " + definition.getId() + "\"/>");
        out.println("<input type=\"submit\" name=\"action\" value=\"DeleteDefinition " + definition.getId() + "\"/>");
        out.println("</td></tr>");
       
        for(ProcessInstance instance : GetProcessInstances(context, definition.getId())) {
            out.println("<tr bgcolor=\"#eeeeff\"><td>");
            out.println("  Instance #" + instance.getId() + " <input type=\"submit\" name=\"action\" value=\"DeleteInstance " + instance.getId() + "\"/>");
            Map<?,?> variables = instance.getContextInstance().getVariables();
            if(variables != null && variables.entrySet() != null) {
            for(Map.Entry<?,?> e : variables.entrySet()) {
                out.println(e.getKey() +" = " + e.getValue());
            }} else {
                out.println("No variables");
            }
            List<Node> nodes = instance.getProcessDefinition().getNodes();
           
            Token token = (Token)instance.findAllTokens().get(0);
            Node currentNode = token.getNode();
            for(Node node : nodes) {
                out.println("<br>    Node: " + node.getName());

                if(node.getId()  == currentNode.getId()) {
                    out.println("<font color=\"red\"> - (active)</font>");
                   
                    //If not the last node
                    if(node.getId() != nodes.get(nodes.size()-1).getId()) {
                        out.println(" <input type=\"submit\" name=\"action\" value=\"Signal " + token.getId() + "\"/>");
                        out.println(" <input type=\"submit\" name=\"action\" value=\"Move " + token.getId() + "\"/>");
                    }
                }
            }
            out.println("</td></tr>");
        }
    }

    context.close();
    %>
    </table>
    </form>
    </body>
    </html>

     

    Note that the template for almost everything done with jBPM looks like this:

    JbpmContext context = JbpmConfiguration.getInstance().createJbpmContext();
    GraphSession session = context.getGraphSession();

    //Get process defintion, processes, tasks, signal, submit, etc.

    context.close();

     

    I hope this has been helpful in showing how you can utilize the jBPM APIs to very easily interact with jBPM processes.  If you are using JBoss Seam or ESB in the SOA Platform, there is other integration that is out of the box.  But anytime you want to interact directly with jBPM via the APIs to create task inboxes, integrate with existing GUIs, or anything like that, it should be as simple as a few lines of code.