2022-07-07 02:15:12 +03:00
|
|
|
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;
|
|
|
|
|
2022-07-12 23:27:20 +03:00
|
|
|
import static java.lang.String.format;
|
|
|
|
|
2022-07-07 02:15:12 +03:00
|
|
|
/**
|
|
|
|
* <p>
|
2022-07-07 23:22:03 +03:00
|
|
|
* <b>Final State Machine</b>
|
2022-07-07 02:15:12 +03:00
|
|
|
* </p>
|
2022-07-07 23:30:46 +03:00
|
|
|
*
|
2022-07-07 02:15:12 +03:00
|
|
|
* <p>
|
2022-07-07 23:22:03 +03:00
|
|
|
* Each state machine must be prepared with:
|
2022-07-07 02:15:12 +03:00
|
|
|
* <ol>
|
|
|
|
* <li>Initial state</li>
|
|
|
|
* <li>Finish state - may be not several states</li>
|
|
|
|
* <li>Intermediate states - optionally</li>
|
|
|
|
* <li>All transitions needed</li>
|
|
|
|
* </ol>
|
|
|
|
*
|
|
|
|
* <p>
|
2022-07-07 23:22:03 +03:00
|
|
|
* Each {@link State} may be specified with handlers:
|
2022-07-07 02:15:12 +03:00
|
|
|
* <ol>
|
|
|
|
* <li>Before handler - is called right before FSM changes INTO this state</li>
|
|
|
|
* <li>After handler - is called right before FSM changes FROM this state to another</li>
|
|
|
|
* <li>Processor - the method to process events</li>
|
|
|
|
* </ol>
|
|
|
|
*
|
|
|
|
* <p>
|
2022-07-07 15:51:37 +03:00
|
|
|
* Transition is the Rule providing FSM the possibility to change between states.
|
2022-07-07 23:22:03 +03:00
|
|
|
* <p>
|
2022-07-07 02:15:12 +03:00
|
|
|
* Each transition must be determined in terms of:
|
|
|
|
* <ol>
|
|
|
|
* <li>From State - mandatory</li>
|
|
|
|
* <li>To State - mandatory</li>
|
|
|
|
* <li>Condition - optionally. If specified, the FSM will check the condition in order to check the possibility
|
|
|
|
* to change from FROM State into TO State</li>
|
|
|
|
* </ol>
|
|
|
|
*
|
2022-07-07 23:22:03 +03:00
|
|
|
* <p>
|
2022-07-07 02:15:12 +03:00
|
|
|
* Simple way to use it - to construct an inherited class specified with the type of events to be processed
|
|
|
|
* during transitions.
|
2022-07-07 23:22:03 +03:00
|
|
|
*
|
2022-07-07 02:15:12 +03:00
|
|
|
* <pre>
|
|
|
|
* {@code
|
2022-07-07 16:07:54 +03:00
|
|
|
* SimpleFsm<String> simpleFsm = Fsm
|
|
|
|
* .<SimpleFsm<String>, String>from(SimpleFsm::new)
|
|
|
|
* .withStates()
|
2022-07-07 02:15:12 +03:00
|
|
|
* .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();
|
2022-07-07 23:22:03 +03:00
|
|
|
* }
|
2022-07-07 02:15:12 +03:00
|
|
|
* </pre>
|
|
|
|
*
|
2022-07-07 23:22:03 +03:00
|
|
|
* <p>
|
|
|
|
* Otherwise you are able to use Old syntax:
|
|
|
|
*
|
|
|
|
* <pre>
|
|
|
|
* NamedFsm namedFsm = new NamedFsm().setName("TEST FSM");
|
2022-07-07 23:30:46 +03:00
|
|
|
* namedFsm.initState(new State<String>("init") {
|
2022-07-07 23:22:03 +03:00
|
|
|
* public void process(String event) {
|
|
|
|
* initStatedProcessed.set(true);
|
|
|
|
* }
|
|
|
|
* });
|
|
|
|
*
|
2022-07-07 23:30:46 +03:00
|
|
|
* namedFsm.addTransition("init", new State<String>("first", true) {
|
2022-07-07 23:22:03 +03:00
|
|
|
* public void process(String event) {
|
|
|
|
* firstStatedProcessed.set(true);
|
|
|
|
* }
|
|
|
|
* });
|
|
|
|
*
|
2022-07-07 23:30:46 +03:00
|
|
|
* namedFsm.addTransition("init", new State<String>("another", true) {
|
2022-07-07 23:22:03 +03:00
|
|
|
* public void process(String event) {
|
|
|
|
* anotherStatedProcessed.set(true);
|
|
|
|
* }
|
2022-07-07 23:30:46 +03:00
|
|
|
* }, (fsm, event) -> false);
|
2022-07-07 23:22:03 +03:00
|
|
|
*
|
|
|
|
* namedFsm.init();
|
2022-07-07 23:30:46 +03:00
|
|
|
* </pre>
|
2022-07-07 02:15:12 +03:00
|
|
|
* {@link SimpleFsm}
|
|
|
|
*/
|
|
|
|
public class Fsm<T extends Fsm, E> {
|
|
|
|
|
2022-07-11 14:24:53 +03:00
|
|
|
protected boolean done = false;
|
|
|
|
protected State<E> initialState;
|
|
|
|
protected State<E> currentState;
|
|
|
|
protected State<E> previousState;
|
2022-07-11 15:15:58 +03:00
|
|
|
protected final Map<String, State<E>> states = new HashMap<>();
|
|
|
|
protected final Map<String, Map<String, Condition<T, E>>> transitions = new HashMap<>();
|
2022-07-07 02:15:12 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Initiate a builder
|
|
|
|
*
|
|
|
|
* @param supplier the original FSM inherited class constructor. You may specify '{@code () -> new SimpleFsm()}' in parameter
|
2022-07-07 23:22:03 +03:00
|
|
|
* @param <T> the original FSM inherited class type
|
|
|
|
* @param <E> the class type of Events to be processed
|
2022-07-07 02:15:12 +03:00
|
|
|
* @return FsmBuilder
|
|
|
|
*/
|
|
|
|
@SuppressWarnings("unchecked")
|
2022-07-07 23:22:03 +03:00
|
|
|
public static <T extends Fsm, E> FsmBuilderInitializer<T, E> from(Supplier<T> supplier) {
|
2022-07-07 16:07:54 +03:00
|
|
|
return new FsmBuilderInitializer<>(supplier);
|
2022-07-07 02:15:12 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* To initialize FSM into initial state
|
|
|
|
*/
|
2022-07-07 23:22:03 +03:00
|
|
|
public void init() {
|
2022-07-07 02:15:12 +03:00
|
|
|
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")
|
2022-07-07 23:22:03 +03:00
|
|
|
public void process(E event) {
|
2022-07-07 02:15:12 +03:00
|
|
|
if (done) {
|
|
|
|
return;
|
|
|
|
}
|
2022-07-07 23:06:38 +03:00
|
|
|
currentState.process(event);
|
2022-07-07 02:15:12 +03:00
|
|
|
currentState.afterEvent();
|
|
|
|
if (currentState.isFinish()) {
|
|
|
|
done = true;
|
|
|
|
return;
|
|
|
|
}
|
2022-07-07 23:06:38 +03:00
|
|
|
switchToNextState(event);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns current state
|
|
|
|
*
|
|
|
|
* @return {@link State}
|
|
|
|
*/
|
|
|
|
public State<E> getCurrentState() {
|
|
|
|
return currentState;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns previous state
|
|
|
|
*
|
|
|
|
* @return {@link State}
|
|
|
|
*/
|
|
|
|
public State<E> getPreviousState() {
|
|
|
|
return previousState;
|
2022-07-07 02:15:12 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* To specify initial state
|
|
|
|
*
|
|
|
|
* @param state {@link State}
|
|
|
|
*/
|
2022-07-07 23:22:03 +03:00
|
|
|
public void initState(State<E> state) {
|
2022-07-07 02:15:12 +03:00
|
|
|
state.setFSM(this);
|
|
|
|
addState(state);
|
|
|
|
initialState = state;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* To add another state
|
|
|
|
*
|
|
|
|
* @param state {@link State}
|
|
|
|
*/
|
2022-07-07 23:22:03 +03:00
|
|
|
public void addState(State<E> state) {
|
2022-07-07 02:15:12 +03:00
|
|
|
checkStateExist(state.getName());
|
|
|
|
state.setFSM(this);
|
|
|
|
this.states.put(state.getName(), state);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* To set the transition up
|
|
|
|
*
|
|
|
|
* @param fromState {@link State}
|
2022-07-07 23:22:03 +03:00
|
|
|
* @param toState {@link State}
|
2022-07-07 02:15:12 +03:00
|
|
|
*/
|
2022-07-07 23:22:03 +03:00
|
|
|
public void addTransition(String fromState, String toState) {
|
2022-07-07 02:15:12 +03:00
|
|
|
storeTransition(fromState, toState, null);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* To set the transition up
|
|
|
|
*
|
|
|
|
* @param fromState {@link State}
|
2022-07-07 23:22:03 +03:00
|
|
|
* @param toState {@link State}
|
2022-07-07 02:15:12 +03:00
|
|
|
* @param condition {@link Condition}
|
|
|
|
*/
|
2022-07-07 23:22:03 +03:00
|
|
|
public void addTransition(String fromState, String toState, Condition<T, E> condition) {
|
2022-07-07 02:15:12 +03:00
|
|
|
storeTransition(fromState, toState, condition);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* To set the transition up
|
|
|
|
*
|
|
|
|
* @param fromState {@link State}
|
2022-07-07 23:22:03 +03:00
|
|
|
* @param toState {@link State}
|
2022-07-07 02:15:12 +03:00
|
|
|
*/
|
2022-07-07 23:22:03 +03:00
|
|
|
public void addTransition(String fromState, State<E> toState) {
|
2022-07-07 02:15:12 +03:00
|
|
|
addState(toState);
|
|
|
|
addTransition(fromState, toState.getName());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* To set the transition up
|
|
|
|
*
|
|
|
|
* @param fromState {@link State}
|
2022-07-07 23:22:03 +03:00
|
|
|
* @param toState {@link State}
|
2022-07-07 02:15:12 +03:00
|
|
|
* @param condition {@link Condition}
|
|
|
|
*/
|
2022-07-07 23:22:03 +03:00
|
|
|
public void addTransition(String fromState, State<E> toState, Condition<T, E> condition) {
|
2022-07-07 02:15:12 +03:00
|
|
|
addState(toState);
|
|
|
|
addTransition(fromState, toState.getName(), condition);
|
|
|
|
}
|
|
|
|
|
2022-07-12 23:27:20 +03:00
|
|
|
/**
|
|
|
|
* 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();
|
|
|
|
}
|
|
|
|
|
2022-07-07 23:06:38 +03:00
|
|
|
private void switchToNextState(E event) {
|
|
|
|
if (!transitions.containsKey(currentState.getName())) {
|
|
|
|
throw new TransitionMissedException(currentState.getName());
|
|
|
|
}
|
2022-07-07 23:22:03 +03:00
|
|
|
Map<String, Condition<T, E>> conditions = transitions.get(currentState.getName());
|
2022-07-07 23:06:38 +03:00
|
|
|
List<String> nextStates = new ArrayList<>();
|
|
|
|
for (String key : conditions.keySet()) {
|
|
|
|
if (conditions.get(key) == null) {
|
|
|
|
nextStates.add(key);
|
2022-07-07 23:22:03 +03:00
|
|
|
} else if (conditions.get(key).check((T) this, event)) {
|
2022-07-07 23:06:38 +03:00
|
|
|
nextStates.add(key);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (nextStates.size() > 1) {
|
|
|
|
throw new AmbiguousTransitionException(currentState.getName(), nextStates);
|
|
|
|
}
|
|
|
|
if (nextStates.size() == 0) {
|
|
|
|
throw new BrokenTransitionException(currentState.getName());
|
|
|
|
}
|
|
|
|
State<E> nextState = states.get(nextStates.get(0));
|
|
|
|
nextState(nextState, event);
|
|
|
|
}
|
|
|
|
|
2022-07-07 02:15:12 +03:00
|
|
|
private void nextState(State<E> state, E event) {
|
|
|
|
previousState = currentState;
|
|
|
|
currentState = state;
|
2022-07-12 23:09:04 +03:00
|
|
|
currentState.beforeEvent();
|
2022-07-07 02:15:12 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
private void checkStateExist(String name) throws StateAlreadyExistsException {
|
|
|
|
if (this.states.containsKey(name)) {
|
|
|
|
throw new StateAlreadyExistsException(name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-07 23:22:03 +03:00
|
|
|
private void storeTransition(String fromState, String toState, Condition<T, E> condition) {
|
2022-07-07 02:15:12 +03:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|