From a981ed43611fe029e4531014695f8151b0ad95be Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Sun, 6 Mar 2016 15:17:37 +0000 Subject: [PATCH] Add base system for StateMachinePersister - New concepts easing persisting operations. - Base implementation of a persister which user can use instead of doing things manually to build a state machine context. Supports basic sm model but doesn't yet 'understand' deep and complex mixed models. - Fixed some features in sm reset functionality if root machine only have regions. - Relates to #184 --- .../AbstractStateMachinePersister.java | 111 ++++++ .../persist/DefaultStateMachinePersister.java | 39 ++ .../persist/StateMachinePersister.java | 50 +++ .../support/AbstractStateMachine.java | 27 +- .../persist/StateMachinePersistTests.java | 361 ++++++++++++++++++ 5 files changed, 583 insertions(+), 5 deletions(-) create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractStateMachinePersister.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/DefaultStateMachinePersister.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/StateMachinePersister.java create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/StateMachinePersistTests.java diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractStateMachinePersister.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractStateMachinePersister.java new file mode 100644 index 00000000..000900b4 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractStateMachinePersister.java @@ -0,0 +1,111 @@ +/* + * 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.persist; + +import java.util.ArrayList; +import java.util.Collection; + +import org.springframework.statemachine.ExtendedState; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.StateMachineContext; +import org.springframework.statemachine.StateMachinePersist; +import org.springframework.statemachine.access.StateMachineAccess; +import org.springframework.statemachine.access.StateMachineFunction; +import org.springframework.statemachine.region.Region; +import org.springframework.statemachine.state.AbstractState; +import org.springframework.statemachine.state.State; +import org.springframework.statemachine.support.DefaultExtendedState; +import org.springframework.statemachine.support.DefaultStateMachineContext; +import org.springframework.util.Assert; + +/** + * Base implementation of a {@link StateMachinePersister} easing persist + * operations with a {@link StateMachinePersist}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + * @param the type of context object + */ +public abstract class AbstractStateMachinePersister implements StateMachinePersister { + + private final StateMachinePersist stateMachinePersist; + + /** + * Instantiates a new abstract state machine persister. + * + * @param stateMachinePersist the state machine persist + */ + public AbstractStateMachinePersister(StateMachinePersist stateMachinePersist) { + Assert.notNull(stateMachinePersist, "StateMachinePersist must be set"); + this.stateMachinePersist = stateMachinePersist; + } + + @Override + public final void persist(StateMachine stateMachine, T contextOjb) throws Exception { + stateMachinePersist.write(buildStateMachineContext(stateMachine), contextOjb); + } + + @Override + public final StateMachine reset(StateMachine stateMachine, T contextOjb) throws Exception { + final StateMachineContext context = stateMachinePersist.read(contextOjb); + stateMachine.stop(); + stateMachine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction>() { + + @Override + public void apply(StateMachineAccess function) { + function.resetStateMachine(context); + } + }); + stateMachine.start(); + return stateMachine; + } + + /** + * Builds the state machine context which is used for persist operation. + * + * @param stateMachine the state machine + * @return the state machine context + */ + protected StateMachineContext buildStateMachineContext(StateMachine stateMachine) { + // TODO: need to make this fully recursive + ExtendedState extendedState = new DefaultExtendedState(); + extendedState.getVariables().putAll(stateMachine.getExtendedState().getVariables()); + + ArrayList> childs = new ArrayList>(); + S id = null; + State state = stateMachine.getState(); + if (state.isSubmachineState()) { + Collection ids1 = state.getIds(); + @SuppressWarnings("unchecked") + S[] ids2 = (S[]) ids1.toArray(); + // TODO: can this be empty as then we'd get error? + id = ids2[ids2.length-1]; + } else if (state.isOrthogonal()) { + Collection> regions = ((AbstractState)state).getRegions(); + for (Region r : regions) { + S s = r.getState().getId(); + childs.add(new DefaultStateMachineContext(s, null, null, null)); + } + } else { + id = state.getId(); + } + + return new DefaultStateMachineContext(childs, id, null, null, + extendedState); + } +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/DefaultStateMachinePersister.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/DefaultStateMachinePersister.java new file mode 100644 index 00000000..aa046e69 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/DefaultStateMachinePersister.java @@ -0,0 +1,39 @@ +/* + * 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.persist; + +import org.springframework.statemachine.StateMachinePersist; + +/** + * Default implementation of a {@link StateMachinePersister}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + * @param the type of context object + */ +public class DefaultStateMachinePersister extends AbstractStateMachinePersister { + + /** + * Instantiates a new default state machine persister. + * + * @param stateMachinePersist the state machine persist + */ + public DefaultStateMachinePersister(StateMachinePersist stateMachinePersist) { + super(stateMachinePersist); + } +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/StateMachinePersister.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/StateMachinePersister.java new file mode 100644 index 00000000..f99d0911 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/StateMachinePersister.java @@ -0,0 +1,50 @@ +/* + * 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.persist; + +import org.springframework.statemachine.StateMachine; + +/** + * Interface persisting a {@link StateMachine}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + * @param the type of context object + */ +public interface StateMachinePersister { + + /** + * Persist a state machine with a given context object. + * + * @param stateMachine the state machine + * @param contextOjb the context ojb + * @throws Exception the exception in case or any persist error + */ + void persist(StateMachine stateMachine, T contextOjb) throws Exception; + + /** + * Reset a state machine with a given context object. + * Returned machine has been reseted and is ready to be used. + * + * @param stateMachine the state machine + * @param contextOjb the context ojb + * @return the state machine + * @throws Exception the exception in case or any persist error + */ + StateMachine reset(StateMachine stateMachine, T contextOjb) throws Exception; +} 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 12bb7a2b..c221f13f 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 the original author or authors. + * Copyright 2015-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. @@ -493,6 +493,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo @Override public void resetStateMachine(StateMachineContext stateMachineContext) { + // TODO: this function needs a serious rewrite if (stateMachineContext == null) { log.info("Got null context, resetting to initial state and clearing extended state"); currentState = initialState; @@ -503,13 +504,10 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo log.debug("Request to reset state machine: stateMachine=[" + this + "] stateMachineContext=[" + stateMachineContext + "]"); } S state = stateMachineContext.getState(); - if (state == null) { - return; - } boolean stateSet = false; for (State s : getStates()) { for (State ss : s.getStates()) { - if (ss.getIds().contains(state)) { + if (state != null && ss.getIds().contains(state)) { currentState = s; // TODO: not sure about starting submachine/regions here, though // needed if we only transit to super state or reset regions @@ -548,6 +546,25 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } stateSet = true; break; + } else if (!stateMachineContext.getChilds().isEmpty()) { + // we're here because root machine only have regions + if (s.isOrthogonal()) { + Collection> regions = ((AbstractState)s).getRegions(); + for (Region region : regions) { + for (final StateMachineContext child : stateMachineContext.getChilds()) { + ((StateMachine)region).getStateMachineAccessor().doWithRegion(new StateMachineFunction>() { + + @Override + public void apply(StateMachineAccess function) { + function.resetStateMachine(child); + } + }); + } + } + for (Region region : regions) { + region.start(); + } + } } } if (stateSet) { diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/StateMachinePersistTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/StateMachinePersistTests.java new file mode 100644 index 00000000..e117518e --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/StateMachinePersistTests.java @@ -0,0 +1,361 @@ +/* + * 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.persist; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +import java.util.HashMap; + +import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.AbstractStateMachineTests; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.StateMachineContext; +import org.springframework.statemachine.StateMachinePersist; +import org.springframework.statemachine.StateMachineSystemConstants; +import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.StateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +public class StateMachinePersistTests extends AbstractStateMachineTests { + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + + @SuppressWarnings("unchecked") + @Test + public void testSimplePersist1() throws Exception { + context.register(Config1.class); + context.refresh(); + StateMachine stateMachine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + stateMachine.start(); + stateMachine.sendEvent("E1"); + assertThat(stateMachine.getState().getIds(), contains("S2")); + + InMemoryStateMachinePersist1 stateMachinePersist = new InMemoryStateMachinePersist1(); + StateMachinePersister persister = new DefaultStateMachinePersister<>(stateMachinePersist); + + persister.persist(stateMachine, "xxx"); + persister.reset(stateMachine, "xxx"); + assertThat(stateMachine.getState().getIds(), contains("S2")); + + stateMachine.sendEvent("E2"); + assertThat(stateMachine.getState().getIds(), contains("S3")); + } + + @SuppressWarnings("unchecked") + @Test + public void testSimplePersist2() throws Exception { + context.register(Config2.class); + context.refresh(); + StateMachine stateMachine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + stateMachine.start(); + stateMachine.sendEvent(TestEvents.E1); + assertThat(stateMachine.getState().getIds(), contains(TestStates.S2)); + + InMemoryStateMachinePersist2 stateMachinePersist = new InMemoryStateMachinePersist2(); + StateMachinePersister persister = new DefaultStateMachinePersister<>(stateMachinePersist); + + persister.persist(stateMachine, "xxx"); + persister.reset(stateMachine, "xxx"); + assertThat(stateMachine.getState().getIds(), contains(TestStates.S2)); + + stateMachine.sendEvent(TestEvents.E2); + assertThat(stateMachine.getState().getIds(), contains(TestStates.S3)); + } + + @SuppressWarnings("unchecked") + @Test + public void testExtendedState() throws Exception { + context.register(Config1.class); + context.refresh(); + StateMachine stateMachine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + stateMachine.start(); + + stateMachine.getExtendedState().getVariables().put("foo", "bar"); + + InMemoryStateMachinePersist1 stateMachinePersist = new InMemoryStateMachinePersist1(); + StateMachinePersister persister = new DefaultStateMachinePersister<>(stateMachinePersist); + + persister.persist(stateMachine, "xxx"); + stateMachine.getExtendedState().getVariables().remove("foo"); + stateMachine = persister.reset(stateMachine, "xxx"); + + assertThat(stateMachine.getExtendedState().get("foo", String.class), is("bar")); + } + + @SuppressWarnings("unchecked") + @Test + public void testSubStates() throws Exception { + context.register(Config3.class); + context.refresh(); + StateMachine stateMachine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + stateMachine.start(); + stateMachine.sendEvent("E1"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S21")); + stateMachine.sendEvent("E3"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S22")); + + InMemoryStateMachinePersist1 stateMachinePersist = new InMemoryStateMachinePersist1(); + StateMachinePersister persister = new DefaultStateMachinePersister<>(stateMachinePersist); + + persister.persist(stateMachine, "xxx"); + stateMachine = persister.reset(stateMachine, "xxx"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S22")); + + stateMachine.sendEvent("E2"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S3", "S31")); + + stateMachine = persister.reset(stateMachine, "xxx"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S22")); + } + + @SuppressWarnings("unchecked") + @Test + public void testRegions() throws Exception { + context.register(Config4.class); + context.refresh(); + StateMachine stateMachine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + stateMachine.start(); + + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S11", "S21", "S31")); + + stateMachine.sendEvent("E1"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S12", "S21", "S31")); + stateMachine.sendEvent("E2"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S12", "S22", "S31")); + stateMachine.sendEvent("E3"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S12", "S22", "S32")); + + InMemoryStateMachinePersist1 stateMachinePersist = new InMemoryStateMachinePersist1(); + StateMachinePersister persister = new DefaultStateMachinePersister<>(stateMachinePersist); + + persister.persist(stateMachine, "xxx"); + stateMachine = persister.reset(stateMachine, "xxx"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S12", "S22", "S32")); + + stateMachine.sendEvent("E4"); + stateMachine.sendEvent("E5"); + stateMachine.sendEvent("E6"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S13", "S23", "S33")); + + stateMachine = persister.reset(stateMachine, "xxx"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S12", "S22", "S32")); + } + + @Configuration + @EnableStateMachine + static class Config1 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S1") + .state("S2") + .state("S3"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1") + .and() + .withExternal() + .source("S2") + .target("S3") + .event("E2"); + } + } + + @Configuration + @EnableStateMachine + static class Config2 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S1) + .state(TestStates.S2) + .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); + } + } + + @Configuration + @EnableStateMachine + static class Config3 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S1") + .state("S2") + .state("S3") + .and() + .withStates() + .parent("S2") + .initial("S21") + .state("S21") + .state("S22") + .and() + .withStates() + .parent("S3") + .initial("S31") + .state("S31") + .state("S32"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1") + .and() + .withExternal() + .source("S2") + .target("S3") + .event("E2") + .and() + .withExternal() + .source("S21") + .target("S22") + .event("E3"); + } + } + + @Configuration + @EnableStateMachine + static class Config4 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S11") + .state("S11") + .state("S12") + .state("S13") + .and() + .withStates() + .initial("S21") + .state("S21") + .state("S22") + .state("S23") + .and() + .withStates() + .initial("S31") + .state("S31") + .state("S32") + .state("S33"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S11") + .target("S12") + .event("E1") + .and() + .withExternal() + .source("S21") + .target("S22") + .event("E2") + .and() + .withExternal() + .source("S31") + .target("S32") + .event("E3") + .and() + .withExternal() + .source("S12") + .target("S13") + .event("E4") + .and() + .withExternal() + .source("S22") + .target("S23") + .event("E5") + .and() + .withExternal() + .source("S32") + .target("S33") + .event("E6"); + } + } + + static class InMemoryStateMachinePersist1 implements StateMachinePersist { + + private final HashMap> contexts = new HashMap<>(); + + @Override + public void write(StateMachineContext context, String contextOjb) throws Exception { + contexts.put(contextOjb, context); + } + + @Override + public StateMachineContext read(String contextOjb) throws Exception { + return contexts.get(contextOjb); + } + } + + static class InMemoryStateMachinePersist2 implements StateMachinePersist { + + private final HashMap> contexts = new HashMap<>(); + + @Override + public void write(StateMachineContext context, String contextOjb) throws Exception { + contexts.put(contextOjb, context); + } + + @Override + public StateMachineContext read(String contextOjb) throws Exception { + return contexts.get(contextOjb); + } + } + +}