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.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; import static java.lang.String.format; /** *

* Final State Machine *

* *

* Each state machine must be prepared with: *

    *
  1. Initial state
  2. *
  3. Finish state - may be not several states
  4. *
  5. Intermediate states - optionally
  6. *
  7. All transitions needed
  8. *
* *

* Each {@link State} may be specified with handlers: *

    *
  1. Before handler - is called right before FSM changes INTO this state
  2. *
  3. After handler - is called right before FSM changes FROM this state to another
  4. *
  5. Processor - the method to process events
  6. *
* *

* Transition is the Rule providing FSM the possibility to change between states. *

* Each transition must be determined in terms of: *

    *
  1. From State - mandatory
  2. *
  3. To State - mandatory
  4. *
  5. Condition - optionally. If specified, the FSM will check the condition in order to check the possibility * to change from FROM State into TO State
  6. *
* *

* Simple way to use it - to construct an inherited class specified with the type of events to be processed * during transitions. * *

 *  {@code
 *  SimpleFsm simpleFsm = Fsm
 *    ., String>from(SimpleFsm::new)
 *    .withStates()
 *      .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();
 *  }
 * 
* *

* Otherwise you are able to use Old syntax: * *

 *  NamedFsm namedFsm = new NamedFsm().setName("TEST FSM");
 *  namedFsm.initState(new State<String>("init") {
 *      public void process(String event) {
 *          initStatedProcessed.set(true);
 *      }
 *  });
 *
 *  namedFsm.addTransition("init", new State<String>("first", true) {
 *      public void process(String event) {
 *          firstStatedProcessed.set(true);
 *      }
 *  });
 *
 *  namedFsm.addTransition("init", new State<String>("another", true) {
 *      public void process(String event) {
 *          anotherStatedProcessed.set(true);
 *      }
 *  }, (fsm, event) -> false);
 *
 *  namedFsm.init();
 * 
* {@link SimpleFsm} */ public class Fsm { protected boolean done = false; protected State initialState; protected State currentState; protected State previousState; protected final Map> states = new HashMap<>(); protected final Map>> transitions = new HashMap<>(); /** * Initiate a builder * * @param supplier the original FSM inherited class constructor. You may specify '{@code () -> new SimpleFsm()}' in parameter * @param the original FSM inherited class type * @param the class type of Events to be processed * @return FsmBuilder */ @SuppressWarnings("unchecked") public static FsmBuilderInitializer from(Supplier supplier) { return new FsmBuilderInitializer<>(supplier); } /** * To initialize FSM into initial state */ public void init() { currentState = initialState; if (currentState == null) { throw new NotInitializedException(); } done = false; previousState = null; currentState.beforeEvent(); } /** * Main method to handle every event * * @param event event */ @SuppressWarnings("unchecked") public void process(E event) { if (done) { return; } currentState.process(event); if (currentState.isFinish()) { done = true; currentState.afterEvent(); return; } switchToNextState(event); } /** * Returns current state * * @return {@link State} */ public State getCurrentState() { return currentState; } /** * Returns previous state * * @return {@link State} */ public State getPreviousState() { return previousState; } /** * To specify initial state * * @param state {@link State} */ public void initState(State state) { state.setFSM(this); addState(state); initialState = state; } /** * To add another state * * @param state {@link State} */ public void addState(State state) { 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} */ public void addTransition(String fromState, String toState) { storeTransition(fromState, toState, null); } /** * To set the transition up * * @param fromState {@link State} * @param toState {@link State} * @param condition {@link Condition} */ public void addTransition(String fromState, String toState, Condition condition) { storeTransition(fromState, toState, condition); } /** * To set the transition up * * @param fromState {@link State} * @param toState {@link State} */ public void addTransition(String fromState, State toState) { addState(toState); addTransition(fromState, toState.getName()); } /** * To set the transition up * * @param fromState {@link State} * @param toState {@link State} * @param condition {@link Condition} */ public void addTransition(String fromState, State toState, Condition condition) { addState(toState); addTransition(fromState, toState.getName(), condition); } /** * Provides a possibility to initialize FSM in custom State * @param name State name (must be added before) */ protected void setCurrentState(String name) { try { this.currentState = this.states.get(name); } catch (NullPointerException e) { throw new NotInitializedException(format("Unable to find state '%s'", name), e); } this.done = currentState.isFinish(); } private void switchToNextState(E event) { 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); } private void nextState(State state, E event) { currentState.afterEvent(); previousState = currentState; currentState = state; currentState.beforeEvent(); } 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) { 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); } }