SCXML Workflow Execution
SCXML Workflow Execution
The Bloomreach Experience Manager SCXML Workflow Engine provides a genericorg.onehippo.repository.scxml.SCXMLWorkflowExecutor to execute SCXML Workflow definitions loaded from the Hippo Repository.
ASCXMLWorkflowExecutor is a generics based class which is instantiated (and possibly extended) using aorg.onehippo.repository.scxml.SCXMLWorkflowContext and an optionalorg.onehippo.repository.scxml.SCXMLWorkflowData interface implementation.
Theorg.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); } }TheSCXMLWorkflowContext provides thescxmlId identifier (node name) of the SCXML Workflow definition in the repository, which is loaded by theSCXMLWorkflowExecutor through theorg.onehippo.repository.scxml.SCXMLRegistry.
TheSCXMLRegistry itself is a service module registered throughthe org.onehippo.cms7.services.HippoServiceRegistry.
Once the SCXML Workflow Definition is loaded from the repository, a SCXML state machine is instantiated using aorg.apache.commons.scxml2.SCXMLExecutor and used during subsequent interactions with the SCXML state machine through theSCXMLWorkflowExecutor.
TheSCXMLWorkflowContext and optionalSCXMLWorkflowData 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 thedefinition of the SCXML state machine and how to interact with it.
Allowable SCXML Workflow state machine actions
To be able to mapand 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 theorg.onehippo.repository.scxml.ActionAction is provided (see alsoSCXML 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 theSCXMLWorkflowContext.getActions() provided map.
The Workflow implementation then can, after the SCXML Workflow state machine has been started, read thisSCXMLWorkflowContext.getActions() map and for instance use this within itshints() method to communicate the allowable operations back to the Workflow invoker, like the CMS.
In addition, theSCXMLWorkflowExecutor will check and restrict the invocation of any workflow operation through itstriggerAction(String action) methods against this actions map.
If the actions map does not contain a matchingand enabled action a WorkflowException will be thrown.
For special cases, theSCXMLWorkflowExecutor also provides an additionaltriggerAction(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:
@Overridepublic void unlock() throws WorkflowException { workflowExecutor.start(); workflowExecutor.triggerAction("unlock");}As also can be seen in the above example, theSCXMLWorkflowExecutor.start() method shouldalways first invoked first, which will (re)evaluate the current SCXML Workflow state machine against a (re)initializedSCXMLWorkflowData
and update the current allowed actions in theSCXMLWorkflowContext,before executing the intendedtriggerAction()method.
Additional feedback
Besides the allowable actions map, a SCXML Workflow state machine can also provide additional feedback through theMap<String, Serializable> SCXMLWorkflowContext.getFeedback() method.
For this purpose theorg.onehippo.repository.scxml.FeedbackAction is provided (see alsoSCXML 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 additionhints() 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 theorg.onehippo.repository.scxml.ResultAction is provided (see alsoSCXML 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 theSCXMLWorkflowContext 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 theSCXMLWorkflowExecutor.triggerAction() method:
@Overridepublic 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.
TheSCXMLWorkflowContext will use its containedWorkflowContext and evaluate the requested privileges against theWorkflowContext.getSubjectSession() JCR Session and alsocache the result.
Using SCXMLWorkflowData
For the SCXML state machine to be able to access external data, theSCXMLWorkflowExecutor can be instantiated with anSCXMLWorkflowData interface implementation object.
TheSCXMLWorkflowData interface itself only defines two methods to initialize and reset the data object, which will be automatically invoked by theSCXMLWorkflowExecutor.
TheSCXMLWorkflowData 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 customSCXML actions.
TheDocumentWorkflow uses a customorg.onehippo.repository.documentworkflow.DocumentHandle which implementsSCXMLWorkflowData to provide the state of the current document.Through thisDocumentHandle object the documentworkflow SCXML state machine is provded access to thecurrent Workflow document handle, its child document variants and possible workflow requests.
When defining a customSCXMLWorkflowData class, it is important to make sure all its volatile internal data is onlyloaded after itsinitialize() method is called, andcleared again when itsreset() 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 asbase 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 asinherited 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 classonce and then automatically cached as long as the SCXML WorkflowDefinition (not instance) is loaded.
Also note that these Groovy scripts and condition expressions areonly compiled on first access basis within the SCXML state machine.
Therefore youshould not use instance variables or conditionally defined methods, andshould not use Groovy closures (as they retain references to their defining classinstance).
But other than this, all Groovy language features should be usable :)
Be careful though with invoking additional Java or Groovy classes: invokingSystem.exit() would terminate the Hippo CMS instance!
TheSCXML DocumentWorkflow uses the global script to define a set of convenient methods which make it much easier and readable to evaluate specificSCXMLWorkflowData 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 theSCXML 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 throughSCXMLWorkflowExecutor.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, theSCXMLWorkflowExecutor 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;}@Overridepublic 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 GroovySave Navigation Operator ?. here: that is needed to guard against aNullPointerException when accessing theoptional (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 itsorg.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 anamelist 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 variablesrequest,workflowType andtargetDate. Also note the usage of thecs namespace prefix, which has to be declared on thescxml element above.
The<send> element is then used to trigger an SCXML internal event using theworkflowType variable value and optionally a payload map based on the variable names specified through itsnamelist attribute (targetDate in this example).
Note: with Commons SCXML 2.0 milestone 1, this is currently theonly supported usage for the<send> element!
Raising a WorkflowException within a SCXML Workflow state machine
It is possible to stop the execution and raise aorg.hippoecm.repository.api.WorkflowException directly from within SCXML Workflow state machine using theorg.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"/>