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 bee6f634..6f6ce7c8 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 @@ -46,6 +46,7 @@ import org.springframework.statemachine.config.model.EntryData; import org.springframework.statemachine.config.model.ExitData; import org.springframework.statemachine.config.model.HistoryData; import org.springframework.statemachine.config.model.JunctionData; +import org.springframework.statemachine.config.model.MalformedConfigurationException; import org.springframework.statemachine.config.model.StateData; import org.springframework.statemachine.config.model.StateMachineModel; import org.springframework.statemachine.config.model.StateMachineModelFactory; @@ -665,6 +666,9 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS State defaultState = null; S s = stateData.getState(); Collection> historys = stateMachineTransitions.getHistorys(); + if (historys == null) { + throw new MalformedConfigurationException("No transitions for state " + s); + } for (HistoryData history : historys) { if (history.getSource().equals(s)) { defaultState = stateMap.get(history.getTarget()); @@ -685,6 +689,9 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS State defaultState = null; S s = stateData.getState(); Collection> historys = stateMachineTransitions.getHistorys(); + if (historys == null) { + throw new MalformedConfigurationException("No transitions for state " + s); + } for (HistoryData history : historys) { if (history.getSource().equals(s)) { defaultState = stateMap.get(history.getTarget()); @@ -706,6 +713,9 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS if (stateData.getPseudoStateKind() == PseudoStateKind.CHOICE) { S s = stateData.getState(); List> list = stateMachineTransitions.getChoices().get(s); + if (list == null) { + throw new MalformedConfigurationException("No transitions for state " + s); + } List> choices = new ArrayList>(); for (ChoiceData c : list) { StateHolder holder = new StateHolder(stateMap.get(c.getTarget())); @@ -723,6 +733,9 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS S s = stateData.getState(); List> list = stateMachineTransitions.getJunctions().get(s); List> junctions = new ArrayList>(); + if (list == null) { + throw new MalformedConfigurationException("No transitions for state " + s); + } for (JunctionData c : list) { StateHolder holder = new StateHolder(stateMap.get(c.getTarget())); if (holder.getState() == null) { @@ -738,6 +751,9 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS } else if (stateData.getPseudoStateKind() == PseudoStateKind.ENTRY) { S s = stateData.getState(); Collection> entrys = stateMachineTransitions.getEntrys(); + if (entrys == null) { + throw new MalformedConfigurationException("No transitions for state " + s); + } for (EntryData entry : entrys) { if (s.equals(entry.getSource())) { PseudoState pseudoState = new EntryPseudoState(stateMap.get(entry.getTarget())); @@ -751,6 +767,9 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS } else if (stateData.getPseudoStateKind() == PseudoStateKind.EXIT) { S s = stateData.getState(); Collection> exits = stateMachineTransitions.getExits(); + if (exits == null) { + throw new MalformedConfigurationException("No transitions for state " + s); + } for (ExitData entry : exits) { if (s.equals(entry.getSource())) { StateHolder holder = new StateHolder(stateMap.get(entry.getTarget())); @@ -769,6 +788,9 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS S s = stateData.getState(); List list = stateMachineTransitions.getForks().get(s); List> forks = new ArrayList>(); + if (list == null) { + throw new MalformedConfigurationException("No transitions for state " + s); + } for (S fs : list) { forks.add(stateMap.get(fs)); } @@ -780,6 +802,9 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS } else if (stateData.getPseudoStateKind() == PseudoStateKind.JOIN) { S s = stateData.getState(); List list = stateMachineTransitions.getJoins().get(s); + if (list == null) { + throw new MalformedConfigurationException("No transitions for state " + s); + } List>> joins = new ArrayList>>(); // if join source is an orthogonal state, assume we're joining by region end states diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/ConfigurationErrorTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/ConfigurationErrorTests.java index 38d9ccef..2d0928c5 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/ConfigurationErrorTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/ConfigurationErrorTests.java @@ -15,6 +15,7 @@ */ package org.springframework.statemachine.config; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -121,6 +122,26 @@ public class ConfigurationErrorTests extends AbstractStateMachineTests { assertThat(verifier.latch.await(2, TimeUnit.SECONDS), is(true)); } + @Test + public void testPseudostateNoTransition() throws Exception { + Exception exception = assertThrows(Exception.class, ()-> { + Builder builder = StateMachineBuilder.builder(); + builder.configureStates() + .withStates() + .initial("S1") + .state("S1") + .choice("S2") + .state("S3"); + builder.configureTransitions() + .withExternal() + .source("S1") + .target("S2"); + builder.build(); + }); + assertTrue(exception.getCause() instanceof MalformedConfigurationException); + assertThat(exception.getLocalizedMessage(), containsString("No transitions for state S2")); + } + @Configuration @EnableStateMachine public static class Config1 extends StateMachineConfigurerAdapter {