zcommon/src/main/java/org/zkoss/fsm/StateMachine.java

Summary

Maintainability
B
6 hrs
Test Coverage
/**
 * 
 */
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;
        }
    }
    

}