Mega Code Archive

 
Categories / Java / Collections Data Structure
 

A programmable Finite State Machine implementation

// $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<String, State> states;   protected List<ChangeListener> 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<String, State>();     this.currentState = null;     this.changeListeners = new ArrayList<ChangeListener>();   }   /**    * 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:    *     * <ol>    * <li>If A and B are distinct, run A's exit code.</li>    * <li>Record current state as B.</li>    * <li>Run B's "alwaysRunCode".</li>    * <li>If A and B are distinct, run B's entry code.</li>    * </ol>    */   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:    *     * <ol>    * <li>Execute the transition's "doBeforeTransition" method</li>    * <li>Run fsm.setState(B) -- see docs for that method</li>    * <li>Execute the transition's "doAfterTransition" method</li>    * <li>Fire a change event, notifying interested observers that the    * transition has completed.</li>    * <li>Now firmly in state B, see if B has a third state C that we must    * automatically transition to via addEvent(C).</li>    * </ol>    */   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<String, Transition> transitions;     String autoTransitionState;     Runnable entryCode;     Runnable exitCode;     Runnable alwaysRunCode;     State(Runnable entryCode, Runnable exitCode, Runnable alwaysRunCode) {       autoTransitionState = null;       transitions = new HashMap<String, Transition>();       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() {     }   } }