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
This commit is contained in:
Janne Valkealahti
2015-11-28 09:13:10 +00:00
parent c0b24a3ca1
commit ed25006075
10 changed files with 564 additions and 4 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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<B extends Annotat
List<Class<? extends Annotation>> annotationTypes = getAnnotations();
Class<? extends Annotation> namedAnnotation = null;
String[] names = null;
ScopedProxyMode proxyMode = null;
if (annotationTypes != null) {
for (Class<? extends Annotation> annotationType : annotationTypes) {
AnnotationAttributes attributes = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(
@@ -73,6 +80,13 @@ public abstract class AbstractImportingAnnotationConfiguration<B extends Annotat
}
}
// check if Scope annotation is defined and get proxyMode from it
AnnotationAttributes scopeAttributes = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(
Scope.class.getName(), false));
if (scopeAttributes != null) {
proxyMode = scopeAttributes.getEnum("proxyMode");
}
BeanDefinition beanDefinition;
try {
beanDefinition = buildBeanDefinition(importingClassMetadata, namedAnnotation);
@@ -96,6 +110,20 @@ public abstract class AbstractImportingAnnotationConfiguration<B extends Annotat
registry.registerAlias(names[0], names[i]);
}
}
// wrap in scoped proxy if needed
if (proxyMode != null && proxyMode != ScopedProxyMode.DEFAULT && proxyMode != ScopedProxyMode.NO) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, names[0]);
BeanDefinitionHolder scopedProxy = null;
if (proxyMode == ScopedProxyMode.TARGET_CLASS) {
scopedProxy = ScopedProxyUtils.createScopedProxy(definitionHolder, registry, true);
} else if (proxyMode == ScopedProxyMode.INTERFACES) {
scopedProxy = ScopedProxyUtils.createScopedProxy(definitionHolder, registry, false);
} else {
throw new IllegalArgumentException("Unknown proxyMode " + proxyMode);
}
BeanDefinitionReaderUtils.registerBeanDefinition(scopedProxy, registry);
}
}
@Override
@@ -147,7 +175,7 @@ public abstract class AbstractImportingAnnotationConfiguration<B extends Annotat
}
protected abstract static class BeanDelegatingFactoryBean<T, B extends AnnotationBuilder<O>, O> implements
FactoryBean<T>, BeanFactoryAware, InitializingBean {
FactoryBean<T>, BeanFactoryAware, InitializingBean, DisposableBean {
private final B builder;
@@ -189,6 +217,10 @@ public abstract class AbstractImportingAnnotationConfiguration<B extends Annotat
this.beanFactory = beanFactory;
}
@Override
public void destroy() throws Exception {
}
public B getBuilder() {
return builder;
}

View File

@@ -21,10 +21,12 @@ import java.util.List;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.SmartLifecycle;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.statemachine.StateMachine;
@@ -40,6 +42,7 @@ import org.springframework.statemachine.config.builders.StateMachineTransitions;
import org.springframework.statemachine.config.common.annotation.AbstractImportingAnnotationConfiguration;
import org.springframework.statemachine.config.common.annotation.AnnotationConfigurer;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* {@link Configuration} which gets imported from {@link EnableStateMachine} and registers
@@ -73,9 +76,19 @@ public class StateMachineConfiguration<S, E> 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<S, E> extends
private String clazzName;
private Boolean contextEvents;
private SmartLifecycle lifecycle;
private DisposableBean disposableBean;
private String beanName;
public StateMachineDelegatingFactoryBean(StateMachineConfigBuilder<S, E> builder, Class<StateMachine<S, E>> clazz,
@@ -137,9 +151,15 @@ public class StateMachineConfiguration<S, E> extends
stateMachineFactory.setBeanName(beanName);
StateMachine<S, E> 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();

View File

@@ -334,6 +334,13 @@ public abstract class AbstractStateMachine<S, E> 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) {

View File

@@ -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() {};
}

View File

@@ -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<String, String> {
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception {
config
.withConfiguration()
.autoStartup(true);
}
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("SI")
.state("S1")
.state("S2");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> 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<String, String> stateMachine;
@RequestMapping(path="/state", method=RequestMethod.POST)
public HttpEntity<Void> setState(@RequestParam("event") String event) {
stateMachine.sendEvent(event);
return new ResponseEntity<Void>(HttpStatus.ACCEPTED);
}
@RequestMapping(path="/state", method=RequestMethod.GET)
@ResponseBody
public String getState() {
stateMachine.start();
return stateMachine.getState().getId();
}
}
}

View File

@@ -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<String, String> stateMachine() throws Exception {
Builder<String, String> 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<String, String> stateMachine = builder.build();
return stateMachine;
}
@Bean
TestController testController() {
return new TestController();
}
}
@Controller
static class TestController {
@Autowired
StateMachine<String, String> stateMachine;
@RequestMapping(path="/state", method=RequestMethod.POST)
public HttpEntity<Void> setState(@RequestParam("event") String event) {
stateMachine.sendEvent(event);
return new ResponseEntity<Void>(HttpStatus.ACCEPTED);
}
@RequestMapping(path="/state", method=RequestMethod.GET)
@ResponseBody
public String getState() {
return stateMachine.getState().getId();
}
}
}

View File

@@ -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<String, String> stateMachine() throws Exception {
Builder<String, String> 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<String, String> stateMachine = builder.build();
return stateMachine;
}
}
// end::snippetB[]
// tag::snippetC[]
@Configuration
@EnableStateMachine
@Scope(scopeName="session", proxyMode=ScopedProxyMode.TARGET_CLASS)
public static class Config4 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception {
config
.withConfiguration()
.autoStartup(true);
}
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("S1")
.state("S2");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("S1")
.target("S2")
.event("E1");
}
}
// end::snippetC[]
// tag::snippetD[]
@Controller
public class StateMachineController {
@Autowired
StateMachine<String, String> stateMachine;
@RequestMapping(path="/state", method=RequestMethod.POST)
public HttpEntity<Void> setState(@RequestParam("event") String event) {
stateMachine.sendEvent(event);
return new ResponseEntity<Void>(HttpStatus.ACCEPTED);
}
@RequestMapping(path="/state", method=RequestMethod.GET)
@ResponseBody
public String getState() {
return stateMachine.getState().getId();
}
}
// end::snippetD[]
}