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:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {};
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[]
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user