From de3a9341dedf92443e6fa1e8baf13c299535c51a Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Mon, 10 Oct 2016 08:54:28 +0100 Subject: [PATCH] Add support for choice states - Adding choice state construction and guard resolving. - Relates to #262 --- .../data/jpa/JpaRepositoryTests.java | 113 ++++++++++++++++++ .../jpa/src/test/resources/data6.json | 78 ++++++++++++ .../RepositoryStateMachineModelFactory.java | 55 +++++++-- 3 files changed, 233 insertions(+), 13 deletions(-) create mode 100644 spring-statemachine-data/jpa/src/test/resources/data6.json diff --git a/spring-statemachine-data/jpa/src/test/java/org/springframework/statemachine/data/jpa/JpaRepositoryTests.java b/spring-statemachine-data/jpa/src/test/java/org/springframework/statemachine/data/jpa/JpaRepositoryTests.java index f0407aac..f5ebe3fa 100644 --- a/spring-statemachine-data/jpa/src/test/java/org/springframework/statemachine/data/jpa/JpaRepositoryTests.java +++ b/spring-statemachine-data/jpa/src/test/java/org/springframework/statemachine/data/jpa/JpaRepositoryTests.java @@ -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 stateMachineFactory = context.getBean(StateMachineFactory.class); + StateMachine stateMachine = stateMachineFactory.getStateMachine(); + + StateMachineTestPlan plan = + StateMachineTestPlanBuilder.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 stateMachineFactory = context.getBean(StateMachineFactory.class); + StateMachine stateMachine = stateMachineFactory.getStateMachine(); + + StateMachineTestPlan plan = + StateMachineTestPlanBuilder.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 stateMachineFactory = context.getBean(StateMachineFactory.class); + StateMachine stateMachine = stateMachineFactory.getStateMachine(); + + StateMachineTestPlan plan = + StateMachineTestPlanBuilder.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 stateMachineFactory = context.getBean(StateMachineFactory.class); + StateMachine stateMachine = stateMachineFactory.getStateMachine(); + + StateMachineTestPlan plan = + StateMachineTestPlanBuilder.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 s30Guard() { + return new ChoiceGuard("s30"); + } + + @Bean + public Guard s31Guard() { + return new ChoiceGuard("s31"); + } + + @Bean + public Guard s32Guard() { + return new ChoiceGuard("s32"); + } + + } + @Configuration @EnableStateMachineFactory public static class FactoryConfig extends StateMachineConfigurerAdapter { @@ -388,4 +487,18 @@ public class JpaRepositoryTests extends AbstractJpaRepositoryTests { } } + private static class ChoiceGuard implements Guard { + + private final String match; + + public ChoiceGuard(String match) { + this.match = match; + } + + @Override + public boolean evaluate(StateContext context) { + return ObjectUtils.nullSafeEquals(match, context.getMessageHeaders().get("choice", String.class)); + } + } + } diff --git a/spring-statemachine-data/jpa/src/test/resources/data6.json b/spring-statemachine-data/jpa/src/test/resources/data6.json new file mode 100644 index 00000000..20e7f045 --- /dev/null +++ b/spring-statemachine-data/jpa/src/test/resources/data6.json @@ -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" + } +] diff --git a/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachineModelFactory.java b/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachineModelFactory.java index dd7f2658..132d3b53 100644 --- a/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachineModelFactory.java +++ b/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachineModelFactory.java @@ -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 statesData = new StatesData<>(stateDatas); Collection> transitionData = new ArrayList<>(); Collection> historys = new ArrayList>(); + Map>> choices = new HashMap>>(); for (RepositoryTransition t : transitionRepository.findByMachineId(machineId)) { Collection> actions = new ArrayList>(); @@ -172,28 +178,51 @@ public class RepositoryStateMachineModelFactory extends AbstractStateMachineMode TransitionKind kind = t.getKind(); - Guard 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 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> list = choices.get(t.getSource().getState()); + if (list == null) { + list = new LinkedList>(); + 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(t.getSource().getState(), t.getTarget().getState(), guard)); + } else { + list.addFirst(new ChoiceData(t.getSource().getState(), t.getTarget().getState(), guard)); + } + + } else if (t.getSource().getKind() == PseudoStateKind.HISTORY_SHALLOW) { historys.add(new HistoryData(t.getSource().getState(), t.getTarget().getState())); } else if (t.getSource().getKind() == PseudoStateKind.HISTORY_DEEP) { historys.add(new HistoryData(t.getSource().getState(), t.getTarget().getState())); } } - TransitionsData transitionsData = new TransitionsData<>(transitionData, null, null, null, null, null, null, historys); + + HashMap>> choicesCopy = new HashMap>>(); + choicesCopy.putAll(choices); + + TransitionsData transitionsData = new TransitionsData<>(transitionData, choicesCopy, null, null, null, null, null, historys); StateMachineModel stateMachineModel = new DefaultStateMachineModel<>(configurationData, statesData, transitionsData); return stateMachineModel; } + + private Guard resolveGuard(RepositoryTransition t) { + Guard 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; + } }