From ed250060758a1f6a2a3634aecebccca634dbf8ae Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Sat, 28 Nov 2015 09:13:10 +0000 Subject: [PATCH] Add support for session scope - Add tweaks to DisposableBean.destroy() which integrates into a lifecycle and properly closes a machine if only destroy() is called which is a case when `session` scoped bean is destroyed when http session is invalidated. - Tests in SessionScopedAnnotationTests and SessionScopedManualTests for both builder and annotation config. - Fixes #112 --- build.gradle | 4 + docs/src/reference/asciidoc/sm.adoc | 35 ++++ gradle.properties | 1 + ...tractImportingAnnotationConfiguration.java | 34 +++- .../StateMachineConfiguration.java | 24 ++- .../support/AbstractStateMachine.java | 7 + .../support/LifecycleObjectSupport.java | 11 +- .../config/SessionScopedAnnotationTests.java | 189 ++++++++++++++++++ .../config/SessionScopedManualTests.java | 165 +++++++++++++++ .../docs/DocsConfigurationSampleTests2.java | 98 +++++++++ 10 files changed, 564 insertions(+), 4 deletions(-) create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/config/SessionScopedAnnotationTests.java create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/config/SessionScopedManualTests.java diff --git a/build.gradle b/build.gradle index 19638015..9678d7ca 100644 --- a/build.gradle +++ b/build.gradle @@ -118,6 +118,10 @@ project('spring-statemachine-core') { compile "org.springframework:spring-messaging:$springVersion" testCompile "org.springframework:spring-test:$springVersion" + testCompile "org.springframework:spring-web:$springVersion" + testCompile "org.springframework:spring-webmvc:$springVersion" + testCompile "org.apache.tomcat.embed:tomcat-embed-core:$tomcatEmbedVersion" + testCompile "org.apache.tomcat.embed:tomcat-embed-logging-juli:$tomcatEmbedVersion" testCompile "org.hamcrest:hamcrest-core:$hamcrestVersion" testCompile "org.hamcrest:hamcrest-library:$hamcrestVersion" testCompile "junit:junit:$junitVersion" diff --git a/docs/src/reference/asciidoc/sm.adoc b/docs/src/reference/asciidoc/sm.adoc index ddc15216..b58b6cc2 100644 --- a/docs/src/reference/asciidoc/sm.adoc +++ b/docs/src/reference/asciidoc/sm.adoc @@ -415,6 +415,41 @@ is attached automatically and then a default _TaskExecutor_ can be found from there. If instances are used outside of a spring application context these methods must be used to setup needed facilities. +[[sm-scopes]] +== Using Scopes +Support for scopes in a state machine is very limited but it is possible +to enable use of _session_ scope using a normal spring `@Scope` annotation. +Firstly if state machine is build manually via a builder and returned into +context as `@Bean`, and secondly via an contifuration adapter. Both of +these simply needs an a `@Scope` to be present where _scopeName_ is set to +_session_ and _proxyMode_ to `ScopedProxyMode.TARGET_CLASS`. Examples for +both use cases are shown below. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests2.java[tags=snippetB] +---- + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests2.java[tags=snippetC] +---- + +Once you have scoped state machine into `session`, autowiring it into +a `@Controller` will give new state machine instance per session. +State machine is then destroyed when `HttpSession` is invalidated. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests2.java[tags=snippetD] +---- + +[NOTE] +==== +Using state machines in a `session` scopes needs a careful planning +mostly because it is a relatively heavy component. +==== + [[sm-actions]] == Using Actions Actions are one of the most useful components from user perspective to diff --git a/gradle.properties b/gradle.properties index 4caf3d98..c72f5f75 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,3 +9,4 @@ version=1.0.2.BUILD-SNAPSHOT hamcrestVersion=1.3 springSessionVersion=1.0.2.RELEASE curatorVersion=2.8.0 +tomcatEmbedVersion=7.0.56 diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/common/annotation/AbstractImportingAnnotationConfiguration.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/common/annotation/AbstractImportingAnnotationConfiguration.java index c1404d40..9a80ceb3 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/common/annotation/AbstractImportingAnnotationConfiguration.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/common/annotation/AbstractImportingAnnotationConfiguration.java @@ -18,20 +18,26 @@ package org.springframework.statemachine.config.common.annotation; import java.lang.annotation.Annotation; import java.util.List; +import org.springframework.aop.scope.ScopedProxyUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.support.DefaultBeanNameGenerator; import org.springframework.context.EnvironmentAware; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotationMetadata; @@ -61,6 +67,7 @@ public abstract class AbstractImportingAnnotationConfiguration> annotationTypes = getAnnotations(); Class namedAnnotation = null; String[] names = null; + ScopedProxyMode proxyMode = null; if (annotationTypes != null) { for (Class annotationType : annotationTypes) { AnnotationAttributes attributes = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes( @@ -73,6 +80,13 @@ public abstract class AbstractImportingAnnotationConfiguration, O> implements - FactoryBean, BeanFactoryAware, InitializingBean { + FactoryBean, BeanFactoryAware, InitializingBean, DisposableBean { private final B builder; @@ -189,6 +217,10 @@ public abstract class AbstractImportingAnnotationConfiguration extends BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder .rootBeanDefinition(StateMachineDelegatingFactoryBean.class); - AnnotationAttributes attributes = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes( + AnnotationAttributes esmAttributes = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes( EnableStateMachine.class.getName(), false)); - Boolean contextEvents = attributes.getBoolean("contextEvents"); + Boolean contextEvents = esmAttributes.getBoolean("contextEvents"); + + // check if Scope annotation is defined and set scope from it + AnnotationAttributes scopeAttributes = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes( + Scope.class.getName(), false)); + if (scopeAttributes != null) { + String scope = scopeAttributes.getAliasedString("value", Scope.class, enableStateMachineEnclosingClass); + if (StringUtils.hasText(scope)) { + beanDefinitionBuilder.setScope(scope); + } + } beanDefinitionBuilder.addConstructorArgValue(builder); beanDefinitionBuilder.addConstructorArgValue(StateMachine.class); @@ -98,6 +111,7 @@ public class StateMachineConfiguration extends private String clazzName; private Boolean contextEvents; private SmartLifecycle lifecycle; + private DisposableBean disposableBean; private String beanName; public StateMachineDelegatingFactoryBean(StateMachineConfigBuilder builder, Class> clazz, @@ -137,9 +151,15 @@ public class StateMachineConfiguration extends stateMachineFactory.setBeanName(beanName); StateMachine stateMachine = stateMachineFactory.getStateMachine(); this.lifecycle = (SmartLifecycle) stateMachine; + this.disposableBean = (DisposableBean) stateMachine; setObject(stateMachine); } + @Override + public void destroy() throws Exception { + disposableBean.destroy(); + } + @Override public void start() { lifecycle.start(); 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 a27414e8..34c6999e 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 @@ -334,6 +334,13 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo initialEnabled = null; } + @Override + protected void doDestroy() { + // if lifecycle methods has not been called, make + // sure we get into those if only destroy() is called. + stop(); + } + @Override public void setStateMachineError(Exception exception) { if (exception == null) { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/LifecycleObjectSupport.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/LifecycleObjectSupport.java index 63b19f5c..831fc1fe 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/LifecycleObjectSupport.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/LifecycleObjectSupport.java @@ -24,6 +24,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.SmartLifecycle; import org.springframework.core.task.TaskExecutor; @@ -37,7 +38,7 @@ import org.springframework.util.Assert; * @author Janne Valkealahti * */ -public abstract class LifecycleObjectSupport implements InitializingBean, SmartLifecycle, BeanFactoryAware { +public abstract class LifecycleObjectSupport implements InitializingBean, DisposableBean, SmartLifecycle, BeanFactoryAware { private static final Log log = LogFactory.getLog(LifecycleObjectSupport.class); @@ -75,6 +76,12 @@ public abstract class LifecycleObjectSupport implements InitializingBean, SmartL } } + @Override + public final void destroy() throws Exception { + log.info("destroy called"); + doDestroy(); + } + @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { Assert.notNull(beanFactory, "beanFactory must not be null"); @@ -255,4 +262,6 @@ public abstract class LifecycleObjectSupport implements InitializingBean, SmartL */ protected void doStop() {}; + protected void doDestroy() {}; + } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/SessionScopedAnnotationTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/SessionScopedAnnotationTests.java new file mode 100644 index 00000000..4b034253 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/SessionScopedAnnotationTests.java @@ -0,0 +1,189 @@ +/* + * Copyright 2015 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.config; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.TestUtils; +import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; +import org.springframework.stereotype.Controller; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.MethodMode; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.context.WebApplicationContext; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes={SessionScopedAnnotationTests.Config2.class, SessionScopedAnnotationTests.Config1.class}) +@WebAppConfiguration +@DirtiesContext(methodMode = MethodMode.AFTER_METHOD) +public class SessionScopedAnnotationTests { + + @Autowired + private WebApplicationContext context; + + private MockMvc mvc; + + @Before + public void setUp() { + mvc = MockMvcBuilders.webAppContextSetup(context).build(); + } + + @Test + public void testScopedMachines() throws Exception { + MockHttpSession session1 = new MockHttpSession(); + MockHttpSession session2 = new MockHttpSession(); + + mvc. + perform(get("/state").session(session1)). + andExpect(status().isOk()). + andExpect(content().string(is("SI"))); + mvc. + perform(get("/state").session(session2)). + andExpect(status().isOk()). + andExpect(content().string(is("SI"))); + + mvc. + perform(post("/state").session(session1).param("event", "E1")). + andExpect(status().isAccepted()); + mvc. + perform(post("/state").session(session2).param("event", "E2")). + andExpect(status().isAccepted()); + + mvc. + perform(get("/state").session(session1)). + andExpect(status().isOk()). + andExpect(content().string(is("S1"))); + mvc. + perform(get("/state").session(session2)). + andExpect(status().isOk()). + andExpect(content().string(is("S2"))); + + session1.invalidate(); + session2.invalidate(); + } + + @Test + public void testDestruction() throws Exception { + MockHttpSession session1 = new MockHttpSession(); + mvc. + perform(get("/state").session(session1)). + andExpect(status().isOk()). + andExpect(content().string(is("SI"))); + + Object machine = session1.getAttribute("scopedTarget.stateMachine"); + machine = TestUtils.readField("object", machine); + assertThat(machine, notNullValue()); + assertThat(TestUtils.readField("running", machine), is(true)); + session1.invalidate(); + assertThat(TestUtils.readField("running", machine), is(false)); + } + + @Configuration + @EnableStateMachine + @Scope(scopeName="session", proxyMode=ScopedProxyMode.TARGET_CLASS) + public static class Config1 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .autoStartup(true); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("SI") + .state("S1") + .state("S2"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("SI") + .target("S1") + .event("E1") + .and() + .withExternal() + .source("SI") + .target("S2") + .event("E2"); + } + + } + + public static class Config2 { + + @Bean + TestController testController() { + return new TestController(); + } + } + + @Controller + static class TestController { + + @Autowired + StateMachine stateMachine; + + @RequestMapping(path="/state", method=RequestMethod.POST) + public HttpEntity setState(@RequestParam("event") String event) { + stateMachine.sendEvent(event); + return new ResponseEntity(HttpStatus.ACCEPTED); + } + + @RequestMapping(path="/state", method=RequestMethod.GET) + @ResponseBody + public String getState() { + stateMachine.start(); + return stateMachine.getState().getId(); + } + + } + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/SessionScopedManualTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/SessionScopedManualTests.java new file mode 100644 index 00000000..da36401c --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/SessionScopedManualTests.java @@ -0,0 +1,165 @@ +/* + * Copyright 2015 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.config; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.core.task.SyncTaskExecutor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.TestUtils; +import org.springframework.statemachine.config.StateMachineBuilder.Builder; +import org.springframework.stereotype.Controller; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.MethodMode; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.context.WebApplicationContext; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@WebAppConfiguration +@DirtiesContext(methodMode = MethodMode.AFTER_METHOD) +public class SessionScopedManualTests { + + @Autowired + private WebApplicationContext context; + + private MockMvc mvc; + + @Before + public void setUp() { + mvc = MockMvcBuilders.webAppContextSetup(context).build(); + } + + @Test + public void testScopedMachines() throws Exception { + MockHttpSession session1 = new MockHttpSession(); + MockHttpSession session2 = new MockHttpSession(); + + mvc. + perform(get("/state").session(session1)). + andExpect(status().isOk()). + andExpect(content().string(is("S1"))); + mvc. + perform(get("/state").session(session2)). + andExpect(status().isOk()). + andExpect(content().string(is("S1"))); + + mvc. + perform(post("/state").session(session1).param("event", "E1")). + andExpect(status().isAccepted()); + mvc. + perform(post("/state").session(session2).param("event", "E1")). + andExpect(status().isAccepted()); + + mvc. + perform(get("/state").session(session1)). + andExpect(status().isOk()). + andExpect(content().string(is("S2"))); + mvc. + perform(get("/state").session(session2)). + andExpect(status().isOk()). + andExpect(content().string(is("S2"))); + } + + @Test + public void testDestruction() throws Exception { + MockHttpSession session1 = new MockHttpSession(); + mvc. + perform(get("/state").session(session1)). + andExpect(status().isOk()). + andExpect(content().string(is("S1"))); + Object machine = session1.getAttribute("scopedTarget.stateMachine"); + assertThat(machine, notNullValue()); + assertThat(TestUtils.readField("running", machine), is(true)); + session1.invalidate(); + assertThat(TestUtils.readField("running", machine), is(false)); + } + + @Configuration + static class Config { + + @Bean + @Scope(scopeName="session", proxyMode=ScopedProxyMode.TARGET_CLASS) + StateMachine stateMachine() throws Exception { + Builder builder = StateMachineBuilder.builder(); + builder.configureConfiguration() + .withConfiguration() + .autoStartup(true) + .taskExecutor(new SyncTaskExecutor()); + builder.configureStates() + .withStates() + .initial("S1").state("S2"); + builder.configureTransitions() + .withExternal() + .source("S1").target("S2").event("E1") + .and() + .withExternal() + .source("S2").target("S1").event("E2"); + StateMachine stateMachine = builder.build(); + return stateMachine; + } + + @Bean + TestController testController() { + return new TestController(); + } + } + + @Controller + static class TestController { + + @Autowired + StateMachine stateMachine; + + @RequestMapping(path="/state", method=RequestMethod.POST) + public HttpEntity setState(@RequestParam("event") String event) { + stateMachine.sendEvent(event); + return new ResponseEntity(HttpStatus.ACCEPTED); + } + + @RequestMapping(path="/state", method=RequestMethod.GET) + @ResponseBody + public String getState() { + return stateMachine.getState().getId(); + } + } +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests2.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests2.java index 6c543f28..c21ec8bc 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests2.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests2.java @@ -15,15 +15,31 @@ */ package org.springframework.statemachine.docs; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.core.task.SyncTaskExecutor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.statemachine.AbstractStateMachineTests; import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.StateMachineBuilder; +import org.springframework.statemachine.config.StateMachineBuilder.Builder; import org.springframework.statemachine.config.StateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; public class DocsConfigurationSampleTests2 extends AbstractStateMachineTests { @@ -71,4 +87,86 @@ public class DocsConfigurationSampleTests2 extends AbstractStateMachineTests { } // end::snippetA[] +// tag::snippetB[] + @Configuration + public class Config3 { + + @Bean + @Scope(scopeName="session", proxyMode=ScopedProxyMode.TARGET_CLASS) + StateMachine stateMachine() throws Exception { + Builder builder = StateMachineBuilder.builder(); + builder.configureConfiguration() + .withConfiguration() + .autoStartup(true) + .taskExecutor(new SyncTaskExecutor()); + builder.configureStates() + .withStates() + .initial("S1") + .state("S2"); + builder.configureTransitions() + .withExternal() + .source("S1") + .target("S2") + .event("E1"); + StateMachine stateMachine = builder.build(); + return stateMachine; + } + + } +// end::snippetB[] + +// tag::snippetC[] + @Configuration + @EnableStateMachine + @Scope(scopeName="session", proxyMode=ScopedProxyMode.TARGET_CLASS) + public static class Config4 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .autoStartup(true); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S2"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1"); + } + + } +// end::snippetC[] + +// tag::snippetD[] + @Controller + public class StateMachineController { + + @Autowired + StateMachine stateMachine; + + @RequestMapping(path="/state", method=RequestMethod.POST) + public HttpEntity setState(@RequestParam("event") String event) { + stateMachine.sendEvent(event); + return new ResponseEntity(HttpStatus.ACCEPTED); + } + + @RequestMapping(path="/state", method=RequestMethod.GET) + @ResponseBody + public String getState() { + return stateMachine.getState().getId(); + } + } +// end::snippetD[] + }