Add hierarchical event processing

- resolves #19
- add base support for handling events in a submachines
This commit is contained in:
Janne Valkealahti
2015-02-22 15:34:10 +00:00
parent e6677f19c5
commit ee2f7b2eb6
16 changed files with 228 additions and 53 deletions

View File

@@ -19,6 +19,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Stack;
@@ -98,7 +99,13 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
// use two stack, first for states and second for machines
Stack<StateMachine<S, E>> machineStack = new Stack<StateMachine<S, E>>();
Stack<StateData<S, E>> stateStack = new Stack<StateData<S, E>>();
for (Node<StateData<S, E>> node : traverser.postOrderTraversal(tree.getRoot())) {
Iterable<Node<StateData<S, E>>> postOrderTraversal = traverser.postOrderTraversal(tree.getRoot());
Iterator<Node<StateData<S, E>>> iterator = postOrderTraversal.iterator();
while (iterator.hasNext()) {
Node<StateData<S, E>> node = iterator.next();
StateData<S, E> stateData = node.getData();
if (stateStack.isEmpty()) {
stateStack.push(stateData);
@@ -114,12 +121,20 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
stateDatas.add(next);
}
stateStack.clear();
Collection<TransitionData<S, E>> transitionsData = null;
if (iterator.hasNext()) {
transitionsData = resolveTransitionData(stateMachineTransitions.getTransitions(), stateDatas);
} else {
transitionsData = resolveTransitionData2(stateMachineTransitions.getTransitions());
}
if (machineStack.isEmpty()) {
machine = buildSimpleMachinexxx(stateMap, stateDatas, stateMachineTransitions, getBeanFactory());
machine = buildSimpleMachine(stateMap, stateDatas, transitionsData, getBeanFactory());
machineStack.push(machine);
} else {
StateMachine<S, E> pop = machineStack.pop();
machine = buildSubMachinexxx(stateMap, pop, stateDatas, stateMachineTransitions, getBeanFactory());
machine = buildSubMachine(stateMap, pop, stateDatas, transitionsData, getBeanFactory());
machineStack.push(machine);
}
stateStack.push(stateData);
@@ -129,9 +144,37 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
return machine;
}
private static <S extends Enum<S>, E extends Enum<E>> StateMachine<S, E> buildSimpleMachinexxx(
private Collection<TransitionData<S, E>> resolveTransitionData(Collection<TransitionData<S, E>> in, Collection<StateData<S, E>> stateDatas) {
ArrayList<TransitionData<S, E>> out = new ArrayList<TransitionData<S,E>>();
Collection<Object> states = new ArrayList<Object>();
for (StateData<S, E> stateData : stateDatas) {
states.add(stateData.getParent());
}
for (TransitionData<S, E> transitionData : in) {
S state = transitionData.getState();
if (state != null && states.contains(state)) {
out.add(transitionData);
}
}
return out;
}
private Collection<TransitionData<S, E>> resolveTransitionData2(Collection<TransitionData<S, E>> in) {
ArrayList<TransitionData<S, E>> out = new ArrayList<TransitionData<S,E>>();
for (TransitionData<S, E> transitionData : in) {
if (transitionData.getState() == null) {
out.add(transitionData);
}
}
return out;
}
private static <S extends Enum<S>, E extends Enum<E>> StateMachine<S, E> buildSimpleMachine(
Map<S, State<S, E>> stateMap, Collection<StateData<S, E>> stateDatas,
StateMachineTransitions<S, E> transitionsData, BeanFactory beanFactory) {
Collection<TransitionData<S, E>> transitionsData, BeanFactory beanFactory) {
State<S, E> initialState = null;
State<S, E> endState = null;
for (StateData<S, E> stateData : stateDatas) {
@@ -153,7 +196,7 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
}
Collection<Transition<S, E>> transitions = new ArrayList<Transition<S, E>>();
for (TransitionData<S, E> transitionData : transitionsData.getTransitions()) {
for (TransitionData<S, E> transitionData : transitionsData) {
S source = transitionData.getSource();
S target = transitionData.getTarget();
E event = transitionData.getEvent();
@@ -178,9 +221,9 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
return machine;
}
private static <S extends Enum<S>, E extends Enum<E>> StateMachine<S, E> buildSubMachinexxx(
private static <S extends Enum<S>, E extends Enum<E>> StateMachine<S, E> buildSubMachine(
Map<S, State<S, E>> stateMap, StateMachine<S, E> submachine, Collection<StateData<S, E>> stateDatas,
StateMachineTransitions<S, E> transitionsData, BeanFactory beanFactory) {
Collection<TransitionData<S, E>> transitionsData, BeanFactory beanFactory) {
Collection<State<S, E>> states = new ArrayList<State<S,E>>();
State<S, E> state = null;
for (StateData<S, E> stateData : stateDatas) {
@@ -192,7 +235,7 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
}
Collection<Transition<S, E>> transitions = new ArrayList<Transition<S, E>>();
for (TransitionData<S, E> transitionData : transitionsData.getTransitions()) {
for (TransitionData<S, E> transitionData : transitionsData) {
S source = transitionData.getSource();
S target = transitionData.getTarget();
E event = transitionData.getEvent();

View File

@@ -61,19 +61,19 @@ public class StateMachineTransitionBuilder<S, E>
public ExternalTransitionConfigurer<S, E> withExternal() throws Exception {
return apply(new DefaultExternalTransitionConfigurer<S, E>());
}
@Override
public InternalTransitionConfigurer<S, E> withInternal() throws Exception {
return apply(new DefaultInternalTransitionConfigurer<S, E>());
}
@Override
public LocalTransitionConfigurer<S, E> withLocal() throws Exception {
return apply(new DefaultLocalTransitionConfigurer<S, E>());
}
public void add(S source, S target, E event, Collection<Action<S, E>> actions, Guard<S, E> guard, TransitionKind kind) {
transitionData.add(new TransitionData<S, E>(source, target, event, actions, guard, kind));
public void add(S source, S target, S state, E event, Collection<Action<S, E>> actions, Guard<S, E> guard, TransitionKind kind) {
transitionData.add(new TransitionData<S, E>(source, target, state, event, actions, guard, kind));
}
}

View File

@@ -36,13 +36,15 @@ public class StateMachineTransitions<S, E> {
public static class TransitionData<S, E> {
S source;
S target;
S state;
E event;
Collection<Action<S, E>> actions;
Guard<S, E> guard;
TransitionKind kind;
public TransitionData(S source, S target, E event, Collection<Action<S, E>> actions, Guard<S, E> guard, TransitionKind kind) {
public TransitionData(S source, S target, S state, E event, Collection<Action<S, E>> actions, Guard<S, E> guard, TransitionKind kind) {
this.source = source;
this.target = target;
this.state = state;
this.event = event;
this.actions = actions;
this.guard = guard;
@@ -54,6 +56,9 @@ public class StateMachineTransitions<S, E> {
public S getTarget() {
return target;
}
public S getState() {
return state;
}
public E getEvent() {
return event;
}

View File

@@ -32,7 +32,7 @@ import org.springframework.statemachine.transition.TransitionKind;
/**
* Default implementation of a {@link ExternalTransitionConfigurer}.
*
*
* @author Janne Valkealahti
*
* @param <S> the type of state
@@ -46,15 +46,17 @@ public class DefaultExternalTransitionConfigurer<S, E>
private S target;
private S state;
private E event;
private Collection<Action<S, E>> actions = new ArrayList<Action<S, E>>();
private Guard<S, E> guard;
@Override
public void configure(StateMachineTransitionBuilder<S, E> builder) throws Exception {
builder.add(source, target, event, actions, guard, TransitionKind.EXTERNAL);
builder.add(source, target, state, event, actions, guard, TransitionKind.EXTERNAL);
}
@Override
@@ -69,6 +71,12 @@ public class DefaultExternalTransitionConfigurer<S, E>
return this;
}
@Override
public ExternalTransitionConfigurer<S, E> state(S state) {
this.state = state;
return this;
}
@Override
public ExternalTransitionConfigurer<S, E> event(E event) {
this.event = event;
@@ -80,13 +88,13 @@ public class DefaultExternalTransitionConfigurer<S, E>
actions.add(action);
return this;
}
@Override
public ExternalTransitionConfigurer<S, E> guard(Guard<S, E> guard) {
this.guard = guard;
return this;
}
@Override
public ExternalTransitionConfigurer<S, E> guardExpression(String expression) {
SpelExpressionParser parser = new SpelExpressionParser(

View File

@@ -32,7 +32,7 @@ import org.springframework.statemachine.transition.TransitionKind;
/**
* Default implementation of a {@link InternalTransitionConfigurer}.
*
*
* @author Janne Valkealahti
*
* @param <S> the type of state
@@ -46,15 +46,17 @@ public class DefaultInternalTransitionConfigurer<S, E>
private S target;
private S state;
private E event;
private Collection<Action<S, E>> actions = new ArrayList<Action<S, E>>();
private Guard<S, E> guard;
@Override
public void configure(StateMachineTransitionBuilder<S, E> builder) throws Exception {
builder.add(source, target, event, actions, guard, TransitionKind.INTERNAL);
builder.add(source, target, state, event, actions, guard, TransitionKind.INTERNAL);
}
@Override
@@ -63,6 +65,12 @@ public class DefaultInternalTransitionConfigurer<S, E>
return this;
}
@Override
public InternalTransitionConfigurer<S, E> state(S state) {
this.state = state;
return this;
}
@Override
public InternalTransitionConfigurer<S, E> event(E event) {
this.event = event;
@@ -74,13 +82,13 @@ public class DefaultInternalTransitionConfigurer<S, E>
actions.add(action);
return this;
}
@Override
public InternalTransitionConfigurer<S, E> guard(Guard<S, E> guard) {
this.guard = guard;
return this;
}
@Override
public InternalTransitionConfigurer<S, E> guardExpression(String expression) {
SpelExpressionParser parser = new SpelExpressionParser(

View File

@@ -32,7 +32,7 @@ import org.springframework.statemachine.transition.TransitionKind;
/**
* Default implementation of a {@link LocalTransitionConfigurer}.
*
*
* @author Janne Valkealahti
*
* @param <S> the type of state
@@ -46,15 +46,17 @@ public class DefaultLocalTransitionConfigurer<S, E>
private S target;
private S state;
private E event;
private Collection<Action<S, E>> actions = new ArrayList<Action<S, E>>();
private Guard<S, E> guard;
@Override
public void configure(StateMachineTransitionBuilder<S, E> builder) throws Exception {
builder.add(source, target, event, actions, guard, TransitionKind.LOCAL);
builder.add(source, target, state, event, actions, guard, TransitionKind.LOCAL);
}
@Override
@@ -69,6 +71,12 @@ public class DefaultLocalTransitionConfigurer<S, E>
return this;
}
@Override
public LocalTransitionConfigurer<S, E> state(S state) {
this.state = state;
return this;
}
@Override
public LocalTransitionConfigurer<S, E> event(E event) {
this.event = event;
@@ -80,13 +88,13 @@ public class DefaultLocalTransitionConfigurer<S, E>
actions.add(action);
return this;
}
@Override
public LocalTransitionConfigurer<S, E> guard(Guard<S, E> guard) {
this.guard = guard;
return this;
}
@Override
public LocalTransitionConfigurer<S, E> guardExpression(String expression) {
SpelExpressionParser parser = new SpelExpressionParser(

View File

@@ -23,7 +23,7 @@ import org.springframework.statemachine.transition.Transition;
/**
* Base {@code TransitionConfigurer} interface for configuring {@link Transition}s.
*
*
* @author Janne Valkealahti
*
* @param <T> the type of a transition configurer
@@ -41,9 +41,11 @@ public interface TransitionConfigurer<T, S, E> extends
*/
T source(S source);
T state(S state);
/**
* Specify event {@code E} for this {@link Transition}.
*
*
* @param event the event for transition
* @return configurer for chaining
*/
@@ -51,7 +53,7 @@ public interface TransitionConfigurer<T, S, E> extends
/**
* Specify {@link Action} for this {@link Transition}.
*
*
* @param action the action
* @return configurer for chaining
*/
@@ -59,7 +61,7 @@ public interface TransitionConfigurer<T, S, E> extends
/**
* Specify a {@link Guard} for this {@link Transition}.
*
*
* @param guard the guard
* @return configurer for chaining
*/
@@ -67,7 +69,7 @@ public interface TransitionConfigurer<T, S, E> extends
/**
* Specify a {@link Guard} backed by a SpEL expression for this {@link Transition}.
*
*
* @param expression the SpEL expression
* @return configurer for chaining
*/

View File

@@ -46,15 +46,17 @@ public interface Region<S, E> {
* Send an event {@code E} wrapped with a {@link Message} to the region.
*
* @param event the wrapped event to send
* @return true if event was accepted
*/
void sendEvent(Message<E> event);
boolean sendEvent(Message<E> event);
/**
* Send an event {@code E} to the region.
*
* @param event the event to send
* @return true if event was accepted
*/
void sendEvent(E event);
boolean sendEvent(E event);
/**
* Gets the current {@link State}.

View File

@@ -146,7 +146,8 @@ public abstract class AbstractState<S, E> implements State<S, E> {
}
@Override
public void sendEvent(Message<E> event) {
public boolean sendEvent(Message<E> event) {
return false;
}
@Override

View File

@@ -94,12 +94,14 @@ public class RegionState<S, E> extends AbstractState<S, E> {
}
@Override
public void sendEvent(Message<E> event) {
public boolean sendEvent(Message<E> event) {
if (getRegions() != null) {
for (Region<S, E> r : getRegions()) {
r.sendEvent(event);
}
return true;
}
return false;
}
@Override

View File

@@ -35,8 +35,9 @@ public interface State<S, E> {
* Send an event {@code E} wrapped with a {@link Message} to the state.
*
* @param event the wrapped event to send
* @return true if event was accepted
*/
void sendEvent(Message<E> event);
boolean sendEvent(Message<E> event);
/**
* Initiate an exit sequence for the state.

View File

@@ -18,6 +18,7 @@ package org.springframework.statemachine.state;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.messaging.Message;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.action.Action;
@@ -144,6 +145,15 @@ public class StateMachineState<S, E> extends AbstractState<S, E> {
}
}
@Override
public boolean sendEvent(Message<E> event) {
StateMachine<S, E> machine = getSubmachine();
if (machine != null) {
return machine.sendEvent(event);
}
return super.sendEvent(event);
}
private boolean isLocal(StateContext<S, E> context) {
Transition<S, E> transition = context.getTransition();
if (transition != null && TransitionKind.LOCAL == transition.getKind() && this == transition.getTarget()) {

View File

@@ -157,9 +157,9 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
}
@Override
public void sendEvent(Message<E> event) {
public boolean sendEvent(Message<E> event) {
if (isComplete()) {
return;
return false;
}
// TODO: machine header looks weird!
event = MessageBuilder.fromMessage(event).setHeader("machine", this).build();
@@ -167,18 +167,14 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
log.debug("Queue event " + event);
}
// TODO: should not do here
if (currentState != null) {
currentState.sendEvent(event);
}
eventQueue.add(event);
boolean accepted = acceptEvent(event);
scheduleEventQueueProcessing();
return accepted;
}
@Override
public void sendEvent(E event) {
sendEvent(MessageBuilder.withPayload(event).build());
public boolean sendEvent(E event) {
return sendEvent(MessageBuilder.withPayload(event).build());
}
@Override
@@ -228,6 +224,36 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
return transitions;
}
protected boolean acceptEvent(Message<E> event) {
boolean accepted = currentState.sendEvent(event);
if (accepted) {
return true;
}
Message<E> defer = null;
for (Transition<S,E> transition : transitions) {
State<S,E> source = transition.getSource();
Trigger<S, E> trigger = transition.getTrigger();
if (StateMachineUtils.containsAtleastOne(source.getIds(), currentState.getIds())) {
if (trigger != null && trigger.evaluate(event.getPayload())) {
eventQueue.add(event);
return true;
} else if (source.getDeferredEvents() != null && source.getDeferredEvents().contains(event.getPayload())) {
defer = event;
}
}
}
if (defer != null) {
log.info("Deferring event " + defer);
deferList.addLast(defer);
return true;
}
return false;
}
private void switchToState(State<S,E> state, Message<E> event, Transition<S,E> transition) {
exitFromState(currentState, event, transition);
notifyStateChanged(currentState, state);

View File

@@ -27,7 +27,7 @@ import org.springframework.util.Assert;
/**
* Base implementation of a {@link Transition}.
*
*
* @author Janne Valkealahti
*
* @param <S> the type of state
@@ -42,7 +42,7 @@ public abstract class AbstractTransition<S, E> implements Transition<S, E> {
private final Collection<Action<S, E>> actions;
private final TransitionKind kind;
private final Guard<S, E> guard;
private Trigger<S, E> trigger;
@@ -50,7 +50,6 @@ public abstract class AbstractTransition<S, E> implements Transition<S, E> {
public AbstractTransition(State<S, E> source, State<S, E> target, Collection<Action<S, E>> actions, E event,
TransitionKind kind, Guard<S, E> guard) {
Assert.notNull(source, "Source must be set");
// Assert.notNull(target, "Target must be set");
Assert.notNull(kind, "Transition type must be set");
this.source = source;
this.target = target;

View File

@@ -62,7 +62,7 @@ public abstract class AbstractStateMachineTests {
public enum TestStates {
SI,S1,S2,S3,S4,SF,
S10,S11,S101,S111,S112,S12,S121,S122,
S10,S11,S101,S111,S112,S12,S121,S122,S13,
S20,S21,S201,S211,
S1011,S1012,S2011,S2012
}

View File

@@ -16,6 +16,7 @@
package org.springframework.statemachine.event;
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 static org.junit.Assert.assertTrue;
@@ -74,6 +75,22 @@ public class StateMachineEventTests extends AbstractStateMachineTests {
assertThat(listener.events.size(), is(6));
}
@Test
public void testSubmachineHandlesEvent() throws Exception {
context.register(BaseConfig.class, StateMachineEventPublisherConfiguration.class, Config2.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
@SuppressWarnings("unchecked")
EnumStateMachine<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class);
machine.start();
assertThat(machine, notNullValue());
assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S10));
machine.sendEvent(TestEvents.E1);
assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S12));
}
@Configuration
@EnableStateMachine
static class Config1 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@@ -123,6 +140,49 @@ public class StateMachineEventTests extends AbstractStateMachineTests {
}
@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)
.and()
.withStates()
.parent(TestStates.S1)
.initial(TestStates.S10)
.state(TestStates.S10)
.state(TestStates.S11)
.state(TestStates.S12)
.state(TestStates.S13);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
transitions
.withExternal()
.source(TestStates.S10)
.target(TestStates.S11)
.event(TestEvents.E1)
.and()
.withExternal()
.state(TestStates.S1)
.source(TestStates.S10)
.target(TestStates.S12)
.event(TestEvents.E1)
.and()
.withExternal()
.state(TestStates.S1)
.source(TestStates.S10)
.target(TestStates.S13)
.event(TestEvents.E2);
}
}
static class TestEventListener implements ApplicationListener<AbstractStateMachineEvent> {
CountDownLatch onEventLatch = new CountDownLatch(6);