SCXML Workflow Execution
SCXML Workflow Execution
The Bloomreach Experience Manager SCXML Workflow Engine provides a generic org.onehippo.repository.scxml.SCXMLWorkflowExecutor to execute SCXML Workflow definitions loaded from the Hippo Repository.
A SCXMLWorkflowExecutor is a generics based class which is instantiated (and possibly extended) using a org.onehippo.repository.scxml.SCXMLWorkflowContext and an optional org.onehippo.repository.scxml.SCXMLWorkflowData interface implementation.
The org.onehippo.repository.documentworkflow.DocumentWorkflowImpl.setNode(Node) method shows an example for creating a SCXMLWorkflowExecutor:
import org.onehippo.repository.scxml.SCXMLWorkflowContext; import org.onehippo.repository.scxml.SCXMLWorkflowExecutor; public static final String SCXML_DEFINITION_KEY = "scxml-definition"; private SCXMLWorkflowExecutor<SCXMLWorkflowContext, DocumentHandle> workflowExecutor; @Override public void setNode(final Node node) throws RepositoryException { super.setNode(node); String scxmlId = "documentworkflow"; try { final RepositoryMap workflowConfiguration = getWorkflowContext().getWorkflowConfiguration(); // check if a custom scxml-definition identifier is configured for this workflow instance if (workflowConfiguration != null && workflowConfiguration.exists() && workflowConfiguration.get(SCXML_DEFINITION_KEY) instanceof String) { // use custom scxml-definition identifier scxmlId = (String) workflowConfiguration.get(SCXML_DEFINITION_KEY); } // instantiate SCXMLWorkflowExecutor using default SCXMLWorkflowContext and extended SCXMLWorkflowData class DocumentHandle workflowExecutor = new SCXMLWorkflowExecutor<>(new SCXMLWorkflowContext(scxmlId, getWorkflowContext()), new DocumentHandle(node)); } catch (WorkflowException wfe) { if (wfe.getCause() != null && wfe.getCause() instanceof RepositoryException) { throw (RepositoryException)wfe.getCause(); } throw new RepositoryException(wfe); } }
The SCXMLWorkflowContext provides the scxmlId identifier (node name) of the SCXML Workflow definition in the repository, which is loaded by the SCXMLWorkflowExecutor through the org.onehippo.repository.scxml.SCXMLRegistry.
The SCXMLRegistry itself is a service module registered through the org.onehippo.cms7.services.HippoServiceRegistry.
Once the SCXML Workflow Definition is loaded from the repository, a SCXML state machine is instantiated using a org.apache.commons.scxml2.SCXMLExecutor and used during subsequent interactions with the SCXML state machine through the SCXMLWorkflowExecutor.
The SCXMLWorkflowContext and optional SCXMLWorkflowData objects are made available in the SCXML state machine through the root context as "workflowContext" and "workflowData" data variables.
Using the SCXML Workflow state machine
For interacting with and executing SCXML Workflow state machines, the Hippo SCXML Workflow Engine and the SCXMLWorkflowExecutor require specific conventions to be followed, both for the definition of the SCXML state machine and how to interact with it.
Allowable SCXML Workflow state machine actions
To be able to map and restrict specific Workflow operations (actions) to specific SCXML state machine events, the <Map<String, Boolean> SCXMLWorkflowContext.getActions() method provides access to an actions map which the SCXML state machine should use to define which actions are currently available and enabled or disabled.
For this purpose the org.onehippo.repository.scxml.ActionAction is provided (see also SCXML Workflow Actions and Tasks) and to be used like:
<!-- if not draft document holder AND granted hippo:admin --> <if cond="!editor and workflowContext.isGranted(draft,'hippo:admin')"> <!-- then "unlock" action" is enabled --> <hippo:action action="unlock" enabledExpr="true"/> </if>
This will store the action "unlock" with value Boolean.TRUE in the SCXMLWorkflowContext.getActions() provided map.
The Workflow implementation then can, after the SCXML Workflow state machine has been started, read this SCXMLWorkflowContext.getActions() map and for instance use this within its hints() method to communicate the allowable operations back to the Workflow invoker, like the CMS.
In addition, the SCXMLWorkflowExecutor will check and restrict the invocation of any workflow operation through its triggerAction(String action) methods against this actions map.
If the actions map does not contain a matching and enabled action a WorkflowException will be thrown.
For special cases, the SCXMLWorkflowExecutor also provides an additional triggerAction(String action, Map<String, Boolean> actionsMap) method through which a custom actions map can be provided to check against.
The SCXML Workflow state machine configured actions should correspond with SCXML event names within the SCXML Workflow definition like:
<transition event="unlock"> <!-- unlock the current draft document by setting the holder to the current (hippo:admin) user --> <hippo:setHolder holder="user"/> </transition>
And the actual Workflow unlock operation then can be 'triggered' like:
@Override public void unlock() throws WorkflowException { workflowExecutor.start(); workflowExecutor.triggerAction("unlock"); }
As also can be seen in the above example, the SCXMLWorkflowExecutor.start() method should always first invoked first, which will (re)evaluate the current SCXML Workflow state machine against a (re)initialized SCXMLWorkflowData
and update the current allowed actions in the SCXMLWorkflowContext, before executing the intended triggerAction()method.
Additional feedback
Besides the allowable actions map, a SCXML Workflow state machine can also provide additional feedback through the Map<String, Serializable> SCXMLWorkflowContext.getFeedback() method.
For this purpose the org.onehippo.repository.scxml.FeedbackAction is provided (see also SCXML Workflow Actions and Tasks) and to be used like:
<!-- provide the current draft document holder as "inUseBy" feedback --> <hippo:feedback key="inUseBy" value="holder"/>
The SCXML Workflow Engine itself doesn't make use of this feedback information, but it can be used (as for the example above) to return addition hints() information back to the Workflow invoker.
Retrieving results
Triggering a SCXML Workflow event might also produce a result to be returned back to the Workflow invoker or for further processing.
For this purpose the org.onehippo.repository.scxml.ResultAction is provided (see also SCXML Workflow Actions and Tasks) and to be used like:
<transition event="obtainEditableInstance"> <if cond="!!unpublished"> <!-- unpublished document exists: copy it to draft first --> <hippo:copyVariant sourceState="unpublished" targetState="draft"/> <elseif cond="!!published"/> <!-- else if published document exists: copy it to draft first --> <hippo:copyVariant sourceState="published" targetState="draft"/> </if> <!-- mark the draft document as modified, set the user as editor and remove possibly copied availabilities --> <hippo:configVariant variant="draft" applyModified="true" setHolder="true" availabilities=""/> <!-- store the newly created or updated draft document as result --> <hippo:result value="draft"/> </transition>
In the above example, a new draft document (variant) is created or else updated. The resulting (Java) document object then is stored in the SCXMLWorkflowContext using the <hippo:result> action.
The Workflow implementation thereafter can retrieve this result through the <Object> SCXMLWorkflowContext.getResult() method, or even directly as returned value from the SCXMLWorkflowExecutor.triggerAction() method:
@Override public Document obtainEditableInstance() throws RepositoryException, WorkflowException { workflowExecutor.start(); return (Document)workflowExecutor.triggerAction("obtainEditableInstance"); }
Checking access privileges from within the SCXML state machine
From within the SCXML Workflow state machine this can be used as already show above in the example for the "unlock" action.
The SCXMLWorkflowContext will use its contained WorkflowContext and evaluate the requested privileges against the WorkflowContext.getSubjectSession() JCR Session and also cache the result.
Using SCXMLWorkflowData
For the SCXML state machine to be able to access external data, the SCXMLWorkflowExecutor can be instantiated with an SCXMLWorkflowData interface implementation object.
The SCXMLWorkflowData interface itself only defines two methods to initialize and reset the data object, which will be automatically invoked by the SCXMLWorkflowExecutor.
The SCXMLWorkflowData instance will be provided in the SCXML state machine through the root context and then can be referenced and accessed using the SCXML expression language (Groovy) as well as from within custom SCXML actions.
The DocumentWorkflow uses a custom org.onehippo.repository.documentworkflow.DocumentHandle which implements SCXMLWorkflowData to provide the state of the current document. Through this DocumentHandle object the documentworkflow SCXML state machine is provded access to the current Workflow document handle, its child document variants and possible workflow requests.
When defining a custom SCXMLWorkflowData class, it is important to make sure all its volatile internal data is only loaded after its initialize() method is called, and cleared again when its reset() method is called.
SCXML state machine global script
The SCXML specification allows defining an global script element which is executed automatically when a SCXML state machine is initialized.
The Apache Commons SCXML implementation provides the very useful enhancement that the so called 'data context' of every state (including compound and parallel states) is 'inherited' within the definition, with the 'root context' being represented by the scxml element itself.
The (optional) global script element, defined as a direct child of the scxml element, also is tied to the 'root context', and therefore you can use the global script to 'extend' the 'root context' which will automatically become available to the rest of the state machine.
You can use the global script element to 'inject' extra or derived data, for example derived from the "workflowContext" and "workflowData" data objects (see above).
And, because the Hippo SCXML Workflow Engine uses Groovy as expression language for the SCXML state machines, you can even define and provide convenient Groovy methods through the script.
The dynamically compiled global Groovy script class will be used as base class for any further child script or condition expression within the SCXML document, and thus any (public) method defined in the
global script will automatically become available as inherited method to those sub classes.
There are a few restrictions and limitations which should be taken into account though.
Each script element and SCXML condition expression is dynamically compiled as a separate Groovy class once and then automatically cached as long as the SCXML Workflow Definition (not instance) is loaded.
Also note that these Groovy scripts and condition expressions are only compiled on first access basis within the SCXML state machine.
Therefore you should not use instance variables or conditionally defined methods, and should not use Groovy closures (as they retain references to their defining class instance).
But other than this, all Groovy language features should be usable :)
Be careful though with invoking additional Java or Groovy classes: invoking System.exit() would terminate the Hippo CMS instance!
The SCXML DocumentWorkflow uses the global script to define a set of convenient methods which make it much easier and readable to evaluate specific SCXMLWorkflowData values in condition expressions, as for example shown in the following fragments:
<scxml version="1.0" xmlns="http://www.w3.org/2005/07/scxml" xmlns:hippo="http://www.onehippo.org/cms7/repository/scxml" xmlns:cs="http://commons.apache.org/scxml" initial="handle"> <script> ... // published variant property method def getPublished() { workflowData.documents['published'] } ... // current requests map property method def getRequests() { workflowData.requests } .. // true if draft exists and currently being edited def boolean isEditing() { !!holder } .. // true if there is an outstanding workflow request def boolean isRequestPending() { workflowData.requestPending } </script> ... <state id="no-request"> <!-- transition to state "requested" when requests exists --> <transition target="requested" cond="!empty(requests)"/> </state> ... <!-- transition to state "editing" when there is no pending request and the draft variant is being edited --> <transition target="editing" cond="!requestPending and editing"/> <!-- else transition to state "editable" when there is no pending request and the draft variant doesn't exist yet or isn't being edited --> <transition target="editable" cond="!requestPending"/> ... <if cond="workflowContext.isGranted(published, 'hippo:editor')"> <hippo:action action="depublish" enabledExpr="true"/> </if> ... </scxml>
Configuring SCXML Workflow state machine states
When defining a SCXML Workflow state machine states, especially when parallel states are used, the following convention is advised to be followed:
<state id="mystate"> <state id="no-mystate"> <transition target="mystate-enabled" cond="mystate.condition == true"/> </state> <state id="mystate-enabled"> ... </state> </state>
A concrete example from the SCXML DocumentWorkflow is:
<state id="versioning"> <state id="no-versioning"> <!-- a document only becomes versionable once an unpublished document variant exists --> <transition target="versionable" cond="!!unpublished"/> </state> <state id="versionable"> ... </state> </state>
Using this convention, the SCXML state machine will only (automatically) move to the "versionable" state, and then enable further events and actions defined within that state, once an unpublished document variant exists. If not, the state machine will stay within the first (initial( "no-versioning" state, not enabling any other (version related) actions and events. And this convention will result in a state machine XML document which is easy to read and better (unit-) testable.
Using SCXML event payload data
When triggering a SCXML event, either as an external event through SCXMLWorkflowExecutor.triggerAction(String action, Map<String, Object> payload) or as an internal event through the SCXML <send> element, additional event payload data can be provided to be used for the SCXML event handling.
While Apache Commons SCXML accepts any object as payload, the SCXMLWorkflowExecutor requires the payload to be provided as a Map object, to (force) align with how the SCXML <send> element provides its (optional) payload for the event. This allows to use the same event handling for both internal and external events.
The provided payload data can then be access from within the SCXML state machine using the standard _event system variable its data member, like for example:
<transition event="copy"> <hippo:copyDocument destinationExpr="_event.data?.destination" newNameExpr="_event.data?.name"/> </transition>
which can be triggered with:
protected Map<String, Object> createPayload(String var1, Object val1, String var2, Object val2) { HashMap<String, Object> map = new HashMap<>(); map.put(var1, val1); map.put(var2, val2); return map; } @Override public void copy(final Document destination, final String newName) throws WorkflowException { workflowExecutor.start(); workflowExecutor.triggerAction("copy", createPayload("destination", destination, "name", newName)); }
Note the usage of the Groovy Save Navigation Operator ?. here: that is needed to guard against a NullPointerException when accessing the optional (copy) event payload. If the event was triggered without providing a payload, the _event.data member would be empty (null) and accessing the payload map elements then is not possible . In the above example the payload of course is provided, but this cannot be assumed from within the SCXML state machine.
Using local SCXML data
Another Commons SCXML specific feature is defining temporary and data variables in the context of the current state element using its org.apache.commons.scxml2.model.Var custom action element.
You can use this <http://commons.apache.org/scxml:var> element to define temporary 'scratch' data variables which only will be accessible within the current active state element context (and its children) which automatically will be erased after the defining state is exited.
This can be very useful for example when triggering internal events using the <send> element which takes a namelist attribute defining a list of named data variables from the current state context to build up a payload to be provided with the triggered event:
<scxml version="1.0" xmlns="http://www.w3.org/2005/07/scxml" xmlns:hippo="http://www.onehippo.org/cms7/repository/scxml" xmlns:cs="http://commons.apache.org/scxml"> ... <transition event="acceptRequest"> <!-- define temporary request variable for the event payload request parameter --> <cs:var name="request" expr="_eventdatamap.acceptRequest?.request"/> <!-- store the request workflow type as temporary variable --> <cs:var name="workflowType" expr="request.workflowType"/> <!-- store the request targetDate as temporary variable --> <cs:var name="targetDate" expr="request.scheduledDate"/> <!-- First delete the request itself. Note: after this the request object no longer can be accessed... which is why we had to create the temporary variables workflowType and targetDate above first! --> <hippo:deleteRequest requestExpr="request"/> <if cond="!targetDate"> <!-- the request didn't have a targetDate defined, simply trigger the "workflowType" value as event --> <send event="workflowType"/> <else/> <!-- the request did have a targetDate: trigger a 'scheduled' workflow action event --> <send event="workflowType" namelist="targetDate"/> </if> </transition>
In the above example the <cs:var> element is used to define temporary variables request, workflowType and targetDate. Also note the usage of the cs namespace prefix, which has to be declared on the scxml element above.
The <send> element is then used to trigger an SCXML internal event using the workflowType variable value and optionally a payload map based on the variable names specified through its namelist attribute (targetDate in this example).
Note: with Commons SCXML 2.0 milestone 1, this is currently the only supported usage for the <send> element!
Raising a WorkflowException within a SCXML Workflow state machine
It is possible to stop the execution and raise a org.hippoecm.repository.api.WorkflowException directly from within SCXML Workflow state machine using the org.onehippo.repository.scxml.WorkflowExceptionAction.
This action requires a error message to be provided as well as the condition expression when the WorkflowExpression should be thrown:
<hippo:workflowException condExpr="!_event?.data" errorExpr="No payload provided for the workflow copy event"/>