From 2eb8788ce339cbba3ccccdd730765a3fde77c2c6 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Sat, 7 May 2016 15:52:12 +0100 Subject: [PATCH] Direct region entry doesn't bypass initial state entry - Enhance state context to have more info about target states which is needed to make a proper decision if region needs to be started. - Fix RegionState to not to start if it looks like direct entry will be done later. - Relates to #221 --- .../buildtests/ForkJoinEntryExitTests.java | 36 ++ .../buildtests/forkjoin-entryexit2.di | 2 + .../buildtests/forkjoin-entryexit2.notation | 421 ++++++++++++++++++ .../buildtests/forkjoin-entryexit2.uml | 51 +++ .../statemachine/StateContext.java | 29 +- .../statemachine/state/RegionState.java | 13 +- .../support/AbstractStateMachine.java | 23 +- .../support/DefaultStateContext.java | 52 ++- 8 files changed, 606 insertions(+), 21 deletions(-) create mode 100644 spring-statemachine-build-tests/src/test/resources/org/springframework/statemachine/buildtests/forkjoin-entryexit2.di create mode 100644 spring-statemachine-build-tests/src/test/resources/org/springframework/statemachine/buildtests/forkjoin-entryexit2.notation create mode 100644 spring-statemachine-build-tests/src/test/resources/org/springframework/statemachine/buildtests/forkjoin-entryexit2.uml diff --git a/spring-statemachine-build-tests/src/test/java/org/springframework/statemachine/buildtests/ForkJoinEntryExitTests.java b/spring-statemachine-build-tests/src/test/java/org/springframework/statemachine/buildtests/ForkJoinEntryExitTests.java index 8d6b71af..f723fc93 100644 --- a/spring-statemachine-build-tests/src/test/java/org/springframework/statemachine/buildtests/ForkJoinEntryExitTests.java +++ b/spring-statemachine-build-tests/src/test/java/org/springframework/statemachine/buildtests/ForkJoinEntryExitTests.java @@ -49,6 +49,25 @@ public class ForkJoinEntryExitTests extends AbstractBuildTests { plan.test(); } + @Test + @SuppressWarnings("unchecked") + public void testForkEntrysViaChoices() throws Exception { + context.register(Config2.class); + context.refresh(); + StateMachine stateMachine = context.getBean(StateMachine.class); + + StateMachineTestPlan plan = + StateMachineTestPlanBuilder.builder() + .stateMachine(stateMachine) + .step().expectState("S1").and() + .step() + .sendEvent("E1") + .expectStateEntered(3) + .expectStates("S2", "S210", "S220").and() + .build(); + plan.test(); + } + @Test @SuppressWarnings("unchecked") public void testJoinExits() throws Exception { @@ -89,6 +108,23 @@ public class ForkJoinEntryExitTests extends AbstractBuildTests { } } + @Configuration + @EnableStateMachine + public static class Config2 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineModelConfigurer model) throws Exception { + model + .withModel() + .factory(modelFactory()); + } + + @Bean + public StateMachineModelFactory modelFactory() { + return new UmlStateMachineModelFactory("classpath:org/springframework/statemachine/buildtests/forkjoin-entryexit2.uml"); + } + } + @Override protected AnnotationConfigApplicationContext buildContext() { return new AnnotationConfigApplicationContext(); diff --git a/spring-statemachine-build-tests/src/test/resources/org/springframework/statemachine/buildtests/forkjoin-entryexit2.di b/spring-statemachine-build-tests/src/test/resources/org/springframework/statemachine/buildtests/forkjoin-entryexit2.di new file mode 100644 index 00000000..bf9abab3 --- /dev/null +++ b/spring-statemachine-build-tests/src/test/resources/org/springframework/statemachine/buildtests/forkjoin-entryexit2.di @@ -0,0 +1,2 @@ + + diff --git a/spring-statemachine-build-tests/src/test/resources/org/springframework/statemachine/buildtests/forkjoin-entryexit2.notation b/spring-statemachine-build-tests/src/test/resources/org/springframework/statemachine/buildtests/forkjoin-entryexit2.notation new file mode 100644 index 00000000..87912c19 --- /dev/null +++ b/spring-statemachine-build-tests/src/test/resources/org/springframework/statemachine/buildtests/forkjoin-entryexit2.notation @@ -0,0 +1,421 @@ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-statemachine-build-tests/src/test/resources/org/springframework/statemachine/buildtests/forkjoin-entryexit2.uml b/spring-statemachine-build-tests/src/test/resources/org/springframework/statemachine/buildtests/forkjoin-entryexit2.uml new file mode 100644 index 00000000..cef1af24 --- /dev/null +++ b/spring-statemachine-build-tests/src/test/resources/org/springframework/statemachine/buildtests/forkjoin-entryexit2.uml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateContext.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateContext.java index 5b92e3f7..52b0d600 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateContext.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 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. @@ -15,6 +15,8 @@ */ package org.springframework.statemachine; +import java.util.Collection; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.statemachine.action.Action; @@ -104,7 +106,17 @@ public interface StateContext { * * @return the source state */ - State getSource(); + State getSource(); + + /** + * Gets the source states of this context. Multiple sources are + * only valid during a context when machine is joining from multiple + * orthogonal regions. + * + * @return the source state + * @see #getSource() + */ + Collection> getSources(); /** * Gets the target state of this context. Generally target @@ -113,7 +125,17 @@ public interface StateContext { * * @return the target state */ - State getTarget(); + State getTarget(); + + /** + * Gets the target states of this context. Multiple targets are + * only valid during a context when machine is forking into multiple + * orthogonal regions. + * + * @return the target states + * @see #getTarget() + */ + Collection> getTargets(); /** * Gets the exception associated with a context. @@ -138,5 +160,4 @@ public interface StateContext { TRANSITION_START, TRANSITION_END; } - } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java index 3681b3c7..cc54b650 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 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. @@ -17,7 +17,6 @@ package org.springframework.statemachine.state; import java.util.ArrayList; import java.util.Collection; -import java.util.List; import org.springframework.messaging.Message; import org.springframework.statemachine.StateContext; @@ -159,14 +158,8 @@ public class RegionState extends AbstractState { if (getPseudoState() != null && getPseudoState().getKind() == PseudoStateKind.INITIAL) { for (Region region : getRegions()) { boolean start = true; - PseudoState ps = context.getTransition().getTarget().getPseudoState(); - if (ps != null && ps.getKind() == PseudoStateKind.FORK) { - List> forks = ((ForkPseudoState)ps).getForks(); - if (StateMachineUtils.containsAtleastOne(region.getStates(), forks)) { - // it looks like fork will take directly into a state so don't start - // as we want to bypass initial entry logic. - start = false; - } + if (StateMachineUtils.containsAtleastOne(region.getStates(), context.getTargets())) { + start = false; } if (start) { region.start(); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java index b34b4463..30fd19ed 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java @@ -735,7 +735,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo ForkPseudoState fps = (ForkPseudoState) toState.getPseudoState(); for (State ss : fps.getForks()) { callPreStateChangeInterceptors(ss, message, transition, stateMachine); - setCurrentState(ss, message, transition, false, stateMachine); + setCurrentState(ss, message, transition, false, stateMachine, null, fps.getForks()); } } else { setCurrentState(toState, message, transition, true, stateMachine); @@ -811,6 +811,12 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo return new DefaultStateContext(stage, message, messageHeaders, extendedState, transition, stateMachine, source, target, null); } + private StateContext buildStateContext(Stage stage, Message message, Transition transition, StateMachine stateMachine, Collection> sources, Collection> targets) { + MessageHeaders messageHeaders = message != null ? message.getHeaders() : new MessageHeaders( + new HashMap()); + return new DefaultStateContext(stage, message, messageHeaders, extendedState, transition, stateMachine, null, null, sources, targets, null); + } + private State findDeepParent(State state) { for (State s : states) { if (s.getStates().contains(state)) { @@ -821,6 +827,11 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } synchronized void setCurrentState(State state, Message message, Transition transition, boolean exit, StateMachine stateMachine) { + setCurrentState(state, message, transition, exit, stateMachine, null, null); + } + + synchronized void setCurrentState(State state, Message message, Transition transition, boolean exit, + StateMachine stateMachine, Collection> sources, Collection> targets) { State findDeep = findDeepParent(state); boolean isTargetSubOf = false; @@ -901,7 +912,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } currentState = findDeep; if (shouldTryEntry) { - entryToState(currentState, message, transition, stateMachine); + entryToState(currentState, message, transition, stateMachine, sources, targets); } if (currentState.isSubmachineState()) { @@ -985,11 +996,16 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } private void entryToState(State state, Message message, Transition transition, StateMachine stateMachine) { + entryToState(state, message, transition, stateMachine, null, null); + } + + private void entryToState(State state, Message message, Transition transition, StateMachine stateMachine, + Collection> sources, Collection> targets) { if (state == null) { return; } log.trace("Trying Enter state=[" + state + "]"); - StateContext stateContext = buildStateContext(Stage.STATE_ENTRY, message, transition, stateMachine); + StateContext stateContext = buildStateContext(Stage.STATE_ENTRY, message, transition, stateMachine, sources, targets); if (transition != null) { State findDeep1 = findDeepParent(transition.getTarget()); @@ -1026,5 +1042,4 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo return false; } } - } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateContext.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateContext.java index f68f635c..39e8a90a 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateContext.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 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. @@ -15,6 +15,8 @@ */ package org.springframework.statemachine.support; +import java.util.Collection; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.statemachine.ExtendedState; @@ -41,6 +43,8 @@ public class DefaultStateContext implements StateContext { private final StateMachine stateMachine; private final State source; private final State target; + private final Collection> sources; + private final Collection> targets; private final Exception exception; /** @@ -67,6 +71,39 @@ public class DefaultStateContext implements StateContext { this.source = source; this.target = target; this.exception = exception; + this.sources = null; + this.targets = null; + } + + /** + * Instantiates a new default state context. + * + * @param stage the stage + * @param message the message + * @param messageHeaders the message headers + * @param extendedState the extended state + * @param transition the transition + * @param stateMachine the state machine + * @param source the source + * @param target the target + * @param sources the sources + * @param targets the targets + * @param exception the exception + */ + public DefaultStateContext(Stage stage, Message message, MessageHeaders messageHeaders, ExtendedState extendedState, + Transition transition, StateMachine stateMachine, State source, State target, + Collection> sources, Collection> targets, Exception exception) { + this.stage = stage; + this.message = message; + this.messageHeaders = messageHeaders; + this.extendedState = extendedState; + this.transition = transition; + this.stateMachine = stateMachine; + this.source = source; + this.target = target; + this.sources = sources; + this.targets = targets; + this.exception = exception; } @Override @@ -119,11 +156,21 @@ public class DefaultStateContext implements StateContext { return source != null ? source : (transition != null ? transition.getSource() : null); } + @Override + public Collection> getSources() { + return sources; + } + @Override public State getTarget() { return target != null ? target : (transition != null ? transition.getTarget() : null); } + @Override + public Collection> getTargets() { + return targets; + } + @Override public Exception getException() { return exception; @@ -133,7 +180,6 @@ public class DefaultStateContext implements StateContext { public String toString() { return "DefaultStateContext [stage=" + stage + ", message=" + message + ", messageHeaders=" + messageHeaders + ", extendedState=" + extendedState + ", transition=" + transition + ", stateMachine=" + stateMachine + ", source=" + source + ", target=" - + target + ", exception=" + exception + "]"; + + target + ", sources=" + sources + ", targets=" + targets + ", exception=" + exception + "]"; } - }