From 7c8b93bd455fdb968b3f711fa5d77f73436f81bf Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Tue, 19 Dec 2017 13:09:41 +0000 Subject: [PATCH] Conflicting transitions cause undefined order behaviour - Add new TransitionConflightPolicy and allow to configure it via an adapter interfaces. - For now, use it in TransitionComparator which sorts transitions based on source states. This gives PARENT and CHILD sorting and allows to choose conflicting transitions either from parent or child. - Add new sync thread used in submachines so that one thread is used in a whole call stach per machine(regions still get their own as well as a root machine). - Fixes #456 --- .../config/AbstractStateMachineFactory.java | 56 +- .../config/ObjectStateMachineFactory.java | 4 +- .../StateMachineConfigurationBuilder.java | 15 +- .../configurers/ConfigurationConfigurer.java | 10 +- .../DefaultConfigurationConfigurer.java | 10 +- .../config/model/ConfigurationData.java | 45 ++ .../support/AbstractStateMachine.java | 26 +- .../support/DefaultStateMachineExecutor.java | 13 +- .../support/TransitionComparator.java | 69 ++ .../transition/AbstractTransition.java | 5 + .../transition/TransitionConflightPolicy.java | 35 + .../DefaultStateMachineExecutorTests.java | 9 +- .../support/TransitionComparatorTests.java | 97 +++ .../transition/TransitionOrderTests.java | 620 ++++++++++++++++++ 14 files changed, 978 insertions(+), 36 deletions(-) create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/support/TransitionComparator.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/TransitionConflightPolicy.java create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/support/TransitionComparatorTests.java create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionOrderTests.java diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java index 46402291..f9f4514a 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java @@ -159,6 +159,8 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS */ @SuppressWarnings("unchecked") public StateMachine getStateMachine(UUID uuid, String machineId) { + ArrayList> machines = new ArrayList<>(); + StateMachineModel stateMachineModel = resolveStateMachineModel(machineId); if (stateMachineModel.getConfigurationData().isVerifierEnabled()) { StateMachineModelVerifier verifier = stateMachineModel.getConfigurationData().getVerifier(); @@ -221,6 +223,7 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS contextEvents, defaultExtendedState, stateMachineModel.getTransitionsData(), resolveTaskExecutor(stateMachineModel), resolveTaskScheduler(stateMachineModel), machineId, null, stateMachineModel); regionStack.push(new MachineStackItem(machine)); + machines.add(machine); } Collection> regions = new ArrayList>(); @@ -244,11 +247,13 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS machineId != null ? machineId : stateMachineModel.getConfigurationData().getMachineId(), uuid, stateMachineModel); machine = m; + machines.add(m); } } else { machine = buildMachine(machineMap, stateMap, holderList, stateDatas, transitionsData, resolveBeanFactory(stateMachineModel), contextEvents, defaultExtendedState, stateMachineModel.getTransitionsData(), resolveTaskExecutor(stateMachineModel), resolveTaskScheduler(stateMachineModel), machineId, uuid, stateMachineModel); + machines.add(machine); if (peek.isInitial() || (!peek.isInitial() && !machineMap.containsKey(peek.getParent()))) { machineMap.put(peek.getParent(), machine); } @@ -289,6 +294,35 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS }); } + // set parent machines for each built machine + for (Entry> mme : machineMap.entrySet()) { + StateMachine m = null; + if (mme.getKey() != null) { + Object sParent = null; + for (StateData sd : stateMachineModel.getStatesData().getStateData()) { + if (ObjectUtils.nullSafeEquals(sd.getState(), mme.getKey())) { + sParent = sd.getParent(); + break; + } + } + m = machineMap.get(sParent); + } + final StateMachine mm = m; + mme.getValue().getStateMachineAccessor().doWithRegion(new StateMachineFunction>(){ + + @Override + public void apply(StateMachineAccess function) { + function.setParentMachine(mm); + } + }); + } + + // init built machines + for (StateMachine m : machines) { + ((LifecycleObjectSupport)m).afterPropertiesSet(); + } + + // TODO: should error out if sec is enabled but spring-security is not in cp if (stateMachineModel.getConfigurationData().isSecurityEnabled()) { final StateMachineSecurityInterceptor securityInterceptor = new StateMachineSecurityInterceptor( @@ -338,28 +372,6 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS holderItem.value.setState(stateMap.get(holderItem.key)); } - // set parent machines for each built machine - for (Entry> mme : machineMap.entrySet()) { - StateMachine m = null; - if (mme.getKey() != null) { - Object sParent = null; - for (StateData sd : stateMachineModel.getStatesData().getStateData()) { - if (ObjectUtils.nullSafeEquals(sd.getState(), mme.getKey())) { - sParent = sd.getParent(); - break; - } - } - m = machineMap.get(sParent); - } - final StateMachine mm = m; - mme.getValue().getStateMachineAccessor().doWithRegion(new StateMachineFunction>(){ - - @Override - public void apply(StateMachineAccess function) { - function.setParentMachine(mm); - } - }); - } return delegateAutoStartup(machine); } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/ObjectStateMachineFactory.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/ObjectStateMachineFactory.java index 85d53d75..3ca51978 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/ObjectStateMachineFactory.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/ObjectStateMachineFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2016 the original author or authors. + * Copyright 2015-2017 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. @@ -76,6 +76,7 @@ public class ObjectStateMachineFactory extends AbstractStateMachineFactory extendedState, uuid); machine.setId(machineId); machine.setHistoryState(historyState); + machine.setTransitionConflightPolicy(stateMachineModel.getConfigurationData().getTransitionConflightPolicy()); if (contextEventsEnabled != null) { machine.setContextEventsEnabled(contextEventsEnabled); } @@ -91,7 +92,6 @@ public class ObjectStateMachineFactory extends AbstractStateMachineFactory if (machine instanceof BeanNameAware) { ((BeanNameAware)machine).setBeanName(beanName); } - machine.afterPropertiesSet(); return machine; } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java index ba210547..98824fc3 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2017 the original author or authors. + * Copyright 2017 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. @@ -46,6 +46,7 @@ import org.springframework.statemachine.monitor.StateMachineMonitor; import org.springframework.statemachine.persist.StateMachineRuntimePersister; import org.springframework.statemachine.security.SecurityRule; import org.springframework.statemachine.support.StateMachineInterceptor; +import org.springframework.statemachine.transition.TransitionConflightPolicy; /** * {@link AnnotationBuilder} for {@link StatesData}. @@ -64,6 +65,7 @@ public class StateMachineConfigurationBuilder private TaskExecutor taskExecutor; private TaskScheduler taskScheculer; private boolean autoStart = false; + private TransitionConflightPolicy transitionConflightPolicy; private StateMachineEnsemble ensemble; private final List> listeners = new ArrayList>(); private boolean securityEnabled = false; @@ -145,7 +147,7 @@ public class StateMachineConfigurationBuilder } return new ConfigurationData(beanFactory, taskExecutor, taskScheculer, autoStart, ensemble, listeners, securityEnabled, transitionSecurityAccessDecisionManager, eventSecurityAccessDecisionManager, eventSecurityRule, - transitionSecurityRule, verifierEnabled, verifier, machineId, stateMachineMonitor, interceptorsCopy); + transitionSecurityRule, verifierEnabled, verifier, machineId, stateMachineMonitor, interceptorsCopy, transitionConflightPolicy); } /** @@ -292,4 +294,13 @@ public class StateMachineConfigurationBuilder public void setStateMachineRuntimePersister(StateMachineRuntimePersister persister) { this.persister = persister; } + + /** + * Sets the transition conflight policy. + * + * @param transitionConflightPolicy the new transition conflight policy + */ + public void setTransitionConflightPolicy(TransitionConflightPolicy transitionConflightPolicy) { + this.transitionConflightPolicy = transitionConflightPolicy; + } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/ConfigurationConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/ConfigurationConfigurer.java index 2ad2205c..c74f0cca 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/ConfigurationConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/ConfigurationConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2017 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. @@ -22,6 +22,7 @@ import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerBuilder; import org.springframework.statemachine.listener.StateMachineListener; +import org.springframework.statemachine.transition.TransitionConflightPolicy; /** * Base {@code ConfigConfigurer} interface for configuring generic config. @@ -86,4 +87,11 @@ public interface ConfigurationConfigurer extends */ ConfigurationConfigurer listener(StateMachineListener listener); + /** + * Speficy a {@link TransitionConflightPolicy}. + * + * @param transitionConflightPolicy the transition conflight policy + * @return the configuration configurer + */ + ConfigurationConfigurer transitionConflightPolicy(TransitionConflightPolicy transitionConflightPolicy); } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultConfigurationConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultConfigurationConfigurer.java index da5e21e4..e5c94a57 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultConfigurationConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultConfigurationConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2016 the original author or authors. + * Copyright 2015-2017 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. @@ -26,6 +26,7 @@ import org.springframework.statemachine.config.builders.StateMachineConfiguratio import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerAdapter; import org.springframework.statemachine.config.model.ConfigurationData; import org.springframework.statemachine.listener.StateMachineListener; +import org.springframework.statemachine.transition.TransitionConflightPolicy; /** * Default implementation of a {@link ConfigurationConfigurer}. @@ -44,6 +45,7 @@ public class DefaultConfigurationConfigurer private TaskExecutor taskExecutor; private TaskScheduler taskScheculer; private boolean autoStart = false; + private TransitionConflightPolicy transitionConflightPolicy; private final List> listeners = new ArrayList>(); @Override @@ -54,6 +56,7 @@ public class DefaultConfigurationConfigurer builder.setTaskScheculer(taskScheculer); builder.setAutoStart(autoStart); builder.setStateMachineListeners(listeners); + builder.setTransitionConflightPolicy(transitionConflightPolicy); } @Override @@ -92,4 +95,9 @@ public class DefaultConfigurationConfigurer return this; } + @Override + public ConfigurationConfigurer transitionConflightPolicy(TransitionConflightPolicy transitionConflightPolicy) { + this.transitionConflightPolicy = transitionConflightPolicy; + return this; + } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/ConfigurationData.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/ConfigurationData.java index 9ac1a5de..7fc56564 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/ConfigurationData.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/ConfigurationData.java @@ -32,6 +32,7 @@ import org.springframework.statemachine.listener.StateMachineListener; import org.springframework.statemachine.monitor.StateMachineMonitor; import org.springframework.statemachine.security.SecurityRule; import org.springframework.statemachine.support.StateMachineInterceptor; +import org.springframework.statemachine.transition.TransitionConflightPolicy; /** * Configuration object used to keep things together in {@link StateMachineConfigurationBuilder}. @@ -48,6 +49,7 @@ public class ConfigurationData { private final TaskExecutor taskExecutor; private final TaskScheduler taskScheduler; private final boolean autoStart; + private final TransitionConflightPolicy transitionConflightPolicy; private final StateMachineEnsemble ensemble; private final List> listeners; private final boolean securityEnabled; @@ -95,6 +97,39 @@ public class ConfigurationData { SecurityRule eventSecurityRule, SecurityRule transitionSecurityRule, boolean verifierEnabled, StateMachineModelVerifier verifier, String machineId, StateMachineMonitor stateMachineMonitor, List> interceptors) { + this(beanFactory, taskExecutor, taskScheduler, autoStart, ensemble, listeners, securityEnabled, + transitionSecurityAccessDecisionManager, eventSecurityAccessDecisionManager, eventSecurityRule, transitionSecurityRule, + verifierEnabled, verifier, machineId, stateMachineMonitor, interceptors, null); + } + + /** + * Instantiates a new state machine configuration config data. + * + * @param beanFactory the bean factory + * @param taskExecutor the task executor + * @param taskScheduler the task scheduler + * @param autoStart the autostart flag + * @param ensemble the state machine ensemble + * @param listeners the state machine listeners + * @param securityEnabled the security enabled flag + * @param transitionSecurityAccessDecisionManager the transition security access decision manager + * @param eventSecurityAccessDecisionManager the event security access decision manager + * @param eventSecurityRule the event security rule + * @param transitionSecurityRule the transition security rule + * @param verifierEnabled the verifier enabled flag + * @param verifier the state machine model verifier + * @param machineId the machine id + * @param stateMachineMonitor the state machine monitor + * @param interceptors the state machine interceptors. + * @param transitionConflightPolicy the transition conflight policy + */ + public ConfigurationData(BeanFactory beanFactory, TaskExecutor taskExecutor, + TaskScheduler taskScheduler, boolean autoStart, StateMachineEnsemble ensemble, + List> listeners, boolean securityEnabled, + AccessDecisionManager transitionSecurityAccessDecisionManager, AccessDecisionManager eventSecurityAccessDecisionManager, + SecurityRule eventSecurityRule, SecurityRule transitionSecurityRule, boolean verifierEnabled, + StateMachineModelVerifier verifier, String machineId, StateMachineMonitor stateMachineMonitor, + List> interceptors, TransitionConflightPolicy transitionConflightPolicy) { this.beanFactory = beanFactory; this.taskExecutor = taskExecutor; this.taskScheduler = taskScheduler; @@ -111,6 +146,7 @@ public class ConfigurationData { this.machineId = machineId; this.stateMachineMonitor = stateMachineMonitor; this.interceptors = interceptors; + this.transitionConflightPolicy = transitionConflightPolicy; } public String getMachineId() { @@ -251,4 +287,13 @@ public class ConfigurationData { public List> getStateMachineInterceptors() { return interceptors; } + + /** + * Gets the transition conflight policy. + * + * @return the transition conflight policy + */ + public TransitionConflightPolicy getTransitionConflightPolicy() { + return transitionConflightPolicy; + } } 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 85186727..fcd385a6 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015-2017 the original author or authors. + * Copyright 2017 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. @@ -21,6 +21,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.context.Lifecycle; +import org.springframework.core.task.SyncTaskExecutor; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.support.MessageBuilder; @@ -51,6 +52,7 @@ import org.springframework.statemachine.state.State; import org.springframework.statemachine.support.StateMachineExecutor.StateMachineExecutorTransit; import org.springframework.statemachine.transition.InitialTransition; import org.springframework.statemachine.transition.Transition; +import org.springframework.statemachine.transition.TransitionConflightPolicy; import org.springframework.statemachine.transition.TransitionKind; import org.springframework.statemachine.trigger.DefaultTriggerContext; import org.springframework.statemachine.trigger.Trigger; @@ -91,6 +93,8 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo private ExtendedState extendedState; + private TransitionConflightPolicy transitionConflightPolicy; + private volatile State currentState; // using this to log last state when machine stops, as @@ -274,12 +278,19 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } DefaultStateMachineExecutor executor = new DefaultStateMachineExecutor(this, getRelayStateMachine(), transitions, - triggerToTransitionMap, triggerlessTransitions, initialTransition, initialEvent); + triggerToTransitionMap, triggerlessTransitions, initialTransition, initialEvent, transitionConflightPolicy); if (getBeanFactory() != null) { executor.setBeanFactory(getBeanFactory()); } if (getTaskExecutor() != null){ - executor.setTaskExecutor(getTaskExecutor()); + // parent machine is set when we're on substates(not regions) + // so then force sync executor which makes things a bit more reliable + // as state execution should anyway get synched with plain substates. + if(parentMachine != null) { + executor.setTaskExecutor(new SyncTaskExecutor()); + } else { + executor.setTaskExecutor(getTaskExecutor()); + } } executor.afterPropertiesSet(); executor.setStateMachineExecutorTransit(new StateMachineExecutorTransit() { @@ -562,6 +573,15 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo forwardedInitialEvent = message; } + /** + * Sets the transition conflight policy. + * + * @param transitionConflightPolicy the new transition conflight policy + */ + public void setTransitionConflightPolicy(TransitionConflightPolicy transitionConflightPolicy) { + this.transitionConflightPolicy = transitionConflightPolicy; + } + private boolean sendEventInternal(Message event) { if (hasStateMachineError()) { // TODO: should we throw exception? diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java index 60518962..a8b4a0f7 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java @@ -47,6 +47,7 @@ import org.springframework.statemachine.state.JoinPseudoState; import org.springframework.statemachine.state.PseudoStateKind; import org.springframework.statemachine.state.State; import org.springframework.statemachine.transition.Transition; +import org.springframework.statemachine.transition.TransitionConflightPolicy; import org.springframework.statemachine.trigger.DefaultTriggerContext; import org.springframework.statemachine.trigger.TimerTrigger; import org.springframework.statemachine.trigger.Trigger; @@ -101,6 +102,8 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im private final ReentrantLock lock = new ReentrantLock(); + private final TransitionComparator transitionComparator;; + /** * Instantiates a new default state machine executor. * @@ -111,10 +114,12 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im * @param triggerlessTransitions the triggerless transitions * @param initialTransition the initial transition * @param initialEvent the initial event + * @param transitionConflightPolicy the transition conflight policy */ public DefaultStateMachineExecutor(StateMachine stateMachine, StateMachine relayStateMachine, Collection> transitions, Map, Transition> triggerToTransitionMap, - List> triggerlessTransitions, Transition initialTransition, Message initialEvent) { + List> triggerlessTransitions, Transition initialTransition, Message initialEvent, + TransitionConflightPolicy transitionConflightPolicy) { this.stateMachine = stateMachine; this.relayStateMachine = relayStateMachine; this.triggerToTransitionMap = triggerToTransitionMap; @@ -122,6 +127,9 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im this.transitions = transitions; this.initialTransition = initialTransition; this.initialEvent = initialEvent; + this.transitionComparator = new TransitionComparator(transitionConflightPolicy); + // anonymous transitions are fixed, sort those now + this.triggerlessTransitions.sort(transitionComparator); registerTriggerListener(); } @@ -417,7 +425,8 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im trans.add(triggerToTransitionMap.get(queueItem.trigger)); } - // go through candidates and transit max one + // go through candidates and transit max one, sort before handling + trans.sort(transitionComparator); handleTriggerTrans(trans, queuedMessage); } if (stateMachine.getState() != null) { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/TransitionComparator.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/TransitionComparator.java new file mode 100644 index 00000000..e01b2540 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/TransitionComparator.java @@ -0,0 +1,69 @@ +/* + * Copyright 2017 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.support; + +import java.util.Comparator; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.statemachine.transition.Transition; +import org.springframework.statemachine.transition.TransitionConflightPolicy; + +/** + * {@link Comparator} for {@link Transition}s. This comparator tries to compare + * transitions simply checking + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +class TransitionComparator implements Comparator> { + + private final static Log log = LogFactory.getLog(TransitionComparator.class); + private final TransitionConflightPolicy transitionConflightPolicy; + + /** + * Instantiates a new transition comparator. + * + * @param transitionConflightPolicy the transition conflight policy + */ + public TransitionComparator(TransitionConflightPolicy transitionConflightPolicy) { + this.transitionConflightPolicy = transitionConflightPolicy == null ? TransitionConflightPolicy.CHILD : transitionConflightPolicy; + } + + @Override + public int compare(Transition left, Transition right) { + if (log.isTraceEnabled()) { + log.trace("Compare left='" + left + "' right='" + right +"'"); + } + if (left == right) { + return 0; + } else { + boolean substate = StateMachineUtils.isSubstate(left.getSource(), right.getSource()); + if (transitionConflightPolicy == TransitionConflightPolicy.CHILD) { + return substate ? 1 : -1; + } else { + return substate ? -1 : 1; + } + } + } + + @Override + public String toString() { + return "TransitionComparator [transitionConflightPolicy=" + transitionConflightPolicy + "]"; + } +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractTransition.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractTransition.java index 18edc661..157bb649 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractTransition.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractTransition.java @@ -168,4 +168,9 @@ public abstract class AbstractTransition implements Transition { } } } + + @Override + public String toString() { + return "AbstractTransition [source=" + source + ", target=" + target + ", kind=" + kind + ", guard=" + guard + "]"; + } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/TransitionConflightPolicy.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/TransitionConflightPolicy.java new file mode 100644 index 00000000..d8eb1210 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/TransitionConflightPolicy.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017 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.transition; + +/** + * Enumerations for possible transition conflight policies. + * + * @author Janne Valkealahti + * + */ +public enum TransitionConflightPolicy { + + /** + * Choose transition from a child. + */ + CHILD, + + /** + * Choose transition from a parent. + */ + PARENT; +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/DefaultStateMachineExecutorTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/DefaultStateMachineExecutorTests.java index fd02a8c4..952ca385 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/DefaultStateMachineExecutorTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/DefaultStateMachineExecutorTests.java @@ -89,7 +89,8 @@ public class DefaultStateMachineExecutorTests { triggerToTransitionMap, triggerlessTransitions, initialTransition, - initialEvent); + initialEvent, + null); executor.setTaskExecutor(taskExecutor); @@ -162,7 +163,8 @@ public class DefaultStateMachineExecutorTests { triggerToTransitionMap, triggerlessTransitions, initialTransition, - initialEvent); + initialEvent, + null); executor.setTaskExecutor(taskExecutor); @@ -237,7 +239,8 @@ public class DefaultStateMachineExecutorTests { triggerToTransitionMap, triggerlessTransitions, initialTransition, - initialEvent); + initialEvent, + null); executor.setTaskExecutor(taskExecutor); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/TransitionComparatorTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/TransitionComparatorTests.java new file mode 100644 index 00000000..acd0a3b2 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/TransitionComparatorTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2017 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.support; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.util.ArrayList; +import java.util.Collection; + +import org.junit.Test; +import org.springframework.statemachine.AbstractStateMachineTests.TestEvents; +import org.springframework.statemachine.AbstractStateMachineTests.TestStates; +import org.springframework.statemachine.ObjectStateMachine; +import org.springframework.statemachine.state.DefaultPseudoState; +import org.springframework.statemachine.state.EnumState; +import org.springframework.statemachine.state.PseudoState; +import org.springframework.statemachine.state.PseudoStateKind; +import org.springframework.statemachine.state.State; +import org.springframework.statemachine.state.StateMachineState; +import org.springframework.statemachine.transition.DefaultExternalTransition; +import org.springframework.statemachine.transition.Transition; +import org.springframework.statemachine.transition.TransitionConflightPolicy; +import org.springframework.statemachine.trigger.EventTrigger; + +/** + * Tests for {@link TransitionComparator}. + * + * @author Janne Valkealahti + * + */ +public class TransitionComparatorTests { + + @Test + public void testCompareWithParentAndChild() { + PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); + + State stateS111 = new EnumState(TestStates.S111, null, null, null, pseudoState); + + // submachine 11 + Collection> substates111 = new ArrayList>(); + substates111.add(stateS111); + Collection> subtransitions111 = new ArrayList>(); + ObjectStateMachine submachine11 = new ObjectStateMachine(substates111, + subtransitions111, stateS111); + + // submachine 1 + StateMachineState stateS11 = new StateMachineState(TestStates.S11, submachine11, + null, null, null, pseudoState); + + Collection> substates11 = new ArrayList>(); + substates11.add(stateS11); + Collection> subtransitions11 = new ArrayList>(); + ObjectStateMachine submachine1 = new ObjectStateMachine(substates11, + subtransitions11, stateS11); + + // machine + StateMachineState stateS1 = new StateMachineState(TestStates.S1, submachine1, null, + null, null, pseudoState); + + DefaultExternalTransition transitionFromS111ToS1 = new DefaultExternalTransition( + stateS111, stateS1, null, TestEvents.E1, null, new EventTrigger(TestEvents.E1)); + DefaultExternalTransition transitionFromS11ToS1 = new DefaultExternalTransition( + stateS11, stateS1, null, TestEvents.E1, null, new EventTrigger(TestEvents.E1)); + + TransitionComparator comparator = new TransitionComparator<>(null); + assertThat(comparator.compare(transitionFromS111ToS1, transitionFromS11ToS1), is(-1)); + assertThat(comparator.compare(transitionFromS11ToS1, transitionFromS111ToS1), is(1)); + assertThat(comparator.compare(transitionFromS111ToS1, transitionFromS111ToS1), is(0)); + assertThat(comparator.compare(transitionFromS11ToS1, transitionFromS11ToS1), is(0)); + + comparator = new TransitionComparator<>(TransitionConflightPolicy.CHILD); + assertThat(comparator.compare(transitionFromS111ToS1, transitionFromS11ToS1), is(-1)); + assertThat(comparator.compare(transitionFromS11ToS1, transitionFromS111ToS1), is(1)); + assertThat(comparator.compare(transitionFromS111ToS1, transitionFromS111ToS1), is(0)); + assertThat(comparator.compare(transitionFromS11ToS1, transitionFromS11ToS1), is(0)); + + comparator = new TransitionComparator<>(TransitionConflightPolicy.PARENT); + assertThat(comparator.compare(transitionFromS111ToS1, transitionFromS11ToS1), is(1)); + assertThat(comparator.compare(transitionFromS11ToS1, transitionFromS111ToS1), is(-1)); + assertThat(comparator.compare(transitionFromS111ToS1, transitionFromS111ToS1), is(0)); + assertThat(comparator.compare(transitionFromS11ToS1, transitionFromS11ToS1), is(0)); + } +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionOrderTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionOrderTests.java new file mode 100644 index 00000000..cd1f9a57 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionOrderTests.java @@ -0,0 +1,620 @@ +/* + * Copyright 2017 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.transition; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertThat; + +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.statemachine.AbstractStateMachineTests; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.StateMachineSystemConstants; +import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; +import org.springframework.statemachine.listener.StateMachineListenerAdapter; +import org.springframework.statemachine.state.State; + +/** + * Tests for transitions for cases where specific order is assumed. + * + * @author Janne Valkealahti + * + */ +public class TransitionOrderTests extends AbstractStateMachineTests { + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + + @SuppressWarnings("unchecked") + @Test + public void testAnonymousTransitionInConfigUseParent1() { + TestListener listener = new TestListener(); + context.register(Config1.class); + context.refresh(); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + machine.addStateListener(listener); + + machine.start(); + assertThat(machine.getState().getIds(), contains(TestStates.S1)); + assertThat(listener.statesEntered, contains(TestStates.S1)); + + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + assertThat(listener.statesEntered, contains(TestStates.S1, TestStates.S10, TestStates.S1011, TestStates.S1)); + } + + @SuppressWarnings("unchecked") + @Test + public void testAnonymousTransitionInConfigUseParent2() { + TestListener listener = new TestListener(); + context.register(Config2.class); + context.refresh(); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + machine.addStateListener(listener); + + machine.start(); + assertThat(machine.getState().getIds(), contains(TestStates.S1)); + + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + assertThat(listener.statesEntered, contains(TestStates.S1, TestStates.S10, TestStates.S1011, TestStates.S1)); + } + + @SuppressWarnings("unchecked") + @Test + public void testAnonymousTransitionInConfigUseChild1() { + TestListener listener = new TestListener(); + context.register(Config3.class); + context.refresh(); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + machine.addStateListener(listener); + + machine.start(); + assertThat(machine.getState().getIds(), contains(TestStates.S1)); + + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + assertThat(listener.statesEntered, contains(TestStates.S1, TestStates.S10, TestStates.S1011, TestStates.S1012, TestStates.S1)); + } + + @SuppressWarnings("unchecked") + @Test + public void testAnonymousTransitionInConfigUseParent3() { + TestListener listener = new TestListener(); + context.register(Config4.class); + context.refresh(); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + machine.addStateListener(listener); + + machine.start(); + assertThat(machine.getState().getIds(), contains(TestStates.S1)); + + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + assertThat(listener.statesEntered, contains(TestStates.S1, TestStates.S10, TestStates.S1011, TestStates.S1)); + } + + @SuppressWarnings("unchecked") + @Test + public void testAnonymousTransitionInConfigUseParent4() { + TestListener listener = new TestListener(); + context.register(Config5.class); + context.refresh(); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + machine.addStateListener(listener); + + machine.start(); + assertThat(machine.getState().getIds(), contains(TestStates.S1)); + + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + assertThat(listener.statesEntered, contains(TestStates.S1, TestStates.S10, TestStates.S1011, TestStates.S1012)); + } + + @SuppressWarnings("unchecked") + @Test + public void testAnonymousTransitionInConfigUseParent5() { + TestListener listener = new TestListener(); + context.register(Config6.class); + context.refresh(); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + machine.addStateListener(listener); + + machine.start(); + assertThat(machine.getState().getIds(), contains(TestStates.S1)); + + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + assertThat(listener.statesEntered, contains(TestStates.S1, TestStates.S10, TestStates.S1011, TestStates.S1)); + } + + @SuppressWarnings("unchecked") + @Test + public void testAnonymousTransitionInConfigUseParent6() { + TestListener listener = new TestListener(); + context.register(Config7.class); + context.refresh(); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + machine.addStateListener(listener); + + machine.start(); + assertThat(machine.getState().getIds(), contains(TestStates.S1)); + + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + assertThat(listener.statesEntered, + contains(TestStates.S1, TestStates.S10, TestStates.S1011, TestStates.S1012, TestStates.S2011, TestStates.S1)); + } + + @SuppressWarnings("unchecked") + @Test + public void testAnonymousTransitionInConfigUseParent7() { + TestListener listener = new TestListener(); + context.register(Config8.class); + context.refresh(); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + machine.addStateListener(listener); + + machine.start(); + assertThat(machine.getState().getIds(), contains(TestStates.S1)); + + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + assertThat(listener.statesEntered, + contains(TestStates.S1, TestStates.S10, TestStates.S1011, TestStates.S1)); + } + + @SuppressWarnings("unchecked") + @Test + public void testAnonymousTransitionInConfigUseParent3Threading() throws InterruptedException { + TestListener listener = new TestListener(); + context.register(Config4.class, StateMachineExecutorConfiguration.class); + context.refresh(); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + machine.addStateListener(listener); + + machine.start(); + assertThat(listener.statesEnteredLatch.await(1, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), contains(TestStates.S1)); + + listener.reset(1, 3); + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + assertThat(listener.statesEnteredLatch.await(1, TimeUnit.SECONDS), is(true)); + assertThat(listener.statesEntered, contains(TestStates.S10, TestStates.S1011, TestStates.S1)); + } + + @Configuration + @EnableStateMachine + public static class Config1 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .transitionConflightPolicy(TransitionConflightPolicy.PARENT); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S10) + .and() + .withStates() + .parent(TestStates.S10) + .initial(TestStates.S1011) + .state(TestStates.S1012); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S10) + .event(TestEvents.E1) + .and() + // Config1 vs. Config2, next 2 transitions are defined in different order + .withExternal() + .source(TestStates.S1011) + .target(TestStates.S1012) + .and() + .withExternal() + .source(TestStates.S10) + .target(TestStates.S1); + } + } + + @Configuration + @EnableStateMachine + public static class Config2 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .transitionConflightPolicy(TransitionConflightPolicy.PARENT); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S10) + .and() + .withStates() + .parent(TestStates.S10) + .initial(TestStates.S1011) + .state(TestStates.S1012); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S10) + .event(TestEvents.E1) + .and() + // Config1 vs. Config2, next 2 transitions are defined in different order + .withExternal() + .source(TestStates.S10) + .target(TestStates.S1) + .and() + .withExternal() + .source(TestStates.S1011) + .target(TestStates.S1012); + } + } + + @Configuration + @EnableStateMachine + public static class Config3 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .transitionConflightPolicy(TransitionConflightPolicy.CHILD); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S10) + .and() + .withStates() + .parent(TestStates.S10) + .initial(TestStates.S1011) + .state(TestStates.S1012); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S10) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S10) + .target(TestStates.S1) + .and() + .withExternal() + .source(TestStates.S1011) + .target(TestStates.S1012); + } + } + + @Configuration + @EnableStateMachine + public static class Config4 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .transitionConflightPolicy(TransitionConflightPolicy.PARENT); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S10) + .and() + .withStates() + .parent(TestStates.S10) + .initial(TestStates.S1011) + .stateEntry(TestStates.S1011, c -> c.getExtendedState().getVariables().put("error", true)) + .state(TestStates.S1012); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S10) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S10) + .target(TestStates.S1) + .guard(c -> c.getExtendedState().getVariables().containsKey("error")) + .and() + .withExternal() + .source(TestStates.S1011) + .target(TestStates.S1012); + } + } + + @Configuration + @EnableStateMachine + public static class Config5 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .transitionConflightPolicy(TransitionConflightPolicy.PARENT); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S10) + .and() + .withStates() + .parent(TestStates.S10) + .initial(TestStates.S1011) + .state(TestStates.S1012); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S10) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S10) + .target(TestStates.S1) + .guard(c -> c.getExtendedState().getVariables().containsKey("error")) + .and() + .withExternal() + .source(TestStates.S1011) + .target(TestStates.S1012); + } + } + + @Configuration + @EnableStateMachine + public static class Config6 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .transitionConflightPolicy(TransitionConflightPolicy.PARENT); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S10) + .and() + .withStates() + .parent(TestStates.S10) + .initial(TestStates.S1011) + .state(TestStates.S1012) + .state(TestStates.S2011); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S10) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S10) + .target(TestStates.S1) + .and() + .withExternal() + .source(TestStates.S1011) + .target(TestStates.S1012) + .and() + .withExternal() + .source(TestStates.S1012) + .target(TestStates.S2011); + } + } + + @Configuration + @EnableStateMachine + public static class Config7 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .transitionConflightPolicy(TransitionConflightPolicy.CHILD); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S10) + .and() + .withStates() + .parent(TestStates.S10) + .initial(TestStates.S1011) + .state(TestStates.S1012) + .state(TestStates.S2011); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S10) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S10) + .target(TestStates.S1) + .and() + .withExternal() + .source(TestStates.S1011) + .target(TestStates.S1012) + .and() + .withExternal() + .source(TestStates.S1012) + .target(TestStates.S2011); + } + } + + @Configuration + @EnableStateMachine + public static class Config8 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .transitionConflightPolicy(TransitionConflightPolicy.PARENT); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S10) + .junction(TestStates.SF) + .and() + .withStates() + .parent(TestStates.S10) + .initial(TestStates.S1011) + .state(TestStates.S1012) + .state(TestStates.S2011); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withJunction() + .source(TestStates.SF) + .last(TestStates.S1) + .and() + .withExternal() + .source(TestStates.S1) + .target(TestStates.S10) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S1011) + .target(TestStates.S1012) + .and() + .withExternal() + .source(TestStates.S10) + .target(TestStates.SF) + .and() + .withExternal() + .source(TestStates.S1012) + .target(TestStates.S2011); + } + } + + @Configuration + public static class StateMachineExecutorConfiguration { + + @Bean(name = StateMachineSystemConstants.TASK_EXECUTOR_BEAN_NAME) + public TaskExecutor stateMachineTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(4); + return executor; + } + } + + + static class TestListener extends StateMachineListenerAdapter { + + volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); + volatile int stateChangedCount = 0; + volatile CountDownLatch statesEnteredLatch = new CountDownLatch(1); + final ArrayList statesEntered = new ArrayList<>(); + + @Override + public void stateChanged(State from, State to) { + stateChangedCount++; + stateChangedLatch.countDown(); + } + + @Override + public void stateEntered(State state) { + statesEntered.add(state.getId()); + statesEnteredLatch.countDown(); + } + + public void reset(int c1) { + stateChangedLatch = new CountDownLatch(c1); + stateChangedCount = 0; + } + + public void reset(int c1, int c2) { + stateChangedLatch = new CountDownLatch(c1); + stateChangedCount = 0; + statesEnteredLatch = new CountDownLatch(c2); + statesEntered.clear(); + } + } +}