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.
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..
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<br />The configuration file wf_config.xml not be read.<br />Make sure that the configuration file wf_config.xml is available and is a valid XML document.<br />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]"):
And then the 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:
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="<a href="http://xmlns.oracle.com/bpel/services/client">http://xmlns.oracle.com/bpel/services/client</a>"> <ejb><br /> <font color="#666666"><!--serverURL>ormi://soaserver:home/hw_services</serverURL --> <!-- for stand alone --><br /></font> <strong><serverURL>opmn:ormi://soaserver/hw_services</serverURL></strong> <font color="#666666"><!-- for opmn managed instance --></font><br /> <user>oc4jadmin</user><br /> <password>secret</password><br /> <initialContextFactory>oracle.j2ee.rmi.RMIInitialContextFactory</initialContextFactory><br /> </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!
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:
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:
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:
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:
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:
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.
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:
The last method of this post will demonstrate how to complete a Task.
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).
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!