Getting started with TinyWorkflow

This document explains great lines of TinyWorkflow usage through an example.

It covers:

  • Overview
  • Workflow definition
  • Workflow runtime usage

The example workflow

This section describes the environment of a typical TinyWorkflow application.

You usually need:

  • Functional analysis of your application with, in particular, a description of the workflow(s) in the form of finite state machine.
  • The business model implementation. TinyWorkflow will interact with the classes of your business model to add workflow behavior to them.

State machine

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:

<i>Diagram 1: example document state diagram</i>

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)

Document class

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");
    }

}

The Workflow Definition

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 workflow element

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:

  • initialTransitions: list the initial transitions of the workflow (the 'init' transition in our example).
  • globalTransitions: a set of transition definitions that can be referenced from any state. This allows reusing the same transition definition in several states.
  • states: the definitions of all the states. Those states will contain transition definitions or references to global transition definitions (if they are not final states).

The first transition

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:

  • <parameters> : This element contains a list of the parameters used by the transition. In this case it specifies that the transition need a string parameter called 'name' and an optional text parameter called 'text'. 'string' means single-line, while text is supposed to be multi-line.  Those parameters will be passed in a Map to the workflow as parameter of the doTransition method. When a transition is started the workflow check if parameter map contains the correct parameters. If not, an org.tinyWorkflow.definition.parameter.ValidationException describing the wrong parameter is thrown.
  • <preFunctions> : The transition pre-functions (or post-functions) are functions executed before (or after) a transition. It is the way used by the workflow to interact with the application code. There are various types of functions; here a BeanShell function is used.

    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.

  • <results> : This element contains a list of possible transition result. When there is more than one result, they provide conditions to choose which one will be used. In this case, there is only one result saying that the state after the transition will be 'draft'.

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).

The workflow ownership

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:

  • lock: Allowing to become the owner of the workflow (only applicable if there was no owner).
  • unlock: Allowing to release a lock (only applicable if the user calling this transition is the current workflow owner).

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:

ExpressionMapped code
${someAttribute}context.getSomeAttribute()
${mappedAttr(key)}context.getMappedAttr("key")
${indexedAttr[i]}context.getIndexedAttr(i)

You can combine expression with dots:

ExpressionMapped 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:

ExpressionMapped codeDescription
${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.

States

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.

  • For transitions defined in the <globalTransitions> section, you just have to put a reference to the transition with the <transitionRef refId="..."/> element.
  • You can also define transitions specific to a state directly in the state. It's the case here for the 'publish' transition. This transition definition is not different from the other transitions definitions or <globalTransitions> sections.

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:

  • public void methodName();
  • public void methodName(TransitionContext context);
  • public Object methodName();
  • public Object methodName(TransitionContext context);

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>

Permission vs. Condition

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:

  • The error reporting is adapted: if the permissions are not verified you will get a message like "Insufficient privileges".
  • A GUI can display possible transitions differently: the transitions for which you don't have permission are not displayed while the transitions with conditions not verified are displayed but with disabled buttons. It allows generating more user-friendly GUIs.
  • Task description can clearly explain that you don't have enough rights to progress the workflow.

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.

Conditional transition

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.

Internationalization

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.

  • The 'name' is a short string to be displayed to the user (rather than the ID). It is supposed to be unique, as the user uses it to identify the objects, but it is not mandatory as the API only uses ids.
  • The 'description' is a small description of the item (few lines). It is useful as internal documentation of the workflow, in reports or in generated GUI.

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).

That's it!

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:

  • The full XML workflow definition here (TODO)
  • The full SimpleDocument class code here (TODO)

Workflow Usage

Now that we have a workflow definition, we should see how to create a workflow instance and use it.

Configuring the repository

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"));
        
  • The simplest way to use the repository is to use it as a singleton. So once a workflow definition is loaded it can be used by any class. As the repository doesn't keep state of the workflow instances, it is totally safe to refer it from any Thread (as long as you don't start to modify the definitions).
  • To add a definition, you just have to load it in the repository by giving a URL pointing to it (as here) or a File or by providing it as a stream.
  • To be able to instantiate a workflow, you have to configure the persistence scheme it will use (how the workflow are loaded and saved to a database or something equivalent). The simplest way is to use a memory-only persistence: nothing is saved or loaded. The repository is configured to use by default this MemoryWorkflowPersistence, so there is nothing special to configure here. You will find more about persistence here: (TODO).

With that you have everything you need to start a workflow.

Running the 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.

Querying the workflow 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("");
    }

More

This concludes our document workflow example.

You can find more in following documents:

  • Running examples (TODO)
  • Developping GUI (TODO)
  • Configuring hibernate persistence (TODO)
  • Using EJB facade (TODO)