zcommon/src/main/java/org/zkoss/fsm/StateMachine.java
/**
*
*/
package org.zkoss.fsm;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* A Finite State Machine implementation. This state machine is callback based,
* which differs from the standard FSM from textbook. Easier to use and faster
* to develop and debug.
* @since 6.0.0
* @author simonpai
*/
public abstract class StateMachine<E, C, IN> {
protected final Map<E, StateCtx<E, C, IN>> _states =
new HashMap<E, StateCtx<E, C, IN>>();
protected E _current;
protected boolean _run;
protected int _step;
protected boolean _debug = false;
/**
* Construct a state machine
*/
public StateMachine() {
init();
reset();
}
// system //
/**
* Set debug mode, where {@link #onDebug(String)} is called at certain timing to
* assist user develop the state machine
*/
public StateMachine<E, C, IN> setDebugMode(boolean mode) {
_debug = mode;
return this;
}
// definition //
/**
* Set the state by token
* @return the state
*/
public StateCtx<E, C, IN> setState(E token, StateCtx<E, C, IN> state) {
if(state == null) throw new IllegalArgumentException(
"State cannot be null. Use removeState() to remove a state.");
_states.put(token, state.setMaster(this));
return state;
}
/**
* Remove the state by token.
* @return the removed state
*/
public StateCtx<E, C, IN> removeState(E token) {
return _states.remove(token).setMaster(null);
}
/**
* Get the state by token. Automatically creates one if it does not exist.
*/
public StateCtx<E, C, IN> getState(E token) {
return getState(token, true);
}
/**
* Get the state by token.
* @param autoCreate if true, automatically creates one if it does not exist.
*/
public StateCtx<E, C, IN> getState(E token, boolean autoCreate) {
StateCtx<E, C, IN> result = _states.get(token);
if(result == null && autoCreate)
_states.put(token, result = new StateCtx<E, C, IN>().setMaster(this));
return result;
}
/**
* Called at the constructor of state machine
*/
protected void init() {}
/**
* Determines the initial state upon meeting the input character and class
*/
protected abstract E getLandingState(IN input, C inputClass);
/**
* Determines the class for an input character.
*/
protected abstract C getClass(IN input);
// event handler //
/**
* This method is called at constructor and when reseting the machine.
*/
protected void onReset() {}
/**
* This method is called when the machine takes the first character.
*/
protected void onStart(IN input, C inputClass, E landing) {}
/**
* This method is called before executing a step
*/
protected void beforeStep(IN input, C inputClass, E origin) {}
/**
* This method is called after executing a step
*/
protected void afterStep(IN input, C inputClass, E origin, E destination) {}
/**
* This method is called when the machine stops
* @param endOfInput true if the machine stops due to end of input
*/
protected void onStop(boolean endOfInput) {}
/**
* This method is called when the machine rejects an input character
*/
protected void onReject(IN input) {
throw new StateMachineException(_step, _current, input);
}
/**
* This method is call at certain situations when debug mode is on.
* @see #setDebugMode(boolean)
*/
protected void onDebug(String message){}
// operation //
/**
* Feed the machine a stream of characters
*/
public final void run(Iterator<IN> inputs) { // TODO: iterable
_run = true;
while(_run && inputs.hasNext())
run(inputs.next());
boolean endOfInput = !inputs.hasNext();
onStop(endOfInput);
if(_current != null)
getState(_current).onStop(endOfInput);
doDebug("");
doDebug("Stop");
doDebug("");
}
/**
* Feed the machine a single character
*/
public final void run(IN input) {
C inputClass = getClass(input);
doDebug("");
doDebug("Step " + _step);
doDebug("* Input: " + input + " (" + inputClass + ")");
final E origin = _current;
E destination = null;
beforeStep(input, inputClass, origin);
if(inputClass == null) {
doReject(input);
return;
}
if(origin == null){
destination = getLandingState(input, inputClass); // dest
if(destination == null) {
doReject(input);
return;
}
onStart(input, inputClass, destination);
getState(destination).onLand(input, inputClass, origin);
} else {
StateCtx<E, C, IN> state = getState(origin);
if(state.isLeaving(input, inputClass)) {
destination = state.getDestination(input, inputClass); // dest
if(destination == null) {
doReject(input);
return;
}
state.onLeave(input, inputClass, destination);
state.doTransit(input, inputClass);
getState(destination).onLand(input, inputClass, origin);
} else if(state.isReturning(input, inputClass)) {
destination = origin; // dest
state.onReturn(input, inputClass);
} else { // rejected by state
state.onReject(input, inputClass);
doReject(input);
return;
}
}
_current = destination;
doDebug("* State: " + origin + " -> " + destination);
afterStep(input, inputClass, origin, destination);
_step++;
}
/**
* Starts the machine with a stream of input characters.
*/
public final void start(Iterator<IN> inputs) { // TODO: take iterable
reset();
run(inputs);
}
/**
* Starts the machine with a single input character.
*/
public final void start(IN input) {
reset();
run(input);
}
/**
* Terminates the machine.
*/
public final void terminate() {
reset();
}
// status query //
// TODO: enhance
/**
* Return the current state
*/
public E getCurrentState() {
return _current;
}
/**
* Return true if the machine is stopped
*/
public boolean isTerminated() {
return !_run && _current == null;
}
/**
* Return true if the machine is suspended
*/
public boolean isSuspended() {
return !_run && _current != null;
}
// default internal operation //
/**
* Suspend the machine
*/
protected final void suspend() {
_run = false;
}
/**
* Reject a character
*/
protected final void doReject(IN input) {
_run = false;
onReject(input);
}
/**
* Manually send a debug message
* @see #onDebug(String)
* @see #setDebugMode(boolean)
*/
protected final void doDebug(String message) {
if(_debug) onDebug(message);
}
private final void reset() {
_current = null;
_run = false;
_step = 0;
doDebug("");
doDebug("Reset");
onReset();
}
/*package*/ final void terminateAt(IN input) {
getState(_current).onLeave(input, null, null);
reset();
}
// exception //
public static class StateMachineException extends RuntimeException {
private static final long serialVersionUID = -6580348498729948101L;
private int _step;
private Object _state;
private Object _input;
public StateMachineException(int step, Object state, Object input) {
this(step, state, input, "Rejected at step " + step +
" with current state: " + state + ", input: " + input);
}
public StateMachineException(int step, Object state, Object input,
String message) {
super(message);
_step = step;
_state = state;
_input = input;
}
public StateMachineException(String message) {
super(message);
}
public int getStep() {
return _step;
}
public Object getState() {
return _state;
}
public Object getInput() {
return _input;
}
}
}