This document explains great lines of TinyWorkflow usage through an example.
It covers:
This section describes the environment of a typical TinyWorkflow application.
You usually need:
In our document flow example, we will use simple document only having two attributes: a name and a text body. Those document can be created and edited, then they must be validated (some automatic rules are applied to check that the document is valid). Once valid, some users with special right can publish the document.
This is described by the following state diagram:
In addition, we want that a user must 'lock' (=take ownership of) a document before doing transition on it. The system must be able to give the current owner of the document, so it is possible to see who is currently working on which document. (The locking/unlocking of a document will be implemented as transitions but they are not represented in the state diagram to keep it readable.)
The only transition not requiring that the user owns the document is 'rename' (it has no great functional meaning, it just to have an example of this behavior)
A tiny workflow is always attached of a java object called the 'workflow peer'. In this example the java object attached to the workflow is a document. The implementation and the behavior of this java object depend of the business application.
In our example the SimpleDocument class is very simple. At this stage, it contains just a name and a text and provides getter and setters for them. During this tutorial we will add the methods required by the application.
package org.tinyWorkflow.examples.simpleDocument; public class SimpleDocument implements WorkflowPeer { private String name; private String text; public String getName() { return name; } public void setName(String newName) { System.out.println(" -- Set name = " + newName); name = newName; } public String getText() { return text; } public void setText(String newText) { System.out.println(" -- Set text = " + newText.replace('\n', '|')); text = newText; } }
TinyWorkflow only requires that this object implements the org.tinyWorkflow.instance.WorkflowPeer interface. So we have to make our SimpleDocument implementing this interface.
The WorkflowPeer interface allows the peer object to be attached to a workflow instance.
When the workflow is created and associated to the peer the setWorkflow method is called to give the workflow reference to it. This method is also called when the workflow and peer are retrieved from the persistence (loaded from the database). It gives the peer the opportunity to always have a reference to its workflow.
For this example we only display a message to System.out when workflow is set. For this simple implementation the peer doesn't need a reference to the workflow.
package org.tinyWorkflow.example; import org.tinyWorkflow.instance.Workflow; import org.tinyWorkflow.instance.WorkflowPeer; public class SimpleDocument implements WorkflowPeer { (...) public void setWorkflow(Workflow wf) { System.out.println(" -- Workflow with id="+wf.getId()+" associated to document"); } }
Now that we have a Document object ready to be associated to a workflow, we have to create the workflow. For that we must provide TinyWorkflow engine with a XML document describing the workflow. The XML workflow definition is simply a transcription in XML of the workflow finite state machine.
Basically the workflow definition is a set of states, each containing a set of transitions. The transitions are inside their origin state. The initial state(s) of the workflow is (are) not represented in the definition. So the initial transitions (the transitions starting from the initial state) are declared in a special section of the workflow definition. The final states are states like others but they cannot define transitions.
The main XML element of the workflow definition is <workflow>. This elements looks like:
<?xml version="1.0" encoding="ISO-8859-1" ?> <workflow xmlns="http://org.tinyWorkflow/2005/WorkflowDefinition" id="simpleDocWorkflow" version="1.0" peer="org.tinyWorkflow.examples.simpleDocument.SimpleDocument"> <initialTransitions> ... </initialTransitions> <globalTransitions> ... </globalTransitions> <states> ... </states> </workflow>
You have to give it an id and a version, they will be used to identify this workflow definition.
The third attribute of the workflow element is 'peer'. It gives the full class name of the workflow peer object.
Then the workflow element contains several sections:
Let's take a look at the first transition: the 'init' initial transition. Initial transitions are not different of other transitions; they just appear in the <initialTransitions> section instead of in a state.
... <initialTransitions> <transition id="init"> <parameters> <stringParameter id="name"/> <textParameter id="text" mandatory="false"/> </parameters> <preFunctions> <beanshellFunction> peer=context.getPeer(); newName=context.getParam("name"); peer.setName(newName); newText=context.getParam("text"); if (newText!=null) peer.setText(newText); </beanshellFunction> </preFunctions> <results> <result state="draft"/> </results> </transition> </initialTransitions> ...
Each transition has a unique id (unique inside the workflow definition).
This transition definition contains 3 XML elements:
A BeanShell function is a little script defined by the BeanShell API (http://www.beanshell.org/). It is basically typeless java code. Inside a BeanShell script, the context variable of type org.tinyWorkflow.instance.TransitionContext is defined. This context let you access the transition parameters, the workflow instance, the workflow peer ... and call any of their public methods. In this case the setName and setText methods of the SimpleDocument are called.
The typical usage of a beanshell script is to make the glue between workflow transition and application code: It extract the transition parameters from the context and call methods on the workflow peer using those parameters.
Note that, though it is possible to do a lot of business logic in BeanShell scripts, it's better to keep them small and put your business logic into real java code.
So after effectuating this initial transition, the workflow will be in the 'draft' state and the associated document will have a name (and possibly a text).
Each TinyWorkflow instance can have an owner (identified by his user id). When a transition is done the result of the transition not only specifies the new state of the workflow but also the new owner of the workflow. In our example workflow we will add two transitions allowing to manage the ownership of the workflow:
Those two transitions don't change the current state of the workflow.
As those transitions must be available everywhere, they will be put in the <globalTransitions> so they can be referenced from all states.
... <globalTransitions> <transition id="lock"> <conditions> <workflowHasNoOwner/> </conditions> <results> <result state="${oldStateId}" owner="${callerId}"/> </results> </transition> <transition id="unlock"> <conditions> <callerIsWorkflowOwner/> </conditions> <results> <result state="${oldStateId}"/> </results> </transition> </globalTransitions> ...
Those two definitions should already look familiar: you can see that they don't define parameters and that TinyWorkflow provide standard conditions allowing to check workflow ownership (their name is self-describing).
Here, we added something new in the result of the transaction: in the attributes of the result XML element you can put expressions in the form of ${expression}. The expression used here is an Apache/Jakarta Commons-BeanUtils expression (http://jakarta.apache.org/commons/beanutils).
Basically this API maps 'attributes' of the expression to java methods like following:
Expression | Mapped code |
${someAttribute} | context.getSomeAttribute() |
${mappedAttr(key)} | context.getMappedAttr("key") |
${indexedAttr[i]} | context.getIndexedAttr(i) |
You can combine expression with dots:
Expression | Mapped code |
${someAttribute.mappedAttr(key).indexedAttr[i]} | context.getSomeAttribute().getMappedAttr("key").getIndexedAttr(i) |
Those expressions apply to the transition context (of type org.tinyWorkflow.instance.TransitionContext). So any attribute reachable from the transition context can be inserted in attribute using those 'beanUtils' expressions.
Here is a list most commonly used expressions:
Expression | Mapped code | Description |
${oldStateId} | context.getOldStateId() | The Id of the workflow state before the transition. It's useful for defining self transitions. |
${oldOwnerId} | context.getOldOwnerId() | The Id of the owner of workflow state before the transition. |
${callerId} | context.getCallerId() | The Id of the user executing the transition. |
${param(paramId)} | context.getParam("paramId") | Get a parameter of the transition. |
${workflow} | context.getWorkflow() | Get the workflow instance. |
${peer} | context.getPeer() | Get the workflow peer. |
The state list simply declares all the states of you Finite State Machine. Just remember that the initial states are not represented (because at this stage the workflow is 'uninitialized').
Let's look at the 'readyForPublishing' state declaration:
... <states> ... <state id="readyForPublishing"> <transitions> <transitionRef refId="lock"/> <transitionRef refId="unlock"/> <transitionRef refId="rename"/> <transitionRef refId="edit"/> <transition id="publish"> <permissions> <allowedCaller ids="Alice,Bill"/> </permissions> <conditions> <callerIsWorkflowOwner/> </conditions> <preFunctions> <peerCall method="publish"/> </preFunctions> <results> <result state="published"/> </results> </transition> </transitions> </state> ... </states> ...
A state definition must specify a unique id for the state. It lists all the transitions starting from this state.
In the 'publish' transition illustrated here, you can see an example of another function call. The <peerCall> function just calls the named method of the peer java object. The signature of the method can be:
The workflow API will search for the best-fitting method signature. The return value is ignored unless a 'resultParamId' is specified (not in this case). If so, the result of the function call is put in the context as parameter using the specified Id. In our example we just add a method
public void publish(TransitionContext context)
in the SimpleDocument object. The implementation of this method is outside of the scope of this example. Typically publishing would be 'posting' the document to a URL obtained from some configuration property.
The list of states can also contain final states. Those states are even simpler because they cannot contain a transition. When a workflow instance reach a final state, it global status switch to 'Workflow.STATUS_FINISHED' and no more transition can be done on it.
In our example, there is one final state: the 'published' state.
... <endState id="published"> </endState> </states> </workflow>
In the 'readyForPublishing' transition definition, you've certainly noticed a new <permissions> section. This section contains conditions for the transition availability just as the <conditions> section. The transition can be executed if all the conditions of both sections are true.
The difference between the two sections is that the <permissions> section contains conditions related only to user access rights while the condition <conditions> contains conditions about the workflow state. Unlike the conditions about workflow state, access rights are not supposed to change (or supposed to rarely change) during usage of the workflow because they are managed externally by a system administrator.
For the workflow core API, <permissions> or <conditions> are equivalent, the only difference is in the interpretation made by external code:
If your application doesn't need those features, you can simply put everything in the <conditions> section and ignore the <permissions>. However, it is good to do it simply to improve readability of the XML definition.
To finish our example workflow we just need to introduce one concept: the conditional result. In the Finite State Machine diagram, you can see that the result of the 'validate' transition is 'draft' if the validation failed or 'readyForPublishing' if the validation succeeded.
To implement that we need first a validate method on the SimpleDocument. Let's implement it like this:
public boolean validate() { boolean valid = false; if (text != null) { String[] tokens = text.split("\n"); valid = tokens.length > 4; } System.out.println(" -- Document is valid = " + valid); return valid; }
We hardcoded a simple validation rule: 'The document must have more than 4 lines'
This method returns true for valid documents and false for invalid ones.
Now we have to wire this method with the workflow to have it governing the transition result.
This is done as follows:
... <state id="draft"> <transitions> <transitionRef refId="lock"/> <transitionRef refId="unlock"/> <transitionRef refId="rename"/> <transitionRef refId="edit"/> <transition id="validate"> <conditions> <callerIsWorkflowOwner/> </conditions> <preFunctions> <peerCall method="validate" resultParamId="valid"/> </preFunctions> <results> <conditionalResult state="readyForPublishing"> <paramEquals paramId="valid" value="true"/> </conditionalResult> <result state="draft"/> </results> </transition> </transitions> </state> ...
In the transition we first call the validate method with a <peerCall> function. We tell the function to put the result in the context as the "valid" parameter.
So after this function call the context will contain a parameter "valid" with the Boolean value true or false.
Then in the result list, we specify a <conditionalResult> containing condition saying to use it when the parameter "valid" is true. This result is followed by an unconditional result.
To get the actual result of a transition the workflow API evaluates the condition of each conditional result sequentially. The first result with a matching condition will be used. If none of the conditions are verified the last result of the list (which must always be an unconditional result) is used.
So, this will lead to the state 'readyForPublishing' when (valid==true) and to the state 'draft' otherwise.
Until now, we only specified Ids for workflows, transitions, states ...
This is enough for the API but it can be difficult for users to understand them, especially if users are not all speaking the same language. So in addition the ids, you can specify 'names' and 'descriptions' for workflows, transitions, states and parameters.
As the name and description is language dependent, you can specify several of them for several locales. In addition you should always define a default name or description used when no specific name/description is found for a given locale.
The way of defining names and description is consistent for all items: you just have to add <name> or <description> elements for each applicable locale.
<?xml version="1.0" encoding="ISO-8859-1" ?> <workflow id="simpleDocWorkflow" ... > <name value="Example of document workflow"/> <name locale="fr" value="Exemple de flux documentaire"/> <description> This is a example workflow for tutorial explaining how to add workflow behavior to a document java object. </description> <description locale="fr"> Ceci est un exemple de workflow utilisé dans un tutoriel expliquant comment associer un comportement de workflow à un objet java repésentant un document. </description> ... </workflow>
So, internally the API will always use the id 'exampleDocWorkflow' to refer this workflow but the users should only see the workflow name 'Example of document workflow' or 'Exemple de flux documentaire' (depending on their locale).
With that you have all you need to write the example document workflow definition.
The few transitions remaining to be defined are just small variations of the others.
You can find:
Now that we have a workflow definition, we should see how to create a workflow instance and use it.
First thing to do is to configure a WorkflowDefinitionRepository and load the XML workflow definition in it.
This is quite simple:
// first create a repository and load the example workflow definition in it. WorkflowDefinitionRepository repo = WorkflowDefinitionRepository.getInstance(); repo.addXmlDefinition(getClass().getResource("SimpleWorkflow.xml"));
With that you have everything you need to start a workflow.
Simply create a workflow instance:
Workflow wf = repo.createWorkflowInstance("exampleDocWorkflow", null);
Just call the repo.createWorkflowInstance of the repository. You have to provide the workflow id and version. If you pass 'null' as version the repository will use the latest version available for the workflow definition.
In this case, we don't pass a persistence object because we have set a default one for the repository.
Then we can execute transitions on the workflow instance:
Map initParams = new HashMap(); initParams.put("name", "My first doc"); wf.doTransition("init", initParams, "Bob");
Doing a transition is always the same: setup a parameter Map (if needed, otherwise pass 'null' as parameter map) and call the doTransition method of the workflow.
That's it ! Doing transitions will call the functions calling themselves the workflow peer business methods to execute the underlying job. Then you simply have to call 'doTransition' until the workflow reachs a final state.
At any time, you can inspect the workflow state to find what is it state in the state machine, who is the current owner, who is the last updator ...
You can also get the list of the transitions available for a user by calling the getAvailableTransitions(params, userId). This method allows you to present to a user the list of transition that he can actually perform. Note that the parameters are usually ignored by this method (so you can safely pass null). Those parameters are there just to give the opportunity to pass specific data required by custom extension. It's just a way to add custom data to the context that can be needed by specific condition implementations.
As an example of that, the displayState method prints to System.out the workflow state, owner, the list of transition available for a user and the parameters expected by those transitions.
public void displayState(Workflow wf, String userId) throws WorkflowException { // display state and owner System.out.println(" * current state is: " + wf.getCurrentStateDefinition().getId() + ", owner=" + wf.getOwnerId()); // display the list of available transitions System.out.print(" * Available transitions for " + userId + " are:"); List transitions = wf.getAvailableTransitions(null, userId); for (Iterator it = transitions.iterator(); it.hasNext();) { TransitionDefinition trans = (TransitionDefinition) it.next(); System.out.print(" "); System.out.print(trans.getId()); // display parameters of the transition Iterator params = trans.getParameterDefinitions(); if (params.hasNext()) { System.out.print("("); for (; params.hasNext();) { TransitionParameterDefinition param = (TransitionParameterDefinition) params.next(); System.out.print(param.getId()); if (params.hasNext()) System.out.print(","); } System.out.print(")"); } } System.out.println(""); }
This concludes our document workflow example.
You can find more in following documents: