In its simplest form, KEW is merely a set of services that can be used to submit documents to a workflow engine and then interact with those documents as the progress through the routing process. Therefore, there are many different ways to build an application that uses KEW. Kuali Rice itself has a few built-in solutions (eDocLite and KNS) that make it easier to build applications that use KEW. Alternatively, an application can be built from scratch or retrofitted to use KEW.
In this section, we will look at some common approaches to designing and building an application which leverages KEW. However, it is by no means exhaustive and is simply meant to get you started and give you ideas as you embark upon development of your own applications that use Kuali Enterprise Workflow.
Determine to whom you want to route the document and when it should be routed. For example, in the Travel Request Sample Workflow Client Application, the steps in the routing process are:
Someone submits a travel request for a traveler
Traveler receives an Approve Action Item
Traveler's supervisor receives Approve Action Item
Traveler's dean/director receives Acknowledge Action Item
Fiscal Officer for account(s) receives Approve Action Item
In KEW, process definitions are attached to Document Types. The Document Type allows for configuration of various pieces of the business process in addition to the process definition.
The Document Type is defined in XML format. KEW can ingest files containing this Document Type configuration to set up the specified workflows and then executes the workflows based on that configuration.
One example of routing configuration is the Travel Request application. The Document Type configuration is defined in the following four XML files:
TravelRoutingConfiguration.xml - Defines the travelDocument Document Type, including PostProcessor, docHandler, and routeNodes:
<?xml version="1.0" encoding="UTF-8"?> <data xmlns="ns:workflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="ns:workflow resource:WorkflowData"> <documentTypes xmlns="ns:workflow/DocumentType" xsi:schemaLocation="ns:workflow/DocumentType resource:DocumentType"> <documentType> <name>TravelRequest</name> <description>Create a New Travel Request</description> <label>Travel Request</label> <postProcessorName>org.kuali.rice.kns.workflow.postprocessor.KualiPostProcessor</postProcessorName> <superUserGroupName namespace="TVL">SuperUserGroup</superUserGroupName> <blanketApproveGroupName namespace="TVL">BlanketApproveGroup</blanketApproveGroupName> <defaultExceptionGroupName namespace="TVL">ExceptionGroup</defaultExceptionGroupName> <docHandler>${application.url}/travelDocument2.do?methodToCall=docHandler</docHandler> <routePaths> <routePath> <start name="Initiated" nextNode="DestinationApproval" /> <requests name="DestinationApproval" nextNode="TravelerApproval" /> <requests name="TravelerApproval" nextNode="SupervisorApproval" /> <requests name="SupervisorApproval" nextNode="AccountApproval" /> <requests name="AccountApproval" /> </routePath> </routePaths> <routeNodes> <start name="Initiated"> <activationType>P</activationType> </start> <requests name="DestinationApproval"> <ruleTemplate>TravelRequest-DestinationRouting</ruleTemplate> </requests> <requests name="TravelerApproval"> <ruleTemplate>TravelRequest-TravelerRouting</ruleTemplate> </requests> <requests name="SupervisorApproval"> <ruleTemplate>TravelRequest-SupervisorRouting</ruleTemplate> </requests> <requests name="AccountApproval"> <ruleTemplate>TravelRequest-AccountRouting</ruleTemplate> </requests> </routeNodes> </documentType> </documentTypes> </data>
TravelRuleAttributes.xml – Defines the attributes used by the Workflow Engine to determine to whom to route to next:
<?xml version="1.0" encoding="UTF-8"?> <data xmlns="ns:workflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="ns:workflow resource:WorkflowData"> <ruleAttributes xmlns="ns:workflow/RuleAttribute" xsi:schemaLocation="ns:workflow/RuleAttribute resource:RuleAttribute"> <ruleAttribute> <name>EmployeeAttribute</name> <className>edu.sampleu.travel.workflow.EmployeeAttribute</className> <label>Employee Routing</label> <description>Employee Routing</description> <serviceNamespace>TRAVEL</serviceNamespace> <type>RuleAttribute</type> </ruleAttribute> <ruleAttribute> <name>AccountAttribute</name> <className>edu.sampleu.travel.workflow.AccountAttribute</className> <label>Account Routing</label> <description>Account Routing</description> <serviceNamespace>TRAVEL</serviceNamespace> <type>RuleAttribute</type> </ruleAttribute> </ruleAttributes> </data>
TravelRuleTemplates.xml - Defines the RuleTemplates that represent each routeNode listed in the Document Type configuration:
<?xml version="1.0" encoding="UTF-8"?> <data xmlns="ns:workflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="ns:workflow resource:WorkflowData"> <ruleTemplates xmlns="ns:workflow/RuleTemplate" xsi:schemaLocation="ns:workflow/RuleTemplate resource:RuleTemplate"> <ruleTemplate allowOverwrite="true"> <name>TravelRequest-DestinationRouting</name> <description>Destination Routing</description> <attributes> <attribute> <name>DestinationAttribute</name> </attribute> </attributes> </ruleTemplate> <ruleTemplate allowOverwrite="true"> <name>TravelRequest-TravelerRouting</name> <description>Traveler Routing</description> <attributes> <attribute> <name>EmployeeAttribute</name> </attribute> </attributes> </ruleTemplate> <ruleTemplate allowOverwrite="true"> <name>TravelRequest-SupervisorRouting</name> <description>Supervisor Routing</description> <attributes> <attribute> <name>EmployeeAttribute</name> </attribute> </attributes> </ruleTemplate> <ruleTemplate allowOverwrite="true"> <name>TravelRequest-AccountRouting</name> <description>Travel Account Routing</description> <attributes> <attribute> <name>AccountAttribute</name> </attribute> </attributes> </ruleTemplate> </ruleTemplates> </data>
TravelRules.xml - Defines the rules (a rule is a combination of Document Type, Rule Template and Responsibilities) that the workflow engine uses to determine to whom to route to next:
<?xml version="1.0" encoding="UTF-8"?> <data xmlns="ns:workflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="ns:workflow resource:WorkflowData"> <rules xmlns="ns:workflow/Rule" xsi:schemaLocation="ns:workflow/Rule resource:Rule"> <rule> <name>TravelRequest-DestinationLasVegas</name> <documentType>TravelRequest</documentType> <ruleTemplate>TravelRequest-DestinationRouting</ruleTemplate> <description>Destination Rule</description> <ruleExtensions> <ruleExtension> <attribute>DestinationAttribute</attribute> <ruleTemplate>TravelRequest-DestinationRouting</ruleTemplate> <ruleExtensionValues> <ruleExtensionValue> <key>destination</key> <value>las vegas</value> </ruleExtensionValue> </ruleExtensionValues> </ruleExtension> </ruleExtensions> <responsibilities> <responsibility> <principalName>user4</principalName> <actionRequested>A</actionRequested> </responsibility> </responsibilities> </rule> <rule> <name>TravelRequest-EmployeeRole</name> <documentType>TravelRequest</documentType> <ruleTemplate>TravelRequest-TravelerRouting</ruleTemplate> <description>Traveler Routing</description> <responsibilities> <responsibility> <role>edu.sampleu.travel.workflow.EmployeeAttribute!employee</role> <actionRequested>A</actionRequested> </responsibility> </responsibilities> </rule> <rule> <name>TravelRequest-SupervisorRole</name> <documentType>TravelRequest</documentType> <ruleTemplate>TravelRequest-SupervisorRouting</ruleTemplate> <description>Supervisor Routing</description> <responsibilities> <responsibility> <role>edu.sampleu.travel.workflow.EmployeeAttribute!supervisr</role> <actionRequested>A</actionRequested> </responsibility> </responsibilities> </rule> <rule> <name>TravelRequest-DirectorRole</name> <documentType>TravelRequest</documentType> <ruleTemplate>TravelRequest-SupervisorRouting</ruleTemplate> <description>Dean/Director Routing</description> <responsibilities> <responsibility> <role>edu.sampleu.travel.workflow.EmployeeAttribute!director</role> <actionRequested>K</actionRequested> </responsibility> </responsibilities </rule> <rule> <name>TravelRequest-FiscalOfficerRole</name> <documentType>TravelRequest</documentType> <ruleTemplate>TravelRequest-AccountRouting</ruleTemplate> <description>Fiscal Officer Routing</description> <responsibilities> <responsibility> <role>edu.sampleu.travel.workflow.AccountAttribute!FO</role> </responsibility> </responsibilities> </rule> </rules> </data>
Your plugin should contain Java classes that correspond to the attributes defined in the XML configuration file. The Travel Request Sample Client contains two attribute classes: EmployeeAttribute and AccountAttribute. Each of these classes implements these two interfaces:
org.kuali.rice.kew.rule.RoleAttribute org.kuali.rice.kew.rule.WorkflowAttribute
Using the EmployeeAttribute as an example, here are the implementations for the RoleAttribute interface:
getRoleNames() - Returns a list of role names to display on the routing rule GUI in the KEW web application:
private static final Map ROLE_INFO; static { ROLE_INFO = new TreeMap(); ROLE_INFO.put(EMPLOYEE_ROLE_KEY, "Employee"); ROLE_INFO.put(SUPERVISOR_ROLE_KEY, "Supervisor"); ROLE_INFO.put(DIRECTOR_ROLE_KEY, "Dean/Director"); } public List getRoleNames() { List roleNames = new ArrayList(); for (Iterator iterator = roles.keySet().iterator(); iterator.hasNext();) { String roleName = (String) iterator.next(); roleNames.add(new Role(getClass(), roleName, roleName)); } return roleNames; }
getQualifiedRoleNames() - Returns a list of strings that represents the qualified role name for the given roleName and XML docContent which is attached to the workflow document:
/** * Returns a String which represent the qualified role name of this role for the given * roleName and docContent. * @param roleName the role name (without class prefix) * @param documentContent the document content */ public List<String> getQualifiedRoleNames(String roleName, DocumentContent documentContent) { List<String> qualifiedRoleNames = new ArrayList<String>(); Map<String, List<String>> qualifiedRoles = (Map<String, List<String>>)roles.get(roleName); if (qualifiedRoles != null) { qualifiedRoleNames.addAll(qualifiedRoles.keySet()); } else { throw new IllegalArgumentException("TestRuleAttribute does not support the given role " + roleName); } return qualifiedRoleNames; }
resolveQualifiedRole() - Returns a list of workflow users that are members of the given Qualified Role. (Used to help determine to whom to route the document.):
/** * Returns a List of Workflow Users which are members of the given qualified role. * @param routeContext the RouteContext * @param roleName the roleName (without class prefix) * @param qualifiedRole one of the the qualified role names returned from the {@link #getQualifiedRoleNames(String, DocumentContent)} method * @return ResolvedQualifiedRole containing recipients, role label (most likely the roleName), and an annotation */ public ResolvedQualifiedRole resolveQualifiedRole(RouteContext routeContext, String roleName, String qualifiedRole) { ResolvedQualifiedRole resolved = new ResolvedQualifiedRole(); Map<String, List<String>> qualifiedRoles = (Map<String, List<String>>)roles.get(roleName); if (qualifiedRoles != null) { List<String> recipients = (List<String>)qualifiedRoles.get(qualifiedRole); if (recipients != null) { resolved.setQualifiedRoleLabel(qualifiedRole); resolved.setRecipients(convertPrincipalIdList(recipients)); } else { throw new IllegalArgumentException("TestRuleAttribute does not support the qualified role " + qualifiedRole); } } else { throw new IllegalArgumentException("TestRuleAttribute does not support the given role " + roleName); } return resolved; }
Using the EmployeeAttribute example, here are the implementations for the WorkflowAttribute interface:
getRoutingDataRows() – Returns a list of RoutingDataRows that contain the user interface level presentation of the ruleData fields. KEW uses the ruleData fields to determine where a given document would be routed according to the associated rule:
public List<Row> getRoutingDataRows() { List<Row> rows = new ArrayList<Row>(); List<Field> fields = new ArrayList<Field>(); fields.add(new Field("Traveler username", "", Field.TEXT, false, USERID_FORM_FIELDNAME, "", false, false, null, null)); rows.add(new Row(fields)); return rows; }
getDocContent() - Returns a string containing this Attribute's routingData values, formatted as a series of XML tags:
public String getDocContent() { String docContent = ""; if (!StringUtils.isBlank(_uuid)) { String uuidContent = XmlUtils.encapsulate(UUID_PARAMETER_TAGNAME, _uuid); docContent = _attributeParser.wrapAttributeContent(uuidContent); } return docContent; }
validateRoutingData() - Validates routingData values in the incoming map and returns a list of errors from the routing data. (The user interface calls validateRoutingData() during rule creation.):
public List validateRoutingData(Map paramMap) { List errors = new ArrayList(); String principalName = StringUtils.trim((String) paramMap.get(PRINCIPAL_NAME_FORM_FIELDNAME)); if (isRequired() && StringUtils.isBlank(principalName)) { errors.add(new WorkflowServiceErrorImpl("principalName is required", "accountattribute.principalName.required")); } if (!StringUtils.isBlank(principalName)) { KimPrincipalInfo principal = KIMServiceLocator.getIdentityService().getPrincipalByPrincipalName(principalName); if (principal == null) { errors.add(new WorkflowServiceErrorImpl("unable to retrieve user for principalName '" + principalName + "'", "accountattribute.principalName.invalid")); } } if ( errors.size() == 0 ) { _principalName = principalName; } return errors; }
The PostProcessor class should implement the interface:
org.kuali.rice.kew.postprocessor.PostProcessorRemote
You should use this interface for business logic that should execute when the document transitions to a new status or when actions are taken on the document. The PostProcessor for the Travel Request Client is the class:
org.kuali.rice.kns.workflow.postprocessor.KualiPostProcessor
that implements the doRouteStatusChange() method to update the status of the travel document in the Travel database. The KualiPostProcessor in this case is the standard PostProcessor used on all documents that are built on the KNS framework.
Depending on how the application has been developed (i.e. embedded workflow engine vs. using the engine as a remote service) it may be necessary to package components like the PostProcessor into a plug-in. See the Workflow PlugIn Guide for details on how to do this.
Begin to build a Kuali Enterprise Workflow the same as you build any other Java-enabled web application. You build it with all the business logic for the application and, for example, communication to the workflow engine using web services.
As an example, the Travel Request Client Web Application uses Struts, Spring, and OJB.
For the rest of this section, this guide refers to the Java application communicating with the Kuali Enterprise Workflow as the Client Application. The Client Application needs a service that will interact with the workflow system. This service will perform actions such as locating a document in the workflow system and routing the document.
Below are examples from the Travel Request Sample Client. The methods in the TravelDocumentService class find a TravelDocument in the workflow system, save and route a TravelDocument, and validate a TravelDocument.
findByDocHeaderId() - Finds a Document in the workflow engine:
public TravelDocument findByDocHeaderId(Long docHeaderId, String principalId) { if (docHeaderId == null) { throw new IllegalArgumentException("invalid (null) docHeaderId"); } TravelDocument result = travelDocumentDao.findByDocHeaderId(docHeaderId); if (result != null) { // convert DocAccountJoins into FinancialAccounts ArrayList accounts = new ArrayList(); for (Iterator joins = result.getDocAccountJoins().iterator(); joins.hasNext();) { DocumentAccountJoin join = (DocumentAccountJoin) joins.next(); FinancialAccount account = financialAccountService.findByAccountNumber(join.getAccountNumber()); accounts.add(account); } result.setFinancialAccounts(accounts); try { WorkflowDocument document = new WorkflowDocument( principalId, result.getDocHeaderId()); } catch (WorkflowException e) { LOG.error("caught WorkflowException: ", e); throw new RuntimeException(e); } } return result; }
The TravelDocumentServiceImpl class populates the attribute values on the workflow document (Employee, Account) that will be used for future routing. It does this by calling its getEmployeeAttribute() and getAccountAttribute() methods and adding the results to the workflow document by calling the addAttributeDefinition() method.
private WorkflowAttributeDefinitionVO getEmployeeAttribute(TravelDocument travelDocument) { WorkflowAttributeDefinitionDTO attrDef = new WorkflowAttributeDefinitionDTO("edu.sampleu.travel.workflow.EmployeeAttribute"); String principalName = travelDocument.getTravelerUsername(); attrDef.addConstructorParameter(principalName); return attrDef; } private List getAccountAttributes(TravelDocument travelDocument) { List accounts = travelDocument.getFinancialAccounts(); List accountAttributes = new ArrayList(); for (Iterator accountIterator = accounts.iterator(); accountIterator.hasNext();) { WorkflowAttributeDefinitionDTO attrDef = new WorkflowAttributeDefinitionDTO("edu.sampleu.travel.workflow.AccountAttribute"); FinancialAccount account = (FinancialAccount)accountIterator.next(); attrDef.addConstructorParameter(account.getAccountNumber()); accountAttributes.add(attrDef); } return accountAttributes; }
In the Travel Request Sample Client, the WorkflowDocHandlerAction struts action class calls the workflow lifecycle methods (approve, acknowledge, etc.) on the workflow document.
WorkflowDocHandlerAction - Take approve action. (Each workflow action - acknowledge, complete, etc. - is like this):
public ActionForward approve(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { LOG.info("entering approve() method ..."); DocHandlerForm docHandlerForm = (DocHandlerForm) form; WorkflowDocument document = docHandlerForm.getWorkflowDocument() document.approve(docHandlerForm.getAnnotation()); saveDocumentActionMessage("general.routing.approved", request); LOG.info("forwarding to actionTaken from approve()"); return mapping.findForward("actionTaken"); }
Set up the WorkflowDocument in the initializeBaseFormState() method of the DispatchActionBase from which the Struts action classes inherit. Obtain the workflow document with this line of code:
String principalId = getUserSession(request).getPrincipalId(); WorkflowDocument document = new WorkflowDocument(principalId, docId);
Package the Client Application (client web application) for deployment the way you normally package web applications. The Travel Request Sample Web Application does this with an Ant build script. The dist step of the build.xml script builds the SampleWorkflowClient.war file.
Deploy the plugin to your workflow installation. Copy the plugin directory structure to your application plugins directory. Please see the Workflow Plugin Guide for more information.