Saturday, March 15, 2008

More Workflow - XML and Spring Integration

Last week, I described how I modeled a Workflow as a Petri Net, an idea I got from the Bossa project. This week, I describe some more improvements that enable this abstraction to be consumed easily by the application it is going to be embedded in. Specifically, they are:

  • XML configuration of workflow(s).
  • Add support multiple workflows in the same application.
  • Address Workflow contention issues.
  • Access work items defined as Spring beans from the workflow.
  • Replace BSF with Java 1.6's built-in scripting.

To do this, I had to refactor the code a bit, so I will provide all of the code once again so it is easy to follow along. There was also a bug in the breadth-first traversal which caused certain transactions to be fired twice, so that is fixed in the new code too.

XML Configuration and Multiple Workflows

Bossa provides a GUI tool that allows you to draw a Petri Net representing a workflow and builds an XML configuration file from it. So if I had used Bossa, I wouldn't have to write this. Starting from XML is definitely a step forward, but it is by no means a panacea. I cannot over-emphasize the importance of drawing the Petri Net first. There are configuration issues that are quite hard to debug by looking at the straight API and the XML, but which are readily apparent when you look at the diagram.

The XML equivalent of the Workflow described in my last post would look something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?xml version="1.0" encoding="UTF-8"?>
<workflows>
  <workflow name="myapp-wf1">
    <places>
      <place name="p0" start="true"/>
      <place name="p1"/>
      <place name="p2"/>
      <place name="p3"/>
      <place name="p4"/>
      <place name="p5"/>
      <place name="p6"/>
      <place name="p7"/>
      <place name="p8"/>
    </places>
    <transitions>
      <transition name="t01" workitem="workItem_t01"/>
      <transition name="t12" workitem="workItem_t12"/>
      <transition name="t23" workitem="workItem_t23"/>
      <transition name="t24" workitem="workItem_t24"/>
      <transition name="t345" workitem="workItem_t345"/>
      <transition name="t16" workitem="workItem_t16"/>
      <transition name="t67" workitem="workItem_t67"/>
      <transition name="t578" workitem="workItem_t578"/>
    </transitions>
    <edges>
      <edge transition="t01" place="p0" type="input" weight="1"/>
      <edge transition="t01" place="p1" type="output" weight="T01_OK"/>
      <edge transition="t12" place="p1" type="input" weight="1"/>
      <edge transition="t12" place="p2" type="output" weight="T12_OK"/>
      <edge transition="t23" place="p2" type="input" weight="1"/>
      <edge transition="t23" place="p3" type="output" weight="T23_OK"/>
      <edge transition="t24" place="p2" type="input" weight="1"/>
      <edge transition="t24" place="p4" type="output" weight="T24_OK"/>
      <edge transition="t345" place="p3" type="input" weight="T23_OK && T24_OK"/>
      <edge transition="t345" place="p4" type="input" weight="T23_OK && T24_OK"/>
      <edge transition="t345" place="p5" type="output" weight="T345_OK"/>
      <edge transition="t16" place="p1" type="input" weight="1"/>
      <edge transition="t16" place="p6" type="output" weight="T16_OK"/>
      <edge transition="t67" place="p6" type="input" weight="1"/>
      <edge transition="t67" place="p7" type="output" weight="T67_OK"/>
      <edge transition="t578" place="p5" type="input" weight="T345_OK && T67_OK"/>
      <edge transition="t578" place="p7" type="input" weight="T345_OK && T67_OK"/>
      <edge transition="t578" place="p8" type="output" weight="T578_OK"/>
    </edges>
    <attribs>
      <attrib key="T01_OK" value="false"/>
      <attrib key="T12_OK" value="false"/>
      <attrib key="T23_OK" value="false"/>
      <attrib key="T24_OK" value="false"/>
      <attrib key="T345_OK" value="false"/>
      <attrib key="T16_OK" value="false"/>
      <attrib key="T67_OK" value="false"/>
      <attrib key="T578_OK" value="false"/>
    </attribs>
  </workflow>
</workflows>

As you can see, multiple workflows are now supported from within a single XML configuration file. The XML configuration file is used to build a WorkflowFactory object. It started out as a standard Singleton Factory object, but since I converted this to Spring (see below), I just put the XML parsing code into the afterPropertiesSet() method, which is called by the Spring BeanFactory on startup.

I renamed the PetriNet class to Workflow, since that is what is being represented. A WorkflowFactory is responsible for giving out Workflow objects to the enclosing application.The code for the WorkflowFactory is shown below:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// WorkflowFactory.java
package com.mycompany.myapp.workflow;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.Resource;

/**
 * Factory that is initialized by the XML configuration file workflow-conf.xml
 * in the application's classpath. Factory returns Workflow objects.
 */
public class WorkflowFactory implements BeanFactoryAware, InitializingBean {

  private BeanFactory beanFactory;
  private Resource xmlConfig;
  private Map<String,Workflow> workflowMap = null;
  private Map<String,Boolean> taken = null;
  private Map<String,Map<String,Boolean>> originalState = null;

  public void setXmlConfig(Resource xmlConfig) {
    this.xmlConfig = xmlConfig;
  }
  
  public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
    this.beanFactory = beanFactory;
  }

  /**
   * Allows a client to take a configured workflow object. Once taken, the
   * Workflow is unavailable to other processes until the client gives it
   * back to the factory.
   * @param name the name of the Workflow to take.
   * @return a Workflow object, or null if the Workflow is unavailable or
   * does not exist. Client must check for null.
   * @throws Exception if one is thrown.
   */
  public Workflow take(String name) throws Exception {
    if (taken.get(name)) {
      // workflow is in use by another process
      return null;
    }
    Workflow workflow = workflowMap.get(name);
    if (workflow != null) {
      taken.put(name, Boolean.TRUE);
      return workflowMap.get(name);
    }
    return null;
  }
  
  /**
   * Allows a client to return a Workflow object back to the factory. The 
   * factory will take care of resetting the state of the Workflow. so it
   * can be used without re-initialization by the next client.
   * @param name the name of the Workflow to return to the factory.
   * @throws Exception if one is thrown.
   */
  public void give(String name) throws Exception {
    Workflow workflow = workflowMap.get(name);
    if (workflow != null) {
      Map<String,Boolean> originalAttributes = originalState.get(name);
      Map<String,Boolean> workflowAttributes = workflow.getAttributes();
      workflowAttributes.clear();
      for (String key : originalAttributes.keySet()) {
        workflowAttributes.put(key, originalAttributes.get(key));
      }
      workflow.setAttributes(workflowAttributes);
      taken.put(name, Boolean.FALSE);
    }
  }

  /**
   * Since this bean has been declared as an InitializingBean to Spring, the
   * Bean Factory will automatically run the afterPropertiesSet() method on
   * startup.
   * @throws Exception if one is thrown.
   */
  public void afterPropertiesSet() throws Exception {
    workflowMap = new HashMap<String,Workflow>();
    taken = new HashMap<String,Boolean>();
    originalState = new HashMap<String,Map<String,Boolean>>();
    SAXBuilder builder = new SAXBuilder();
    Document doc = builder.build(xmlConfig.getInputStream());
    Element workflowsElement = doc.getRootElement();
    List<Element> workflows = workflowsElement.getChildren();
    for (Element workflowElement : workflows) {
      Workflow workflow = new Workflow();
      String workflowName = workflowElement.getAttributeValue("name");
      workflow.setName(workflowName);
      // grab the attribs since we will need them later
      List<Element> attribElements = workflowElement.getChild("attribs").getChildren();
      for (Element attribElement : attribElements) {
        String key = attribElement.getAttributeValue("key");
        String value = attribElement.getAttributeValue("value");
        workflow.setAttributeValue(key, Boolean.valueOf(value));
      }
      workflowMap.put(workflowName, workflow);
      taken.put(workflowName, Boolean.FALSE);
      // make a copy of the original state for repeated use
      Map<String,Boolean> attribCopy = new HashMap<String,Boolean>();
      for (String key : workflow.getAttributes().keySet()) {
        attribCopy.put(key, workflow.getAttributes().get(key));
      }
      originalState.put(workflowName, attribCopy);
      // places
      Map<String,Place> placeMap = new HashMap<String,Place>();
      List<Element> placeElements = workflowElement.getChild("places").getChildren();
      for (Element placeElement : placeElements) {
        String placeName = placeElement.getAttributeValue("name");
        String isStartPlace = placeElement.getAttributeValue("start");
        Place place = new Place(placeName);
        if (isStartPlace != null) {
          workflow.addPlace(place, Boolean.valueOf(isStartPlace));
        } else {
          workflow.addPlace(place);
        }
        placeMap.put(placeName, place);
      }
      // transitions
      Map<String,Transition> transitionMap = new HashMap<String,Transition>();
      List<Element> transitionElements = 
        workflowElement.getChild("transitions").getChildren();
      for (Element transitionElement : transitionElements) {
        String transitionName = transitionElement.getAttributeValue("name");
        String workItemName = transitionElement.getAttributeValue("workitem");
        WorkItem workItem = (WorkItem) beanFactory.getBean(workItemName);
        workItem.setAttribs(workflow.getAttributes());
        Transition transition = new Transition(transitionName, workItem); 
        workflow.addTransition(transition);
        transitionMap.put(transitionName, transition);
      }
      // edges
      List<Element> edgeElements = workflowElement.getChild("edges").getChildren();
      for (Element edgeElement : edgeElements) {
        String transitionName = edgeElement.getAttributeValue("transition");
        String placeName = edgeElement.getAttributeValue("place");
        String type = edgeElement.getAttributeValue("type");
        String weight = edgeElement.getAttributeValue("weight");
        if (type.equals("input")) {
          transitionMap.get(transitionName).addInput(placeMap.get(placeName), weight);
        } else {
          transitionMap.get(transitionName).addOutput(placeMap.get(placeName), weight);
        }
      }
    }
  }
}

Since I also made quite a few changes in the other classes - PetriNet (now called Workflow), Place, Transition and Edge, to accomodate the changes to the application, they are also shown below in their current incarnation.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
// Workflow.java
package com.mycompany.myapp.workflow;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Models a workflow as a Petri Net. See the Bossa Manifesto for details about
 * what a Petri Net is and how it can be used to model a workflow. In a nutshell,
 * a Petri Net is a graph of Places and Transitions connected by Edges with
 * numeric weights. In this implementation, edge weights are represented by 0 and
 * 1 (for false and true respectively) or Javascript expressions that evaluate to
 * true or false.
 * @link {http://www.bigbross.com/bossa/overview.shtml}
 */
public class Workflow {

  private String name;
  private Map<String,Place> places = new HashMap<String,Place>();
  private List<Transition> transitions = new ArrayList<Transition>();
  private String initialPlaceName;
  private Map<String,Boolean> attributes = new HashMap<String,Boolean>();

  public Workflow() { super(); }
  
  public void setName(String name) {
    this.name = name;
  }
  
  public String getName() {
    return name;
  }
  
  /**
   * Add a Place object to the Petri net. 
   * @param place the Place to be added.
   * @return the Place that was added.
   */
  public Place addPlace(Place place) {
    places.put(place.getName(), place);
    return place;
  }

  /**
   * Overloaded version of {@see PetriNet#addPlace(Place)} to specify the
   * initial Place object. A Petri net can have only a single start place.
   * This is the Place from which the traversal will be started.
   * @param isStartPlace true if this is the start Place.
   * @return the Place that was added.
   */
  public Place addPlace(Place place, boolean isStartPlace) {
    if (isStartPlace) {
      if (initialPlaceName != null) {
        throw new IllegalArgumentException("Initial Place is already set");
      }
      initialPlaceName = place.getName();
    }
    return addPlace(place);
  }
  
  /**
   * Return a List of Place objects in the Petri Net.
   * @return a List of Place objects.
   */
  public List<Place> getPlaces() {
    return new ArrayList<Place>(places.values());
  }

  /**
   * Add a Transition object to the Petri Net.
   * @param transition the Transition to add.
   * @return
   */
  public Transition addTransition(Transition transition) {
    transitions.add(transition);
    return transition;
  }
  
  /**
   * Returns a List of Transitions mapped into the Petri net.
   * @return a List of all Transition objects.
   */
  public List<Transition> getTransitions() {
    return transitions;
  }

  /**
   * Allows setting initial values for Javascript variables that are reset
   * during the traversal of the Petri net by the Closures mapped to the 
   * Transition objects.
   * @param variable the name of the Javascript variable.
   * @param value the initial value (usually false).
   */
  public void setAttributeValue(String variable, boolean value) {
    attributes.put(variable, value);
  }
  
  /**
   * Sets all the attributes in a single method call.
   * @param attributes the attributes to set.
   */
  public void setAttributes(Map<String,Boolean> attributes) {
    this.attributes = attributes;
  }
  
  /**
   * Returns the Javascript variables and their current values in the Petri Net.
   * @return the Map of Javascript variable names and their current values.
   */
  public Map<String,Boolean> getAttributes() {
    return attributes;
  }

  /**
   * Traverse a Petri Net in either a depth first or breadth first order. Sometimes
   * it may make sense to order the transitions so that the larger jobs get completed
   * first, so this parameter could be used to influence the ordering of the jobs.
   * When a Transition is encountered, the Closure associated with the Transition 
   * will be executed, so a traversal will end up running all the jobs. If you wish
   * to test without actually running any jobs, consider using a MockClosure object.
   * This method delegates to the recursive traverse_r().
   * @param depthFirst true or false. If true, the Petri Net will be traversed depth
   * first, and if false, it will be traversed breadth first.
   * @throws Exception if one is thrown.
   */
  public void traverse(boolean depthFirst) throws Exception {
    if (depthFirst) {
      traverseDfs_r(null);
    } else {
      Set<Transition> firedTransitions = new HashSet<Transition>();
      traverseBfs_r(null, firedTransitions);
    }
  }

  /**
   * Returns a List of Transitions that may be reached from the specified Place
   * on the Petri net. All output Transitions that are immediate neighbors of the
   * specified Place are considered, and reachability is determined by evaluating
   * the weight of the edge separating the Place and the Transition.
   * @param place the Place from which to determine next fireable Transitions.
   * @return a List of Transition objects that can be fired from the Place.
   * @throws Exception if one is thrown.
   */
  protected List<Transition> getNextFireableTransitions(Place place) throws Exception {
    List<Transition> fireableTransitions = new ArrayList<Transition>();
    if (place == null) {
      place = places.get(initialPlaceName);
    }
    List<Transition> outputTransitions = place.getOutputTransitions();
    for (Transition outputTransition : outputTransitions) {
      if (outputTransition.isFireable(attributes)) {
        fireableTransitions.add(outputTransition);
      }
    }
    return fireableTransitions;
  }
  
  /**
   * Returns a List of Places which can be reached from the specified Transition.
   * All output edges of the specified Transition are considered, and reachability
   * is determined by evaluating the weights of the Edge connecting the Transition
   * and the neighboring Place objects.
   * @param transition the Transition from which to determine reachable Places.
   * @return a List of reachable Places from the Transition.
   * @throws Exception if one is thrown.
   */
  protected List<Place> getNextReachablePlaces(Transition transition) throws Exception {
    List<Place> reachablePlaces = new ArrayList<Place>();
    if (transition == null) {
      return reachablePlaces;
    }
    List<Edge> outputEdges = transition.getOutputs();
    for (Edge outputEdge : outputEdges) {
      Place place = outputEdge.getPlace();
      if (transition.canReachTo(place, outputEdge.getEdgeWeightExpr(), attributes)) {
        reachablePlaces.add(place);
      }
    }
    return reachablePlaces;
  }
  
  /**
   * Implements a breadth first traversal of the Petri Net, so that each transition
   * is not fired more than once.
   * @param place the starting Place from which to fire.
   * @param alreadyFiredTransitions a Set of already fired transitions.
   * @throws Exception if one is thrown.
   */
  private void traverseBfs_r(Place place, Set<Transition> alreadyFiredTransitions) 
      throws Exception {
    List<Transition> transitions = getNextFireableTransitions(place);
    if (transitions.size() == 0) {
      return;
    }
    Set<Place> reachablePlaces = new HashSet<Place>();
    for (Transition transition : transitions) {
      if (alreadyFiredTransitions.contains(transition)) {
        continue;
      }
      alreadyFiredTransitions.add(transition);
      WorkItem workItem = transition.getWorkItem();
      workItem.execute();
      reachablePlaces.addAll(getNextReachablePlaces(transition));
    }
    for (Place reachablePlace : reachablePlaces) {
      traverseBfs_r(reachablePlace, alreadyFiredTransitions);
    }
  }
  
  /**
   * Implements a depth first traversal of the Petri Net.
   * @param place the starting Place.
   * @throws Exception if one is thrown.
   */
  private void traverseDfs_r(Place place) throws Exception {
    List<Transition> transitions = getNextFireableTransitions(place);
    if (transitions.size() == 0) {
      return;
    }
    for (Transition transition : transitions) {
      WorkItem workItem = transition.getWorkItem();
      workItem.execute();
      List<Place> reachablePlaces = getNextReachablePlaces(transition);
      for (Place reachablePlace : reachablePlaces) {
        traverseDfs_r(reachablePlace);
      }
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// Place.java
package com.mycompany.myapp.workflow;

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;

/**
 * Models a Place node in a Petri Net.
 */
public class Place {

  private String name;
  private List<gTransition> inputTransitions = new ArrayList<Transition>();
  private List<gTransition> outputTransitions = new ArrayList<Transition>();

  public Place(String name) {
    setName(name);
  }
  
  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public List<gTransition> getInputTransitions() {
    return inputTransitions;
  }

  public void addInputTransition(Transition transition) {
    inputTransitions.add(transition);
  }

  public List<gTransition> getOutputTransitions() {
    return outputTransitions;
  }

  public void addOutputTransition(Transition transition) {
    outputTransitions.add(transition);
  }

  @Override
  public int hashCode() {
    return name.hashCode();
  }
  
  @Override
  public boolean equals(Object obj) {
    if (Place.class.isInstance(obj)) {
      Place that = Place.class.cast(obj);
      return (this.getName().equals(that.getName()));
    }
    return false;
  }
  
  @Override
  public String toString() {
    return ReflectionToStringBuilder.reflectionToString(this, ToStringStyle.DEFAULT_STYLE);
  }
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// Transition.java
package com.mycompany.myapp.workflow;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.commons.lang.math.NumberUtils;

/**
 * Models a Transition node in a Petri Net.
 */
public class Transition {

  private String name;
  private WorkItem workItem;
  private List<Edge> inputs = new ArrayList<Edge>();
  private List<Edge> outputs = new ArrayList<Edge>();
  
  public Transition(String name, WorkItem workItem) {
    this.name = name;
    this.workItem = workItem;
  }

  public String getName() {
    return name;
  }
  
  public WorkItem getWorkItem() {
    return workItem;
  }
  
  public void addInput(Place place, String weightExpr) {
    place.addOutputTransition(this);
    Edge edge = new Edge();
    edge.setPlace(place);
    edge.setEdgeWeightExpr(weightExpr);
    inputs.add(edge);
  }

  public List<Edge> getInputs() {
    return inputs;
  }
  
  public void addOutput(Place place, String weightExpr) {
    place.addInputTransition(this);
    Edge edge = new Edge();
    edge.setPlace(place);
    edge.setEdgeWeightExpr(weightExpr);
    outputs.add(edge);
  }
  
  public List<Edge> getOutputs() {
    return outputs;
  }
  
  public boolean isFireable(Map<String,Boolean> attributes) throws Exception {
    boolean fireable = true;
    for (Edge edge : inputs) {
      String edgeWeightExpr = edge.getEdgeWeightExpr();
      if (NumberUtils.isNumber(edgeWeightExpr)) {
        fireable = (edgeWeightExpr.equals("1") ? true : false);
      } else {
        Boolean canFire = evaluate(attributes, edgeWeightExpr);
        fireable = fireable && canFire;
      }
    }
    return fireable;
  }
  
  public boolean canReachTo(Place place, String weightExpr, Map<String,Boolean> attributes)
      throws Exception {
    if (NumberUtils.isNumber(weightExpr)) {
      return (weightExpr.equals("1") ? true : false);
    } else {
      Boolean canReach = evaluate(attributes, weightExpr);
      return canReach;
    }
  }
  
  @Override
  public int hashCode() {
    return name.hashCode();
  }
  
  @Override
  public boolean equals(Object obj) {
    if (Transition.class.isInstance(obj)) {
      Transition that = Transition.class.cast(obj);
      return (this.getName().equals(that.getName()));
    }
    return false;
  }
  
  @Override
  public String toString() {
    return ReflectionToStringBuilder.reflectionToString(this, ToStringStyle.DEFAULT_STYLE);
  }
  
  private Boolean evaluate(Map<String,Boolean> attributes, String edgeWeightExpr) 
      throws Exception {
    ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
    ScriptEngine engine = scriptEngineManager.getEngineByName("js");
    for (String key : attributes.keySet()) {
      engine.put(key, attributes.get(key));
    }
    Boolean result = (Boolean) engine.eval(edgeWeightExpr);
    return result;
  }
}

I don't think Edge has changed, but here it is again for completeness.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Edge.java
package com.mycompany.myapp.workflow;

import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.commons.lang.math.NumberUtils;

/**
 * Models and Edge connecting a Place and Transition in a Petri Net.
 */
public class Edge {

  private Place place;
  private String edgeWeightExpr;
  
  public Place getPlace() {
    return place;
  }
  
  public void setPlace(Place place) {
    this.place = place;
  }
  
  public String getEdgeWeightExpr() {
    return edgeWeightExpr;
  }
  
  public void setEdgeWeightExpr(String edgeWeightExpr) {
    if (NumberUtils.isNumber(edgeWeightExpr)) {
      if (!edgeWeightExpr.equals("1") && !edgeWeightExpr.equals("0")) {
        throw new IllegalArgumentException("Numeric edge weights can only be 0 or 1");
      }
    }
    this.edgeWeightExpr = edgeWeightExpr;
  }
  
  @Override
  public String toString() {
    return ReflectionToStringBuilder.reflectionToString(this, ToStringStyle.DEFAULT_STYLE);
  }
}

Workflow contention issues

Since a Workflow would be "checked out" for a period of time, application code should not be able to access a Workflow while it is being used by another (or the same) component. So it exposes a take() method which will return a null Workflow if it is not found or if it is already "taken", and a give() method which resets the Workflow to its initial state and makes it available to the application when it calls take(). Client code must check for a null result from the take() call.

Replacing BSF with built-in ScriptEngine

The Bossa project used a unreleased version of Apache BSF, so I did the same, pulling in the nightly snapshot when I set it up. Java 1.6 already bundles a scripting framework, so I used that instead. That way users of this code don't have to mess with having to work with an unreleased version of a project. You can see the code in the Transition.evaluate() method.

Spring Integration

Our original design mapped a self-contained piece of code (modeled with a Closure) to each Transition. The Closure would operate on the state of the Workflow represented by its attributes map. This turned out to be too limiting, so we now map a WorkItem object to each Transition. The WorkItem is an abstract class (containing about as much state as our original Closure), but now can be extended as needed by the application.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// WorkItem.java
package com.mycompany.myapp.workflow;

import java.util.Map;

/**
 * Holder class to model a work that happens for a Transition. This class
 * contains the minimal information necessary to execute a piece of work
 * during a transition. Clients are expected to extend this class to meet
 * the requirements of their workflow. 
 */
public abstract class WorkItem {

  private String name;
  private Map<String,Boolean> attribs;

  public WorkItem() {
    ;;
  }
  
  public WorkItem(String name, Map<String,Boolean> attribs) {
    setName(name);
    setAttribs(attribs);
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public Map<String, Boolean> getAttribs() {
    return attribs;
  }

  public void setAttribs(Map<String, Boolean> attribs) {
    this.attribs = attribs;
  }
  
  public void execute() throws Exception {
    // :NOOP:
  }
}

I plan on providing two template extensions of WorkItem, a SynchronousWorkItem and an AsynchronousWorkItem. The SynchronousWorkItem is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// SynchronousWorkItem.java
package com.mycompany.myapp.workflow.workitems;

import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.mycompany.myapp.workflow.WorkItem;

/**
 * A mock closure which is instantiated with the process name, and which prints out
 * the name of the process in its execute() method, and updates the ${processName}_OK 
 * attribute to true to indicate to the Workflow that it completed successfully.
 */
public class SynchronousWorkItem extends WorkItem {

  public SynchronousWorkItem() {
    super();
  }
  
  public SynchronousWorkItem(String name, Map<String,Boolean> attribs) {
    super(name, attribs);
  }

  @Override
  public void execute() throws Exception {
    System.out.println("Executing " + getName());
    String key = StringUtils.upperCase(getName()) + "_OK";
    getAttribs().put(key, Boolean.TRUE);
  }
}

Our applicationContext.xml file now contains the bean definition for WorkflowFactory, and beans that are referenced by id from the workflow-conf.xml file. It is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
       http://www.springframework.org/schema/util 
       http://www.springframework.org/schema/util/spring-util-2.0.xsd">

  <bean 
    class="org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor"/>

  <bean id="workflowFactory" class="com.mycompany.myapp.workflow.WorkflowFactory">
    <property name="xmlConfig" value="classpath:workflow-conf.xml"/>
  </bean>
  
  <bean id="workItem_t01" 
      class="com.mycompany.myapp.workflow.workitems.SynchronousWorkItem">
    <property name="name" value="t01"/>
  </bean>

  <bean id="workItem_t12" 
      class="com.mycompany.myapp.workflow.workitems.SynchronousWorkItem">
    <property name="name" value="t12"/>
  </bean>
  
  <bean id="workItem_t23" 
      class="com.mycompany.myapp.workflow.workitems.SynchronousWorkItem">
    <property name="name" value="t23"/>
  </bean>
  
  <bean id="workItem_t24" 
      class="com.mycompany.myapp.workflow.workitems.SynchronousWorkItem">
    <property name="name" value="t24"/>
  </bean>
  
  <bean id="workItem_t345" 
      class="com.mycompany.myapp.workflow.workitems.SynchronousWorkItem">
    <property name="name" value="t345"/>
  </bean>
  
  <bean id="workItem_t16" 
      class="com.mycompany.myapp.workflow.workitems.SynchronousWorkItem">
    <property name="name" value="t16"/>
  </bean>
  
  <bean id="workItem_t67" 
      class="com.mycompany.myapp.workflow.workitems.SynchronousWorkItem">
    <property name="name" value="t67"/>
  </bean>
  
  <bean id="workItem_t578" 
      class="com.mycompany.myapp.workflow.workitems.SynchronousWorkItem">
    <property name="name" value="t578"/>
  </bean>

</beans>

Testing these changes

Our code for testing these changes is a standard JUnit test as before, except it now uses the WorkflowFactory to get instances of a Workflow from the XML configuration. Here it is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// WorkflowFactoryTest.java
package com.mycompany.myapp.workflow;

import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * Test for testing XML configuration of a collection of work flows.
 */
public class WorkflowFactoryTest {
  
  private static WorkflowFactory factory;
  private Workflow workflow;

  @BeforeClass
  public static void setUpBeforeClass() throws Exception {
    ApplicationContext context = new ClassPathXmlApplicationContext(
      "classpath:applicationContext.xml");
    factory = (WorkflowFactory) context.getBean("workflowFactory");
  }
  
  @Before
  public void setUp() throws Exception {
    workflow = factory.take("myapp-wf1");
  }

  @After
  public void tearDown() throws Exception {
    factory.give("myapp-wf1");
  }
  
  @Test
  public void testTraversePetriDepthFirst() throws Exception {
    System.out.println("Depth first traversal");
    workflow.traverse(true);
  }

  @Test
  public void testTraversePetriBreadthFirst() throws Exception {
    System.out.println("Breadth first traversal");
    workflow.traverse(false);
  }
}

The output of this test, as expected, is similar to the previous post's test. However, this code reflects the bug fix that checks to see if a process associated with a Transition has already been fired, and if so, does not fire it again.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Depth first traversal
Executing t01
Executing t12
Executing t23
Executing t24
Executing t345
Executing t16
Executing t67
Executing t578

Breadth first traversal
Executing t01
Executing t12
Executing t16
Executing t67
Executing t23
Executing t24
Executing t345
Executing t578

What's next?

I am working on adding support for asynchronous WorkItem templates, but the code is not quite there yet, so I will defer the discussion of that till next week.

2 comments:

  1. Hi Sujit,

    This is working good. But what about the resources used by the Bossa how do you use that in your application

    ReplyDelete
  2. Not sure I understand the question...in this particular case, I use ideas from Bossa, but not Bossa itself.

    ReplyDelete

Comments are moderated to prevent spam.