Table of contents
Overview
The jStates library helps developers to design and easily implement and use simple state machines. The machines created using this library are intended for general use, not restricted to a certain framework or Java platform (J2SE, J2EE).
This library version requires J2SE 1.4.2 or above.
This documentation shortly describes the jStates library and API. See the jstates-utils and jstates-examples sourceforge packages documentation and javadoc for more information about Jakarta Commons Validator integration and library usage examples.
Introduction
State machines are used in many scientific and technological fields: mathematics, computer science, engineering and many more.
There are several types of state machines, each one suited for different uses. This library does *not* offer a complete implementation of any of them, but a simplified and generic approach to some of them. This simplified approach makes learning jStates easy. Using and integrating jStates machines is easier than using other more specific state machines. You can use jStates to easily define application flow, GUI (desktop and HTML) complex form navigation, form data validation, serialize state machines, generate localized error messages, use your own validators or scripting languages to check state values and more.
A jStates state machine can be divided into the following parts:
- The machine itself, has a name that identifies it and a XML file that defines it. Its main functions are to act as a container of states and to act as an interface between the developer and these states. A machine is always in a well defined state ("current state"). A machine can change its current state by "forwarding" to another state through a "transition".
- A state is a set of properties and transitions. Properties store the current value of a state, while transitions show what other states can be reached from a state.
- A transition is a little more complex concept. Think of it as a condition check to change the current state of a machine. A transition contains a set of validator rules that state properties must conform, for a machine to be able to reach another state from the current one. When we ask a machine to follow a transition ("forward"), the machine tests all of the validator rules of the transition and, if all of the tests are correct, the machine changes its current state to the one pointed by the transition. Transitions of a state can only be followed if that state is the current one.
- A validator rule is a set of conditions that a state machine must verify before a transition can be followed. A validator rule can be written as a custom class ("validator") or a script ("script"). More on this in the following sections.
To create and use a jStates state machine, we should determine the possible states of a machine, determine the transitions from each state to the other states and determine what validator rules must conform a state to allow transitions to another states. The following sections describe these steps in more detail.
A short example
This section shows an example of how to use jStates. The presented example is just a couple of commented code blocks, for a more detailed explanation of this code, please refer to the next sections.
Machine definition files are XML files where a developer can define a state machine and all of its elements: states, transitions, validators and so on. Currently, the DTD for XML validation of machine definition files is statemachine_0_9.dtd. Take a look at this DTD file for detailed documentation about XML tags and attributes. This is a simple but complete example of a XML definition file, mytestmachine.xml: *** TODO I DON'T LIKE THIS CODE
<!DOCTYPEstatemachine PUBLIC "-//jStates State Machines//DTD Machine Definition//EN"
"http://www.jstates.com/dtds/statemachine_0_9.dtd">
<!-- State machine -->
<statemachine name="mytestmachine" initialStateName="myfirststate" info="a test machine">
<!-- State -->
<state name="myfirststate" info="some info about this state">
<!-- Property: always string type -->
<property name="fooproperty" value="some value" />
<!-- Transition: change state to "mysecondstate" -->
<transition name="trans1" to="mysecondstate" info="transition to second state">
<!-- Validation: contains this transition's validators -->
<validation>
<!-- Custom validator: Jakarta Commons Validator binding,
see jstates-utils sourceforge package -->
<validator type="net.sf.jstates.commons.validator.CommonsValidator"
parameters="messages, validator-rules.xml, validation.xml"/>
<!-- Script validator -->
<script name="ascript" info="a named script" language="javascript">
<![CDATA[
/*
* Entry point for validation, "state" is a reference to
* this validator's parent state. Must return a boolean value.
*/
function validate(state) {
if(state.get('fooproperty') != 'bar') {
// Implicit TransitionErrors object.
errors.addError('fooproperty',
'fooproperty should be "bar"');
return false;
}
return true;
}
]]>
</script>
</validation>
</transition>
<!-- Empty transition: change state to "athirdstate" with no validation -->
<transition name="trans2" to="athirdstate" info="an empty transition" />
</state>
<!-- A final state: once reached, a machine cannot go forward to other states,
just return to previous states -->
<state name="mysecondstate" info="this state has no transitions, it's a final state">
<!-- Property: must exist at least one, always has a string value-->
<property name="someotherproperty" value="25.44" />
</state>
<!-- Another final state -->
<state name="athirdstate" info="another final state">
<!-- A property value can be empty -->
<property name="someotherproperty" value="" />
</state>
</statemachine>
After machine definition comes machine use. This is a Java source code snippet, demonstrating how to create and use the previously defined machine:
// Load machine definition. Currently, XML machine definition file must be in the classpath.
StateMachineFactory.addMachineDefinition("machines/mytestmachine.xml");
// Create a machine based on a loaded machine definition. Current state is "myfirststate"
StateMachine sm = StateMachineFactory.createMachine("mytestmachine");
// Try to reach the state pointed by transition "trans1"
if(!sm.forward("trans1")) {
// Validation failed, show validation errors.
TransitionErrors errors = sm.getTransitionErrors();
[... iterate over error messages ...]
// Change current state property values and try forward again
State st = sm.getCurrentState();
// At least, the script validator should succeed.
st.set("fooproperty", "bar");
if(!sm.forward("trans1")) {
System.out.println("failed again!");
System.exit(0);
}
} else {
// Validation OK, show current state and property values
State st = sm.getCurrentState();
System.out.println( st.get("someotherproperty") );
[... print each property ...]
}
See jstates-examples package in sourceforge.net package downloads for more Java source code and XML machine definition examples.
State machine
As previously said in the "Introduction" section, a state machine is both a states container and an interface to interact with these states. The first step to use a machine is to define it. The second should be to use Java code to instantiate it and use it.
The XML tag to define a machine is the <statemachine>
tag. This tag has three attributes:
-
name
: contains the unique name of the machine. It's used for machine instance creation, more on this later in this section. -
initialStateName
: contains the name of the initial state, the first "current state" of the machine just created. -
info
: contains extra information about the machine, think of this as a place to include metainformation - a descriptive test of machine usage, a URL, version information, ...
Once defined, we should load the machine definition into the StateMachineFactory
, using the method addMachineDefinition
. Once loaded, we can create instances by calling the createMachine
method, with the machine name
as argument. Any machine instance is independent of the other instances, so we can create any number of machines of the same type:
// Add a new machine prototype to the factory
StateMachineFactory.addMachineDefinition("machinedefinition.xml");
// Instantiate the same machine several times
StateMachine machine1 = StateMachineFactory.createMachine("mymachine");
StateMachine machine2 = StateMachineFactory.createMachine("mymachine");
StateMachine machine3 = StateMachineFactory.createMachine("mymachine");
[...]
These are the most common operations that we will perform on a machine:
-
Examinate the current state: call the
getCurrentState
method. It will return aState
object. More onState
objects in the next section.
-
Change the current state to another: call the
forward
overloaded method, see theStateMachine
javadoc for more information about these methods. These methods return a boolean value, indicating whether the machine changed its current state ("true") or not ("false"). If the state change failed, there could be stored error messages about the validation that aborted this change. Call thegetTransitionErrors
method to obtain these errors. More on this in the "Transitions" section.
-
Change the internal value of the current state: we can do this in two ways. First way is to get the current state by calling
getCurrentState
and then calling theset
method on this state (more on this in the next section). Second way is by calling theupdateCurrentState
method, and passing a JavaBean object that has properties with the same name as the state properties. The properties with the same name will be copied to the current state and the rest will be ignored. This last method is useful if you want to populate the current state with form field values contained in a bean, for example.
A StateMachine
object can perform other interesting operations, including going back to previous states, serialization and more. See the javadoc documentation for more about this.
States
The State
objects represent the status of a StateMachine
object and the possible states a machine can reach from its current state. To accomplish these goals, a State
contains two types of objects:
-
Properties: they have the same functionality as JavaBean properties or classic variables, storing the internal value of a state. In a machine definition XML, a property can have a default value or no value (empty String), asigned by the
value
attribute in the<property>
tag. However, this value can be changed by calling theset
method of aState
object or by calling theupdateXXXState
methods ofStateMachine
. In the same way, property values can be read by using the get methodsgetXXX
of aState
object. Internally, properties are stored as String objects.
A machine can be in a well defined state ("current state") and its status will not change if the internal properties of the current state do change their values. Properties are used to decide if its possible to change to another state from the current one, so they are related to transitions, not to the machine status. See the "form validation" (TODO: IMPLEMENT) example in the jstates-examples package to see a practical use of this feature.
-
Transitions: there is a transition for each state that can be reached from the state this transition is contained into. When a machine tries to change its current state by calling "forward" methods, it must call a transition of the current state to do so. See the next section, "Transitions", for a more complete explanation about this process.
There can be any number of transitions contained into aState
, even none of them. If a state has no transitions, it is called a "final" state.
A XML example:
<state name="firststate" info="this is the first state">
<property name="foo1" value="some string" />
<property name="foo2" value="25" />
<property name="foo3" value="true" />
<transition name="trans" to="secondstate" info="transition to second state" />
</state>
<state name="secondstate" info="a second state">
<property name="bar1" value="property bar in the second state" />
</state>
And this is a Java code snippet, demonstrating state and property handling:
// State machine is in the first state
State firstState = machine.getCurrentState();
// Print current state properties
System.out.println("foo1=" + firstState.getStr("foo1");
System.out.println("foo2=" + firstState.getInt("foo2");
System.out.println("foo3=" + firstState.getBool("foo3");
// Change some of the property values: use Strings
firstState.set("foo1", "changed value");
firstState.set("foo2", "0");
firstState.set("foo3", "false");
// Use a transition to change the current state from "firststate" to "secondstate"
machine.forward("trans");
// Get the current state
State secondState = machine.getCurrentState();
// Print current state property
System.out.println("bar1=" + secondState.getStr("bar1"));
Transitions
As previously mentioned, a state machine can be only in a well-known state, that we called the "current state". When first created, the machine sets its current state to the state referenced by the initialStateName
attribute of the
<statemachine>
tag. From there, a machine is supposed to change its current state to another state when some significative event happens, like changing from a form web page to another form page, for example.
Transitions are the way to change the current state. Each state contains zero or more transitions, each one pointing to a destination state. When a machine wishes to change its current state, it "talks" to its current state and asks it for a transition to the destination state. From this point on, two things can happen: the current state properties have correct values and the transition is successful, or the current state properties do not have correct values and the transition fails. If the transition is successful, the current state of the machine changes to the destination state; if not, the machine's current state does not change.
So, a successful transition really depends on two conditions: state property values and "correctness" of their values.
How to check that values are correct? The answer is by using "validator rules". A validator rule is an executable code
that gets a State
object as argument (always the current state) and returns a boolean "true" or "false" after verification
of certain conditions about that state's parameters values.
This is a diagram of a transition with a validation rule:
(A)
firststate secondstate
+---------------------+ +---------------------+
| | trans | |
| foo1="some string" | [condition: foo2 == "25"] | bar1="property bar"|
| foo2="25" |------------- OK ----------------->| |
| foo3="true" | | |
| | | |
+---------------------+ +---------------------+
INITIAL STATE FINAL STATE
(B)
firststate secondstate
+---------------------+ +---------------------+
| | trans | |
| foo1="some string" | [condition: foo2 == "25"] | bar1="property bar"|
| foo2="0" |------------- FAILS -------------->| |
| foo3="true" | | |
| | | |
+---------------------+ +---------------------+
INITIAL STATE
FINAL STATE
An this is the XML code for the previous diagram:
<state name="firststate" info="this is the first state">
<property name="foo1" value="some string" />
<property name="foo2" value="25" />
<property name="foo3" value="true" />
<transition name="trans" to="secondstate" info="transition to second state" >
<!-- Validation: contains this transition's validators -->
<validation>
<!-- Script validation rule -->
<script name="rulescript"
info="a validation rule script" language="javascript">
<![CDATA[
// Entry point for the validation rule
function validate(state) {
// Property value check
if(state.get('foo2') == '25')
return true;
else
return false;
}
]]>
</script>
</validation>
</transition>
</state>
<state name="secondstate" info="a second state">
<property name="bar1" value="property bar in the second state" />
</state>
The following source code demonstrates what happens in both the diagram cases, (A) and (B), using a different machine for each case:
// State variable
State state = null;
// Result variable
boolean res = false;
// Both machines are instances of the same definition
StateMachine machineA = StateMachineFactory.createMachine("mymachine");
StateMachine machineB = StateMachineFactory.createMachine("mymachine");
/* First case (A): correct transition */
// res = true
res = machineA.forward("trans");
// Get current state
state = machineA.getCurrentState();
// Prints "secondstate"
System.println(state.getName());
/* Second case (B): incorrect transition */
// Get current state and change property value
state = machineB.getCurrentState();
state.set("foo2", "0");
// res = false
res = machineB.forward("trans");
// Get current state
state = machineB.getCurrentState();
// Prints "firststate"
System.println(state.getName());
This example demonstrates the use of a validator rule to check state property conditions. This is a script validator rule, but there is a "validator" type rule too (more on this later).
Instead of using just one simple rule, we could have used a more complicated one
or many of them to perform the transition check. If we used more than one rule, the machine
would have executed each of them, no matter if some or all of them failed. This "rule execution"
process is called "validation". All of the validator rules must be contained into a
<validation>
tag.
There are two types of validator rules, "script" rules and "validator" rules. They are called "Scripts" and "Validators" for short. Besides returning "true" or "false", both rules can store text messages into an "errors" object, so these messages can be recovered after a failed validation and shown to the final user in a GUI.
-
Scripts
This type of validator rule was the one used in the previous example. A script is a code that gets executed when we call "forward" on the machine. As you can see, the language used is JavaScript (ECMAScript, in fact). The script engine jStates uses is Rhino, the Mozilla scripting engine. Take a look at the Rhino documentation if you want to get the most from your scripts.
The entry point for the rule is the "validate" function, though you can define another functions that get called from this one. There is an implicit "errors" object too, placed into the script global scope, that allows you to place error messages related to failed validation checks. This errors object has the type
TransitionErrors
, see the javadoc for usage instructions. -
Validators
For more complex validations, where a script is cumbersome to use or we need a better Java integration, the best option is to use "validators". A validator is a Java class that implements the
net.sf.jstates.validation.StateValidator
interface and has a constructor with just a String argument, see the javadoc for details.To include a validator into the validation process, we must include a
<validator>
tag at the same level as the scripts tags, into the<validation>
tag.These are the attributes of the
<validator>
tag:-
type: Contains the fully qualified class name of the class implementing the
StateValidator
interface. This class must be in the class path for jStates to be able to instantiate it. - parameters: This is a String containing whatever information needs the validator class for initialization. This String is passed to the validator constructor during instantiation as its only argument.
Validators can be used as bindings or connectors to another validation frameworks, such as Jakarta Commons Validator. Currently, there is a connector to Commons Validator, see jstates-utils sourceforge package for usage and integration instructions.
-
type: Contains the fully qualified class name of the class implementing the