package org.webpieces.javasm.impl; import java.awt.event.ActionListener; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import javax.swing.event.EventListenerList; import org.webpieces.javasm.api.Memento; import org.webpieces.javasm.api.State; import org.webpieces.javasm.api.StateMachine; import org.webpieces.javasm.api.Transition; import com.webpieces.util.locking.PermitQueue; /** */ public class StateMachineImpl implements StateMachine { private final Map<String, StateImpl> nameToState = new HashMap<String, StateImpl>(); private final EventListenerList globalEntryListeners = new EventListenerList(); private final EventListenerList globalExitListeners = new EventListenerList(); private final String rawMapId; /** * Creates an instance of StateMachineImpl. * @param executor * @param id */ public StateMachineImpl(String id) { if(id == null) rawMapId = "unnamed"; else rawMapId = id; } public Memento createMementoFromState(String stateMachineId, State state) { State name = nameToState.get(state.getName()); if(name == null) throw new IllegalArgumentException(this + "This state does not exist in this statemachine. name="+name); return new StateMachineState(rawMapId, stateMachineId, state, this); } /** * @see org.webpieces.javasm.api.StateMachine#createState(java.lang.String) */ public State createState(String name) { StateImpl state = nameToState.get(name); if(state != null) throw new IllegalArgumentException("This state already exists. You can't create the same state twice"); state = new StateImpl(name); nameToState.put(name, state); for(ActionListener l : globalEntryListeners.getListeners(ActionListener.class)) { state.addEntryActionListener(l); } for(ActionListener l : globalExitListeners.getListeners(ActionListener.class)) { state.addExitActionListener(l); } return state; } public Transition createTransition(State[] startStates, State endState, Object... events) { if(events.length < 1) { throw new IllegalArgumentException(this + "You must specify at least one event"); } else if(!(endState instanceof StateImpl)) { throw new IllegalArgumentException(rawMapId + "endState are not created using this StateMachine"); } if(startStates.length < 1) { throw new IllegalArgumentException(rawMapId + "You must specify at least one event"); } TransitionImpl transition = new TransitionImpl((StateImpl)endState); for(State startState : startStates) { StateImpl startImpl = (StateImpl)startState; for(Object event : events) { startImpl.addTransition(event, transition); } } return transition; } /** * @see org.webpieces.javasm.api.StateMachine#createTransition(org.webpieces.javasm.api.State, org.webpieces.javasm.api.State, org.webpieces.javasm.api.Event[]) */ public Transition createTransition(State startState, State endState, Object... events) { State[] startStates = {startState}; return createTransition(startStates, endState, events); } @Override public CompletableFuture<State> fireEvent(Memento memento, Object evt) { if(memento == null) throw new IllegalArgumentException(this + "memento cannot be null"); else if(evt == null) throw new IllegalArgumentException(this + "evt cannot be null"); else if(!(memento instanceof StateMachineState)) throw new IllegalArgumentException(this + "memento was not created using StateMachine.createMementoFromIntialState and must be"); else if( ((StateMachineState)memento).getStateMachine() != this) throw new IllegalArgumentException(this + "memento was not created with this specific statemachine. " + "you got your statemachines mixed up with the mementos"); StateMachineState smState = (StateMachineState)memento; PermitQueue<State> queue = smState.getPermitQueue(); CompletableFuture<State> f = new CompletableFuture<State>(); queue.runRequest(() -> fire(evt, smState)).handle((s, t) -> release(s, t, f, queue)); return f; } private Void release(State s, Throwable t, CompletableFuture<State> f, PermitQueue<State> queue) { queue.releasePermit(); //release the next event to the statemachine if(t != null) f.completeExceptionally(t); else f.complete(s); return null; } private CompletableFuture<State> fire(Object evt, StateMachineState smState) { CompletableFuture<State> future = new CompletableFuture<>(); try { //get the current state StateImpl state = nameToState.get(smState.getCurrentState().getName()); state.fireEvent(smState, evt); future.complete(smState.getCurrentState()); } catch(RuntimeException e) { //NOTE: Stack trace is not logged here. That is the responsibility of the javasm client //so exceptions don't get logged multiple times. smState.getLogger().warn(this+"Exception occurred going out of state="+smState.getCurrentState()+", event="+evt); future.completeExceptionally(e); } return future; } /** * @see org.webpieces.javasm.api.StateMachine#addGlobalStateEntryAction(java.awt.event.ActionListener) */ public StateMachine addGlobalStateEntryAction(ActionListener l) { //first add it to all created states for(State state : nameToState.values()) { state.addEntryActionListener(l); } globalEntryListeners.add(ActionListener.class, l); return this; } /** * @see org.webpieces.javasm.api.StateMachine#addGlobalStateExitAction(java.awt.event.ActionListener) */ public StateMachine addGlobalStateExitAction(ActionListener l) { //first add it to all created states for(State state : nameToState.values()) { state.addExitActionListener(l); } globalExitListeners.add(ActionListener.class, l); return this; } @Override public String toString() { return "[" + rawMapId + "] "; } }