StateMachine.java

/*******************************************************************************
 * Copyright (c) 2004, 2013 Steve Flasby
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * <ul>
 *     <li>Redistributions of source code must retain the above copyright notice,
 *         this list of conditions and the following disclaimer.</li>
 *     <li>Redistributions in binary form must reproduce the above copyright notice,
 *         this list of conditions and the following disclaimer in the documentation
 *         and/or other materials provided with the distribution.</li>
 * </ul>
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ******************************************************************************/
package org.flasby.util.statemachine;

import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.logging.Logger;

/**
 * implements a Finite State Machine.
 *
 * @author steve
 *
 */
public class StateMachine<State, Event> {

	private static final Logger LOG = Logger.getLogger("StateMachine");

	public final BiConsumer<State, Event> NULL_CONSUMER = (a,b)->{};
	
	private static class Transition<State, Event> {

		private final State mToState;
		private final BiConsumer<State, Event> mEnterStateAction;
		private final BiConsumer<State, Event> mExitStateAction;

		public Transition(State toState, BiConsumer<State, Event> enterStateAction,
				BiConsumer<State, Event> exitStateAction) {
			mToState = toState;
			mEnterStateAction = enterStateAction;
			mExitStateAction = exitStateAction;
		}

		public State getNextState() {
			return mToState;
		}

		public BiConsumer<State, Event> getExitStateAction() {
			return mExitStateAction;
		}

		public BiConsumer<State, Event> getEnterStateAction() {
			return mEnterStateAction;
		}

		@Override
		public String toString() {
			return "Transition [mEnterStateAction=" + mEnterStateAction + ", mExitStateAction=" + mExitStateAction
					+ ", mToState=" + mToState + "]";
		}
	}

	private State mState;
	private final Map<State, Map<Event, Transition<State, Event>>> mTransitions = new HashMap<>();
	private final Map<Event, Transition<State, Event>> mGlobalTransition = new HashMap<>();
	private Transition<State, Event> mCurrentTransition = new Transition<State, Event>(null, NULL_CONSUMER, NULL_CONSUMER);

	private final BiConsumer<State, Event> mMonitor;

	/**
	 * creates a new FSM in the defined initialState.
	 *
	 * @param initialState
	 */
	public StateMachine(State initialState) {
		this(initialState, (s, e) -> {} );
	}

	/**
	 * creates a new FSM in the defined initialState.
	 *
	 * @param initialState
	 */
	public StateMachine(State initialState, BiConsumer<State, Event> monitor) {
		setState(initialState);
		this.mMonitor = monitor;
	}

	/**
	 * process the event and move from the current state to the next one if a
	 * transition is defined for the event.
	 *
	 * If no such transition is found then call handleNoTransition(...) and remain
	 * in the current state. If a transition is found the the FSM: - invokes the
	 * exitStateAction on the transition if such is defined. - moves to the next
	 * state - invokes the enterStateAction on the transition if such is defined.
	 *
	 * @param event The event to process
	 */
	public void process(Event event) {

		Transition<State, Event> t = findTransition(event);

		if (t == null) {
			handleNoTransition(event);
		} else {
			transition(t, event);
		}
		// // is there a globalTransition defined?
		// if ( mGlobalTransition.containsKey(event) ) {
		// LOG.fine("Moving from "+getState() +" to
		// "+mGlobalTransition.get(event).getNextState() +" because of global transition
		// of : "
		// +event);

		// transition(mGlobalTransition.get(event),event);

		// //setState(mGlobalTransition.get(event).getNextState() );

		// //mMonitor.accept(getState(), event);

		// // And execute the enter state action if one exists
		// // if ( null != mGlobalTransition.get(event).getEnterStateAction() ) {
		// // mGlobalTransition.get(event).getEnterStateAction().accept(getState(),
		// event);
		// //}
		// } else {
		// handleNoTransition(event);
		// }
		// } else {
		// LOG.fine("Moving from "+getState() +" to "+t.getNextState()+" because:
		// "+event);

		// transition(t,event);

		// // Get the Action for the current state and execute it if there is one
		// // mCurrentTransition.getExitStateAction().accept(getState(), event);
		// // Move to the next state
		// //setState( t.getNextState() );
		// //mMonitor.accept(getState(), event);

		// // And execute the enter state action if one exists
		// // if ( null != t.getEnterStateAction() ) {
		// // t.getEnterStateAction().accept(getState(), event);
		// // }
		// // mCurrentTransition = t;
		// }
	}

	// Can return null!
	private Transition<State, Event> findTransition(Event event) {
		Map<Event, Transition<State, Event>> ts = getTransitions(getState());
		if (ts == null) {
			if (mGlobalTransition.size() == 0) {
				throw new IllegalStateException(
						"No transitions defined for current state: " + getState() + " correct your state machine");
			} else {
				LOG.warning("Warning: only global transitions defined for current state: " + getState()
						+ " correct your state machine");
			}
		}

		Transition<State, Event> t = null;
		if (ts != null) {
			// If we have a Transition for this
			LOG.info("Normal Transitions for this event are: " + ts.get(event));
			t = ts.get(event);
		} else {
			LOG.info("No Normal Transitions for this event: " + event);
		}
		LOG.info("Global Transitions for this event are: " + mGlobalTransition.get(event));
		if (t == null) {
			t = mGlobalTransition.get(event);
		}
		LOG.info("Returning the Transition for this event: " + t);
		return t;
	}

	private void transition(Transition<State, Event> transition, Event event) {
		// Get the Action for the current state and execute it if there is one
		mCurrentTransition.getExitStateAction().accept(getState(), event);
		mState = transition.getNextState();
		mMonitor.accept(getState(), event);
		transition.getEnterStateAction().accept(getState(), event);
		mCurrentTransition = transition;
	}

	protected void handleNoTransition(Event event) {
		LOG.warning("No Transition defined from State:" + getState() + " for Event:" + event);
	}

	protected Map<Event, Transition<State, Event>> getTransitions(State state) {
		return mTransitions.get(state);
	}

	public StateMachine<State, Event> addTransition(State from, State to, Event event) {
		return addTransition(from, to, event, NULL_CONSUMER, NULL_CONSUMER);
	}

	public StateMachine<State, Event> addTransition(State from, State to, Event event,
			BiConsumer<State, Event> enterStateAction) {
		return addTransition(from, to, event, enterStateAction, NULL_CONSUMER);
	}

	public StateMachine<State, Event> addTransition(State from, State to, Event event,
			BiConsumer<State, Event> enterStateAction, BiConsumer<State, Event> exitStateAction) {
		Map<Event, Transition<State, Event>> ts = getTransitions(from);
		if (ts == null) {
			ts = new HashMap<>();
			mTransitions.put(from, ts);
		}
		ts.put(event, createTransition(to, enterStateAction, exitStateAction));
		return this;
	}

	public StateMachine<State, Event> addGlobalTransition(Event event, State to,
			BiConsumer<State, Event> enterStateAction) {
		mGlobalTransition.put(event, createTransition(to, enterStateAction, NULL_CONSUMER));
		return this;
	}

	public StateMachine<State, Event> addGlobalTransition(Event event, State to,
			BiConsumer<State, Event> enterStateAction, BiConsumer<State, Event> exitStateAction) {
		mGlobalTransition.put(event, createTransition(to, enterStateAction, exitStateAction));
		return this;
	}

	private Transition<State, Event> createTransition(State to, BiConsumer<State, Event> enterStateAction,
			BiConsumer<State, Event> exitStateAction) {
		return new Transition<State, Event>(to, enterStateAction, exitStateAction);
	}

	public State getState() {
		return mState;
	}

	protected void setState(State state) {
		mState = state;
	}

	public void forceTransition(State state, Event event) {
		mCurrentTransition.getExitStateAction().accept(getState(), event);
		mMonitor.accept(getState(), event);
		// and then force the event
		mState = state;
	}

}