Moving Maximo business logic from Java classes into Automation Scripts on an example of generating custom Workflow Assignments

Maximo starting with version 7.5 allows customizations using scripting languages. This possibility significantly speed-ups deployment process, since no Maximo restart needed. Although the documentation limits the cases where automation scripts can be used with a several types of launch points, in fact you may invoke an automation script from any place of your business logic. In this article I’ll show you how to use a single automation script to generate assignments for all task nodes within a workflow process. This approach has the following advantages:

  • You need only one custom role per workflow process, regardless of how many task nodes configured in the process
  • The script will do the assignments according to your custom logic, depending on the current workflow node and the controlled mbo’s data
  • You will not need to restart Maximo if you change your custom assignment logic or add a new task node into workflow

Since Maximo does not provide any documented way on how to invoke an automation script upon role resolution, we will implement a custom role class that will execute our script. Fortunately, this class will not depend on the workflow logic and mbo context, so it can be implemented once and reused for any task node within the workflow (or even for multiple workflow processes). The custom role class will determine the name of the current workflow task node then invoke the automation script and pass the node name into the script. The script will then generate assignments according to the current workflow node. Here is the source code of the custom role class with some comments:


package custom.common.role;

import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.Map;
import com.ibm.tivoli.maximo.script.ScriptDriverFactory;
import psdi.common.role.CustomRoleAdapter;
import psdi.common.role.CustomRoleInterface;
import psdi.common.role.MaxRole;
import psdi.mbo.MboRemote;
import psdi.mbo.MboSetRemote;
import psdi.util.MXException;
import psdi.util.MXSystemException;
import psdi.workflow.WFAssignmentSet;
import psdi.workflow.WFCallStack;
import psdi.workflow.WFCallStackSet;
import psdi.workflow.WFInstance;
import psdi.workflow.WFNode;

public class ScriptRole extends CustomRoleAdapter implements CustomRoleInterface {

@Override
public MboRemote evaluateCustomRole(MaxRole maxrole, MboRemote mbo) throws MXException, RemoteException {

    WFInstance wfinst = getActiveWFInstance(mbo);
    MboRemote node = getCurrentNode(wfinst);
    // get the default assignments configured for the current task node and retrieve the
    // assignment configured for this role
    WFAssignmentSet assignSet = (WFAssignmentSet) node.getMboSet("DEFINEDASSIGNMENTS");
    MboRemote assign = null;
    // get assignment that is based on current role
    for (assign = assignSet.moveFirst(); assign != null; assign = assignSet.moveNext()) {
        if (assign.getString("roleid").equals(maxrole.getString("maxrole"))) {
            break;
        }
    }

    // Here we setup a map of parameters that will be passed to the automation script
    // We also include a reference to returnContext to allow the script to return the
    // last resolved Person mbo
    Map <String, Object> context = new HashMap<String, Object>(); 
    Map <String, Object> returnContext = new HashMap<String, Object>();
    context.put("NODE", node.getString("title"));
    context.put("TEMPLATE", assign);
    context.put("MBO", mbo);
    context.put("RETURN", returnContext);
    ScriptDriverFactory.getInstance().getScriptDriver("ROLEASSIGN").runScript("ROLEASSIGN", context);
    
    // The role class must return an MBO which is either a person or person group. Here
    // return the last person resolved by the script. Maximo will create the assignment 
    // for it
    return (MboRemote) returnContext.get("LASTPERSON");
}

public ScriptRole() {
    super();
}


private WFInstance getActiveWFInstance(MboRemote mbo) throws RemoteException, MXException {

    // If the process has just been started and is on the first task node, then the
    // WF instance is not in the database yet. In this case the virtual WF instance
    // can be retrieved using system &WFINSTANCE& relation
    try {
        MboSetRemote mboSR = mbo.getMboSet("&WFINSTANCE&");
        MboRemote mboR = mboSR.getMbo(0);
        if (mboR!=null) {
            return (WFInstance) mboR;
        }
    } catch (MXSystemException e) {
        if (e.getErrorKey().equals("norelationship")) {
            // if system relation &WFINSTANCE& does not exist, we will get the active WF
            // Instance from the database.
        } else {
            throw e;
        }
    }
    String mboName = mbo.getName();
    MboSetRemote mboSR = null;
    try {
        mboSR = mbo.getMboSet("$WFINSTANCE_", "WFINSTANCE");
        String sSQLWhere = "ownertable='" + mboName + "' and ownerid=" + 
                           mbo.getUniqueIDValue() + " and active=1";
        mboSR.setWhere(sSQLWhere);
        mboSR.reset();
        if (mboSR.isEmpty()) {
            return null;
        } else {
            return (WFInstance) mboSR.getMbo(0);
        }
    } finally {
        if (mboSR != null) {
            mboSR.close();
        }
    }
}

private MboRemote getCurrentNode(WFInstance wfinst) throws RemoteException, MXException {
    WFCallStackSet callStack = (WFCallStackSet) wfinst.getMboSet("CALLSTACK");
    WFCallStack stackTop = callStack.getWFCallStack();
    WFNode node = stackTop.getCurrentNode();
    return node;
}

}

In order to use this class we need to configure a custom role class (COMMONROLE) and reference the class here:

Now as an example let’s configure the following workflow with 4 task nodes:

In the assignments table for every task node specify the same previously created role and optionally specify the Task description:

And finally it’s time to create the automation script that will generate workflow assignments depending on the current workflow node (create it as a ‘groovy’ script with no launchpoints, the name should be ROLEASSIGN):

The source of the groovy script will look as follows:


import java.util.ArrayList;
import java.util.List;
import psdi.app.person.PersonRemote;
import psdi.mbo.MboConstants;
import psdi.mbo.MboRemote;
import psdi.mbo.MboSetRemote;
import psdi.server.MXServer;
import psdi.util.MXException;
import psdi.workflow.WFAssignment;

// get the parameters passed into the script
// WF node name
String nodeName = NODE;
// Controlled mbo
MboRemote wfMbo = MBO;
// Assignment template
WFAssignment assignment = (WFAssignment) TEMPLATE;
// Map to return the results
Map <String, Object> returnContext = RETURN;

String description = assignment.getString("DESCRIPTION");
// The assignList array accumulates all person id's to create assignments for
List <String> assignList = new ArrayList<String>();

System.out.println("Script ROLEASSIGN for node: " + NODE + ", workorder: " + MBO.getString("wonum"));
if (nodeName.equalsIgnoreCase("CORRECT")) {
    // write your logic to generate the assignee person ids for task node CORRECT...
    assignList.add(wfMbo.getString("reportedby"));
    // you may also change the assignment description here or other parameters
    description = "Correction required for order #" + wfMbo.getString("wonum");
} else if (nodeName.equalsIgnoreCase("FSTLNAPPR")) {
    // write your logic to generate the assignee person ids for task node FSTLNAPPR...
    assignList.add(wfMbo.getString("REPORTEDBY.SUPERVISOR.PERSONID"));
    description = "First line approval for order #" + wfMbo.getString("wonum");
} else if (nodeName.equalsIgnoreCase("SCNDLNAPPR")) {
    // write your logic to generate the assignee person ids for task node SCNDLNAPPR...
    assignList.add("MAXADMIN");
    description = "Second line approval for order #" + wfMbo.getString("wonum");
} else if (nodeName.equalsIgnoreCase("EXECUTE")) {
    // write your logic to generate the assignee person ids for task node EXECUTE...
    assignList.add("MAXADMIN");
    assignList.add(wfMbo.getString("OWNERPERSON.PERSONID"));
    description = "Execution of order #" + wfMbo.getString("wonum");
}
assignment.setValue("DESCRIPTION", description, MboConstants.NOACCESSCHECK);
StringBuffer ids = new StringBuffer();
for (String personId : assignList) {
    String append = (ids.length() > 0) ? ",'" + personId + "'" : "'" + personId + "'";
    ids.append(append);
}
// Retrieve the PERSON MboSet according to the list of assignees
MboSetRemote personSetR = MXServer.getMXServer().getMboSet("PERSON", wfMbo.getUserInfo());
String where = "personid in (" + ids.toString() + ")";
personSetR.setWhere(where);
personSetR.reset();
int count = personSetR.count();
if (count > 0) {
    // generate assignments using the passed in template, except the last person
    for (int i = 0; i < count-1; i++) {
        MboRemote mboPerson = personSetR.getMbo(i);
        System.out.println("## Script ROLEASSIGN, generating assignment for person: " + mboPerson.getString("personid"));
        assignment.generateForPerson((PersonRemote) mboPerson);
    }
    // return the last person MBO back to the role class.
    // Maximo will generate this last assignment when the evaluateCustomRole() completes.
    returnContext.put("LASTPERSON", personSetR.getMbo(count-1));
}

That’s all. Now you can control all the assignments within your workflow process in one place. You can easily modify the logic without the need to restart Maximo or even activate/deactivate the workflow. This example still needs further improvement if you need to configure notifications on the task nodes in the standardized way, however in other cases it may be an acceptable approach for assignment generation. Anyway, this example demonstrates how significant parts of business logic can be moved out of the custom classes into the automation scripts, which require significantly less time for further deployments while remain integrated with the rest of Java code.