Invoking BPEL Worklist API from Remote Server with Java

Recently, I was working on an Forms application that required a Form to retrieve and complete a Human Workflow task from the worklist of a BPEL 10.1.3 server (also see this post, where you’ll see how Java code very similar to the code that I will provide here can be embedded in Oracle Forms). Although it wasn’t all that difficult, it did take some time getting everything configured correctly, and judging from some of the forum posts out there I was not the only one to struggle with it. This post will take you through the setup of a new JDeveloper project and the creation of a simple POJO that will be able to query and manipulate the worklist of a remote BPM server.

Environment Requirements

I have tested the steps in this blog post using a SOA Suite 10.1.3.1 installation, and JDeveloper 10.1.3.1. Although the process will probably be pretty similar in older versions, I cannot vouch for this..

Project Setup

As usual, the most difficult part isn’t the Java code but setting up the classpath and the environment description files. Especially since the only error message you seem to get when _anything_ is wrong is:

ORABPEL-30028 Invalid configuration file wf_config.xml
The configuration file wf_config.xml not be read.
Make sure that the configuration file wf_config.xml is available and is a valid XML document.
Contact oracle support if error is not fixable.

That sure sounds like the ticket to solving all your problems is a file called wf_config.xml, isn’t it? Well, maybe in some cases, but as we will see this file isn’t even part of the solution (at least not the one used here). But we’re getting ahead of things. Lets first create the JDeveloper Application (make sure the Application Template is “No Template [All Technologies]“):

Create Application

And then the project:

Create Project

Next thing is to set the classpath/libraries on the newly created Project. You can do that by double-clicking the newly created Project and choosing the “Libraries” page.

You can get all the necessary code to compile using the few standard JDeveloper libraries shown below (add them using the “Add Library” button), but unfortunately you can not get it to run correctly. Apparently, it is expected that you deploy all your code to the BPM server, but in my case the Java code was running on a different application server. Therefore, in addition to the standard libraries shown below, I had to add the orabpel jars to the classpath as well. You can locate these jars in the bpel/bin directory below the OracleAS root directory. You can copy these jar files to your project directory and add them using the “Add Jar/Directory” button.

The final classpath of the project therefore looks like this:

Project Libraries

Note: The library “Oracle9iAS” has a confusing name, it actually points to the embedded OC4J of JDeveloper 10.1.3.1 which is, of course, an OC4J 10.1.3.1.

Now before we can start writing code, there’s one more thing we need to do. When connecting remotely to the BPM Server (SOA Suite 10.1.3.1 in my case), you need a file called “wf_client_config.xml” on your classpath. You can copy this file from the /bpel/system/services/config directory below the OracleAS root directory. Place the file in the src directory of your JDeveloper project (create it if it isn’t there), and hit the refresh button to have it shown in JDeveloper. In all likeliness, the only thing you’ll need to change in the content of the file is the first part, where you should use the commented as we are dealing with an OPMN managed instance rather than a standalone OC4J:

<servicesClientConfigurations xmlns="http://xmlns.oracle.com/bpel/services/client">
   <ejb>
      <!--serverURL>ormi://soaserver:home/hw_services</serverURL --> <!-- for stand alone -->
      <serverURL>opmn:ormi://soaserver/hw_services</serverURL>  <!-- for opmn managed instance -->
      <user>oc4jadmin</user>
      <password>secret</password>
      <initialContextFactory>oracle.j2ee.rmi.RMIInitialContextFactory</initialContextFactory>
   </ejb>

If for some reason you run into problems connecting to the SOA Suite later on (which problably manifests itself as beforementioned message “ORABPEL-30028 Invalid configuration file wf_config.xml “), chances are the solution is in the content of this file. Also be aware of the fact that the OPMN is very particular about the IP address on which it is invoked; if it is started with primary IP address xxx.xxx.xxx.xxx, it won’t respond if the DNS resolves its name (soaserver in this case) to yyy.yyy.yyy.yyy, even though this might be a perfectly valid address for the machine on which the OPMN is running!

The Code

Now that the project has been set up, you’ll find that the actual code with which you can access the Worklist is actually fairly straightforward. As an example, we’ll create a simple service-like class:

public class WorklistService
{
  //  Change these constants to match your setup
  private static final String WF_MANANAGER_UN  = “oc4jadmin”;
  private static final String WF_MANANAGER_PW  = “welcome1”;
  private static final String LDAP_DOMAIN  = “jazn.com”;

  String worklistUser;
  IWorkflowServiceClient client;
  ITaskQueryService taskQueryService;
  ITaskService taskService;
IWorkflowContext workflowContext;

  public WorklistService(String userName)
  {
    worklistUser = userName;
  }
}  

To this class, we’ll add a few methods one by one that will explain the use of the Worklist API.

Connecting to the Workflow Service

The “main point of entry” for all worklist-related activities is the IWorkflowServiceClient. This is actually an interface for which a couple of implementations exist, which can be obtained through a factory:

  public IWorkflowServiceClient getWorkflowServiceClient()
  {
    if (client == null)
    {
      System.out.println("Attempting to create WorkflowServiceClient");
      client = WorkflowServiceClientFactory.getWorkflowServiceClient(WorkflowServiceClientFactory.REMOTE_CLIENT);
      System.out.println("Created WorkflowServiceClient");
    }
    return client;
  }

The REMOTE_CLIENT constant that is passed into the factory method returns a WorkflowServiceClient that is able to correspond with a remote SOA Suite installation. Other “flavours” are JAVA_CLIENT, LOCAL_CLIENT, WSIF_CLIENT and SOAP_CLIENT, with which I do not have much experience at this time.

With an instance of IWorkflowServiceClient, we can now get access to a number of more specialized Service objects. We’ll start with a Service that allows us to perform queries against the worklist, the ITaskQueryService:

  public ITaskQueryService getTaskQueryService()
  {
    if (taskQueryService == null)
    {
      System.out.println("Obtaining TaskQueryService");
      taskQueryService = getWorkflowServiceClient().getTaskQueryService();
    }
    return taskQueryService;
  }

On this specialized ITaskQueryService object, we can invoke several interesting methods, such as queryTasks(), getTaskDetailsById() etc. All these methods, however, have a required parameter of type IWorkflowContext. Simply put, this means that you can not invoke these methods unless you are “logged in” to the worklist of a specific user. Therefore, we’ll need the following method first:

  public IWorkflowContext getWorkflowContext() throws Exception
  {
    if (workflowContext == null)
    {
        System.out.println("Creating WorkflowContext");
        workflowContext = getTaskQueryService().authenticate(WF_MANANAGER_UN, WF_MANANAGER_PW, LDAP_DOMAIN, worklistUser);
    }
    return workflowContext;
  }

The authenticate method takes 4 String arguments, the last of which is optional (that is, could be null). They are: username, password, ldap domain and “on-behalf-of-user”. If your code has access to the username and password of the Workflow user of which you want to query the worklist, you only need to supply values for the first three arguments and leave the fourth null. However, if, as is very likely, you know the username but NOT the password of this user, you can use the fourth argument to create a IWorkflowContext on behalf of this user, while logging in as the OC4J administrator. In this example we use the latter approach.

Querying the Worklist

Well, we have the “low level plumbing” in place, so lets get to the more interesting stuff: perfoming a query against the Worklist:

  public String[] getTasksForUser() throws Exception
  {
    System.out.println("Finding tasks for user "+worklistUser);
    // The Predicate class is used to construct a “whereclause” for querying the worklist.

    Predicate whereclause = new Predicate(TableConstants.WFTASK_STATE_COLUMN,
                                          Predicate.OP_EQ,
                                          IWorkflowConstants.TASK_STATE_ASSIGNED);

    // Using the addClause method, you can create much more complex whereclauses.
    // Note that we do not need a clause to restrict the results to the current user, that
    // is already guaranteed by means of the IWorklistContext.
    //
    // whereclause.addClause(Predicate.AND,
    //                       TableConstants.WFTASK_TITLE_COLUMN,
    //                       Predicate.OP_CONTAINS,
    //                       “<any string that the Task Title should contain>”);
   

    // set list of columns / optional info 
    List displayColumns = new ArrayList();
    displayColumns.add(“CREATEDDATE”);
    displayColumns.add(“TITLE”);
    displayColumns.add(“IDENTIFICATIONKEY”);
    List optionalInfo = new ArrayList();
    //optionalInfo.add(“Actions”); // actions not needed for listing page

    List tasks = getTaskQueryService().queryTasks(getWorkflowContext(), displayColumns, optionalInfo
                              “My+Group”, null, whereclause, null, 00);

    if (tasks != null)
    {
      String[] taskList = new String[tasks.size()];
      for (int i=0; i< tasks.size(); i++)
      {
        Task t = (Task)tasks.get(i)

=”#000000″>;
        taskList[i= retrieveTaskData(t);
        System.out.println(“Found task: “+taskList[i]);
      }
      return taskList;
    }
    else
    {
      System.out.println(“Did not find any tasks”);
      return new String[]{};
    }
  }
  
  
  protected String retrieveTaskData(Task t)
  {
    return t.getSystemAttributes().getTaskId();
  }

There are a couple interesting things to notice here. First is the use of the Predicate class to effectively construct a whereclause with which to query the worklist. Notice in the commented code an example of how arbitrarily complex whereclauses can be constructed using the addClause() method, using Predicate.AND and Predicate.OR and a large number of logical operators such as OP_EQUALS, OP_CONTAINS, OP_IS_NOT_NULL, OP_GTE etcetera.

Second is the bit where a List of display columns is constructed. Here you can see that the API of the ITaskQueryService is created with the typical needs of a worklist application in mind. Each individual Task contains quite a lot of data, and then there is the custom “task payload” that could also be of considerable size. It would be very inefficient if ALL the data for EACH Task in the result set has to be queried into memory, just to show a few fields in a worklist table. Therefore, the Tasks that are returned by the queryTasks() method are not “full blown” Tasks, but rather like placeholders, surrogates. Although they implement the Task interface, all methods will return null except for the “display columns” that are provided to the method as a List. In the code above, I have chosen to query the creation date, title and identification key attributes. Unfortunately, I have not found a constants class for these “display columns”, so I had to make educated guesses. If anyone knows where these constants can be found, a comment on this blog would be greatly appreciated!

Finally, you might wonder about the return type: a String array where each String is constructed by invoking method retrieveTaskData. This was the most convenient signature for embedding this Java code in an Oracle Forms application, see this post for further details.

Completing Tasks

Now that we have retrieved Task information from the worklist, let’s add a few methods to demonstrate how Tasks that have just been queried can be completed programmatically. First we’ll need a new Service object because the ITaskQueryService does not (what’s in a name) provide us with any methods that allow us to make changes to Tasks. For that, we’ll need an instance of ITaskService:

  private ITaskService getTaskService()
  {
    if (taskService == null)
    {
      log.debug("Obtaining TaskService");
      taskService = getWorkflowServiceClient().getTaskService();
    }
    return taskService;
  }  
  

The last method of this post will demonstrate how to complete a Task.

  public void completeTask(String taskId, String outcomethrows Throwable
  {
    Task task = getTaskQueryService().getTaskDetailsById(getWorkflowContext(), taskId);
    if (task != null)
    {
      System.out.println("Completing task: "+task.getTitle());
      getTaskService().updateTaskOutcome(getWorkflowContext(), task, outcome);
    }
  }

The thing to notice here is that because the Tasks that were returned earlier by the queryTasks() method are “surrogates” and not “the real thing”, we will first need to retrieve the full Task object. For this we use the getTaskById() method. With this Task, we can invoke the updateTaskOutcome() method of the TaskService.

Interesting detail here is that, had we made any changes to the Task here, these changes would have been persisted as well. In a future post, I’ll show how to manipulate the “Task payload” programmatically.

Finally, and just for completeness’ sake, I’ll provide a main method to test the methods listed above (as if you couldn’t come up with this yourself ;-D).

  public static void main(String[] args)
  {
    try
    {
      WorklistService worker = new WorklistService("jcooper");
      String[] taskIds = worker.getTasksForUser();
      // Quite arbitrarily, we complete the first Task in the worklist
      if (taskIds.length > 0)
      {
        worker.completeTask(taskIds[0],"APPROVE");
      }
    }
    catch (Throwable e)
    {
      e.printStackTrace();
    }
  }
  

Conclusion

Creating a remote connection to a BPEL PM server to programmatically manipulate the Tasks in the Worklist requires little and rather trivial coding. The trickiest part is getting the compile-time and runtime classpath right, and getting the all-important wf-client-config.xml file in place. From there, the possibilities are endless. For instance, check this blog entry!

34 Comments

  1. Irshad Buchh March 22, 2011
  2. rjoshi October 12, 2009
  3. rjoshi October 9, 2009
  4. Rajshekhar September 16, 2009
  5. Craig April 2, 2009
  6. Telman March 18, 2009
  7. Telman March 17, 2009
  8. Peter Ebell March 6, 2009
  9. Amit February 28, 2009
  10. Uday November 27, 2008
  11. Muditha November 11, 2008
  12. Peter Ebell December 12, 2007
  13. Murali December 11, 2007
  14. Peter Ebell December 6, 2007
  15. Thirumal December 4, 2007
  16. Thirumal December 4, 2007
  17. Istvan Kiss November 2, 2007
  18. Istvan Kiss November 1, 2007
  19. Alexandre October 9, 2007
  20. Rama October 4, 2007
  21. Benoît de CHATEAUVIEUX October 2, 2007
  22. Bharanidhar October 1, 2007
  23. Bharanidhar September 30, 2007
  24. Rama September 25, 2007
  25. Bharanidhar September 23, 2007
  26. Bharanidhar September 23, 2007
  27. Peter Ebell September 21, 2007
  28. Bharanidhar September 19, 2007
  29. Gunnar September 11, 2007
  30. Patrick May 30, 2007
  31. Jose Espinzoa May 24, 2007
  32. Dave May 14, 2007
  33. sukri April 28, 2007
  34. Mario Rios February 23, 2007