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 cac6636e..4068beb4 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 @@ -307,7 +307,7 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS return delegate; } - private BeanFactory resolveBeanFactory() { + protected BeanFactory resolveBeanFactory() { if (stateMachineModel.getConfigurationData().getBeanFactory() != null) { return stateMachineModel.getConfigurationData().getBeanFactory(); } else { @@ -315,7 +315,7 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS } } - private TaskExecutor resolveTaskExecutor() { + protected TaskExecutor resolveTaskExecutor() { if (stateMachineModel.getConfigurationData().getTaskExecutor() != null) { return stateMachineModel.getConfigurationData().getTaskExecutor(); } else { @@ -323,7 +323,7 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS } } - private TaskScheduler resolveTaskScheduler() { + protected TaskScheduler resolveTaskScheduler() { if (stateMachineModel.getConfigurationData().getTaskScheduler() != null) { return stateMachineModel.getConfigurationData().getTaskScheduler(); } else { @@ -486,7 +486,7 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS continue; } state = buildStateInternal(stateData.getState(), stateData.getDeferred(), stateData.getEntryActions(), - stateData.getExitActions(), pseudoState); + stateData.getExitActions(), stateData.getStateActions(), pseudoState); if (stateData.isInitial()) { initialState = state; initialAction = stateData.getInitialAction(); @@ -514,7 +514,7 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS } PseudoState pseudoState = new HistoryPseudoState(PseudoStateKind.HISTORY_SHALLOW, defaultStateHolder, containingStateHolder); state = buildStateInternal(stateData.getState(), stateData.getDeferred(), stateData.getEntryActions(), - stateData.getExitActions(), pseudoState); + stateData.getExitActions(), stateData.getStateActions(), pseudoState); states.add(state); stateMap.put(stateData.getState(), state); historyState = pseudoState; @@ -534,7 +534,7 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS } PseudoState pseudoState = new HistoryPseudoState(PseudoStateKind.HISTORY_DEEP, defaultStateHolder, containingStateHolder); state = buildStateInternal(stateData.getState(), stateData.getDeferred(), stateData.getEntryActions(), - stateData.getExitActions(), pseudoState); + stateData.getExitActions(), stateData.getStateActions(), pseudoState); states.add(state); stateMap.put(stateData.getState(), state); historyState = pseudoState; @@ -553,7 +553,7 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS } PseudoState pseudoState = new ChoicePseudoState(choices); state = buildStateInternal(stateData.getState(), stateData.getDeferred(), stateData.getEntryActions(), - stateData.getExitActions(), pseudoState); + stateData.getExitActions(), stateData.getStateActions(), pseudoState); states.add(state); stateMap.put(stateData.getState(), state); } else if (stateData.getPseudoStateKind() == PseudoStateKind.JUNCTION) { @@ -569,7 +569,7 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS } PseudoState pseudoState = new JunctionPseudoState(junctions); state = buildStateInternal(stateData.getState(), stateData.getDeferred(), stateData.getEntryActions(), - stateData.getExitActions(), pseudoState); + stateData.getExitActions(), stateData.getStateActions(), pseudoState); states.add(state); stateMap.put(stateData.getState(), state); } else if (stateData.getPseudoStateKind() == PseudoStateKind.ENTRY) { @@ -579,7 +579,7 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS if (s.equals(entry.getSource())) { PseudoState pseudoState = new EntryPseudoState(stateMap.get(entry.getTarget())); state = buildStateInternal(stateData.getState(), stateData.getDeferred(), stateData.getEntryActions(), - stateData.getExitActions(), pseudoState); + stateData.getExitActions(), stateData.getStateActions(), pseudoState); states.add(state); stateMap.put(stateData.getState(), state); break; @@ -596,7 +596,7 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS } PseudoState pseudoState = new ExitPseudoState(holder); state = buildStateInternal(stateData.getState(), stateData.getDeferred(), stateData.getEntryActions(), - stateData.getExitActions(), pseudoState); + stateData.getExitActions(), stateData.getStateActions(), pseudoState); states.add(state); stateMap.put(stateData.getState(), state); break; @@ -611,7 +611,7 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS } PseudoState pseudoState = new ForkPseudoState(forks); state = buildStateInternal(stateData.getState(), stateData.getDeferred(), stateData.getEntryActions(), - stateData.getExitActions(), pseudoState); + stateData.getExitActions(), stateData.getStateActions(), pseudoState); states.add(state); stateMap.put(stateData.getState(), state); } else if (stateData.getPseudoStateKind() == PseudoStateKind.JOIN) { @@ -655,7 +655,7 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS JoinPseudoState pseudoState = new JoinPseudoState(joins, joinTargets); state = buildStateInternal(stateData.getState(), stateData.getDeferred(), stateData.getEntryActions(), - stateData.getExitActions(), pseudoState); + stateData.getExitActions(), stateData.getStateActions(), pseudoState); states.add(state); stateMap.put(stateData.getState(), state); } @@ -735,8 +735,9 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS Boolean contextEventsEnabled, BeanFactory beanFactory, TaskExecutor taskExecutor, TaskScheduler taskScheduler, String beanName, String machineId); - protected abstract State buildStateInternal(S id, Collection deferred, Collection> entryActions, Collection> exitActions, - PseudoState pseudoState); + protected abstract State buildStateInternal(S id, Collection deferred, + Collection> entryActions, Collection> exitActions, + Collection> stateActions, PseudoState pseudoState); private Iterator>> buildStateDataIterator() { Tree> tree = new Tree>(); 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 4054da75..8ac2fbcc 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 @@ -84,9 +84,23 @@ public class ObjectStateMachineFactory extends AbstractStateMachineFactory } @Override - protected State buildStateInternal(S id, Collection deferred, Collection> entryActions, - Collection> exitActions, PseudoState pseudoState) { - return new ObjectState(id, deferred, entryActions, exitActions, pseudoState); + protected State buildStateInternal(S id, Collection deferred, + Collection> entryActions, Collection> exitActions, + Collection> stateActions, PseudoState pseudoState) { + ObjectState objectState = new ObjectState(id, deferred, entryActions, exitActions, stateActions, pseudoState, null, null); + BeanFactory beanFactory = resolveBeanFactory(); + if (beanFactory != null) { + objectState.setBeanFactory(beanFactory); + } + TaskExecutor taskExecutor = resolveTaskExecutor(); + if (taskExecutor != null) { + objectState.setTaskExecutor(taskExecutor); + } + TaskScheduler taskScheduler = resolveTaskScheduler(); + if (taskScheduler != null) { + objectState.setTaskScheduler(taskScheduler); + } + return objectState; } @Override diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java index ca136bc2..4ea72cd2 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java @@ -148,10 +148,26 @@ public class DefaultStateConfigurer return this; } + @Override + public StateConfigurer state(S state, Collection> stateActions) { + addIncomplete(null, state, null, null, null, stateActions); + return this; + } + + @Override + public StateConfigurer state(S state, Action stateAction) { + Collection> stateActions = null; + if (stateAction != null) { + stateActions = new ArrayList>(1); + stateActions.add(stateAction); + } + return state(state, stateActions); + } + @Override public StateConfigurer state(S state, Collection> entryActions, Collection> exitActions) { - addIncomplete(null, state, null, entryActions, exitActions); + addIncomplete(null, state, null, entryActions, exitActions, null); return this; } @@ -177,7 +193,7 @@ public class DefaultStateConfigurer if (deferred != null) { d = Arrays.asList(deferred); } - addIncomplete(null, state, d, null, null); + addIncomplete(null, state, d, null, null, null); return this; } @@ -240,7 +256,8 @@ public class DefaultStateConfigurer } private void addIncomplete(Object parent, S state, Collection deferred, - Collection> entryActions, Collection> exitActions) { + Collection> entryActions, Collection> exitActions, + Collection> stateActions) { StateData stateData = incomplete.get(state); if (stateData == null) { stateData = new StateData(parent, region, state, deferred, entryActions, exitActions); @@ -261,6 +278,9 @@ public class DefaultStateConfigurer if (stateData.getExitActions() == null) { stateData.setExitActions(exitActions); } + if (stateData.getStateActions() == null) { + stateData.setStateActions(stateActions); + } } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java index a417e862..61d0e0a0 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java @@ -91,6 +91,24 @@ public interface StateConfigurer extends */ StateConfigurer state(S state, StateMachineFactory stateMachineFactory); + /** + * Specify a state {@code S} with state {@link Action}s. + * + * @param state the state + * @param stateActions the state actions + * @return configurer for chaining + */ + StateConfigurer state(S state, Collection> stateActions); + + /** + * Specify a state {@code S} with state {@link Action}. + * + * @param state the state + * @param stateAction the state action + * @return configurer for chaining + */ + StateConfigurer state(S state, Action stateAction); + /** * Specify a state {@code S} with entry and exit {@link Action}s. * diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/StateData.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/StateData.java index 009a61fa..cdac5fd2 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/StateData.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/StateData.java @@ -44,6 +44,7 @@ public class StateData { private Collection deferred; private Collection> entryActions; private Collection> exitActions; + private Collection> stateActions; private boolean initial = false; private Action initialAction; private boolean end = false; @@ -243,6 +244,24 @@ public class StateData { this.exitActions = exitActions; } + /** + * Gets the state actions. + * + * @return the state actions + */ + public Collection> getStateActions() { + return stateActions; + } + + /** + * Sets the state actions. + * + * @param stateActions the state actions + */ + public void setStateActions(Collection> stateActions) { + this.stateActions = stateActions; + } + /** * Gets the parent. * diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractSimpleState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractSimpleState.java index 32a97578..af149186 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractSimpleState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractSimpleState.java @@ -127,6 +127,26 @@ public abstract class AbstractSimpleState extends AbstractState { this.ids.add(id); } + /** + * Instantiates a new abstract simple state. + * + * @param id the id + * @param deferred the deferred + * @param entryActions the entry actions + * @param exitActions the exit actions + * @param stateActions the state actions + * @param pseudoState the pseudo state + * @param regions the regions + * @param submachine the submachine + */ + public AbstractSimpleState(S id, Collection deferred, Collection> entryActions, + Collection> exitActions, Collection> stateActions, + PseudoState pseudoState, Collection> regions, StateMachine submachine) { + super(id, deferred, entryActions, exitActions, stateActions, pseudoState, regions, submachine); + this.ids = new ArrayList(); + this.ids.add(id); + } + @Override public Collection getIds() { return Collections.unmodifiableCollection(ids); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java index a38ad5c6..0d7ddb19 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java @@ -17,13 +17,19 @@ package org.springframework.statemachine.state; import java.util.ArrayList; import java.util.Collection; +import java.util.Date; import java.util.List; +import java.util.concurrent.ScheduledFuture; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.messaging.Message; +import org.springframework.scheduling.TaskScheduler; import org.springframework.statemachine.StateContext; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.region.Region; +import org.springframework.statemachine.support.LifecycleObjectSupport; import org.springframework.statemachine.trigger.Trigger; /** @@ -34,17 +40,21 @@ import org.springframework.statemachine.trigger.Trigger; * @param the type of state * @param the type of event */ -public abstract class AbstractState implements State { +public abstract class AbstractState extends LifecycleObjectSupport implements State { + + private static final Log log = LogFactory.getLog(AbstractState.class); private final S id; private final PseudoState pseudoState; private final Collection deferred; private final Collection> entryActions; private final Collection> exitActions; + private final Collection> stateActions; private final Collection> regions = new ArrayList>(); private final StateMachine submachine; private List> triggers = new ArrayList>(); private final CompositeStateListener stateListener = new CompositeStateListener(); + private final List> cancellableActions = new ArrayList<>(); /** * Instantiates a new abstract state. @@ -134,13 +144,32 @@ public abstract class AbstractState implements State { * @param regions the regions * @param submachine the submachine */ - private AbstractState(S id, Collection deferred, Collection> entryActions, + public AbstractState(S id, Collection deferred, Collection> entryActions, Collection> exitActions, PseudoState pseudoState, Collection> regions, StateMachine submachine) { + this(id, deferred, entryActions, exitActions, null, pseudoState, regions, submachine); + } + + /** + * Instantiates a new abstract state. + * + * @param id the state identifier + * @param deferred the deferred + * @param entryActions the entry actions + * @param exitActions the exit actions + * @param stateActions the state actions + * @param pseudoState the pseudo state + * @param regions the regions + * @param submachine the submachine + */ + public AbstractState(S id, Collection deferred, Collection> entryActions, + Collection> exitActions, Collection> stateActions, + PseudoState pseudoState, Collection> regions, StateMachine submachine) { this.id = id; this.deferred = deferred; this.entryActions = entryActions; this.exitActions = exitActions; + this.stateActions = stateActions; this.pseudoState = pseudoState; // use of private ctor should prevent user to @@ -163,6 +192,7 @@ public abstract class AbstractState implements State { @Override public void exit(StateContext context) { + cancelStateActions(); stateListener.onExit(context); for (Trigger trigger : triggers) { trigger.disarm(); @@ -175,6 +205,7 @@ public abstract class AbstractState implements State { for (Trigger trigger : triggers) { trigger.arm(); } + scheduleStateActions(context); } @Override @@ -256,19 +287,79 @@ public abstract class AbstractState implements State { return regions; } + /** + * Sets the triggers. + * + * @param triggers the triggers + */ public void setTriggers(List> triggers) { this.triggers = triggers; } + /** + * Gets the triggers. + * + * @return the triggers + */ public List> getTriggers() { return triggers; } - @Override - public String toString() { - return "AbstractState [id=" + id + ", pseudoState=" + pseudoState + ", deferred=" + deferred - + ", entryActions=" + entryActions + ", exitActions=" + exitActions + ", regions=" + regions - + ", submachine=" + submachine + "]"; + /** + * Cancel existing state actions and clear list. + */ + protected void cancelStateActions() { + for (ScheduledFuture future : cancellableActions) { + future.cancel(true); + } + cancellableActions.clear(); } + /** + * Schedule state actions and store futures into list to + * be cancelled. + * + * @param context the context + */ + protected void scheduleStateActions(StateContext context) { + if (stateActions == null) { + return; + } + for (Action action : stateActions) { + ScheduledFuture future = scheduleAction(action, context); + if (future != null) { + cancellableActions.add(future); + } + } + } + + /** + * Schedule action and return future which can be used to cancel it. + * + * @param action the action + * @param context the context + * @return the scheduled future + */ + protected ScheduledFuture scheduleAction(final Action action, final StateContext context) { + TaskScheduler taskScheduler = getTaskScheduler(); + if (taskScheduler == null) { + log.error("Unable to schedule action as taskSchedule is not set, action=[" + action + "]"); + return null; + } + ScheduledFuture future = taskScheduler.schedule(new Runnable() { + + @Override + public void run() { + action.execute(context); + } + }, new Date()); + return future; + } + + @Override + public String toString() { + return "AbstractState [id=" + id + ", pseudoState=" + pseudoState + ", deferred=" + deferred + ", entryActions=" + + entryActions + ", exitActions=" + exitActions + ", stateActions=" + stateActions + ", regions=" + + regions + ", submachine=" + submachine + "]"; + } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ObjectState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ObjectState.java index f0521220..6653f967 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ObjectState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ObjectState.java @@ -121,6 +121,24 @@ public class ObjectState extends AbstractSimpleState { super(id, deferred, entryActions, exitActions, pseudoState, submachine); } + /** + * Instantiates a new object state. + * + * @param id the id + * @param deferred the deferred + * @param entryActions the entry actions + * @param exitActions the exit actions + * @param stateActions the state actions + * @param pseudoState the pseudo state + * @param regions the regions + * @param submachine the submachine + */ + public ObjectState(S id, Collection deferred, Collection> entryActions, + Collection> exitActions, Collection> stateActions, + PseudoState pseudoState, Collection> regions, StateMachine submachine) { + super(id, deferred, entryActions, exitActions, stateActions, pseudoState, regions, submachine); + } + @Override public void exit(StateContext context) { super.exit(context); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java index c5e726d4..5d7f3c78 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java @@ -18,6 +18,7 @@ package org.springframework.statemachine; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -165,6 +166,8 @@ public abstract class AbstractStateMachineTests { long sleep; long now; + public AtomicBoolean interrupted = new AtomicBoolean(false); + public CountDownLatch interruptedLatch = new CountDownLatch(1); public TestSleepAction(long sleep) { super(); @@ -178,6 +181,9 @@ public abstract class AbstractStateMachineTests { try { Thread.sleep(sleep); } catch (InterruptedException e) { + System.out.println("XXXXX " + e); + interrupted.set(true); + interruptedLatch.countDown(); } } super.execute(context); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/action/StateDoActivityActionTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/action/StateDoActivityActionTests.java new file mode 100644 index 00000000..1bb746a0 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/action/StateDoActivityActionTests.java @@ -0,0 +1,252 @@ +/* + * Copyright 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. + * 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.action; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +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.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.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +@SuppressWarnings("unchecked") +public class StateDoActivityActionTests extends AbstractStateMachineTests { + + @Test + public void testSimpleStateActions() throws Exception { + context.register(Config1.class); + context.refresh(); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestAction testActionS1 = context.getBean("testActionS1", TestAction.class); + TestAction testActionS2 = context.getBean("testActionS2", TestAction.class); + + assertThat(machine, notNullValue()); + machine.start(); + + machine.sendEvent(TestEvents.E1); + assertThat(testActionS1.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true)); + + machine.sendEvent(TestEvents.E2); + assertThat(testActionS2.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true)); + } + + @Test + public void testExitAbortsAction() throws Exception { + context.register(Config2.class); + context.refresh(); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestSleepAction testActionS1 = context.getBean("testActionS1", TestSleepAction.class); + TestSleepAction testActionS2 = context.getBean("testActionS2", TestSleepAction.class); + + assertThat(machine, notNullValue()); + machine.start(); + + machine.sendEvent(TestEvents.E1); + assertThat(testActionS1.interruptedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(testActionS1.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true)); + + machine.sendEvent(TestEvents.E2); + assertThat(testActionS2.interruptedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(testActionS2.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S3)); + } + + @Test + public void testInternalTransitionDoesNotAbort() throws Exception { + context.register(Config3.class); + context.refresh(); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestSleepAction testActionS1 = context.getBean("testActionS1", TestSleepAction.class); + TestSleepAction testActionS2 = context.getBean("testActionS2", TestSleepAction.class); + TestAction testActionS1I = context.getBean("testActionS1I", TestAction.class); + TestAction testActionS2I = context.getBean("testActionS2I", TestAction.class); + + assertThat(machine, notNullValue()); + machine.start(); + machine.sendEvent(TestEvents.E3); + assertThat(testActionS1I.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(testActionS1.interruptedLatch.await(2, TimeUnit.SECONDS), is(false)); + machine.sendEvent(TestEvents.E1); + + machine.sendEvent(TestEvents.E4); + assertThat(testActionS2I.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(testActionS2.interruptedLatch.await(2, TimeUnit.SECONDS), is(false)); + machine.sendEvent(TestEvents.E2); + + assertThat(testActionS1.interruptedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(testActionS2.interruptedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(testActionS1.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(testActionS2.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S3)); + } + + @Configuration + @EnableStateMachine + static class Config1 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S1, testActionS1()) + .state(TestStates.S2, testActionS2()) + .state(TestStates.S3); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S2) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S2) + .target(TestStates.S3) + .event(TestEvents.E2); + } + + @Bean + public TestAction testActionS1() { + return new TestAction(); + } + + @Bean + public TestAction testActionS2() { + return new TestAction(); + } + } + + @Configuration + @EnableStateMachine + static class Config2 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S1, testActionS1()) + .state(TestStates.S2, testActionS2()) + .state(TestStates.S3); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S2) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S2) + .target(TestStates.S3) + .event(TestEvents.E2); + } + + @Bean + public TestSleepAction testActionS1() { + return new TestSleepAction(5000); + } + + @Bean + public TestSleepAction testActionS2() { + return new TestSleepAction(5000); + } + } + + @Configuration + @EnableStateMachine + static class Config3 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S1, testActionS1()) + .state(TestStates.S2, testActionS2()) + .state(TestStates.S3); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S2) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S2) + .target(TestStates.S3) + .event(TestEvents.E2) + .and() + .withInternal() + .source(TestStates.S1) + .event(TestEvents.E3) + .action(testActionS1I()) + .and() + .withInternal() + .source(TestStates.S2) + .event(TestEvents.E4) + .action(testActionS2I()); + } + + @Bean + public TestSleepAction testActionS1() { + return new TestSleepAction(5000); + } + + @Bean + public TestSleepAction testActionS2() { + return new TestSleepAction(5000); + } + + @Bean + public TestAction testActionS1I() { + return new TestAction(); + } + + @Bean + public TestAction testActionS2I() { + return new TestAction(); + } + } + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } +} diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/support/UmlModelParser.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/support/UmlModelParser.java index 372ba0a1..b10e3342 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/support/UmlModelParser.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/support/UmlModelParser.java @@ -438,6 +438,26 @@ public class UmlModelParser { } } } + if (state.getDoActivity() instanceof OpaqueBehavior) { + String beanId = UmlUtils.resolveBodyByLanguage(LANGUAGE_BEAN, (OpaqueBehavior)state.getDoActivity()); + if (StringUtils.hasText(beanId)) { + Action bean = resolver.resolveAction(beanId); + if (bean != null) { + ArrayList> stateActions = new ArrayList>(); + stateActions.add(bean); + stateData.setStateActions(stateActions); + } + } else { + String expression = UmlUtils.resolveBodyByLanguage(LANGUAGE_SPEL, (OpaqueBehavior)state.getDoActivity()); + if (StringUtils.hasText(expression)) { + SpelExpressionParser parser = new SpelExpressionParser( + new SpelParserConfiguration(SpelCompilerMode.MIXED, null)); + ArrayList> stateActions = new ArrayList>(); + stateActions.add(new SpelExpressionAction(parser.parseExpression(expression))); + stateData.setExitActions(stateActions); + } + } + } if (state.getEntry() instanceof Activity) { String beanId = ((Activity)state.getEntry()).getName(); Action bean = resolver.resolveAction(beanId); @@ -456,6 +476,15 @@ public class UmlModelParser { stateData.setExitActions(exits); } } + if (state.getDoActivity() instanceof Activity) { + String beanId = ((Activity)state.getDoActivity()).getName(); + Action bean = resolver.resolveAction(beanId); + if (bean != null) { + ArrayList> stateActions = new ArrayList>(); + stateActions.add(bean); + stateData.setStateActions(stateActions); + } + } return stateData; } diff --git a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java index 4f4c10b5..24989a3d 100644 --- a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java +++ b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java @@ -658,6 +658,24 @@ public class UmlStateMachineModelFactoryTests extends AbstractUmlTests { assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S21", "S31")); } + @Test + @SuppressWarnings("unchecked") + public void testSimpleStateActions() throws Exception { + context.register(Config22.class); + context.refresh(); + StateMachine stateMachine = context.getBean(StateMachine.class); + LatchAction e1Action = context.getBean("e1Action", LatchAction.class); + LatchAction e2Action = context.getBean("e2Action", LatchAction.class); + stateMachine.start(); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S1")); + stateMachine.sendEvent("E1"); + assertThat(e1Action.latch.await(1, TimeUnit.SECONDS), is(true)); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2")); + stateMachine.sendEvent("E2"); + assertThat(e2Action.latch.await(1, TimeUnit.SECONDS), is(true)); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S3")); + } + @Configuration @EnableStateMachine public static class Config2 extends StateMachineConfigurerAdapter { @@ -1068,6 +1086,33 @@ public class UmlStateMachineModelFactoryTests extends AbstractUmlTests { } } + @Configuration + @EnableStateMachine + public static class Config22 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/uml/simple-state-actions.uml"); + } + + @Bean + public LatchAction e1Action() { + return new LatchAction(); + } + + @Bean + public LatchAction e2Action() { + return new LatchAction(); + } + } + public static class LatchAction implements Action { CountDownLatch latch = new CountDownLatch(1); @Override diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-state-actions.di b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-state-actions.di new file mode 100644 index 00000000..bf9abab3 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-state-actions.di @@ -0,0 +1,2 @@ + + diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-state-actions.notation b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-state-actions.notation new file mode 100644 index 00000000..476fd524 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-state-actions.notation @@ -0,0 +1,142 @@ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-state-actions.uml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-state-actions.uml new file mode 100644 index 00000000..5e1ec881 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-state-actions.uml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + bean + e1Action + + + + + bean + e2Action + + + + + + + + + + +