Support history pseudostates
- Fixes #50 bug which were revealed by trying to use history state concepts. - Fixes #38 for history features. - Base functionality for deep and shallow histories. - Some housekeeping and polish before doing more system wide spring cleaning.
This commit is contained in:
@@ -35,6 +35,7 @@ import org.springframework.statemachine.region.Region;
|
||||
import org.springframework.statemachine.state.ChoicePseudoState;
|
||||
import org.springframework.statemachine.state.DefaultPseudoState;
|
||||
import org.springframework.statemachine.state.EnumState;
|
||||
import org.springframework.statemachine.state.HistoryPseudoState;
|
||||
import org.springframework.statemachine.state.PseudoState;
|
||||
import org.springframework.statemachine.state.PseudoStateKind;
|
||||
import org.springframework.statemachine.state.RegionState;
|
||||
@@ -148,7 +149,8 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
|
||||
for (MachineStackItem<S, E> si : regionStack) {
|
||||
regions.add(si.machine);
|
||||
}
|
||||
RegionState<S, E> rstate = new RegionState<S, E>(null, regions, null, null, null, new DefaultPseudoState(PseudoStateKind.INITIAL));
|
||||
RegionState<S, E> rstate = new RegionState<S, E>(null, regions, null, null, null,
|
||||
new DefaultPseudoState<S, E>(PseudoStateKind.INITIAL));
|
||||
Collection<State<S, E>> states = new ArrayList<State<S, E>>();
|
||||
states.add(rstate);
|
||||
EnumStateMachine<S, E> m = new EnumStateMachine<S, E>(states, new ArrayList<Transition<S, E>>(), rstate,
|
||||
@@ -277,6 +279,7 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
|
||||
StateMachineTransitions<S, E> stateMachineTransitions) {
|
||||
State<S, E> state = null;
|
||||
State<S, E> initialState = null;
|
||||
PseudoState<S, E> historyState = null;
|
||||
Action<S, E> initialAction = null;
|
||||
Collection<State<S, E>> states = new ArrayList<State<S,E>>();
|
||||
|
||||
@@ -287,7 +290,7 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
|
||||
StateMachine<S, E> stateMachine = machineMap.get(stateData.getState());
|
||||
if (stateMachine != null) {
|
||||
state = new StateMachineState<S, E>(stateData.getState(), stateMachine, stateData.getDeferred(),
|
||||
stateData.getEntryActions(), stateData.getExitActions(), new DefaultPseudoState(
|
||||
stateData.getEntryActions(), stateData.getExitActions(), new DefaultPseudoState<S, E>(
|
||||
PseudoStateKind.INITIAL));
|
||||
// TODO: below if/else doesn't feel right
|
||||
if (stateDatas.size() > 1 && stateData.isInitial()) {
|
||||
@@ -304,6 +307,12 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
|
||||
pseudoState = new DefaultPseudoState<S, E>(PseudoStateKind.INITIAL);
|
||||
} else if (stateData.isEnd()) {
|
||||
pseudoState = new DefaultPseudoState<S, E>(PseudoStateKind.END);
|
||||
} else if (stateData.getPseudoStateKind() == PseudoStateKind.HISTORY_SHALLOW) {
|
||||
pseudoState = new HistoryPseudoState<S, E>(PseudoStateKind.HISTORY_SHALLOW);
|
||||
historyState = pseudoState;
|
||||
} else if (stateData.getPseudoStateKind() == PseudoStateKind.HISTORY_DEEP) {
|
||||
pseudoState = new HistoryPseudoState<S, E>(PseudoStateKind.HISTORY_DEEP);
|
||||
historyState = pseudoState;
|
||||
} else if (stateData.getPseudoStateKind() == PseudoStateKind.CHOICE) {
|
||||
continue;
|
||||
}
|
||||
@@ -377,6 +386,7 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
|
||||
|
||||
EnumStateMachine<S, E> machine = new EnumStateMachine<S, E>(states, transitions, initialState,
|
||||
initialTransition, null, defaultExtendedState);
|
||||
machine.setHistoryState(historyState);
|
||||
if (contextEvents != null) {
|
||||
machine.setContextEventsEnabled(contextEvents);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@ public class DefaultStateConfigurer<S, E>
|
||||
|
||||
private S end;
|
||||
|
||||
private S history;
|
||||
|
||||
private History historyType;
|
||||
|
||||
private final Collection<S> choices = new ArrayList<S>();
|
||||
|
||||
@Override
|
||||
@@ -73,6 +77,13 @@ public class DefaultStateConfigurer<S, E>
|
||||
if (choices.contains(s.getState())) {
|
||||
s.setPseudoStateKind(PseudoStateKind.CHOICE);
|
||||
}
|
||||
if (s.getState() == history) {
|
||||
if (History.SHALLOW == historyType) {
|
||||
s.setPseudoStateKind(PseudoStateKind.HISTORY_SHALLOW);
|
||||
} else if (History.DEEP == historyType) {
|
||||
s.setPseudoStateKind(PseudoStateKind.HISTORY_DEEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.addStateData(stateDatas);
|
||||
}
|
||||
@@ -152,6 +163,14 @@ public class DefaultStateConfigurer<S, E>
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StateConfigurer<S, E> history(S history, History type) {
|
||||
this.history = history;
|
||||
this.historyType = type;
|
||||
state(history);
|
||||
return this;
|
||||
}
|
||||
|
||||
private void addIncomplete(Object parent, S state, Collection<E> deferred,
|
||||
Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions) {
|
||||
incomplete.add(new StateData<S, E>(parent, region, state, deferred, entryActions, exitActions));
|
||||
|
||||
@@ -123,4 +123,31 @@ public interface StateConfigurer<S, E> extends
|
||||
*/
|
||||
StateConfigurer<S, E> choice(S choice);
|
||||
|
||||
/**
|
||||
* Specify a state {@code S} to be history pseudo state.
|
||||
*
|
||||
* @param history the history pseudo state
|
||||
* @param type the history pseudo state type
|
||||
* @return configurer for chaining
|
||||
*/
|
||||
StateConfigurer<S, E> history(S history, History type);
|
||||
|
||||
/**
|
||||
* Enumeration of a possible history pseudostate type.
|
||||
*/
|
||||
public enum History {
|
||||
|
||||
/**
|
||||
* Shallow history is a pseudo state representing the most
|
||||
* recent substate of a submachine.
|
||||
*/
|
||||
SHALLOW,
|
||||
|
||||
/**
|
||||
* Deep history is a shallow history recursively reactivating
|
||||
* the substates of the most recent substate.
|
||||
*/
|
||||
DEEP
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.statemachine.state;
|
||||
|
||||
import org.springframework.statemachine.StateContext;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* History implementation of a {@link PseudoState}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*
|
||||
* @param <S> the type of state
|
||||
* @param <E> the type of event
|
||||
*/
|
||||
public class HistoryPseudoState<S, E> extends AbstractPseudoState<S, E> {
|
||||
|
||||
private State<S, E> state;
|
||||
|
||||
/**
|
||||
* Instantiates a new history pseudo state.
|
||||
*
|
||||
* @param kind the kind
|
||||
*/
|
||||
public HistoryPseudoState(PseudoStateKind kind) {
|
||||
super(kind);
|
||||
Assert.isTrue(PseudoStateKind.HISTORY_SHALLOW == kind || PseudoStateKind.HISTORY_DEEP == kind,
|
||||
"Pseudo state must be either shallow or deep");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current recorded state.
|
||||
*
|
||||
* @param state the state
|
||||
*/
|
||||
public void setState(State<S, E> state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current recorded state.
|
||||
*
|
||||
* @return the current recorded state.
|
||||
*/
|
||||
public State<S, E> getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State<S, E> entry(E event, StateContext<S, E> context) {
|
||||
return state;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
package org.springframework.statemachine.state;
|
||||
|
||||
/**
|
||||
* Defines enumeration of a {@link PseudoState} kind. This is uses within a
|
||||
* Defines enumeration of a {@link PseudoState} kind. This is used within a
|
||||
* transitive states indicating its kind.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
@@ -31,6 +31,12 @@ public enum PseudoStateKind {
|
||||
END,
|
||||
|
||||
/** Choice kind */
|
||||
CHOICE
|
||||
CHOICE,
|
||||
|
||||
/** History deep kind */
|
||||
HISTORY_DEEP,
|
||||
|
||||
/** History shallow kind */
|
||||
HISTORY_SHALLOW
|
||||
|
||||
}
|
||||
|
||||
@@ -137,6 +137,8 @@ public class StateMachineState<S, E> extends AbstractState<S, E> {
|
||||
// enable default transition and state
|
||||
if (getSubmachine().getState() != null && context.getTransition().getSource().getId() != getSubmachine().getState().getId()) {
|
||||
getSubmachine().stop();
|
||||
} else if (!isSubstate(context.getTransition().getTarget(), context.getTransition().getSource())) {
|
||||
getSubmachine().stop();
|
||||
}
|
||||
Collection<? extends Action<S, E>> actions = getExitActions();
|
||||
if (actions != null && !isLocal(context)) {
|
||||
@@ -178,6 +180,12 @@ public class StateMachineState<S, E> extends AbstractState<S, E> {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSubstate(State<S, E> left, State<S, E> right) {
|
||||
Collection<State<S, E>> c = left.getStates();
|
||||
c.remove(left);
|
||||
return c.contains(right);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "StateMachineState [getIds()=" + getIds() + ", toString()=" + super.toString() + ", getClass()="
|
||||
|
||||
@@ -51,6 +51,8 @@ import org.springframework.statemachine.processor.StateMachineOnTransitionHandle
|
||||
import org.springframework.statemachine.processor.StateMachineRuntime;
|
||||
import org.springframework.statemachine.region.Region;
|
||||
import org.springframework.statemachine.state.AbstractState;
|
||||
import org.springframework.statemachine.state.HistoryPseudoState;
|
||||
import org.springframework.statemachine.state.PseudoState;
|
||||
import org.springframework.statemachine.state.PseudoStateKind;
|
||||
import org.springframework.statemachine.state.State;
|
||||
import org.springframework.statemachine.transition.Transition;
|
||||
@@ -94,6 +96,8 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
|
||||
|
||||
private volatile State<S,E> currentState;
|
||||
|
||||
private volatile PseudoState<S, E> history;
|
||||
|
||||
private volatile Runnable task;
|
||||
|
||||
private final Map<String, StateMachineOnTransitionHandler<S, E>> handlers = new HashMap<String, StateMachineOnTransitionHandler<S,E>>();
|
||||
@@ -166,6 +170,10 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
|
||||
return extendedState;
|
||||
}
|
||||
|
||||
public void setHistoryState(PseudoState<S, E> history) {
|
||||
this.history = history;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sendEvent(Message<E> event) {
|
||||
if (isComplete() || !isRunning()) {
|
||||
@@ -205,12 +213,20 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
|
||||
region.addStateListener(new StateMachineListenerRelay());
|
||||
}
|
||||
}
|
||||
if (state.getPseudoState() != null
|
||||
&& (state.getPseudoState().getKind() == PseudoStateKind.HISTORY_DEEP || state.getPseudoState()
|
||||
.getKind() == PseudoStateKind.HISTORY_DEEP)) {
|
||||
history = state.getPseudoState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() {
|
||||
super.doStart();
|
||||
// if state is set assume nothing to do
|
||||
if (currentState != null) {
|
||||
return;
|
||||
}
|
||||
registerTriggerListener();
|
||||
switchToState(initialState, initialEvent, null, this);
|
||||
// TODO: for now execute outside of switchToState
|
||||
@@ -224,7 +240,6 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
|
||||
|
||||
@Override
|
||||
protected void doStop() {
|
||||
super.doStop();
|
||||
notifyStateMachineStopped(this);
|
||||
currentState = null;
|
||||
}
|
||||
@@ -328,6 +343,20 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
|
||||
messageHeaders, extendedState, transition, stateMachine);
|
||||
State<S, E> entry = state.getPseudoState().entry(event.getPayload(), stateContext);
|
||||
setCurrentState(entry, event, transition, true, stateMachine);
|
||||
} else if (state.getPseudoState() != null && state.getPseudoState().getKind() == PseudoStateKind.HISTORY_SHALLOW) {
|
||||
MessageHeaders messageHeaders = event != null ? event.getHeaders() : new MessageHeaders(
|
||||
new HashMap<String, Object>());
|
||||
StateContext<S, E> stateContext = new DefaultStateContext<S, E>(event != null ? event.getPayload() : null,
|
||||
messageHeaders, extendedState, transition, stateMachine);
|
||||
State<S, E> entry = state.getPseudoState().entry(event.getPayload(), stateContext);
|
||||
setCurrentState(entry, event, transition, true, stateMachine);
|
||||
} else if (state.getPseudoState() != null && state.getPseudoState().getKind() == PseudoStateKind.HISTORY_DEEP) {
|
||||
MessageHeaders messageHeaders = event != null ? event.getHeaders() : new MessageHeaders(
|
||||
new HashMap<String, Object>());
|
||||
StateContext<S, E> stateContext = new DefaultStateContext<S, E>(event != null ? event.getPayload() : null,
|
||||
messageHeaders, extendedState, transition, stateMachine);
|
||||
State<S, E> entry = state.getPseudoState().entry(event.getPayload(), stateContext);
|
||||
setCurrentState(entry, event, transition, true, stateMachine);
|
||||
} else {
|
||||
setCurrentState(state, event, transition, true, stateMachine);
|
||||
}
|
||||
@@ -354,7 +383,7 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setCurrentState(State<S, E> state, Message<E> event, Transition<S, E> transition, boolean exit, StateMachine<S, E> stateMachine) {
|
||||
void setCurrentState(State<S, E> state, Message<E> event, Transition<S, E> transition, boolean exit, StateMachine<S, E> stateMachine) {
|
||||
State<S, E> findDeep = findDeepParent(state);
|
||||
boolean isTargetSubOf = false;
|
||||
if (transition != null) {
|
||||
@@ -370,9 +399,12 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
|
||||
}
|
||||
State<S, E> notifyFrom = currentState;
|
||||
currentState = state;
|
||||
if (!isRunning()) {
|
||||
start();
|
||||
}
|
||||
entryToState(state, event, transition, stateMachine);
|
||||
notifyStateChanged(notifyFrom, state);
|
||||
} else if (currentState != null && currentState.isSubmachineState()) {
|
||||
} else if (currentState != null) {
|
||||
if (findDeep != null) {
|
||||
if (exit) {
|
||||
exitCurrentState(state, event, transition, stateMachine);
|
||||
@@ -396,7 +428,13 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
|
||||
((AbstractStateMachine<S, E>)submachine).setCurrentState(state, event, transition, false, stateMachine);
|
||||
}
|
||||
}
|
||||
|
||||
if (history != null) {
|
||||
if (history.getKind() == PseudoStateKind.HISTORY_SHALLOW) {
|
||||
((HistoryPseudoState<S, E>)history).setState(findDeep);
|
||||
} else if (history.getKind() == PseudoStateKind.HISTORY_DEEP){
|
||||
((HistoryPseudoState<S, E>)history).setState(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void exitCurrentState(State<S, E> state, Message<E> event, Transition<S, E> transition, StateMachine<S, E> stateMachine) {
|
||||
@@ -435,6 +473,7 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
|
||||
} else if (!isSubOfSource && !isSubOfTarget && currentState == transition.getSource()) {
|
||||
} else if (!isSubOfSource && !isSubOfTarget && currentState == transition.getTarget()) {
|
||||
} else if (isTargetSubOfOtherState) {
|
||||
} else if (!isSubOfSource && !isSubOfTarget && findDeep == null) {
|
||||
} else if (!isSubOfSource && !isSubOfTarget) {
|
||||
return;
|
||||
}
|
||||
@@ -467,6 +506,7 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
|
||||
if (currentState == transition.getSource() && currentState == transition.getTarget()) {
|
||||
} else if (!isSubOfSource && !isSubOfTarget && currentState == transition.getTarget()) {
|
||||
} else if (isComingFromOtherSubmachine) {
|
||||
} else if (!isSubOfSource && !isSubOfTarget && findDeep2 == null) {
|
||||
} else if (isSubOfSource && !isSubOfTarget && currentState == transition.getTarget()) {
|
||||
return;
|
||||
} else if (!isSubOfSource && !isSubOfTarget) {
|
||||
|
||||
@@ -63,9 +63,9 @@ public abstract class AbstractStateMachineTests {
|
||||
}
|
||||
|
||||
public enum TestStates {
|
||||
SI,S1,S2,S3,S4,SF,
|
||||
SI,S1,S2,S3,S4,SF,SH,
|
||||
S10,S11,S101,S111,S112,S12,S121,S122,S13,
|
||||
S20,S21,S201,S211,
|
||||
S20,S21,S201,S211,S212,
|
||||
S1011,S1012,S2011,S2012,
|
||||
S30,S31,S32,S33
|
||||
}
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.statemachine.state;
|
||||
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.statemachine.AbstractStateMachineTests;
|
||||
import org.springframework.statemachine.EnumStateMachine;
|
||||
import org.springframework.statemachine.StateMachineSystemConstants;
|
||||
import org.springframework.statemachine.config.EnableStateMachine;
|
||||
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
|
||||
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||
import org.springframework.statemachine.config.configurers.StateConfigurer.History;
|
||||
|
||||
public class HistoryStateTests extends AbstractStateMachineTests {
|
||||
|
||||
@Override
|
||||
protected AnnotationConfigApplicationContext buildContext() {
|
||||
return new AnnotationConfigApplicationContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testShallowInSubmachine() {
|
||||
context.register(BaseConfig.class, Config1.class);
|
||||
context.refresh();
|
||||
EnumStateMachine<TestStates,TestEvents> machine =
|
||||
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class);
|
||||
assertThat(machine, notNullValue());
|
||||
machine.start();
|
||||
machine.sendEvent(TestEvents.E1);
|
||||
machine.sendEvent(TestEvents.E2);
|
||||
machine.sendEvent(TestEvents.E3);
|
||||
machine.sendEvent(TestEvents.E4);
|
||||
|
||||
assertThat(machine.getState().getIds(), contains(TestStates.S2, TestStates.S21));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testDeep() {
|
||||
context.register(BaseConfig.class, Config2.class);
|
||||
context.refresh();
|
||||
EnumStateMachine<TestStates,TestEvents> machine =
|
||||
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class);
|
||||
assertThat(machine, notNullValue());
|
||||
machine.start();
|
||||
machine.sendEvent(TestEvents.E1);
|
||||
machine.sendEvent(TestEvents.E2);
|
||||
machine.sendEvent(TestEvents.E3);
|
||||
machine.sendEvent(TestEvents.E4);
|
||||
|
||||
assertThat(machine.getState().getIds(), contains(TestStates.S2, TestStates.S21, TestStates.S212));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testShallow() {
|
||||
context.register(BaseConfig.class, Config3.class);
|
||||
context.refresh();
|
||||
EnumStateMachine<TestStates,TestEvents> machine =
|
||||
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class);
|
||||
assertThat(machine, notNullValue());
|
||||
machine.start();
|
||||
machine.sendEvent(TestEvents.E1);
|
||||
machine.sendEvent(TestEvents.E2);
|
||||
machine.sendEvent(TestEvents.E3);
|
||||
machine.sendEvent(TestEvents.E4);
|
||||
|
||||
assertThat(machine.getState().getIds(), contains(TestStates.S2, TestStates.S21, TestStates.S211));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine
|
||||
static class Config1 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(TestStates.S1)
|
||||
.state(TestStates.S1)
|
||||
.state(TestStates.S2)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent(TestStates.S2)
|
||||
.initial(TestStates.S20)
|
||||
.state(TestStates.S20)
|
||||
.state(TestStates.S21)
|
||||
.history(TestStates.SH, History.SHALLOW);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
|
||||
transitions
|
||||
.withExternal()
|
||||
.source(TestStates.S1)
|
||||
.target(TestStates.S2)
|
||||
.event(TestEvents.E1)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(TestStates.S20)
|
||||
.target(TestStates.S21)
|
||||
.event(TestEvents.E2)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(TestStates.S2)
|
||||
.target(TestStates.S1)
|
||||
.event(TestEvents.E3)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(TestStates.S1)
|
||||
.target(TestStates.SH)
|
||||
.event(TestEvents.E4);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine
|
||||
static class Config2 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(TestStates.S1)
|
||||
.state(TestStates.S1)
|
||||
.state(TestStates.S2)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent(TestStates.S2)
|
||||
.initial(TestStates.S20)
|
||||
.state(TestStates.S20)
|
||||
.state(TestStates.S21)
|
||||
.history(TestStates.SH, History.DEEP)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent(TestStates.S21)
|
||||
.initial(TestStates.S211)
|
||||
.state(TestStates.S211)
|
||||
.state(TestStates.S212);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
|
||||
transitions
|
||||
.withExternal()
|
||||
.source(TestStates.S1)
|
||||
.target(TestStates.S211)
|
||||
.event(TestEvents.E1)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(TestStates.S211)
|
||||
.target(TestStates.S212)
|
||||
.event(TestEvents.E2)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(TestStates.S212)
|
||||
.target(TestStates.S1)
|
||||
.event(TestEvents.E3)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(TestStates.S1)
|
||||
.target(TestStates.SH)
|
||||
.event(TestEvents.E4);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine
|
||||
static class Config3 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(TestStates.S1)
|
||||
.state(TestStates.S1)
|
||||
.state(TestStates.S2)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent(TestStates.S2)
|
||||
.initial(TestStates.S20)
|
||||
.state(TestStates.S20)
|
||||
.state(TestStates.S21)
|
||||
.history(TestStates.SH, History.SHALLOW)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent(TestStates.S21)
|
||||
.initial(TestStates.S211)
|
||||
.state(TestStates.S211)
|
||||
.state(TestStates.S212);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
|
||||
transitions
|
||||
.withExternal()
|
||||
.source(TestStates.S1)
|
||||
.target(TestStates.S211)
|
||||
.event(TestEvents.E1)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(TestStates.S211)
|
||||
.target(TestStates.S212)
|
||||
.event(TestEvents.E2)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(TestStates.S212)
|
||||
.target(TestStates.S1)
|
||||
.event(TestEvents.E3)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(TestStates.S1)
|
||||
.target(TestStates.SH)
|
||||
.event(TestEvents.E4);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,15 +17,25 @@ package org.springframework.statemachine.state;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.task.SyncTaskExecutor;
|
||||
import org.springframework.statemachine.AbstractStateMachineTests;
|
||||
import org.springframework.statemachine.EnumStateMachine;
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
import org.springframework.statemachine.StateMachineSystemConstants;
|
||||
import org.springframework.statemachine.TestUtils;
|
||||
import org.springframework.statemachine.config.EnableStateMachine;
|
||||
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
|
||||
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||
import org.springframework.statemachine.transition.DefaultExternalTransition;
|
||||
import org.springframework.statemachine.transition.Transition;
|
||||
import org.springframework.statemachine.trigger.EventTrigger;
|
||||
@@ -38,6 +48,11 @@ import org.springframework.statemachine.trigger.EventTrigger;
|
||||
*/
|
||||
public class SubmachineStateTests extends AbstractStateMachineTests {
|
||||
|
||||
@Override
|
||||
protected AnnotationConfigApplicationContext buildContext() {
|
||||
return new AnnotationConfigApplicationContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleSubmachineState() {
|
||||
|
||||
@@ -80,8 +95,232 @@ public class SubmachineStateTests extends AbstractStateMachineTests {
|
||||
assertThat(state.isSubmachineState(), is(true));
|
||||
|
||||
assertThat(state.getIds(), contains(TestStates.S4, TestStates.SI));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testFromSimpleToOtherSubstate() {
|
||||
context.register(BaseConfig.class, Config1.class);
|
||||
context.refresh();
|
||||
EnumStateMachine<TestStates,TestEvents> machine =
|
||||
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class);
|
||||
assertThat(machine, notNullValue());
|
||||
machine.start();
|
||||
machine.sendEvent(TestEvents.E1);
|
||||
machine.sendEvent(TestEvents.E2);
|
||||
machine.sendEvent(TestEvents.E3);
|
||||
machine.sendEvent(TestEvents.E4);
|
||||
|
||||
assertThat(machine.getState().getIds(), contains(TestStates.S2, TestStates.S21));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testAllSubmachinesRunningInitialsTakesToDeep() throws Exception {
|
||||
context.register(BaseConfig.class, Config2.class);
|
||||
context.refresh();
|
||||
EnumStateMachine<TestStates,TestEvents> machine =
|
||||
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class);
|
||||
assertThat(machine, notNullValue());
|
||||
machine.start();
|
||||
machine.sendEvent(TestEvents.E1);
|
||||
|
||||
assertThat(machine.isRunning(), is(true));
|
||||
|
||||
State<TestStates, TestEvents> s = machine.getState();
|
||||
StateMachine<TestStates, TestEvents> m = ((StateMachineState<TestStates, TestEvents>) s).getSubmachine();
|
||||
boolean r = TestUtils.readField("running", m);
|
||||
assertThat(r, is(true));
|
||||
|
||||
s = m.getState();
|
||||
m = ((StateMachineState<TestStates, TestEvents>) s).getSubmachine();
|
||||
r = TestUtils.readField("running", m);
|
||||
assertThat(r, is(true));
|
||||
|
||||
assertThat(machine.getState().getIds(), contains(TestStates.S2, TestStates.S20, TestStates.S2011));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testAllSubmachinesRunningInitialsNotTakeToDeep() throws Exception {
|
||||
context.register(BaseConfig.class, Config3.class);
|
||||
context.refresh();
|
||||
EnumStateMachine<TestStates,TestEvents> machine =
|
||||
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class);
|
||||
assertThat(machine, notNullValue());
|
||||
machine.start();
|
||||
machine.sendEvent(TestEvents.E1);
|
||||
|
||||
assertThat(machine.isRunning(), is(true));
|
||||
|
||||
State<TestStates, TestEvents> s = machine.getState();
|
||||
StateMachine<TestStates, TestEvents> m = ((StateMachineState<TestStates, TestEvents>) s).getSubmachine();
|
||||
boolean r = TestUtils.readField("running", m);
|
||||
assertThat(r, is(true));
|
||||
|
||||
s = m.getState();
|
||||
m = ((StateMachineState<TestStates, TestEvents>) s).getSubmachine();
|
||||
r = TestUtils.readField("running", m);
|
||||
assertThat(r, is(true));
|
||||
|
||||
assertThat(machine.getState().getIds(), contains(TestStates.S2, TestStates.S21, TestStates.S212));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testAllSubmachinesStopped() throws Exception {
|
||||
context.register(BaseConfig.class, Config3.class);
|
||||
context.refresh();
|
||||
EnumStateMachine<TestStates,TestEvents> machine =
|
||||
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class);
|
||||
assertThat(machine, notNullValue());
|
||||
machine.start();
|
||||
machine.sendEvent(TestEvents.E1);
|
||||
machine.sendEvent(TestEvents.E2);
|
||||
|
||||
assertThat(machine.isRunning(), is(true));
|
||||
|
||||
State<TestStates, TestEvents> s1 = machine.getState();
|
||||
StateMachine<TestStates, TestEvents> m1 = ((StateMachineState<TestStates, TestEvents>) s1).getSubmachine();
|
||||
|
||||
State<TestStates, TestEvents> s2 = m1.getState();
|
||||
StateMachine<TestStates, TestEvents> m2 = ((StateMachineState<TestStates, TestEvents>) s2).getSubmachine();
|
||||
|
||||
machine.sendEvent(TestEvents.E3);
|
||||
|
||||
boolean r1 = TestUtils.readField("running", m1);
|
||||
assertThat(r1, is(false));
|
||||
boolean r2 = TestUtils.readField("running", m2);
|
||||
assertThat(r2, is(false));
|
||||
|
||||
assertThat(machine.getState().getIds(), contains(TestStates.S1));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine
|
||||
static class Config1 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(TestStates.S1)
|
||||
.state(TestStates.S1)
|
||||
.state(TestStates.S2)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent(TestStates.S2)
|
||||
.initial(TestStates.S20)
|
||||
.state(TestStates.S20)
|
||||
.state(TestStates.S21);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
|
||||
transitions
|
||||
.withExternal()
|
||||
.source(TestStates.S1)
|
||||
.target(TestStates.S2)
|
||||
.event(TestEvents.E1)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(TestStates.S20)
|
||||
.target(TestStates.S21)
|
||||
.event(TestEvents.E2)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(TestStates.S2)
|
||||
.target(TestStates.S1)
|
||||
.event(TestEvents.E3)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(TestStates.S1)
|
||||
.target(TestStates.S21)
|
||||
.event(TestEvents.E4);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine
|
||||
static class Config2 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(TestStates.S1)
|
||||
.state(TestStates.S1)
|
||||
.state(TestStates.S2)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent(TestStates.S2)
|
||||
.initial(TestStates.S20)
|
||||
.state(TestStates.S20)
|
||||
.state(TestStates.S21)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent(TestStates.S20)
|
||||
.initial(TestStates.S2011)
|
||||
.state(TestStates.S2011)
|
||||
.state(TestStates.S2012);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
|
||||
transitions
|
||||
.withExternal()
|
||||
.source(TestStates.S1)
|
||||
.target(TestStates.S2)
|
||||
.event(TestEvents.E1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine
|
||||
static class Config3 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(TestStates.S1)
|
||||
.state(TestStates.S1)
|
||||
.state(TestStates.S2)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent(TestStates.S2)
|
||||
.initial(TestStates.S20)
|
||||
.state(TestStates.S20)
|
||||
.state(TestStates.S21)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent(TestStates.S21)
|
||||
.initial(TestStates.S211)
|
||||
.state(TestStates.S211)
|
||||
.state(TestStates.S212);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
|
||||
transitions
|
||||
.withExternal()
|
||||
.source(TestStates.S1)
|
||||
.target(TestStates.S212)
|
||||
.event(TestEvents.E1)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(TestStates.S211)
|
||||
.target(TestStates.S212)
|
||||
.event(TestEvents.E2)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(TestStates.S212)
|
||||
.target(TestStates.S1)
|
||||
.event(TestEvents.E3);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user