
org.squirrelframework.foundation.fsm.impl.AbstractStateMachine Maven / Gradle / Ivy
package org.squirrelframework.foundation.fsm.impl;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.squirrelframework.foundation.component.Observable;
import org.squirrelframework.foundation.component.SquirrelProvider;
import org.squirrelframework.foundation.component.impl.AbstractSubject;
import org.squirrelframework.foundation.event.AsyncEventListener;
import org.squirrelframework.foundation.event.ListenerMethod;
import org.squirrelframework.foundation.exception.ErrorCodes;
import org.squirrelframework.foundation.exception.TransitionException;
import org.squirrelframework.foundation.fsm.*;
import org.squirrelframework.foundation.fsm.ActionExecutionService.*;
import org.squirrelframework.foundation.fsm.annotation.*;
import org.squirrelframework.foundation.util.Pair;
import org.squirrelframework.foundation.util.ReflectUtils;
import org.squirrelframework.foundation.util.TypeReference;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import static com.google.common.base.Preconditions.checkState;
/**
* The Abstract state machine provide several extension ability to cover different extension granularity.
*
* - Method beforeStateExit/afterStateEntry is used to add custom logic on all kinds of state exit/entry.
* - Method exit[stateName]/entry[stateName] is extension method which is used to add custom logic on specific state.
* - Method beforeTransitionBegin/afterTransitionComplete is used to add custom logic on all kinds of transition
* accepted all conditions.
* - Method transitFrom[fromStateName]To[toStateName]On[eventName] is used to add custom logic on specific transition
* accepted all conditions.
* - Method transitFromAnyTo[toStateName]On[eventName] is used to add custom logic on any state transfer to specific target
* state on specific event happens, so as the transitFrom[fromStateName]ToAnyOn[eventName], transitFrom[fromState]To[ToStateName],
* and on[EventName].
*
* @author Henry.He
*
* @param state machine type
* @param state type
* @param event type
* @param context type
*/
public abstract class AbstractStateMachine, S, E, C> extends AbstractSubject implements StateMachine {
private static final Logger logger = LoggerFactory.getLogger(AbstractStateMachine.class);
private final ActionExecutionService executor = SquirrelProvider.getInstance().newInstance(
new TypeReference>(){});
private StateMachineData data;
private volatile StateMachineStatus status = StateMachineStatus.INITIALIZED;
private LinkedBlockingDeque> queuedEvents = new LinkedBlockingDeque>();
private LinkedBlockingQueue> queuedTestEvents = new LinkedBlockingQueue>();
private volatile boolean isProcessingTestEvent = false;
private E startEvent, finishEvent, terminateEvent;
protected final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
protected final Lock writeLock = rwLock.writeLock();
protected final Lock readLock = rwLock.readLock();
private MvelScriptManager scriptManager;
// state machine options
private boolean isAutoStartEnabled = true;
private boolean isAutoTerminateEnabled = true;
private boolean isDelegatorModeEnabled = false;
@SuppressWarnings("unused")
private long transitionTimeout = -1;
private boolean isDataIsolateEnabled = false;
private boolean isDebugModeEnabled = false;
private boolean isRemoteMonitorEnabled = false;
private Class>[] extraParamTypes;
private TransitionException lastException = null;
void prePostConstruct(S initialStateId, Map> states,
StateMachineConfiguration configuration, Runnable cb) {
data = FSM.newStateMachineData(states);
data.write().initialState(initialStateId);
data.write().currentState(null);
data.write().identifier(configuration.getIdProvider().get());
// retrieve options value from state machine configuration
this.isAutoStartEnabled = configuration.isAutoStartEnabled();
this.isAutoTerminateEnabled = configuration.isAutoTerminateEnabled();
this.isDataIsolateEnabled = configuration.isDataIsolateEnabled();
this.isDebugModeEnabled = configuration.isDebugModeEnabled();
this.isDelegatorModeEnabled = configuration.isDelegatorModeEnabled();
this.isRemoteMonitorEnabled = configuration.isRemoteMonitorEnabled();
cb.run();
prepare();
}
@Override
public boolean isRemoteMonitorEnabled() {
return isRemoteMonitorEnabled;
}
protected void prepare() {
if(isDebugModeEnabled) {
final StateMachineLogger logger = new StateMachineLogger(this);
addStartListener(new StartListener() {
@Override
public void started(StartEvent event) {
logger.startLogging();
}
});
addTerminateListener(new TerminateListener() {
@Override
public void terminated(TerminateEvent event) {
logger.stopLogging();
}
});
}
}
private boolean processEvent(E event, C context, StateMachineData originalData,
ActionExecutionService executionService, boolean isDataIsolateEnabled) {
StateMachineData localData = originalData;
ImmutableState fromState = localData.read().currentRawState();
S fromStateId = fromState.getStateId(), toStateId = null;
try {
beforeTransitionBegin(fromStateId, event, context);
fireEvent(new TransitionBeginEventImpl(fromStateId, event, context, getThis()));
if(isDataIsolateEnabled) {
// use local data to isolation transition data write
localData = FSM.newStateMachineData(originalData.read().originalStates());
localData.dump(originalData.read());
}
TransitionResult result = FSM.newResult(false, fromState, null);
StateContext stateContext = FSM.newStateContext(this, localData,
fromState, event, context, result, executionService);
fromState.internalFire(stateContext);
toStateId = result.getTargetState().getStateId();
if(result.isAccepted()) {
executionService.execute();
localData.write().lastState(fromStateId);
localData.write().currentState(toStateId);
if(isDataIsolateEnabled) {
// import local data after transition accepted
originalData.dump(localData.read());
}
fireEvent(new TransitionCompleteEventImpl(fromStateId, toStateId,
event, context, getThis()));
afterTransitionCompleted(fromStateId, getCurrentState(), event, context);
return true;
} else {
fireEvent(new TransitionDeclinedEventImpl(fromStateId, event, context, getThis()));
afterTransitionDeclined(fromStateId, event, context);
}
} catch (Exception e) {
// set state machine in error status first which means state machine cannot process event anymore
// unless this exception has been resolved and state machine status set back to normal again.
setStatus(StateMachineStatus.ERROR);
// wrap any exception into transition exception
lastException = (e instanceof TransitionException) ? (TransitionException) e :
new TransitionException(e, ErrorCodes.FSM_TRANSITION_ERROR,
new Object[]{fromStateId, toStateId, event, context, "UNKNOWN", e.getMessage()});
fireEvent(new TransitionExceptionEventImpl(lastException, fromStateId,
localData.read().currentState(), event, context, getThis()));
afterTransitionCausedException(fromStateId, toStateId, event, context);
} finally {
executionService.reset();
fireEvent(new TransitionEndEventImpl(fromStateId, toStateId, event, context, getThis()));
afterTransitionEnd(fromStateId, getCurrentState(), event, context);
}
return false;
}
private void processEvents() {
if (isIdle()) {
writeLock.lock();
setStatus(StateMachineStatus.BUSY);
try {
Pair eventInfo;
E event;
C context = null;
while ((eventInfo=queuedEvents.poll())!=null) {
// response to cancel operation
if(Thread.interrupted()) {
queuedEvents.clear();
break;
}
event = eventInfo.first();
context = eventInfo.second();
processEvent(event, context, data, executor, isDataIsolateEnabled);
}
ImmutableState rawState = data.read().currentRawState();
if(isAutoTerminateEnabled && rawState.isRootState() && rawState.isFinalState()) {
terminate(context);
}
} finally {
if(getStatus()==StateMachineStatus.BUSY)
setStatus(StateMachineStatus.IDLE);
writeLock.unlock();
}
}
}
private void internalFire(E event, C context, boolean insertAtFirst) {
if(getStatus()==StateMachineStatus.INITIALIZED) {
if(isAutoStartEnabled) {
start(context);
} else {
throw new IllegalStateException("The state machine is not running.");
}
}
if(getStatus()==StateMachineStatus.TERMINATED) {
throw new IllegalStateException("The state machine is already terminated.");
}
if(getStatus()==StateMachineStatus.ERROR) {
throw new IllegalStateException("The state machine is corruptted.");
}
if(insertAtFirst) {
queuedEvents.addFirst(new Pair(event, context));
} else {
queuedEvents.addLast(new Pair(event, context));
}
processEvents();
}
private boolean isEntryPoint() {
return StateMachineContext.currentInstance()==null;
}
private void fire(E event, C context, boolean insertAtFirst) {
boolean isEntryPoint = isEntryPoint();
if(isEntryPoint) {
StateMachineContext.set(getThis());
} else if(isDelegatorModeEnabled && StateMachineContext.currentInstance()!=this) {
T currentInstance = StateMachineContext.currentInstance();
currentInstance.fire(event, context);
return;
}
try {
if(StateMachineContext.isTestEvent()) {
internalTest(event, context);
} else {
internalFire(event, context, insertAtFirst);
}
} finally {
if(isEntryPoint) {
StateMachineContext.set(null);
}
}
}
@Override
public void fire(E event, C context) {
fire(event, context, false);
}
public void untypedFire(Object event, Object context) {
fire(typeOfEvent().cast(event), typeOfContext().cast(context));
}
@Override
public void fireImmediate(E event, C context) {
fire(event, context, true);
}
@Override
public void fire(E event) {
fire(event, null);
}
@Override
public void fireImmediate(E event) {
fireImmediate(event, null);
}
/**
* Clean all queued events
*/
protected void cleanQueuedEvents() {
queuedEvents.clear();
}
private ActionExecutionService getDummyExecutor() {
ActionExecutionService dummyExecutor = SquirrelProvider.getInstance().newInstance(
new TypeReference>(){});
dummyExecutor.setDummyExecution(true);
return dummyExecutor;
}
private S internalTest(E event, C context) {
checkState(status!=StateMachineStatus.ERROR && status!=StateMachineStatus.TERMINATED,
"Cannot test state machine under "+status+" status.");
S testResult = null;
queuedTestEvents.add(new Pair(event, context));
if(!isProcessingTestEvent) {
isProcessingTestEvent = true;
@SuppressWarnings("unchecked")
StateMachineData cloneData = (StateMachineData)dumpSavedData();
ActionExecutionService dummyExecutor = getDummyExecutor();
if(getStatus()==StateMachineStatus.INITIALIZED) {
if(isAutoStartEnabled) {
internalStart(context, cloneData, dummyExecutor);
} else {
throw new IllegalStateException("The state machine is not running.");
}
}
try {
Pair eventInfo = null;
while ((eventInfo=queuedTestEvents.poll())!=null) {
E testEvent = eventInfo.first();
C testContext = eventInfo.second();
processEvent(testEvent, testContext, cloneData, dummyExecutor, false);
}
testResult = resolveState(cloneData.read().currentState(), cloneData);
} finally {
isProcessingTestEvent = false;
}
}
return testResult;
}
@Override
public S test(E event, C context) {
boolean isEntryPoint = isEntryPoint();
if(isEntryPoint) {
StateMachineContext.set(getThis(), true);
}
try {
return internalTest(event, context);
} finally {
if(isEntryPoint) {
StateMachineContext.set(null);
}
}
}
@Override
public S test(E event) {
return test(event, null);
}
@Override
public boolean canAccept(E event) {
ImmutableState testRawState = getCurrentRawState();
if(testRawState==null) {
if(isAutoStartEnabled) {
testRawState = getInitialRawState();
} else {
return false;
}
}
return testRawState.getAcceptableEvents().contains(event);
}
protected boolean isIdle() {
return getStatus()!=StateMachineStatus.BUSY;
}
protected void afterTransitionCausedException(S fromState, S toState, E event, C context) {
if(getLastException().getTargetException()!=null)
logger.error("Transition caused exception", getLastException().getTargetException());
throw getLastException();
}
protected void beforeTransitionBegin(S fromState, E event, C context) {
}
protected void afterTransitionCompleted(S fromState, S toState, E event, C context) {
}
protected void afterTransitionEnd(S fromState, S toState, E event, C context) {
}
protected void afterTransitionDeclined(S fromState, E event, C context) {
}
protected void beforeActionInvoked(S fromState, S toState, E event, C context) {
}
protected void afterActionInvoked(S fromState, S toState, E event, C context) {
}
private ImmutableState resolveRawState(ImmutableState rawState) {
ImmutableState resolvedRawState = rawState;
if(resolvedRawState instanceof ImmutableLinkedState) {
@SuppressWarnings("unchecked")
T linkedStateMachine = (T) ((ImmutableLinkedState)rawState).
getLinkedStateMachine(getThis());
resolvedRawState = linkedStateMachine.getCurrentRawState();
}
return resolvedRawState;
}
@Override
public ImmutableState getCurrentRawState() {
readLock.lock();
try {
ImmutableState rawState = data.read().currentRawState();
return resolveRawState(rawState);
} finally {
readLock.unlock();
}
}
@Override
public ImmutableState getLastRawState() {
readLock.lock();
try {
ImmutableState lastRawState = data.read().lastRawState();
return resolveRawState(lastRawState);
} finally {
readLock.unlock();
}
}
@Override
public ImmutableState getInitialRawState() {
return getRawStateFrom(getInitialState());
}
@Override
public ImmutableState getRawStateFrom(S stateId) {
readLock.lock();
try {
return data.read().rawStateFrom(stateId);
} finally {
readLock.unlock();
}
}
@Override
public Collection> getAllRawStates() {
readLock.lock();
try {
return data.read().rawStates();
} finally {
readLock.unlock();
}
}
@Override
public Collection getAllStates() {
readLock.lock();
try {
return data.read().states();
} finally {
readLock.unlock();
}
}
private S resolveState(S state, StateMachineData localData) {
S resolvedState = state;
ImmutableState rawState = localData.read().rawStateFrom(resolvedState);
if(rawState instanceof ImmutableLinkedState) {
ImmutableLinkedState linkedRawState = (ImmutableLinkedState)rawState;
resolvedState = linkedRawState.getLinkedStateMachine(getThis()).getCurrentState();
}
return resolvedState;
}
@Override
public S getCurrentState() {
readLock.lock();
try {
return resolveState(data.read().currentState(), data);
} finally {
readLock.unlock();
}
}
@Override
public S getLastState() {
readLock.lock();
try {
return resolveState(data.read().lastState(), data);
} finally {
readLock.unlock();
}
}
@Override
public S getInitialState() {
readLock.lock();
try {
return data.read().initialState();
} finally {
readLock.unlock();
}
}
private void entryAll(ImmutableState origin, StateContext stateContext) {
Stack> stack = new Stack>();
ImmutableState state = origin;
while (state != null) {
stack.push(state);
state = state.getParentState();
}
while (stack.size() > 0) {
state = stack.pop();
state.entry(stateContext);
}
}
@Override
public synchronized void start(C context) {
if(isStarted()) {
return;
}
setStatus(StateMachineStatus.BUSY);
internalStart(context, data, executor);
setStatus(StateMachineStatus.IDLE);
processEvents();
}
private void internalStart(C context, StateMachineData localData,
ActionExecutionService executionService) {
ImmutableState initialRawState = localData.read().initialRawState();
StateContext stateContext = FSM.newStateContext(
this, localData, initialRawState, getStartEvent(), context, null, executionService);
entryAll(initialRawState, stateContext);
ImmutableState historyState = initialRawState.enterByHistory(stateContext);
executionService.execute();
localData.write().currentState(historyState.getStateId());
localData.write().startContext(context);
fireEvent(new StartEventImpl(getThis()));
}
@Override
public void start() {
start(null);
}
@Override
public boolean isStarted() {
return getStatus()==StateMachineStatus.IDLE || getStatus()==StateMachineStatus.BUSY;
}
@Override
public boolean isTerminated() {
return getStatus()==StateMachineStatus.TERMINATED;
}
@Override
public boolean isError() {
return getStatus()==StateMachineStatus.ERROR;
}
@Override
public StateMachineStatus getStatus() {
return status;
}
protected void setStatus(StateMachineStatus status) {
this.status = status;
}
@Override
public S getLastActiveChildStateOf(S parentStateId) {
readLock.lock();
try {
return data.read().lastActiveChildStateOf(parentStateId);
} finally {
readLock.unlock();
}
}
@Override
public List getSubStatesOn(S parentStateId) {
readLock.lock();
try {
return data.read().subStatesOn(parentStateId);
} finally {
readLock.unlock();
}
}
@Override
public synchronized void terminate(C context) {
if(isTerminated()) {
return;
}
StateContext stateContext = FSM.newStateContext(
this, data, data.read().currentRawState(), getTerminateEvent(),
context, null, executor);
exitAll(data.read().currentRawState(), stateContext);
executor.execute();
setStatus(StateMachineStatus.TERMINATED);
fireEvent(new TerminateEventImpl(getThis()));
}
@Override
public void terminate() {
terminate(null);
}
private void exitAll(ImmutableState current, StateContext stateContext) {
ImmutableState state = current;
while (state != null) {
state.exit(stateContext);
state = state.getParentState();
}
}
@SuppressWarnings("unchecked")
@Override
public T getThis() {
return (T)this;
}
@Override
public void accept(Visitor visitor) {
visitor.visitOnEntry(this);
for(ImmutableState state : getAllRawStates()) {
if(state.getParentState()==null)
state.accept(visitor);
}
visitor.visitOnExit(this);
}
void setTypeOfStateMachine(Class extends T> stateMachineType) {
data.write().typeOfStateMachine(stateMachineType);
}
void setTypeOfState(Class stateType) {
data.write().typeOfState(stateType);
}
void setTypeOfEvent(Class eventType) {
data.write().typeOfEvent(eventType);
}
void setTypeOfContext(Class contextType) {
data.write().typeOfContext(contextType);
}
void setScriptManager(MvelScriptManager scriptManager) {
checkState(this.scriptManager==null);
this.scriptManager = scriptManager;
}
void setStartEvent(E startEvent) {
checkState(this.startEvent==null);
this.startEvent=startEvent;
}
E getStartEvent() {
return startEvent;
}
void setTerminateEvent(E terminateEvent) {
checkState(this.terminateEvent==null);
this.terminateEvent=terminateEvent;
}
E getTerminateEvent() {
return terminateEvent;
}
void setFinishEvent(E finishEvent) {
checkState(this.finishEvent==null);
this.finishEvent=finishEvent;
}
E getFinishEvent() {
return finishEvent;
}
void setExtraParamTypes(Class>[] extraParamTypes) {
checkState(this.extraParamTypes==null);
this.extraParamTypes = extraParamTypes;
}
@Override
public StateMachineData.Reader dumpSavedData() {
readLock.lock();
try {
StateMachineData savedData =
FSM.newStateMachineData(data.read().originalStates());
savedData.dump(data.read());
// process linked state if any
saveLinkedStateData(data.read(), savedData.write());
return savedData.read();
} finally {
readLock.unlock();
}
}
private void saveLinkedStateData(StateMachineData.Reader src, StateMachineData.Writer target) {
dumpLinkedStateFor(src.currentRawState(), target);
// dumpLinkedStateFor(src.lastRawState(), target);
// TODO-hhe: dump linked state info for last active child state
// TODO-hhe: dump linked state info for parallel state
}
private void dumpLinkedStateFor(ImmutableState rawState, StateMachineData.Writer target) {
if(rawState!=null && rawState instanceof ImmutableLinkedState) {
ImmutableLinkedState linkedRawState = (ImmutableLinkedState)rawState;
StateMachineData.Reader extends StateMachine, S, E, C>, S, E, C> linkStateData =
linkedRawState.getLinkedStateMachine(getThis()).dumpSavedData();
target.linkedStateDataOn(rawState.getStateId(), linkStateData);
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public boolean loadSavedData(StateMachineData.Reader savedData) {
Preconditions.checkNotNull(savedData, "Saved data cannot be null");
if(writeLock.tryLock()) {
try {
data.dump(savedData);
// process linked state if any
for(S linkedState : savedData.linkedStates()) {
StateMachineData.Reader linkedStateData = savedData.linkedStateDataOf(linkedState);
ImmutableState rawState = data.read().rawStateFrom(linkedState);
if(linkedStateData!=null && rawState instanceof ImmutableLinkedState) {
ImmutableLinkedState linkedRawState = (ImmutableLinkedState)rawState;
linkedRawState.getLinkedStateMachine(getThis()).loadSavedData(linkedStateData);
}
}
setStatus(StateMachineStatus.IDLE);
return true;
} finally {
writeLock.unlock();
}
}
return false;
}
@Override
public boolean isContextSensitive() {
return true;
}
@Override
public Class typeOfContext() {
return data.read().typeOfContext();
}
@Override
public Class typeOfEvent() {
return data.read().typeOfEvent();
}
@Override
public Class typeOfState() {
return data.read().typeOfState();
}
@Override
public TransitionException getLastException() {
return lastException;
}
protected void setLastException(TransitionException lastException) {
this.lastException = lastException;
}
private interface DeclarativeListener {
Object getListenTarget();
}
private Object newListenerMethodProxy(final Object listenTarget,
final Method listenerMethod, final Class> listenerInterface, final String condition) {
final String listenerMethodName = ReflectUtils.getStatic(
ReflectUtils.getField(listenerInterface, "METHOD_NAME")).toString();
AsyncExecute asyncAnnotation = ReflectUtils.getAnnotation(listenTarget.getClass(), AsyncExecute.class);
if(asyncAnnotation==null) {
asyncAnnotation = listenerMethod.getAnnotation(AsyncExecute.class);
}
final boolean isAsync = asyncAnnotation!=null;
final long timeout = asyncAnnotation!=null ? asyncAnnotation.timeout() : -1;
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().equals("getListenTarget")) {
return listenTarget;
} else if(method.getName().equals(listenerMethodName)) {
if(args[0] instanceof TransitionEvent) {
@SuppressWarnings("unchecked")
TransitionEvent event = (TransitionEvent)args[0];
return invokeTransitionListenerMethod(listenTarget, listenerMethod, condition, event);
} else if(args[0] instanceof ActionEvent) {
@SuppressWarnings("unchecked")
ActionEvent event = (ActionEvent)args[0];
return invokeActionListenerMethod(listenTarget, listenerMethod, condition, event);
} else if(args[0] instanceof StartEvent || args[0] instanceof TerminateEvent) {
@SuppressWarnings("unchecked")
StateMachineEvent event = (StateMachineEvent)args[0];
return invokeStateMachineListenerMethod(listenTarget, listenerMethod, condition, event);
} else {
throw new IllegalArgumentException("Unable to recognize argument type "+args[0].getClass().getName()+".");
}
} else if(method.getName().equals("equals")) {
return super.equals(args[0]);
} else if(method.getName().equals("hashCode")) {
return super.hashCode();
} else if(method.getName().equals("toString")) {
return super.toString();
} else if(isAsync && method.getName().equals("timeout")) {
return timeout;
}
throw new UnsupportedOperationException("Cannot invoke method "+method.getName()+".");
}
};
Class>[] implementedInterfaces = isAsync ?
new Class>[]{listenerInterface, DeclarativeListener.class, AsyncEventListener.class} :
new Class>[]{listenerInterface, DeclarativeListener.class};
Object proxyListener = Proxy.newProxyInstance(StateMachine.class.getClassLoader(),
implementedInterfaces, invocationHandler);
return proxyListener;
}
private Object invokeStateMachineListenerMethod(final Object listenTarget,
final Method listenerMethod, final String condition,
final StateMachineEvent event) {
Class>[] parameterTypes = listenerMethod.getParameterTypes();
final Map variables = Maps.newHashMap();
variables.put(MvelScriptManager.VAR_STATE_MACHINE, event.getStateMachine());
boolean isSatisfied = true;
if(condition!=null && condition.length()>0) {
isSatisfied = scriptManager.evalBoolean(condition, variables);
}
if(!isSatisfied) return null;
if(parameterTypes.length == 0) {
return ReflectUtils.invoke(listenerMethod, listenTarget);
}
// parameter values infer
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy