Add support for choice states
- Adding choice state construction and guard resolving. - Relates to #262
This commit is contained in:
@@ -31,6 +31,8 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.statemachine.StateContext;
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
import org.springframework.statemachine.config.EnableStateMachineFactory;
|
||||
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
|
||||
@@ -43,9 +45,11 @@ import org.springframework.statemachine.data.RepositoryTransition;
|
||||
import org.springframework.statemachine.data.StateRepository;
|
||||
import org.springframework.statemachine.data.TransitionRepository;
|
||||
import org.springframework.statemachine.data.support.StateMachineJackson2RepositoryPopulatorFactoryBean;
|
||||
import org.springframework.statemachine.guard.Guard;
|
||||
import org.springframework.statemachine.test.StateMachineTestPlan;
|
||||
import org.springframework.statemachine.test.StateMachineTestPlanBuilder;
|
||||
import org.springframework.statemachine.transition.TransitionKind;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
public class JpaRepositoryTests extends AbstractJpaRepositoryTests {
|
||||
|
||||
@@ -281,6 +285,74 @@ public class JpaRepositoryTests extends AbstractJpaRepositoryTests {
|
||||
plan.test();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testMachine6First() throws Exception {
|
||||
context.register(Config6.class, FactoryConfig.class);
|
||||
context.refresh();
|
||||
StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
|
||||
StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();
|
||||
|
||||
StateMachineTestPlan<String, String> plan =
|
||||
StateMachineTestPlanBuilder.<String, String>builder()
|
||||
.stateMachine(stateMachine)
|
||||
.step().expectStates("SI").and()
|
||||
.step().sendEvent(MessageBuilder.withPayload("E1").setHeader("choice", "s30").build()).expectStates("S30").and()
|
||||
.build();
|
||||
plan.test();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testMachine6Then1() throws Exception {
|
||||
context.register(Config6.class, FactoryConfig.class);
|
||||
context.refresh();
|
||||
StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
|
||||
StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();
|
||||
|
||||
StateMachineTestPlan<String, String> plan =
|
||||
StateMachineTestPlanBuilder.<String, String>builder()
|
||||
.stateMachine(stateMachine)
|
||||
.step().expectStates("SI").and()
|
||||
.step().sendEvent(MessageBuilder.withPayload("E1").setHeader("choice", "s31").build()).expectStates("S31").and()
|
||||
.build();
|
||||
plan.test();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testMachine6Then2() throws Exception {
|
||||
context.register(Config6.class, FactoryConfig.class);
|
||||
context.refresh();
|
||||
StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
|
||||
StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();
|
||||
|
||||
StateMachineTestPlan<String, String> plan =
|
||||
StateMachineTestPlanBuilder.<String, String>builder()
|
||||
.stateMachine(stateMachine)
|
||||
.step().expectStates("SI").and()
|
||||
.step().sendEvent(MessageBuilder.withPayload("E1").setHeader("choice", "s32").build()).expectStates("S32").and()
|
||||
.build();
|
||||
plan.test();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testMachine6Last() throws Exception {
|
||||
context.register(Config6.class, FactoryConfig.class);
|
||||
context.refresh();
|
||||
StateMachineFactory<String, String> stateMachineFactory = context.getBean(StateMachineFactory.class);
|
||||
StateMachine<String, String> stateMachine = stateMachineFactory.getStateMachine();
|
||||
|
||||
StateMachineTestPlan<String, String> plan =
|
||||
StateMachineTestPlanBuilder.<String, String>builder()
|
||||
.stateMachine(stateMachine)
|
||||
.step().expectStates("SI").and()
|
||||
.step().sendEvent(MessageBuilder.withPayload("E1").build()).expectStates("S33").and()
|
||||
.build();
|
||||
plan.test();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPopulate1() {
|
||||
context.register(Config2.class);
|
||||
@@ -365,6 +437,33 @@ public class JpaRepositoryTests extends AbstractJpaRepositoryTests {
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
static class Config6 {
|
||||
|
||||
@Bean
|
||||
public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
|
||||
StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
|
||||
factoryBean.setResources(new Resource[]{new ClassPathResource("data6.json")});
|
||||
return factoryBean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Guard<String, String> s30Guard() {
|
||||
return new ChoiceGuard("s30");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Guard<String, String> s31Guard() {
|
||||
return new ChoiceGuard("s31");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Guard<String, String> s32Guard() {
|
||||
return new ChoiceGuard("s32");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachineFactory
|
||||
public static class FactoryConfig extends StateMachineConfigurerAdapter<String, String> {
|
||||
@@ -388,4 +487,18 @@ public class JpaRepositoryTests extends AbstractJpaRepositoryTests {
|
||||
}
|
||||
}
|
||||
|
||||
private static class ChoiceGuard implements Guard<String, String> {
|
||||
|
||||
private final String match;
|
||||
|
||||
public ChoiceGuard(String match) {
|
||||
this.match = match;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(StateContext<String, String> context) {
|
||||
return ObjectUtils.nullSafeEquals(match, context.getMessageHeaders().get("choice", String.class));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
78
spring-statemachine-data/jpa/src/test/resources/data6.json
Normal file
78
spring-statemachine-data/jpa/src/test/resources/data6.json
Normal file
@@ -0,0 +1,78 @@
|
||||
[
|
||||
{
|
||||
"@id": "1",
|
||||
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
|
||||
"initial": true,
|
||||
"state": "SI"
|
||||
},
|
||||
{
|
||||
"@id": "2",
|
||||
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
|
||||
"initial": false,
|
||||
"state": "S3",
|
||||
"kind": "CHOICE"
|
||||
},
|
||||
{
|
||||
"@id": "3",
|
||||
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
|
||||
"initial": false,
|
||||
"state": "S30"
|
||||
},
|
||||
{
|
||||
"@id": "4",
|
||||
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
|
||||
"initial": false,
|
||||
"state": "S31"
|
||||
},
|
||||
{
|
||||
"@id": "5",
|
||||
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
|
||||
"initial": false,
|
||||
"state": "S32"
|
||||
},
|
||||
{
|
||||
"@id": "6",
|
||||
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
|
||||
"initial": false,
|
||||
"state": "S33"
|
||||
},
|
||||
{
|
||||
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
|
||||
"source": "1",
|
||||
"target": "2",
|
||||
"event": "E1",
|
||||
"kind": "EXTERNAL"
|
||||
},
|
||||
{
|
||||
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
|
||||
"source": "2",
|
||||
"target": "3",
|
||||
"guard": {
|
||||
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryGuard",
|
||||
"name": "s30Guard"
|
||||
}
|
||||
},
|
||||
{
|
||||
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
|
||||
"source": "2",
|
||||
"target": "4",
|
||||
"guard": {
|
||||
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryGuard",
|
||||
"name": "s31Guard"
|
||||
}
|
||||
},
|
||||
{
|
||||
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
|
||||
"source": "2",
|
||||
"target": "5",
|
||||
"guard": {
|
||||
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryGuard",
|
||||
"name": "s32Guard"
|
||||
}
|
||||
},
|
||||
{
|
||||
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
|
||||
"source": "2",
|
||||
"target": "6"
|
||||
}
|
||||
]
|
||||
@@ -17,6 +17,10 @@ package org.springframework.statemachine.data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.expression.spel.SpelCompilerMode;
|
||||
@@ -25,6 +29,7 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.statemachine.action.Action;
|
||||
import org.springframework.statemachine.action.SpelExpressionAction;
|
||||
import org.springframework.statemachine.config.model.AbstractStateMachineModelFactory;
|
||||
import org.springframework.statemachine.config.model.ChoiceData;
|
||||
import org.springframework.statemachine.config.model.ConfigurationData;
|
||||
import org.springframework.statemachine.config.model.DefaultStateMachineModel;
|
||||
import org.springframework.statemachine.config.model.HistoryData;
|
||||
@@ -149,6 +154,7 @@ public class RepositoryStateMachineModelFactory extends AbstractStateMachineMode
|
||||
StatesData<String, String> statesData = new StatesData<>(stateDatas);
|
||||
Collection<TransitionData<String, String>> transitionData = new ArrayList<>();
|
||||
Collection<HistoryData<String, String>> historys = new ArrayList<HistoryData<String, String>>();
|
||||
Map<String, LinkedList<ChoiceData<String, String>>> choices = new HashMap<String, LinkedList<ChoiceData<String,String>>>();
|
||||
for (RepositoryTransition t : transitionRepository.findByMachineId(machineId)) {
|
||||
|
||||
Collection<Action<String, String>> actions = new ArrayList<Action<String, String>>();
|
||||
@@ -172,28 +178,51 @@ public class RepositoryStateMachineModelFactory extends AbstractStateMachineMode
|
||||
|
||||
TransitionKind kind = t.getKind();
|
||||
|
||||
Guard<String, String> guard = null;
|
||||
RepositoryGuard repositoryGuard = t.getGuard();
|
||||
if (repositoryGuard != null) {
|
||||
if (StringUtils.hasText(repositoryGuard.getName())) {
|
||||
guard = resolveGuard(repositoryGuard.getName());
|
||||
} else if (StringUtils.hasText(repositoryGuard.getSpel())) {
|
||||
SpelExpressionParser parser = new SpelExpressionParser(
|
||||
new SpelParserConfiguration(SpelCompilerMode.MIXED, null));
|
||||
guard = new SpelExpressionGuard<>(parser.parseExpression(repositoryGuard.getSpel()));
|
||||
}
|
||||
}
|
||||
Guard<String, String> guard = resolveGuard(t);
|
||||
transitionData.add(new TransitionData<>(t.getSource().getState(), t.getTarget().getState(), t.getEvent(), actions, guard, kind != null ? kind : TransitionKind.EXTERNAL));
|
||||
|
||||
if (t.getSource().getKind() == PseudoStateKind.HISTORY_SHALLOW) {
|
||||
if (t.getSource().getKind() == PseudoStateKind.CHOICE) {
|
||||
LinkedList<ChoiceData<String, String>> list = choices.get(t.getSource().getState());
|
||||
if (list == null) {
|
||||
list = new LinkedList<ChoiceData<String, String>>();
|
||||
choices.put(t.getSource().getState(), list);
|
||||
}
|
||||
guard = resolveGuard(t);
|
||||
// we want null guards to be at the end
|
||||
if (guard == null) {
|
||||
list.addLast(new ChoiceData<String, String>(t.getSource().getState(), t.getTarget().getState(), guard));
|
||||
} else {
|
||||
list.addFirst(new ChoiceData<String, String>(t.getSource().getState(), t.getTarget().getState(), guard));
|
||||
}
|
||||
|
||||
} else if (t.getSource().getKind() == PseudoStateKind.HISTORY_SHALLOW) {
|
||||
historys.add(new HistoryData<String, String>(t.getSource().getState(), t.getTarget().getState()));
|
||||
} else if (t.getSource().getKind() == PseudoStateKind.HISTORY_DEEP) {
|
||||
historys.add(new HistoryData<String, String>(t.getSource().getState(), t.getTarget().getState()));
|
||||
}
|
||||
}
|
||||
TransitionsData<String, String> transitionsData = new TransitionsData<>(transitionData, null, null, null, null, null, null, historys);
|
||||
|
||||
HashMap<String, List<ChoiceData<String, String>>> choicesCopy = new HashMap<String, List<ChoiceData<String, String>>>();
|
||||
choicesCopy.putAll(choices);
|
||||
|
||||
TransitionsData<String, String> transitionsData = new TransitionsData<>(transitionData, choicesCopy, null, null, null, null, null, historys);
|
||||
|
||||
StateMachineModel<String, String> stateMachineModel = new DefaultStateMachineModel<>(configurationData, statesData, transitionsData);
|
||||
return stateMachineModel;
|
||||
}
|
||||
|
||||
private Guard<String, String> resolveGuard(RepositoryTransition t) {
|
||||
Guard<String, String> guard = null;
|
||||
RepositoryGuard repositoryGuard = t.getGuard();
|
||||
if (repositoryGuard != null) {
|
||||
if (StringUtils.hasText(repositoryGuard.getName())) {
|
||||
guard = resolveGuard(repositoryGuard.getName());
|
||||
} else if (StringUtils.hasText(repositoryGuard.getSpel())) {
|
||||
SpelExpressionParser parser = new SpelExpressionParser(
|
||||
new SpelParserConfiguration(SpelCompilerMode.MIXED, null));
|
||||
guard = new SpelExpressionGuard<>(parser.parseExpression(repositoryGuard.getSpel()));
|
||||
}
|
||||
}
|
||||
return guard;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user