package me.bvn13.fsm;
import me.bvn13.fsm.exceptions.AmbiguousTransitionException;
import me.bvn13.fsm.exceptions.BrokenTransitionException;
import me.bvn13.fsm.exceptions.ConditionAlreadyExistsException;
import me.bvn13.fsm.exceptions.FsmException;
import me.bvn13.fsm.exceptions.NotInitializedException;
import me.bvn13.fsm.exceptions.StateAlreadyExistsException;
import me.bvn13.fsm.exceptions.TransitionMissedException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
/**
*
* Final State Machine
*
*
*
* Each state machine must be prepared with:
* - Initial state
* - Finish state - may be not several states
* - Intermediate states - optionally
* - All transitions needed
*
*
*
*
*
* Each {@link State} may be specified with handlers:
* - Before handler - is called right before FSM changes INTO this state
* - After handler - is called right before FSM changes FROM this state to another
* - Processor - the method to process events
*
*
*
*
* Transition is the Rule provides FSM the possibility to change between states.
*
* Each transition must be determined in terms of:
*
* - From State - mandatory
* - To State - mandatory
* - Condition - optionally. If specified, the FSM will check the condition in order to check the possibility
* to change from FROM State into TO State
*
*
*
*
* Simple way to use it - to construct an inherited class specified with the type of events to be processed
* during transitions.
*
* {@code
* SimpleFsm simpleFsm = SimpleFsm
* ., String>withStates(SimpleFsm::new)
* .from("init")
* .withBeforeHandler(fsm -> initBefore.set(true))
* .withAfterHandler(fsm -> initAfter.set(true))
* .withProcessor((fsm, event) -> initProcess.set(true))
* .end()
* .finish("finish")
* .withBeforeHandler(fsm -> finishBefore.set(true))
* .withAfterHandler(fsm -> finishAfter.set(true))
* .withProcessor((fsm, event) -> finishProcess.set(true))
* .end()
* .withTransition()
* .from("init")
* .to("finish")
* .checking((fsm, event) -> true)
* .end()
* .create();
* }
*
*
* {@link SimpleFsm}
*/
public class Fsm {
private boolean done = false;
private State initialState;
private State currentState;
private State previousState;
private final Map> states = new HashMap<>();
private final Map>> transitions = new HashMap<>();
/**
* Initiate a builder
*
* @param supplier the original FSM inherited class constructor. You may specify '{@code () -> new SimpleFsm()}' in parameter
* @return FsmBuilder
* @param the original FSM inherited class type
* @param the class type of Events to be processed
*/
@SuppressWarnings("unchecked")
public static FsmBuilder withStates(Supplier supplier) {
return new FsmBuilder<>(supplier);
}
/**
* To initialize FSM into initial state
* @throws NotInitializedException
*/
public void init() throws NotInitializedException {
currentState = initialState;
if (currentState == null) {
throw new NotInitializedException();
}
done = false;
previousState = null;
currentState.beforeEvent();
}
/**
* Returns current state
*
* @return {@link State}
*/
public State getCurrentState() {
return currentState;
}
/**
* Returns previous state
*
* @return {@link State}
*/
public State getPreviousState() {
return previousState;
}
/**
* Main method to handle every event
*
* @param event event
* @throws FsmException
*/
@SuppressWarnings("unchecked")
public void process(E event) throws FsmException {
if (done) {
return;
}
currentState.afterEvent();
if (currentState.isFinish()) {
done = true;
return;
}
if (!transitions.containsKey(currentState.getName())) {
throw new TransitionMissedException(currentState.getName());
}
Map> conditions = transitions.get(currentState.getName());
List nextStates = new ArrayList<>();
for (String key : conditions.keySet()) {
if (conditions.get(key) == null) {
nextStates.add(key);
} else if(conditions.get(key).check((T) this, event)) {
nextStates.add(key);
}
}
if (nextStates.size() > 1) {
throw new AmbiguousTransitionException(currentState.getName(), nextStates);
}
if (nextStates.size() == 0) {
throw new BrokenTransitionException(currentState.getName());
}
State nextState = states.get(nextStates.get(0));
nextState(nextState, event);
}
/**
* To specify initial state
*
* @param state {@link State}
* @throws FsmException
*/
public void initState(State state) throws FsmException {
state.setFSM(this);
addState(state);
initialState = state;
}
/**
* To add another state
*
* @param state {@link State}
* @throws FsmException
*/
public void addState(State state) throws FsmException {
checkStateExist(state.getName());
state.setFSM(this);
this.states.put(state.getName(), state);
}
/**
* To set the transition up
*
* @param fromState {@link State}
* @param toState {@link State}
* @throws FsmException
*/
public void addTransition(String fromState, String toState) throws FsmException {
storeTransition(fromState, toState, null);
}
/**
* To set the transition up
*
* @param fromState {@link State}
* @param toState {@link State}
* @param condition {@link Condition}
* @throws FsmException
*/
public void addTransition(String fromState, String toState, Condition condition) throws FsmException {
storeTransition(fromState, toState, condition);
}
/**
* To set the transition up
*
* @param fromState {@link State}
* @param toState {@link State}
* @throws FsmException
*/
public void addTransition(String fromState, State toState) throws FsmException {
addState(toState);
addTransition(fromState, toState.getName());
}
/**
* To set the transition up
*
* @param fromState {@link State}
* @param toState {@link State}
* @param condition {@link Condition}
* @throws FsmException
*/
public void addTransition(String fromState, State toState, Condition condition) throws FsmException {
addState(toState);
addTransition(fromState, toState.getName(), condition);
}
private void nextState(State state, E event) {
state.beforeEvent();
previousState = currentState;
currentState = state;
currentState.process(event);
}
private void checkStateExist(String name) throws StateAlreadyExistsException {
if (this.states.containsKey(name)) {
throw new StateAlreadyExistsException(name);
}
}
private void storeTransition(String fromState, String toState, Condition condition) throws FsmException {
if (!transitions.containsKey(fromState)) {
transitions.put(fromState, new HashMap<>());
}
if (transitions.get(fromState).containsKey(toState)) {
throw new ConditionAlreadyExistsException(fromState, toState);
}
transitions.get(fromState).put(toState, condition);
}
}