Customization of TinyWorkflow

This document explains how and when to customize TinyWorkflow components like functions, parameter descriptions, conditions, meta-attributes.

This document will not explain the basics of TinyWorkflow, please read the 'Getting Started with TinyWorkflow' document for an introduction.

Overview

Some components of the TinyWorkflow API are customizable. They are the components allowing binding the workflow with your business code.

They are:

  • Functions: functions simply allow executing a piece of code before and after transition.
  • Conditions: conditions are guards to be valid before a transition if started.
  • ParameterDescriptior: describe and validate parameters of a transition
  • Meta-data: encode meta-data (data not interpreted by the workflow) for definition components.
  • PriorityCalculator: calculate the priority of the workflow.
  • WorkflowFlag: calculate some flags allowing to easily display the workflow sate (or the state of one of it sub-workflow).

For each of those components type, the workflow API provides a set of pre-defined implementation. There are, in addition, several mechanisms that allow extending the functionalities of those pre-defined implementations:

  • Beanshell scripts: Beanshell is a java-like scripting language (http://www.beanshell.org/). It allows you to embed java code directly in workflow XML definition.
  • Call a method on the workflow peer: from the XML definition you can call a method of the workflow peer object and implement your custom logic in it.
  • Implement a java interface: You can implement a specific java interface and call your custom object from XML workflow definition.
  • New XML object: You can provide a new XML object (with customized XML tags, attributes and structure) and add it to the set of pre-defined XML elements.

All the methods are not available for all types of components. The following table summarizes the availability of extension mechanism along with the name of interface to implement.

Component typeBeanshellPeer methodJava InterfaceCustom XML element
Function<beanshellFunction><peerCall><proxyFunction> with class implementing:
org.tinyWorkflow.definition.function.WorkflowExternalFunction
Class implementing:
org.tinyWorkflow.definition.function.WorkflowFunction and
org.tinyWorkflow.utils.xml.serialize.XMLSerializable
Condition<beanshellCondition><peerCondition><proxyCondition> with class implementing:
org.tinyWorkflow.definition.condition.WorkflowExternalCondition
Class implementing:
org.tinyWorkflow.definition.condition.WorkflowCondition and
org.tinyWorkflow.utils.xml.serialize.XMLSerializable
Parameter Description--<proxyParameter> with class implementing:
org.tinyWorkflow.definition.parameter.TransitionParameterExternalDefinition
Class implementing:
org.tinyWorkflow.definition.parameter.TransitionParameterDefinition and
org.tinyWorkflow.utils.xml.serialize.XMLSerializable
Meta-attribute---Class implementing:
org.tinyWorkflow.definition.metadata.MetaData and
org.tinyWorkflow.utils.xml.serialize.XMLSerializable
PriorityCalculator-<peerPriority><proxyPriority> with class implementing:
org.tinyWorkflow.definition.priority.ExternalPriorityCalculator
Class implementing:
org.tinyWorkflow.definition.priority.PriorityCalculator and
org.tinyWorkflow.utils.xml.serialize.XMLSerializable
WorkflowFlag-<peerBooleanFlag><proxyFlag> with class implementing:
org.tinyWorkflow.definition.flags.ExternalWorkflowFalg
Class implementing:
org.tinyWorkflow.definition.flags.WorkflowFlag and
org.tinyWorkflow.utils.xml.serialize.XMLSerializable

Example function

In the next chapter of this document we will show how to implement the same function in the 4 different ways supported by TinyWorkflow. As the mechanism of extension is the same for every type of component, it will be easy to apply what you have learned to customize other component types.

Note that all the predefined components provided by the TinyWorkflow API (see WorkflowXmlReference.html for an exhaustive list) are implemented as 'Custom XML elements'. So by looking at the code of those components you can have examples of this type of extension for all components.

Let's define a very simple function.

Our function will:

  • Print a message (defined in the XML definition) to System.out
  • Put a variable 'done' with value true in the transition context.

It's very basic but this it demonstrate the two major functionalities of functions: run any code and interact with the transition context.

Beanshell function

The advantage of beanshell is that you can put all the logic doing the 'glue' between the workflow and the business object inside the XML definition. So you can have you business code residing in your java business objects and you can use them from the workflow without having those business objects depending on the workflow.

Beanshell implementation of the function is straightforward:

  • You have access to the standard java objects like System
  • There is always a 'context' variable defined giving you the TransitionContext.

So the beanshell function call is simply:

        ...
        <preFunctions>
            <beanshellFunction>
                System.out.println(" * Hello from beanshell function");
                context.setParam("done", Boolean.TRUE);
             </beanshellFunction>
        </preFunctions>
        ...

Peer Method call

When your workflow is associated to a peer object, you can simply call a method on this peer. It's the simplest way to call code on business object.

Just use the <peerCall> function in the XML definition to specify which method is called:

        ...
        <preFunctions>
            <peerCall method="myFunction1" resultParamId="done1"/>
            <peerCall method="myFunction2"/>
        </preFunctions>
        ...

For this example we use 2 <peerCall> to illustrate the various ways of calling method on a peer object:

  • The peer method can define no parameters or accept the TransitionContext as parameter.
  • The peer method can return a value or not.

The first <peerCall> calls the following method:

    public boolean myFunction1() {
        System.out.println(" * Hello from peer function 1");
        return true;
    }

In this case, the transition context is not passed to the method. The return value is put in the transition context with the id specified by the 'resultParamId' attribute.

The second <peerCall> calls the following method:

    public void myFunction2(TransitionContext context) {
        System.out.println(" * Hello from peer function 2");
        context.setParam("done2", Boolean.TRUE);
    }

In this case, the transition context is given as a parameter of the method. This method doesn't return a value but put itself the 'done2' parameter in the transition context.

Java Class implementation

With java class implementation you can easily make a reusable function that can be parameterized from the XML definition. All of this without having to bother about XML parsing.

Inside the XML definition, you will use a <proxyFunction> element mapped to a ProxyFunction object. The ProxyFunction handle the XML parsing, it allows also specifying meta-data. In the XML, you also specify the class or your reusable function. The proxy will then delegate the actual function execution to this class.

You just have to implement the org.tinyWorkflow.definition.function.WorkflowExternalFunction interface:

public class MyExternalFunction implements WorkflowExternalFunction {

    /** Id of the message property passed to the function */
    public static final String MESSAGE_PROPERTY_ID = "message";
    private String message;

    public MyExternalFunction() {
    }

    public void init(ProxyFunction proxy) throws SAXException {
        message = (String) proxy.getMetaData(MESSAGE_PROPERTY_ID);
        if (message == null) {
            throw new SAXException("The '"+MESSAGE_PROPERTY_ID+"' meta-data is mandatory");
        }
    }

    public Object execute(TransitionContext context) throws WorkflowException {
        System.out.println(message);
        return Boolean.TRUE;
    }

}

The external function can be parameterized with the init method. The init method gives you a ProxyFunction containing all the meta-data objects declared in the XML. Here we use a simple property meta-data holding the message to display. The init method is called during parsing so you can validate the expected properties and can throw a SAXException if something is invalid or missing.

The implementation of the function is the same as usual. As the transition context is given as parameter, you have the choice to put the result directly in the context or to return it and specify the 'resultParamId' attribute in the XML (just as for the <peerCall> example). Here we have chosen the second possibility.

We can then use the class with a <proxyFunction> XML element:

        ...
        <preFunctions>
            <proxyFunction class="org.tinyWorkflow.examples.customisation.MyExternalFunction" 
                           resultParamId="done">
                <meta>
                    <property id="message" value=" * Hello from the External function"/>
                </meta>
            </proxyFunction>
        </preFunctions>
        ...

Customizing the XML

So far we shown how to add you own reusable functions but having to pass the full class name in the XML the entire the configuration as meta-data it tedious work. It is also error-prone because you only have few syntactical checks.

In this section, we will go further. You will learn how to define your own XML elements to extend the set of pre-defined elements.

Adding a new XML element involves:

  • Creating a class implementing the element interface (like org.tinyWorkflow.definition.function.WorkflowFunction). This class must also be able to parse itself, to do so it must implement the org.tinyWorkflow.utils.xml.serialize.XMLSerializable interface.
  • Extending the W3C XML Schema describing the workflow XML definition to add definition of the new XML element.
  • Customizing the workflow repository to register the new XML element and its associated class. The new repository must also refer be XML Schema extension to have the workflow definitions properly defined.

To explain how to do those points, we will implement a custom function as defined in previous section.

The class

The XML parsing is done with a light customization of the SAX parsing API. It uses the standard SAX parser provided with java (Xerces). So, no special XML API is required.

See (TODO) for explanation of the parsing mechanism.

public class MyFunction implements WorkflowFunction, XMLSerializable {

    /** Tag for class function definition in XML */
    public static final String MY_URI = "http://myCompnay.org/my-2005";
    /** Tag for class function definition in XML */
    public static final String MY_FUNCTION_TAG = "myFunction";
    /** Name of the XML attribute holding the message displayed by the function */
    private static final String MESSAGE_ATTRIBUTE = "message";

    private String message;

    public MyFunction() {
    }

    public void execute(TransitionContext context) throws WorkflowException {
        System.out.println(message);
        context.setParam("done", Boolean.TRUE);
    }


    public void startElement(String uri, String localName, XMLEventParser parser) throws SAXException {
        if (MY_URI.equals(uri)) {
            if (localName.equals(MY_FUNCTION_TAG)) {
                message = parser.getMandatoryAttributeValue(MESSAGE_ATTRIBUTE);
            } else {
                throw new SAXParseException("Unexpected xml element <" + localName + ">", parser.getLocator());
            }
        } else {
            throw new SAXParseException("Unexpected xml uri '" + uri + "'", parser.getLocator());
        }
    }

    public void endElement(String uri, String localName, XMLEventParser parser) throws SAXException {
        if (MY_URI.equals(uri) && localName.equals(MY_FUNCTION_TAG)) {
            parser.endParsing(this);
        }
    }

    public void serialize(XMLSerializer serializer) throws XMLSerializeException {
        // TODO
    }

}

As you cannot modify the TinyWorkflow schema directly, you have to extend it using another namespace. (see next section)

So now you can directly use the new XML element with it custom parameters in the XML definition.

Note that the new elements will be in a new XML namespace (this is the regular way to do schema extension), so don't forget to declare you new namespace before to use it (It's a good practice to declare it in the top XML element).

<?xml version="1.0" encoding="ISO-8859-1" ?>
<workflow xmlns="http://org.tinyWorkflow/2005/WorkflowDefinition"
          xmlns:my="http://myCompnay.org/my-2005"
          id="myWorkflow" version="1.0"
          peer="org.tinyWorkflow.examples.customisation.TestPeer">
          
    ...     
                <preFunctions>
                        <my:myFunction message=" * Hello from myFunction"/>
                </preFunctions>
        ...

The Schema

To extend the schema, you have to declare your new element in a new schema (using a new namespace). Your element has to be declared as part of a substitution group so it's possible to use it everywhere the other members of the substitution group are used. This implies that you declare your element as a global element of your namespace and that the type of this element extends the substitution group head element type.

See W3C XML Schema for more explanation about schemas. In particular the XML Schema Part 0: Primer Second Edition is a good technical introduction.

Don't forget to import the TinyWorkflow XML schema to be able to use the elements/types it declares.

Component typeSubstitution group head elementSubstitution group head type
FunctionfunctionbaseFunctionType
ConditionconditionbaseConditionType
Parameter DescriptionparameterbaseParameterType
Meta-attributemetaDatabaseMetaDataType
WorkflowFlagflagbaseFlagType

For our custom function the resulting schema is:

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" 
           xmlns="http://myCompnay.org/my-2005" 
           xmlns:wf="http://org.tinyWorkflow/2005/WorkflowDefinition" 
           targetNamespace="http://myCompnay.org/my-2005" 
           elementFormDefault="qualified">

        <xs:import namespace="http://org.tinyWorkflow/2005/WorkflowDefinition"/>

        <xs:element name="myFunction" type="myFunctionType" substitutionGroup="wf:function"/>
        
    <xs:complexType name="myFunctionType">
        <xs:complexContent>
                <xs:extension base="wf:baseFunctionType">
                                <xs:attribute name="message" type="xs:string" use="required"/>
                </xs:extension>
        </xs:complexContent>
        </xs:complexType>
        
</xs:schema>

The Repository

The repository has to be customized of know the new XML elements. The simplest way to do it is to create a new repository class extending the default repository (org.tinyWorkflow.definition.WorkflowDefinitionRepository).

  • In this new repository you have to add a mapping from the XML uri+tag of your custom element to the corresponding implementing class. The repository contains a DefinitionObjectFactory holding those mappings (the standard objects use the same mechanism).

    You can simply add this mapping by overriding the method creating the DefinitionObjectFactory:

        protected DefinitionObjectFactory createFactory() {
            DefinitionObjectFactory factory = super.createFactory();
            factory.defineElement(MyFunction.MY_URI, MyFunction.MY_FUNCTION_TAG, MyFunction.class.getName());
            return factory;
        }
    
  • You have also to tell to the repository where is your schema extension so it can validate the XML containing your custom objects.

    You can simply add the new schema location to the method building the schema locations list:

        protected String getSchemaLocations() {
            String schemaLocations = super.getSchemaLocations();
            URL schemaUrl = getClass().getResource("MyWorkflowExtension.xsd");
            if (schemaUrl == null) {
                throw new IllegalStateException("Cannot find workflow definition schema in classpath: /"
                        + getClass().getPackage().getName().replace('.', '/') + "/MyWorkflowExtension.xsd");
            }
            return schemaLocations + " " + MyFunction.MY_URI + " " + schemaUrl.toExternalForm();
        }
    

    Note: another way is to disable schema validation by calling setSchemaValidation(false) on the repository. If you do that, you don't have to provide schema for your new elements. Of course, then, you just have the very basic XML validation coded in the java objects (In general the object themselves only check what is crucial for them to have a valid state, everything else is ignored).

Running the example

TODO