Creating the domain layer for the worklist

This post is part of a series on building a custom worklist for BPM/SOA 11g.

Introduction

In the earlier posts in this series we have set up our development environment and created a skeleton project.  Now we are ready to create our ‘business logic’ for the worklist application.  This is the Model part of the application, the part that interacts with the BPM API.

We are going to build four classes:

  • com.oracle.ateam.domain.MTaskList
    This class is a wapper around the BPM API that provides us access to the APIs that we need and handles caching of important objects.
  • com.oracle.ateam.domain.MTask
    This class is a wrapper around the Task class in the BPM API that makes the interface simpler, so that it will be easier for us to build the View.
  • com.oracle.ateam.domain.ContextCache
    This class is used to cache security credentials for the duration of a user’s session.
  • com.oracle.ateam.util.MLog
    This class is used for logging to the WebLogic console.

Tour of the BPM API

Before we begin, let’s take a quick tour of the important parts of the BPM API to get familiar with the problem space.

The BPM API is documented here.  The three key classes that we are interested in are:

  • oracle.bpel.services.workflow.query.ejb.TaskQueryServiceBean, and matching interface oracle.bpel.services.workflow.query.ITaskQueryService
  • oracle.bpel.services.workflow.task.ejb.TaskServiceBean, and matching interface oracle.bpel.services.workflow.task.ITaskService
  • oracle.bpel.services.workflow.services.task.model.Task

Before we can use any of the BPM APIs, we need to authenticate with the workflow engine.  We took a design decision to delegate security to WebLogic Server.  This means that we do not have to handle any user information, including credentials, in our application.  This contributes to making our application more secure and also simplifies administration and maintenance.

To authenticate to the workflow engine, we use the following API:

IWorkflowContext TaskQueryService.createContext(
  HttpServletRequest request)

We will configure authentication rules in the WebLogic deployment descriptor to control when a user must login.  This will be covered in a later post.  We will use an HTML FORM to collect the username and password and send these to WebLogic for authentication.  Once the user has successfully authenticated to WebLogic, we will have an authenticated HttpServletRequest coming in to our Controllers.  In our LoginController (which we will cover in a later post) we will pass this to the createContext() method to obtain an IWorkflowContext.  We will then need to send this context back to the workflow engine with every subsequent request.

We use the TaskQueryService to obtain a list of tasks from the workflow engine.  This is done using the queryTasks() method.  Let’s take a look at that method:

List ITaskQueryService.queryTasks(
  IWorkflowContext ctx,
  java.util.List displayColumns,
  java.util.List optionalInformation,
  ITaskQueryService.AssignmentFilter assignmentFilter,
  java.lang.String keywords,
  Predicate predicate,
  Ordering ordering,
  int startRow,
  int endRow)

This method is going to return a List of tasks.  We will take a look at the Task class shortly.  The first parameter is the context we discussed in the previous paragraph.

Second, we need to pass a list of the columns that we want populated in the response.  The API will just return the information that we ask for.  It is designed this way to minimise the load on the system and data volumes.  All other columns will not be populated in the response.

We will pass an ArrayList which contains a list of the columns we want.  In this example, we ask for TASKID, TASKNUMBER, TITLE, STATE, OUTCOME and PRIORITY.  The column names are documented  in the JavaDoc for the ITaskQueryService interface (see link above).

The optionalInformation parameter is used to request that additional information be populated in the response, e.g. the payload, attachments, etc.  We will not be using this.

The assignmentFilter is used to tell the workflow engine which tasks we are interested in, based on who they are assigned to (if anyone).  There are a set of constants defined in ITaskQueryService.AssignmentFilter that we will use to specify what we want.  Our default will be ITaskQueryService.AssignmentFilter.MY_AND_GROUP, indicating we are interested in tasks that are assigned exclusively to us, or to any group that we are a member of.

keywords is used to further narrow the search.  We will not be using this.

predicate is used to filter on additional characteristics of the task.  We will use it to filter based on the status of the task.  There are a set of constants defined in IWorkflowConstants, with names beginning with ‘TASK_STATE_.’  Our default will be IWorkflowConstants.TASK_STATE_ASSIGNED.

We will not use the remaining parameters.  They can be used to order the results and to control paging.  Note that the API will return a maximum of 200 tasks, so in order to get all tasks, it is necessary to call the API repeatedly until less that 200 tasks are returned in the result.

We will see this API used in our com.oracle.ateam.domain.MTaskList.getMTasks() method.

The second API we are interested in is the getTaskDetailsByTaskNumber() method, also on ITaskQueryService.

This API is used to obtain a fully populated Task object for the task we are interested in.  Again, we pass in our context, and then the taskNumber (as the name suggests).  There is a similar method to lookup a Task based on the taskId.

Next, let’s look at the ITaskService.  There are a number of methods in this Interface that we are interested in:

  • updateTaskOutcome() allows us to take an action on a task, or to process the task, and set the outcome.  The allowed outcomes are set in the definition of the task.  We will see this later.
  • escalateTask() will escalate the task to the current assignee’s manager.
  • withdrawTask() will withdraw the task.
  • suspendTask() will suspend the task, i.e. take it out of the active assignments and stop the expiry and notification timers.
  • resumeTask() will resume a task that has previously been suspended.
  • purgeTask() will delete the task from the system.
  • errorTask() will put the task into an error state.
  • acquireTask() will acquire (claim) the task for the current user – the opposite of withdraw.

These are reasonably self explanatory, and we will see them in com.oracle.ateam.domain.MTaskList.processTask().

Now, let’s take a look at Task.  This class is used to hold details about an individual task in the workflow engine.  Some of the details that we are interested in are accessed by getters on Task itself, and others from Task.getSystemAttributes().  We will build a wrapper class to hide this complexity from our view design, and also to centralise some of the extra processing we want to do.

Of particular interest is the payload.  We will retrieve this using the org.w3c.dom.Element Task.getPayloadAsElement() method.  A task can have multiple (or even no) payloads, and these can be complex or simple.  For our ‘Version 1.0’ we opted not to do anything fancy, but just to render the payload(s) as Strings in the View.  In our com.oracle.ateam.domain.MTask class, we have use the javax.xml.transform package to convert the Element(s) into String(s) that we can display in the View.

The MTask wrapper class

Let’s go ahead and take a look at this class now.  Here is the source code:

package com.oracle.ateam.domain;

import oracle.bpel.services.workflow.task.model.Task;

import java.util.List;
import java.util.Date;
import java.util.ArrayList;

import java.io.StringWriter;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;

import oracle.xml.parser.v2.XMLElement;

import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import com.oracle.ateam.util.MLog;

public class MTask {

  private int    number;
  private String id;
  private String title;
  private String state;
  private String outcome;
  private int    priority;
  private List   comments;
  private List   attachments;
  private List   systemActions;
  private List   customActions;
  private List   payload;
  private String creator;
  private String acquirer;
  private Date   createDate;
  private Date   expiryDate;
  private Date   updateDate;

  public MTask() {}

  public MTask(int number, String id, String title, String outcome, int priority, String state) {
    this.number        = number;
    this.id            = id;
    this.title         = title;
    this.state         = state;
    this.outcome       = outcome;
    this.priority      = priority;
  }

  public MTask(Task task) {
    this.number        = task.getSystemAttributes().getTaskNumber();
    this.id            = task.getSystemAttributes().getTaskId();
    this.title         = task.getTitle();
    this.state         = task.getSystemAttributes().getState();
    this.outcome       = task.getSystemAttributes().getOutcome();
    this.priority      = task.getPriority();
    this.comments      = task.getUserComment();
    this.attachments   = task.getAttachment();
    this.systemActions = task.getSystemAttributes().getSystemActions();
    this.customActions = task.getSystemAttributes().getCustomActions();

    Element xPayload = task.getPayloadAsElement();
    if (xPayload == null) {
      MLog.log("MTask", "payload is null");
      this.payload = null;
    } else if (xPayload.hasChildNodes()) {
      this.payload = new ArrayList();
      NodeList children = xPayload.getChildNodes();
      if (children != null) {
        for (int i = 0; i < children.getLength(); i++) {
          if (children.item(i) instanceof oracle.xml.parser.v2.XMLElement) {
            try {
              Source source = new DOMSource(children.item(i));
              StringWriter sw = new StringWriter();
              Result result = new StreamResult(sw);
              TransformerFactory.newInstance().newTransformer().transform(source, result);
              payload.add(sw.getBuffer().toString());
            } catch (TransformerConfigurationException e) {
              e.printStackTrace();
            } catch (TransformerException e) {
              e.printStackTrace();
            }
          }
        }
      }
    }

    this.creator       = task.getCreatorDisplayName();
    this.acquirer      = ((task.getSystemAttributes().getAcquiredBy() == null) ? null :
                         task.getSystemAttributes().getAcquiredBy());
    this.createDate    = task.getSystemAttributes().getCreatedDate().getTime();
    this.expiryDate    = ((task.getSystemAttributes().getExpirationDate() == null) ? null :
                         task.getSystemAttributes().getExpirationDate().getTime());
    this.updateDate    = ((task.getSystemAttributes().getUpdatedDate() == null) ? null :
                         task.getSystemAttributes().getUpdatedDate().getTime());
  }

  public int    getNumber()        { return number;        }
  public String getId()            { return id;            }
  public String getTitle()         { return title;         }
  public String getState()         { return state;         }
  public String getOutcome()       { return outcome;       }
  public int    getPriority()      { return priority;      }
  public List   getComments()      { return comments;      }
  public List   getAttachments()   { return attachments;   }
  public List   getSystemActions() { return systemActions; }
  public List   getCustomActions() { return customActions; }
  public List   getPayload()       { return payload;       }
  public String getCreator()       { return creator;       }
  public String getAcquirer()      { return acquirer;      }
  public Date   getCreateDate()    { return createDate;    }
  public Date   getExpiryDate()    { return expiryDate;    }
  public Date   getUpdateDate()    { return updateDate;    }

  public List getPayloadAsHtml() {
    if ((this.payload == null) || (this.payload.size() < 1)) return null;
    List result = new ArrayList();
    for (Object p : this.payload) {
      result.add(((String)p).replaceAll("<", "<").replaceAll(">", ">"));
    }
    return result;
  }

  public void setNumber  (int number)               { this.number        = number;        }
  public void setId      (String id)                { this.id            = id;            }
  public void setTitle   (String title)             { this.title         = title;         }
  public void setState   (String state)             { this.state         = state;         }
  public void setOutcome (String outcome)           { this.outcome       = outcome;       }
  public void setPriority(int priority)             { this.priority      = priority;      }
  public void setComments(List comments)            { this.comments      = comments;      }
  public void setAttachments(List attachments)      { this.attachments   = attachments;   }
  public void setSystemActions(List systemActions)  { this.systemActions = systemActions; }
  public void setCustomActions(List customActions)  { this.customActions = customActions; }
  public void setPayload(List payload)              { this.payload       = payload;       }
  public void setCreator(String creator)            { this.creator       = creator;       }
  public void setAcquirer(String acquirer)          { this.acquirer      = acquirer;      }
  public void setCreateDate(Date createDate)        { this.createDate    = createDate;    }
  public void setExpiryDate(Date expiryDate)        { this.expiryDate    = expiryDate;    }
  public void setUpdateDate(Date updateDate)        { this.updateDate    = updateDate;    }

}

As you see, most of this class is properties with simple getters and setters.  You can see from the code which properties come from the Task object and which from a child object.  The dates need to be checked to ensure they are not null, and you can see the handling of the conversion from the DOM tree to Strings in the MTask(Task) constructor.

Please note that the source code in Subversion has more comments in the code.   See this page for instructions on retrieving the source code from Subversion.

The ContextCache

Next, let’s take a look at the com.oracle.ateam.domain.ContextCache class.  This class is used to cache users’ IWorkflowContext objects so that we do not have to authenticate each time we call an API.  Here is the source code:

package com.oracle.ateam.domain;

import java.util.HashMap;
import oracle.bpel.services.workflow.verification.IWorkflowContext;
import com.oracle.ateam.util.MLog;

public final class ContextCache {

  private static ContextCache contextCache;
  private static HashMap theCache;

  private ContextCache() {
    MLog.log("ContextCache", "Constructed");
  }

  public static synchronized ContextCache getContextCache() {
    MLog.log("ContextCache", "Entering getContextCache()");
    if (contextCache == null) {
      contextCache = new ContextCache();
    }
    return contextCache;
  }

  public Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
  }

  public static synchronized void put(String user, IWorkflowContext ctx) {
    MLog.log("ContextCache", "Got put for user " + user + " ctx=" + ctx);
    if (theCache == null) {
      theCache = new HashMap();
    }
    theCache.put(user, ctx);
  }

  public static synchronized IWorkflowContext get(String user) {
    MLog.log("ContextCache", "Get for user " + user);
    if (user == null) return null;
    if (theCache.containsKey(user)) {
      MLog.log("ContextCache", "Found " + user);
      return (IWorkflowContext)theCache.get(user);
    }
    return null;
  }

  public static synchronized void remove(String user) {
    MLog.log("ContextCache", "Remove user " + user);
    if (user != null) {
      if (theCache.containsKey(user)) {
        theCache.remove(user);
      }
    }
  }

}

This class implements the Singleton pattern, meaning there will only ever be one instance of this class in the application.  The cache is implemented as a HashMap keyed on the username, which is obtained from WebLogic using the HttpServletRequest.getRemoteUser() API.  The put(), get(), and remove() methods are synchronized to ensure there is no concurrent attempts to access the cache.

The MLog logging utility class

Next, let’s take a look at our utility logging class, com.oracle.ateam.util.MLog.  This class is responsible for printing formatted log messages to the WebLogic Server log and system.out.  Again, there are more comments in the actual file in Subversion.  Here are the contents:

package com.oracle.ateam.util;

import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;

public class MLog {
  private static MLog theLogger;
  private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

  public MLog() {
    if (theLogger == null)
      theLogger = new MLog();
  }

  public static void log(String source, String message) {
    System.out.println(sdf.format(new Date()) + " [worklist] " + source + ": " + message);
  }
}

MTaskList – The main business logic class

Ok, now that we have all of the basics in place, it is time to take a look at the main workhorse of the whole application, the com.oracle.ateam.domain.MTaskList class, which implements all of the logic for dealing with the BPM API.

This class carries out a number of operations, and we will go through them one by one.  Take a look at the file in Subversion to see the full source code and comments.

The MTaskList is responsible for:

  • retrieving a list of tasks, in getMTasks(),
  • getting details for a task, in getTaskDetails(),
  • take an action on a task, in processTask(),
  • add a comment to a task, in addComment(),
  • authenticate the user to the workflow engine, in login(),
  • get a list of tasks that the user can initiate, in getInitiateLinks(), and
  • initiate a task (start a process instance), in initiateTask().

Additionally, MTaskList handles caching of some important BPM API classes – the ITaskQueryService, ITaskService, and BPMClientServiceFactory – which were discussed earlier.

Let’s start by looking at the caching.  All of these are much the same, the object is kept as a static and is accessed through a getter method.  Here is the code for the ITaskQueryService:

private static ITaskQueryService tqs; 

private static ITaskQueryService getTaskQueryService() {

    if (tqs == null) {
      try {

        MLog.log("MTaskList", "Initialising the Task Query Service");

        // set up connection properties
        properties = new
           HashMap<IWorkflowServiceClientConstants.CONNECTION_PROPERTY,java.lang.String>();

        // create workflow service client
        wfsc = WorkflowServiceClientFactory.getWorkflowServiceClient(
                 WorkflowServiceClientFactory.REMOTE_CLIENT, properties, null);

        // get the task query service
        tqs = wfsc.getTaskQueryService();

      } catch (Exception e) {
        return null;
      }
    }
    return tqs;
  }

Note that we are using the REMOTE_CLIENT option.  There are three client options: LOCAL_CLIENT which uses local EJB references, SOAP_CLIENT which uses the SOAP web services and REMOTE_CLIENT which uses remote EJB references, i.e. EJBs in another Java EE enterprise application.  We are using the REMOTE_CLIENT because it is the only option that allows us to integrate with WebLogic security the way we want to, and allows us to write the application without ever needing to obtain or store a user credential.

Login

Let’s take a look at the login() method.  Here is the code:

  public static boolean login(HttpServletRequest request) {
    MLog.log("MTaskList", "Entering login()");

    try {

      // login
      ctx = getTaskQueryService().createContext(request);
      MLog.log("MTaskList", "ctx=" + ctx);

      // save the context in the cache
      MLog.log("MTaskList", "+++ request.getRemoteUser() returns " + request.getRemoteUser());
      String user = ((request.getUserPrincipal() == null) ? null : request.getUserPrincipal().getName());
      if (user == null) {
        MLog.log("MTaskList", "Problem getting user principal from request");
        return false;
      }
      ContextCache.getContextCache().put(user, ctx);

      MLog.log("MTaskList", "Leaving login() ... success");
      return true;

    } catch (Exception e) {
      e.printStackTrace();
    }
    MLog.log("MTaskList", "Leaving login() ... failed");
    return false;
  }

Here you can see that we authenticate to the workflow engine by calling the IWorkflowContext ITaskQueryService.createContext(HttpServletRequest) method, passing in the pre-authenticated WebLogic HttpServletRequest.  This will only work if the user has successfully authenticated to WebLogic server.  When we get the IWorkflowContext back, we store it in our ContextCache class (see above) for later use.  We will need to use this context each time we want to call a BPM API.  It is destroyed when the user logs out (or an error occurs).

The Task List

After a user logs in, the first thing they will see is the Task List.  Let’s take a look at the method that gathers the data for this page.  Here is the code:

  public static List getMTasks(String user, String filter, String state) {

    MLog.log("MTaskList", "Entering getMTasks()");

    try {

      // get login credentials
      ctx = ContextCache.getContextCache().get(user);
      MLog.log("MTaskList", "Got context... " + ctx);

      // setup query columns
      columns = new ArrayList();
      columns.add("TASKID");
      columns.add("TASKNUMBER");
      columns.add("TITLE");
      columns.add("STATE");
      columns.add("OUTCOME");
      columns.add("PRIORITY");

      // build predicate
      Predicate pred = new Predicate(TableConstants.WFTASK_STATE_COLUMN,
        Predicate.OP_EQ,
        IWorkflowConstants.TASK_STATE_ASSIGNED);
      if ("any".compareTo(state) == 0) {
        pred = null;
      } else if ("completed".compareTo(state) == 0) {
        pred = new Predicate(TableConstants.WFTASK_STATE_COLUMN,
          Predicate.OP_EQ,
          IWorkflowConstants.TASK_STATE_COMPLETED);
      } else if ("suspended".compareTo(state) == 0) {
        pred = new Predicate(TableConstants.WFTASK_STATE_COLUMN,
          Predicate.OP_EQ,
          IWorkflowConstants.TASK_STATE_SUSPENDED);
      } else if ("withdrawn".compareTo(state) == 0) {
        pred = new Predicate(TableConstants.WFTASK_STATE_COLUMN,
          Predicate.OP_EQ,
          IWorkflowConstants.TASK_STATE_WITHDRAWN);
      } else if ("expired".compareTo(state) == 0) {
        pred = new Predicate(TableConstants.WFTASK_STATE_COLUMN,
          Predicate.OP_EQ,
          IWorkflowConstants.TASK_STATE_EXPIRED);
      } else if ("errored".compareTo(state) == 0) {
        pred = new Predicate(TableConstants.WFTASK_STATE_COLUMN,
          Predicate.OP_EQ,
          IWorkflowConstants.TASK_STATE_ERRORED);
      } else if ("alerted".compareTo(state) == 0) {
        pred = new Predicate(TableConstants.WFTASK_STATE_COLUMN,
          Predicate.OP_EQ,
          IWorkflowConstants.TASK_STATE_ALERTED);
      } else if ("info".compareTo(state) == 0) {
        pred = new Predicate(TableConstants.WFTASK_STATE_COLUMN,
          Predicate.OP_EQ,
          IWorkflowConstants.TASK_STATE_INFO_REQUESTED);
      }

      // set assignment filter
      ITaskQueryService.AssignmentFilter aFilter = ITaskQueryService.AssignmentFilter.MY_AND_GROUP;
      if ("me".compareTo(filter) == 0) {
        aFilter = ITaskQueryService.AssignmentFilter.MY;
      } else if ("group".compareTo(filter) == 0) {
        aFilter = ITaskQueryService.AssignmentFilter.GROUP;
      } else if ("megroup".compareTo(filter) == 0) {
        aFilter = ITaskQueryService.AssignmentFilter.MY_AND_GROUP;
      } else if ("previous".compareTo(filter) == 0) {
        aFilter = ITaskQueryService.AssignmentFilter.PREVIOUS;
      } else if ("reviewer".compareTo(filter) == 0) {
        aFilter = ITaskQueryService.AssignmentFilter.REVIEWER;
      }

      // get list of tasks
      // TODO - fix paging - will only get the first 200 tasks, need to loop until <200 tasks returned
      MLog.log("MTaskList", "About to queryTasks()");
      tasks = getTaskQueryService().queryTasks(
                ctx,
                columns,
                null,    // additional info
                aFilter, // asssignment filter
                null,    // keywords
                pred,    // custom predicate
                null,    // order
                0,       // paging - start
                0);      // paging - end

      // iterate over tasks and build return data
      List result = new ArrayList();
      for (int i = 0; i < tasks.size(); i++) {
        Task task = (Task)tasks.get(i);
        result.add(new MTask(
           task.getSystemAttributes().getTaskNumber(),
           notNull(task.getTitle()),
           notNull(task.getSystemAttributes().getTaskId()),
           notNull(task.getSystemAttributes().getOutcome()),
           task.getPriority(),
           notNull(task.getSystemAttributes().getState())
           ));
      }
      return result;

    } catch (Exception e) {
      e.printStackTrace();
    }
    return null;
  }

Let’s walk through this method.  First, we retrieve the user’s context from the ContextCache.  We will need that to call the BPM APIs.

Next, we set up the list of columns that we want to include in the response.  Note that the underlying BPM API will only populate the columns that we ask for, so as to maximise the performance.  This API would be called very often, so it is important that we only ask for what we actually need, to avoid slowing down the system with unnecessary work.  We are going to ask for the following columns:

  • TASKID – the task’s internal system identifier,
  • TASKNUMBER – the task number we show in the user interface,
  • TITLE – the title (name) of the task,
  • STATE – the current state of the task (assigned, suspended, errored, etc.),
  • OUTCOME – the outcome of the task (if it has been processed/actioned), and
  • PRIORITY – the priority of the task – 1 (high) to 5 (low).

Next, we need to construct a Predicate for the query.  The predicate is used to filter (reduce) the number of tasks in the output.  In our example, we are using the predicate to filter the task list by the status that the user has selected in a drop down box on the view.

In the code, you will notice a large if/then/else if… construct that will set the predicate according to the selection the user has made.  The user’s selection will be passed in to this method as the state parameter by the com.oracle.ateam.ListTaskController.

Following the predicate, we set the assignment filter.  This allows us to filter the task list depending on who the tasks are assigned to.  This follows the same pattern as the predicate – the filter is passed in by the controller and we use an if/then/else if… construct to create the appropriate filter based on that input.

Note that we use constants defined in ITaskQueryService.AssignmentFilter.  Note that there were some older constants in ITaskQueryService itself which have been deprecated and refactored into the AssignmentFilter.

Now we are ready to call the queryTasks() API (discussed earlier).  We pass in the user context, the columns we want, the predicate and the assignment filter.  This API also supports additional filtering criteria which we are not currently using, as well as ordering and paging.  We have not implemented these in our ‘Version 1’ application.

This API returns a List<Task> which we then wrap in our com.oracle.ateam.domain.MTask object to simplify our View development.  We use the simplified MTask() constructor here, so we are going to end up with a partially populated MTask, which is all we need for our task list view.

Finally, we return a List<MTask> to the caller, i.e. the com.oracle.ateam.TaskListController controller, which will put the List<MTask> into the model and forward it to the view.  We will see this in a later post.

Task Detail

Now, let’s take a look at how we get the details for a task.  Here is the source code for the getTaskDetail() method:

  public static MTask getTaskDetails(String user, String taskNumber) {
    MLog.log("MTaskList", "Entering getTaskDetails()");

    try {

      // login
      ctx = ContextCache.getContextCache().get(user);

      // get task details
      Task task = getTaskQueryService().getTaskDetailsByNumber(ctx, Integer.parseInt(taskNumber));

      MLog.log("MTaskList", "Leaving getTaskDetails()");
      return new MTask(task);

    } catch (Exception e) {
      e.printStackTrace();
    }
    return null;
  }

This is a relatively simple method.  Again, we start by retrieving the user’s context from the ContextCache.  Then we get the fully populated Task from the ITaskQueryService.getTaskDetailsByNumber() method.  Finally, we wrap the Task in an MTask by using the MTask(Task) constructor, and we return the MTask to the caller.

Processing a Task

Next, let’s take a look at how we process (take an action) on a task.  Here is the source code for the processTask() method:

  public static void processTask(String user, String taskNumber, String outcome) {
    MLog.log("MTaskList", "Entering processTask()");

    try {

      // login
      ctx = ContextCache.getContextCache().get(user);

      // get task details
      Task task = getTaskQueryService().getTaskDetailsByNumber(ctx, Integer.parseInt(taskNumber));

      if ("SYS_ESCALATE".compareTo(outcome) == 0) {
        getTaskService().escalateTask(ctx, task);
      } else if ("SYS_WITHDRAW".compareTo(outcome) == 0) {
        getTaskService().withdrawTask(ctx, task);
      } else if ("SYS_SUSPEND" .compareTo(outcome) == 0) {
        getTaskService().suspendTask(ctx, task);
      } else if ("SYS_RESUME"  .compareTo(outcome) == 0) {
        getTaskService().resumeTask(ctx, task);
      } else if ("SYS_PURGE"   .compareTo(outcome) == 0) {
        getTaskService().purgeTask(ctx, task);
      } else if ("SYS_ERROR"   .compareTo(outcome) == 0) {
        getTaskService().errorTask(ctx, task);
      } else if ("SYS_ACQUIRE" .compareTo(outcome) == 0) {
        getTaskService().acquireTask(ctx, task);
      } else {
        // this is a CustomAction
        MLog.log("MTaskList", "Updating outcome of task " + task.getSystemAttributes().getTaskNumber() + " to " + outcome);
        getTaskService().updateTaskOutcome(ctx, task.getSystemAttributes().getTaskId(), outcome);
      }

      MLog.log("MTaskList", "Leaving processTask()");

    } catch (Exception e) {
      // TODO need to handle execptions properly - if the user does not
      //      have permission to execute the requested outcome, an
      //      exception will be thrown
      e.printStackTrace();
    }
  }

Again, we start by retrieving the user’s context from the ContextCache.  Then we retrieve the fully populated Task as in the previous example.  Then we need to work our which action to take on the task.  Each system action is a different method on the ITaskService.  You will see an if/then/else if… construct in the code that steps through the various system actions and calls the appropriate API.

If the action is not a system action, then it must be a custom action, i.e. an action that was defined in the Human Task Definition.  These are all handled by the updateTaskOutcome() method on the ITaskService.

Note that we have not handled all of the possible exceptions in this method.  If a user does not have permission to take a particular action on a task, we will just throw an exception and move on with the rest of our lives.  We will need to tidy this up in a future version 🙂

Adding a Comment to a Task

You may be starting to see a pattern by now!  To add a comment to a task, we are going to retrieve the user’s context, then the task, then call the addComment() method on the ITaskService.  Here is the code:

  public static void addComment(String user, String taskNumber, String comment) {
    MLog.log("MTaskList", "Entering addComment()");

    try {

      // login
      ctx = ContextCache.getContextCache().get(user);

      // get task details
      Task task = getTaskQueryService().getTaskDetailsByNumber(ctx, Integer.parseInt(taskNumber));

      // add the comment
      getTaskService();
      MLog.log("MTaskList", "Adding comment to task " + task.getSystemAttributes().getTaskNumber() + ": " + comment);
      getTaskService().addComment(ctx, task.getSystemAttributes().getTaskId(), comment);

      MLog.log("MTaskList", "Leaving addComment()");

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

Getting a List of Initiatable Tasks

This one is slightly different – we will use the BPMServiceClientFactory to obtain a IProcessMetadataService, and then call its getInitiatableProcesses() method to get a list of the processes/tasks that can be started/initiated by the current user.  Here is the code:

  public static List getInitiateLinks(String user) {
    MLog.log("MTaskList", "Entering getInitiateLinks()");

    try {

      // get the process metadata service
      IProcessMetadataService pms = getBPMServiceClientFactory().getBPMServiceClient().getProcessMetadataService();

      // get the list of initiatable proceses
      List result = pms.getInitiatableProcesses(
          (IBPMContext) ContextCache.getContextCache().get(user));
      MLog.log("MTaskList", "Leaving getInitiateLinks()");
      return result;

    } catch (Exception e) {
      e.printStackTrace();
    }
    MLog.log("MTaskList", "Leaving getInitiateLinks() ... failed");
    return null;

  }

Note that we need to cast the IWorkflowContext to an IBPMContext to use this API.

Initiating a Task

Initiating a task is done in the initiateTask() method, which is slightly more complicated.  We need to do two things in this method: actually start the task, and get a URL for the task form so that the user can actually interact with the task.  Here is the code:

  public static String initiateTask(String user, String compositeDN) {
    MLog.log("MTaskList", "Entering initiateTask()");

    try {

      MLog.log("MTaskList", "got a request to initiate task " + compositeDN + " for user " + user);
      Map parameters = new HashMap();
      Task task = getBPMServiceClientFactory().getBPMServiceClient().getInstanceManagementService().createProcessInstanceTask(
              (IBPMContext) ContextCache.getContextCache().get(user),
              compositeDN);
      parameters.put(Constants.BPM_WORKLIST_TASK_ID, task.getSystemAttributes().getTaskId());
      parameters.put(Constants.BPM_WORKLIST_CONTEXT, ContextCache.getContextCache().get(user).getToken());
      parameters.put(Constants.BPM_PARENT_URL, "javascript:window.close();");
      String url = WorklistUtil.getTaskDisplayURL(
          getBPMServiceClientFactory().getWorkflowServiceClient(),
          ContextCache.getContextCache().get(user),
          task,
          null,
          "worklist",
          parameters);
      return url;

    } catch (Exception e) {
      e.printStackTrace();
    }
    return null;
  }

Again, we use the BPMServiceClientFactory. We get the InstanceManagementService and use its createProcessInstanceTask() method to actually create the new task instance.

Then we need to get the URL.  We need to set a number of parameters which are passed into the WorklistUtil.getTaskDisplayURL() method so that we will get a valid URL.  This includes the taskId for the newly created task instance, and a token from our user’s context.

We return the URL to the com.oracle.ateam.InitiateTaskController, which will pass it through to the view, which will in turn open up the task form for the user.

Summary

In this post, we have covered the main business logic of the application, the part that actually deals with the BPM API.  This is probably the most complicated part of the application.  You should take time to make sure you understand how this part of the application works.  The rest of the application is basically using information from these domain classes, or providing information to them.

The code in Subversion has all of the import statements, and many more comments.  See this page for information on how to get the code from Subversion.

In the next post, we will start building our View layer.

About Mark Nelson

Mark Nelson is a Developer Evangelist at Oracle, focusing on microservices and messaging. Before this role, Mark was an Architect in the Enterprise Cloud-Native Java Team, the Verrazzano Enterprise Container Platform project, worked on Wercker, WebLogic and was a senior member of the A-Team since 2010, and worked in Sales Consulting at Oracle since 2006 and various roles at IBM since 1994.
This entry was posted in Uncategorized and tagged , , , , . Bookmark the permalink.

5 Responses to Creating the domain layer for the worklist

  1. Pingback: Viewing Task Attachments | RedStack

  2. vbmigration says:

    HI Mark

    Thanks a lot for your posts related to BPM worklist application.

    I found a java code to update many human tasks to completed status with one command. I tired this with SOA 10g and works fine. I am planning to try the same with SOA 11g also. I also want to make an ADF page to run this java code from UI and mass update Human tasks in BPM worklist application. This is very helpful in case of mass update of human tasks in BPM worklist is required.

    Do your custom worklist application have this feature? if not, are you planning to have this feature in future?

    Do you have any ideas or suggestions to achieve the mass update functionality in BPM worklist 10g or BPM worklist 11g or your custom worklist application.

    Please let me know.

    Thanks a ton
    Arun

    • Mark Nelson says:

      Hi,

      Thanks for your feedback. I am planning to add bulk actions to the sample. The out of the box workspace application in 11g suport bulk updates, as long as the tasks have the same actions, you just select all the tasks and then use the action menu to pick the action. I think 10g did that too, I have not used it for a while, I dont remember.

      Mark

  3. Hi mark

    Thanks for great tutorial sequence. I’m kind of guy who have been working with direct code-code/service-service interactions in all my projects and i havent found any good tutorials from oracle on how to interface with its Services/components. This is really great tutorial which have every baby steps so that any novice can write code… I have started working on Oracle SOA Suite and till now appreciated all its features (I was disappointed last time i worked with it around 3-4 years back). And your articles helped me a lot.

    Keep up your work

    Thanks
    Ashwin

Leave a comment