// $Id: FSM.java 12 2009-11-09 22:58:47Z gabe.johnson $
//package org.six11.util.data;
//import org.six11.util.Debug;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
/**
* A programmable Finite State Machine implementation. To use this class,
* establish any number of states with the 'addState' method. Next, add some
* FSM.Transition objects (the Transition class is designed to be used as an
* superclass for your anonymous implementation). Each Transition object has two
* useful methods that can be defined by your implementation: doBeforeTransition
* and doAfterTransition. To drive your FSM, simply give it events using the
* addEvent method with the name of an event. If there is an appropriate
* transition for the current state, the transition's doBefore/doAfter methods
* are called and the FSM is put into the new state. It is legal (and highly
* useful) for the start/end states of a transition to be the same state.
**/
public class FSM { // This class implements a Flying Spaghetti Monster
protected String name;
protected String currentState;
protected Map states;
protected List changeListeners;
protected boolean debug;
/**
* Create a blank FSM with the given name (which is arbitrary).
*/
public FSM(String name) {
this.name = name;
this.states = new HashMap();
this.currentState = null;
this.changeListeners = new ArrayList();
}
/**
* Turn debugging on/off.
*/
public void setDebugMode(boolean debug) {
this.debug = debug;
}
/**
* Report the current state of the finite state machine.
*/
public String getState() {
return currentState;
}
/**
* Adds a new state with no entry or exit code.
*/
public void addState(String state) {
addState(state, null, null, null);
}
/**
* Establish a new state the FSM is aware of. If the FSM does not currently
* have any states, this state becomes the current, initial state. This is
* the only way to put the FSM into an initial state.
*
* The entryCode, exitCode, and alwaysRunCode are Runnables that the FSM
* executes during the course of a transition. entryCode and exitCode are
* run only if the transition is between two distinct states (i.e. A->B
* where A != B). alwaysRunCode is executed even if the transition is
* re-entrant (i.e. A->B where A = B).
**/
public void addState(String state, Runnable entryCode, Runnable exitCode,
Runnable alwaysRunCode) {
boolean isInitial = (states.size() == 0);
if (!states.containsKey(state)) {
states.put(state, new State(entryCode, exitCode, alwaysRunCode));
}
if (isInitial) {
setState(state);
}
}
public void setStateEntryCode(String state, Runnable entryCode) {
states.get(state).entryCode = entryCode;
}
public void setStateExitCode(String state, Runnable exitCode) {
states.get(state).exitCode = exitCode;
}
public void setStateAlwaysRunCode(String state, Runnable alwaysRunCode) {
states.get(state).alwaysRunCode = alwaysRunCode;
}
/**
* There are cases where a state is meant to be transitional, and the FSM
* should always immediately transition to some other state. In those cases,
* use this method to specify the start and end states. After the startState
* has fully transitioned (and any change events have been fired) the FSM
* will check to see if there is another state that the FSM should
* automatically transition to. If there is one, addEvent(endState) is
* called.
*
* Note: this creates a special transition in the lookup table called
* "(auto)".
*/
public void setAutoTransition(String startState, String endState) {
// if (debug) {
// Debug.out("FSM", "Establishing auto transition for " + startState +
// " -> " + endState);
// }
states.get(startState).autoTransitionState = endState;
addTransition(new Transition("(auto)", startState, endState));
}
/**
* Sets the current state without following a transition. This will cause a
* change event to be fired.
*/
public void setState(String state) {
setState(state, true);
}
/**
* Sets the current state without followign a transition, and optionally
* causing a change event to be triggered. During state transitions (with
* the 'addEvent' method), this method is used with the triggerEvent
* parameter as false.
*
* The FSM executes non-null runnables according to the following logic,
* given start and end states A and B:
*
*
* - If A and B are distinct, run A's exit code.
* - Record current state as B.
* - Run B's "alwaysRunCode".
* - If A and B are distinct, run B's entry code.
*
*/
public void setState(String state, boolean triggerEvent) {
boolean runExtraCode = !state.equals(currentState);
if (runExtraCode && currentState != null) {
states.get(currentState).runExitCode();
}
currentState = state;
states.get(currentState).runAlwaysCode();
if (runExtraCode) {
states.get(currentState).runEntryCode();
}
if (triggerEvent) {
fireChangeEvent();
}
}
/**
* Establish a new transition. You might use this method something like
* this:
*
* fsm.addTransition(new FSM.Transition("someEvent", "firstState",
* "secondState") { public void doBeforeTransition() {
* System.out.println("about to transition..."); } public void
* doAfterTransition() { fancyOperation(); } });
*/
public void addTransition(Transition trans) {
State st = states.get(trans.startState);
if (st == null) {
throw new NoSuchElementException("Missing state: "
+ trans.startState);
}
st.addTransition(trans);
}
/**
* Add a change listener -- this is a standard java change listener and is
* only used to report changes that have already happened. ChangeEvents are
* only fired AFTER a transition's doAfterTransition is called.
*/
public void addChangeListener(ChangeListener cl) {
if (!changeListeners.contains(cl)) {
changeListeners.add(cl);
}
}
/**
* Feed the FSM with the named event. If the current state has a transition
* that responds to the given event, the FSM will performed the transition
* using the following steps, assume start and end states are A and B:
*
*
* - Execute the transition's "doBeforeTransition" method
* - Run fsm.setState(B) -- see docs for that method
* - Execute the transition's "doAfterTransition" method
* - Fire a change event, notifying interested observers that the
* transition has completed.
* - Now firmly in state B, see if B has a third state C that we must
* automatically transition to via addEvent(C).
*
*/
public void addEvent(String evtName) {
State state = states.get(currentState);
if (state.transitions.containsKey(evtName)) {
Transition trans = state.transitions.get(evtName);
// if (debug) {
// Debug.out("FSM", "Event: " + evtName + ", " + trans.startState +
// " --> " + trans.endState);
// }
trans.doBeforeTransition();
setState(trans.endState, false);
trans.doAfterTransition();
fireChangeEvent();
if (states.get(trans.endState).autoTransitionState != null) {
// if (debug) {
// Debug.out("FSM", "Automatically transitioning from " +
// trans.endState + " to "
// + states.get(trans.endState).autoTransitionState);
// }
addEvent("(auto)");
}
}
}
/**
* Fire a change event to registered listeners.
*/
protected void fireChangeEvent() {
ChangeEvent changeEvent = new ChangeEvent(this);
for (ChangeListener cl : changeListeners) {
cl.stateChanged(changeEvent);
}
}
/**
* Represents a state with some number of associated transitions.
*/
private static class State {
Map transitions;
String autoTransitionState;
Runnable entryCode;
Runnable exitCode;
Runnable alwaysRunCode;
State(Runnable entryCode, Runnable exitCode, Runnable alwaysRunCode) {
autoTransitionState = null;
transitions = new HashMap();
this.entryCode = entryCode;
this.exitCode = exitCode;
this.alwaysRunCode = alwaysRunCode;
}
public void addTransition(Transition trans) {
transitions.put(trans.evtName, trans);
}
public void runEntryCode() {
if (entryCode != null) {
entryCode.run();
}
}
public void runExitCode() {
if (exitCode != null) {
exitCode.run();
}
}
public void runAlwaysCode() {
if (alwaysRunCode != null) {
alwaysRunCode.run();
}
}
}
/**
* Create a new transition. See the documentation for addEvent and
* addTransition in FSM.
*/
public static class Transition {
String evtName;
String startState;
String endState;
/**
* Create a transition object that responds to the given event when in
* the given startState, and puts the FSM into the endState provided.
*/
public Transition(String evtName, String startState, String endState) {
this.evtName = evtName;
this.startState = startState;
this.endState = endState;
}
/**
* Override this to have FSM execute code immediately before following a
* state transition.
*/
public void doBeforeTransition() {
}
/**
* Override this to have FSM execute code immediately after following a
* state transition.
*/
public void doAfterTransition() {
}
}
}