diff --git a/build.gradle b/build.gradle index 98916615..fec836e3 100644 --- a/build.gradle +++ b/build.gradle @@ -129,6 +129,21 @@ project('spring-statemachine-core') { } } +project('spring-statemachine-boot') { + description = "Spring State Machine Boot" + + dependencies { + compile project(":spring-statemachine-core") + compile "org.springframework.boot:spring-boot-autoconfigure:$springBootVersion" + compile "org.springframework.boot:spring-boot-actuator:$springBootVersion" + testCompile "org.springframework.boot:spring-boot-test:$springBootVersion" + testCompile "org.springframework:spring-test:$springVersion" + testCompile "org.hamcrest:hamcrest-core:$hamcrestVersion" + testCompile "org.hamcrest:hamcrest-library:$hamcrestVersion" + testCompile "junit:junit:$junitVersion" + } +} + project('spring-statemachine-test') { description = "Spring State Machine Test" diff --git a/settings.gradle b/settings.gradle index 96555abb..04e11360 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,6 +9,7 @@ include 'spring-statemachine-cluster' include 'spring-statemachine-uml' include 'spring-statemachine-build-tests' include 'spring-statemachine-recipes' +include 'spring-statemachine-boot' include 'spring-statemachine-samples' include 'spring-statemachine-samples:turnstile' @@ -25,6 +26,7 @@ include 'spring-statemachine-samples:eventservice' include 'spring-statemachine-samples:deploy' include 'spring-statemachine-samples:ordershipping' include 'spring-statemachine-samples:datajpa' +include 'spring-statemachine-samples:monitoring' include 'spring-statemachine-data' include 'spring-statemachine-data:jpa' diff --git a/spring-statemachine-boot/src/main/java/org/springframework/statemachine/boot/BootStateMachineMonitor.java b/spring-statemachine-boot/src/main/java/org/springframework/statemachine/boot/BootStateMachineMonitor.java new file mode 100644 index 00000000..e429d73b --- /dev/null +++ b/spring-statemachine-boot/src/main/java/org/springframework/statemachine/boot/BootStateMachineMonitor.java @@ -0,0 +1,96 @@ +/* + * 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.boot; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.actuate.metrics.CounterService; +import org.springframework.boot.actuate.metrics.GaugeService; +import org.springframework.boot.actuate.trace.TraceRepository; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.monitor.AbstractStateMachineMonitor; +import org.springframework.statemachine.monitor.StateMachineMonitor; +import org.springframework.statemachine.state.State; +import org.springframework.statemachine.transition.Transition; + +/** + * Implementation of a {@link StateMachineMonitor} which converts monitoring + * events and bridges those into supported format handled by Spring Boot's + * tracing and metrics frameworks. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class BootStateMachineMonitor extends AbstractStateMachineMonitor { + + private final String METRIC_BASE = "ssm.transition"; + private final CounterService counterService; + private final GaugeService gaugeService; + private final TraceRepository traceRepository; + + /** + * Instantiates a new boot state machine monitor. + * + * @param counterService the counter service + * @param gaugeService the gauge service + * @param traceRepository the trace repository + */ + public BootStateMachineMonitor(CounterService counterService, GaugeService gaugeService, + TraceRepository traceRepository) { + this.counterService = counterService; + this.gaugeService = gaugeService; + this.traceRepository = traceRepository; + } + + @Override + public void transition(StateMachine stateMachine, Transition transition, long duration) { + String transitionName = transitionToName(transition); + this.counterService.increment(METRIC_BASE + "." + transitionName + ".transit"); + this.gaugeService.submit(METRIC_BASE + "." + transitionName + ".duration", duration); + Map traceInfo = new HashMap<>(); + traceInfo.put("transition", transitionToName(transition)); + traceInfo.put("duration", duration); + traceInfo.put("machine", stateMachine.getId()); + traceRepository.add(traceInfo); + } + + private static String transitionToName(Transition transition) { + String sourceId = nullStateId(transition.getSource()); + String targetId = nullStateId(transition.getTarget()); + StringBuilder buf = new StringBuilder(); + buf.append(transition.getKind()); + if (sourceId != null) { + buf.append("_"); + buf.append(sourceId); + } + if (targetId != null) { + buf.append("_"); + buf.append(targetId); + } + return buf.toString(); + } + + private static String nullStateId(State state) { + if (state == null) { + return null; + } + S id = state.getId(); + return id != null ? id.toString() : null; + } +} diff --git a/spring-statemachine-boot/src/main/java/org/springframework/statemachine/boot/StateMachineAutoConfiguration.java b/spring-statemachine-boot/src/main/java/org/springframework/statemachine/boot/StateMachineAutoConfiguration.java new file mode 100644 index 00000000..2d36cdd1 --- /dev/null +++ b/spring-statemachine-boot/src/main/java/org/springframework/statemachine/boot/StateMachineAutoConfiguration.java @@ -0,0 +1,61 @@ +/* + * 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.boot; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.metrics.CounterService; +import org.springframework.boot.actuate.metrics.GaugeService; +import org.springframework.boot.actuate.trace.TraceRepository; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Spring Statemachine. + * + * @author Janne Valkealahti + * + */ +@Configuration +@EnableConfigurationProperties({ StateMachineProperties.class }) +public class StateMachineAutoConfiguration { + + @Configuration + @ConditionalOnClass(CounterService.class) + @ConditionalOnProperty(prefix = "spring.statemachine.monitor", name = "enabled", havingValue = "true", matchIfMissing = true) + public static class StateMachineMonitoringConfiguration { + + private final CounterService counterService; + private final GaugeService gaugeService; + private final TraceRepository traceRepository; + + public StateMachineMonitoringConfiguration(ObjectProvider counterServiceProvider, + ObjectProvider gaugeServiceProvider, + ObjectProvider traceRepositoryProvider) { + this.counterService = counterServiceProvider.getIfAvailable(); + this.gaugeService = gaugeServiceProvider.getIfAvailable(); + this.traceRepository = traceRepositoryProvider.getIfAvailable(); + } + + @Bean + public BootStateMachineMonitor bootStateMachineMonitor() { + return new BootStateMachineMonitor<>(counterService, gaugeService, traceRepository); + } + } +} diff --git a/spring-statemachine-boot/src/main/java/org/springframework/statemachine/boot/StateMachineProperties.java b/spring-statemachine-boot/src/main/java/org/springframework/statemachine/boot/StateMachineProperties.java new file mode 100644 index 00000000..9e2e2db2 --- /dev/null +++ b/spring-statemachine-boot/src/main/java/org/springframework/statemachine/boot/StateMachineProperties.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.boot; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Spring Boot {@link ConfigurationProperties} for spring.statemachine. + * + * @author Janne Valkealahti + * + */ +@ConfigurationProperties(value = "spring.statemachine") +public class StateMachineProperties { + + private StateMachineMonitoringProperties monitor; + + public StateMachineMonitoringProperties getMonitor() { + return monitor; + } + + public void setMonitor(StateMachineMonitoringProperties monitor) { + this.monitor = monitor; + } + + public static class StateMachineMonitoringProperties { + private boolean enabled = false; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + } +} diff --git a/spring-statemachine-boot/src/main/resources/META-INF/spring.factories b/spring-statemachine-boot/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..d5293ae7 --- /dev/null +++ b/spring-statemachine-boot/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configure +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.statemachine.boot.StateMachineAutoConfiguration diff --git a/spring-statemachine-boot/src/test/java/org/springframework/statemachine/boot/StateMachineAutoConfigurationTests.java b/spring-statemachine-boot/src/test/java/org/springframework/statemachine/boot/StateMachineAutoConfigurationTests.java new file mode 100644 index 00000000..86ada6e5 --- /dev/null +++ b/spring-statemachine-boot/src/test/java/org/springframework/statemachine/boot/StateMachineAutoConfigurationTests.java @@ -0,0 +1,110 @@ +/* + * 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.boot; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +import java.util.List; + +import org.junit.After; +import org.junit.Test; +import org.springframework.boot.test.util.EnvironmentTestUtils; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.StateMachine; +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; + +/** + * Tests for {@link StateMachineAutoConfiguration}. + * + * @author Janne Valkealahti + * + */ +@SuppressWarnings("rawtypes") +public class StateMachineAutoConfigurationTests { + + private AnnotationConfigApplicationContext context; + + @After + public void close() { + if (context != null) { + context.close(); + } + } + + @Test + public void testDefaults() throws Exception { + context = new AnnotationConfigApplicationContext(); + context.register(StateMachineAutoConfiguration.class); + context.refresh(); + assertThat(context.containsBean("bootStateMachineMonitor"), is(true)); + } + + @Test + public void testMonitorDisabled() throws Exception { + context = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(context, "spring.statemachine.monitor.enabled:false"); + context.register(StateMachineAutoConfiguration.class); + context.refresh(); + assertThat(context.containsBean("bootStateMachineMonitor"), is(false)); + } + + @Test + public void testMonitoringAddedViaAutoconfig() throws Exception { + context = new AnnotationConfigApplicationContext(); + context.register(StateMachineAutoConfiguration.class, Config1.class); + context.refresh(); + StateMachine stateMachine = context.getBean(StateMachine.class); + Object compositeStateMachineMonitor = TestUtils.readField("stateMachineMonitor", stateMachine); + Object orderedCompositeItem = TestUtils.readField("items", compositeStateMachineMonitor); + List list = TestUtils.readField("list", orderedCompositeItem); + assertThat(list, notNullValue()); + assertThat(list.size(), is(1)); + assertThat(list.get(0), instanceOf(BootStateMachineMonitor.class)); + } + + @Configuration + @EnableStateMachine + public static class Config1 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("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"); + } + } +} diff --git a/spring-statemachine-boot/src/test/java/org/springframework/statemachine/boot/TestUtils.java b/spring-statemachine-boot/src/test/java/org/springframework/statemachine/boot/TestUtils.java new file mode 100644 index 00000000..b7fe4911 --- /dev/null +++ b/spring-statemachine-boot/src/test/java/org/springframework/statemachine/boot/TestUtils.java @@ -0,0 +1,94 @@ +/* + * 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.boot; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.springframework.util.ReflectionUtils; + +/** + * Utils for tests. + * + * @author Janne Valkealahti + * + */ +public class TestUtils { + + @SuppressWarnings("unchecked") + public static T readField(String name, Object target) throws Exception { + Field field = null; + Class clazz = target.getClass(); + do { + try { + field = clazz.getDeclaredField(name); + } catch (Exception ex) { + } + + clazz = clazz.getSuperclass(); + } while (field == null && !clazz.equals(Object.class)); + + if (field == null) + throw new IllegalArgumentException("Cannot find field '" + name + "' in the class hierarchy of " + + target.getClass()); + field.setAccessible(true); + return (T) field.get(target); + } + + @SuppressWarnings("unchecked") + public static T callMethod(String name, Object target) throws Exception { + Class clazz = target.getClass(); + Method method = ReflectionUtils.findMethod(clazz, name); + + if (method == null) + throw new IllegalArgumentException("Cannot find method '" + method + "' in the class hierarchy of " + + target.getClass()); + method.setAccessible(true); + return (T) ReflectionUtils.invokeMethod(method, target); + } + + public static void setField(String name, Object target, Object value) throws Exception { + Field field = null; + Class clazz = target.getClass(); + do { + try { + field = clazz.getDeclaredField(name); + } catch (Exception ex) { + } + + clazz = clazz.getSuperclass(); + } while (field == null && !clazz.equals(Object.class)); + + if (field == null) + throw new IllegalArgumentException("Cannot find field '" + name + "' in the class hierarchy of " + + target.getClass()); + field.setAccessible(true); + field.set(target, value); + } + + @SuppressWarnings("unchecked") + public static T callMethod(String name, Object target, Object[] args, Class[] argsTypes) throws Exception { + Class clazz = target.getClass(); + Method method = ReflectionUtils.findMethod(clazz, name, argsTypes); + + if (method == null) + throw new IllegalArgumentException("Cannot find method '" + method + "' in the class hierarchy of " + + target.getClass()); + method.setAccessible(true); + return (T) ReflectionUtils.invokeMethod(method, target, args); + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/access/StateMachineAccess.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/access/StateMachineAccess.java index 4f7fe938..5554bb59 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/access/StateMachineAccess.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/access/StateMachineAccess.java @@ -18,6 +18,7 @@ package org.springframework.statemachine.access; import org.springframework.messaging.Message; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.StateMachineContext; +import org.springframework.statemachine.monitor.StateMachineMonitor; import org.springframework.statemachine.support.StateMachineInterceptor; /** @@ -51,6 +52,13 @@ public interface StateMachineAccess { */ void addStateMachineInterceptor(StateMachineInterceptor interceptor); + /** + * Adds the state machine monitor. + * + * @param monitor the monitor + */ + void addStateMachineMonitor(StateMachineMonitor monitor); + /** * Sets if initial state is enabled when a state machine is * using sub states. diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java index 9881be18..815b1a20 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java @@ -53,6 +53,7 @@ import org.springframework.statemachine.config.model.verifier.CompositeStateMach import org.springframework.statemachine.config.model.verifier.StateMachineModelVerifier; import org.springframework.statemachine.ensemble.DistributedStateMachine; import org.springframework.statemachine.listener.StateMachineListener; +import org.springframework.statemachine.monitor.StateMachineMonitor; import org.springframework.statemachine.region.Region; import org.springframework.statemachine.security.StateMachineSecurityInterceptor; import org.springframework.statemachine.state.AbstractState; @@ -112,6 +113,8 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS private String beanName; + private StateMachineMonitor defaultStateMachineMonitor; + /** * Instantiates a new abstract state machine factory. * @@ -265,6 +268,23 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS } }); + // add monitoring hooks + final StateMachineMonitor stateMachineMonitor = stateMachineModel.getConfigurationData().getStateMachineMonitor(); + if (stateMachineMonitor != null || defaultStateMachineMonitor != null) { + fmachine.getStateMachineAccessor().doWithRegion(new StateMachineFunction>() { + + @Override + public void apply(StateMachineAccess function) { + if (defaultStateMachineMonitor != null) { + function.addStateMachineMonitor(defaultStateMachineMonitor); + } + if (stateMachineMonitor != null) { + function.addStateMachineMonitor(stateMachineMonitor); + } + } + }); + } + // TODO: should error out if sec is enabled but spring-security is not in cp if (stateMachineModel.getConfigurationData().isSecurityEnabled()) { final StateMachineSecurityInterceptor securityInterceptor = new StateMachineSecurityInterceptor( @@ -324,6 +344,15 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS this.contextEvents = contextEvents; } + /** + * Sett state machine monitor. + * + * @param stateMachineMonitor the state machine monitor + */ + public void setStateMachineMonitor(StateMachineMonitor stateMachineMonitor) { + this.defaultStateMachineMonitor = stateMachineMonitor; + } + private StateMachine delegateAutoStartup(StateMachine delegate) { if (handleAutostartup && delegate instanceof SmartLifecycle && ((SmartLifecycle) delegate).isAutoStartup()) { ((SmartLifecycle)delegate).start(); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java index fb6fa115..85862314 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java @@ -28,9 +28,11 @@ import org.springframework.statemachine.config.common.annotation.ObjectPostProce import org.springframework.statemachine.config.configurers.ConfigurationConfigurer; import org.springframework.statemachine.config.configurers.DefaultConfigurationConfigurer; import org.springframework.statemachine.config.configurers.DefaultDistributedStateMachineConfigurer; +import org.springframework.statemachine.config.configurers.DefaultMonitoringConfigurer; import org.springframework.statemachine.config.configurers.DefaultSecurityConfigurer; import org.springframework.statemachine.config.configurers.DefaultVerifierConfigurer; import org.springframework.statemachine.config.configurers.DistributedStateMachineConfigurer; +import org.springframework.statemachine.config.configurers.MonitoringConfigurer; import org.springframework.statemachine.config.configurers.SecurityConfigurer; import org.springframework.statemachine.config.configurers.VerifierConfigurer; import org.springframework.statemachine.config.model.ConfigurationData; @@ -38,6 +40,7 @@ import org.springframework.statemachine.config.model.StatesData; import org.springframework.statemachine.config.model.verifier.StateMachineModelVerifier; import org.springframework.statemachine.ensemble.StateMachineEnsemble; import org.springframework.statemachine.listener.StateMachineListener; +import org.springframework.statemachine.monitor.StateMachineMonitor; import org.springframework.statemachine.security.SecurityRule; /** @@ -66,6 +69,7 @@ public class StateMachineConfigurationBuilder private AccessDecisionManager eventSecurityAccessDecisionManager; private SecurityRule eventSecurityRule; private SecurityRule transitionSecurityRule; + private StateMachineMonitor stateMachineMonitor; /** * Instantiates a new state machine configuration builder. @@ -114,11 +118,16 @@ public class StateMachineConfigurationBuilder return apply(new DefaultVerifierConfigurer()); } + @Override + public MonitoringConfigurer withMonitoring() throws Exception { + return apply(new DefaultMonitoringConfigurer()); + } + @Override protected ConfigurationData performBuild() throws Exception { return new ConfigurationData(beanFactory, taskExecutor, taskScheculer, autoStart, ensemble, listeners, securityEnabled, transitionSecurityAccessDecisionManager, eventSecurityAccessDecisionManager, eventSecurityRule, - transitionSecurityRule, verifierEnabled, verifier, machineId); + transitionSecurityRule, verifierEnabled, verifier, machineId, stateMachineMonitor); } /** @@ -203,6 +212,15 @@ public class StateMachineConfigurationBuilder this.verifierEnabled = verifierEnabled; } + /** + * Sets the state machine monitor. + * + * @param stateMachineMonitor the state machine monitor + */ + public void setStateMachineMonitor(StateMachineMonitor stateMachineMonitor) { + this.stateMachineMonitor = stateMachineMonitor; + } + /** * Sets the security transition access decision manager. * diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationConfigurer.java index 2ba75b6f..33213e29 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationConfigurer.java @@ -17,6 +17,7 @@ package org.springframework.statemachine.config.builders; import org.springframework.statemachine.config.configurers.ConfigurationConfigurer; import org.springframework.statemachine.config.configurers.DistributedStateMachineConfigurer; +import org.springframework.statemachine.config.configurers.MonitoringConfigurer; import org.springframework.statemachine.config.configurers.SecurityConfigurer; import org.springframework.statemachine.config.configurers.VerifierConfigurer; @@ -61,4 +62,12 @@ public interface StateMachineConfigurationConfigurer { * @throws Exception if configuration error happens */ VerifierConfigurer withVerifier() throws Exception; + + /** + * Gets a configurer for state machine monitoring. + * + * @return {@link MonitoringConfigurer} for chaining + * @throws Exception if configuration error happens + */ + MonitoringConfigurer withMonitoring() throws Exception; } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineConfiguration.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineConfiguration.java index adacf27e..ba1b36b5 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineConfiguration.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineConfiguration.java @@ -22,6 +22,7 @@ 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.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.context.SmartLifecycle; @@ -42,6 +43,7 @@ import org.springframework.statemachine.config.model.DefaultStateMachineModel; import org.springframework.statemachine.config.model.ConfigurationData; import org.springframework.statemachine.config.model.StatesData; import org.springframework.statemachine.config.model.TransitionsData; +import org.springframework.statemachine.monitor.StateMachineMonitor; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -114,6 +116,7 @@ public class StateMachineConfiguration extends private SmartLifecycle lifecycle; private DisposableBean disposableBean; private String beanName; + private StateMachineMonitor stateMachineMonitor; public StateMachineDelegatingFactoryBean(StateMachineConfigBuilder builder, Class> clazz, String clazzName, Boolean contextEvents) { @@ -160,6 +163,9 @@ public class StateMachineConfiguration extends stateMachineFactory.setContextEventsEnabled(contextEvents); stateMachineFactory.setBeanName(beanName); stateMachineFactory.setHandleAutostartup(stateMachineConfigurationConfig.isAutoStart()); + if (stateMachineMonitor != null) { + stateMachineFactory.setStateMachineMonitor(stateMachineMonitor); + } StateMachine stateMachine = stateMachineFactory.getStateMachine(); this.lifecycle = (SmartLifecycle) stateMachine; this.disposableBean = (DisposableBean) stateMachine; @@ -201,6 +207,10 @@ public class StateMachineConfiguration extends lifecycle.stop(callback); } + @Autowired(required = false) + public void setStateMachineMonitor(StateMachineMonitor stateMachineMonitor) { + this.stateMachineMonitor = stateMachineMonitor; + } } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultMonitoringConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultMonitoringConfigurer.java new file mode 100644 index 00000000..b70983d3 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultMonitoringConfigurer.java @@ -0,0 +1,48 @@ +/* + * 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.config.configurers; + +import org.springframework.statemachine.config.builders.StateMachineConfigurationBuilder; +import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; +import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerAdapter; +import org.springframework.statemachine.config.model.ConfigurationData; +import org.springframework.statemachine.monitor.StateMachineMonitor; + +/** + * Default implementation of a {@link MonitoringConfigurer}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class DefaultMonitoringConfigurer + extends AnnotationConfigurerAdapter, StateMachineConfigurationConfigurer, StateMachineConfigurationBuilder> + implements MonitoringConfigurer { + + private StateMachineMonitor monitor; + + @Override + public void configure(StateMachineConfigurationBuilder builder) throws Exception { + builder.setStateMachineMonitor(monitor); + } + + @Override + public MonitoringConfigurer monitor(StateMachineMonitor monitor) { + this.monitor = monitor; + return this; + } +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/MonitoringConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/MonitoringConfigurer.java new file mode 100644 index 00000000..2d657993 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/MonitoringConfigurer.java @@ -0,0 +1,40 @@ +/* + * 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.config.configurers; + +import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; +import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerBuilder; +import org.springframework.statemachine.monitor.StateMachineMonitor; + +/** + * Base {@code MonitoringConfigurer} interface for configuring state machine monitoring. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public interface MonitoringConfigurer extends + AnnotationConfigurerBuilder> { + + /** + * Specify a state machine monitor. + * + * @param monitor the state machine monitor + * @return configurer for chaining + */ + MonitoringConfigurer monitor(StateMachineMonitor monitor); +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/ConfigurationData.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/ConfigurationData.java index 96f41b0b..d4b340de 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/ConfigurationData.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/ConfigurationData.java @@ -29,6 +29,7 @@ import org.springframework.statemachine.config.model.verifier.DefaultStateMachin import org.springframework.statemachine.config.model.verifier.StateMachineModelVerifier; import org.springframework.statemachine.ensemble.StateMachineEnsemble; import org.springframework.statemachine.listener.StateMachineListener; +import org.springframework.statemachine.monitor.StateMachineMonitor; import org.springframework.statemachine.security.SecurityRule; /** @@ -55,13 +56,14 @@ public class ConfigurationData { private final AccessDecisionManager eventSecurityAccessDecisionManager; private final SecurityRule eventSecurityRule; private final SecurityRule transitionSecurityRule; + private final StateMachineMonitor stateMachineMonitor; /** * Instantiates a new state machine configuration config data. */ public ConfigurationData() { this(null, new SyncTaskExecutor(), new ConcurrentTaskScheduler(), false, null, new ArrayList>(), false, - null, null, null, null, true, new DefaultStateMachineModelVerifier(), null); + null, null, null, null, true, new DefaultStateMachineModelVerifier(), null, null); } /** @@ -87,7 +89,7 @@ public class ConfigurationData { List> listeners, boolean securityEnabled, AccessDecisionManager transitionSecurityAccessDecisionManager, AccessDecisionManager eventSecurityAccessDecisionManager, SecurityRule eventSecurityRule, SecurityRule transitionSecurityRule, boolean verifierEnabled, - StateMachineModelVerifier verifier, String machineId) { + StateMachineModelVerifier verifier, String machineId, StateMachineMonitor stateMachineMonitor) { this.beanFactory = beanFactory; this.taskExecutor = taskExecutor; this.taskScheduler = taskScheduler; @@ -102,6 +104,7 @@ public class ConfigurationData { this.verifierEnabled = verifierEnabled; this.verifier = verifier; this.machineId = machineId; + this.stateMachineMonitor = stateMachineMonitor; } public String getMachineId() { @@ -189,6 +192,15 @@ public class ConfigurationData { return verifier; } + /** + * Gets the state machine monitor. + * + * @return the state machine monitor + */ + public StateMachineMonitor getStateMachineMonitor() { + return stateMachineMonitor; + } + /** * Gets the transition security access decision manager. * diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/monitor/AbstractStateMachineMonitor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/monitor/AbstractStateMachineMonitor.java new file mode 100644 index 00000000..ef912e0e --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/monitor/AbstractStateMachineMonitor.java @@ -0,0 +1,34 @@ +/* + * 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.monitor; + +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.transition.Transition; + +/** + * Base implementation of a {@link StateMachineMonitor}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public abstract class AbstractStateMachineMonitor implements StateMachineMonitor { + + @Override + public void transition(StateMachine stateMachine, Transition transition, long duration) { + } +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/monitor/CompositeStateMachineMonitor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/monitor/CompositeStateMachineMonitor.java new file mode 100644 index 00000000..c72b2d06 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/monitor/CompositeStateMachineMonitor.java @@ -0,0 +1,42 @@ +/* + * 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.monitor; + +import java.util.Iterator; + +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.support.AbstractCompositeItems; +import org.springframework.statemachine.transition.Transition; + +/** + * Implementation of a {@link StateMachineMonitor} backed by a multiple monitors. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class CompositeStateMachineMonitor extends AbstractCompositeItems> + implements StateMachineMonitor { + + @Override + public void transition(StateMachine stateMachine, Transition transition, long duration) { + for (Iterator> iterator = getItems().reverse(); iterator.hasNext();) { + StateMachineMonitor monitor = iterator.next(); + monitor.transition(stateMachine, transition, duration); + } + } +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/monitor/StateMachineMonitor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/monitor/StateMachineMonitor.java new file mode 100644 index 00000000..ee7259c9 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/monitor/StateMachineMonitor.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.monitor; + +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.transition.Transition; + +/** + * {@code StateMachineMonitor} for various state machine monitoring events. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public interface StateMachineMonitor { + + /** + * Notified duration of a particular transition. + * + * @param stateMachine the state machine + * @param transition the transition + * @param duration the transition duration + */ + void transition(StateMachine stateMachine, Transition transition, long duration); +} 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 db8a2a10..8c13afe2 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 @@ -33,6 +33,7 @@ import org.springframework.statemachine.access.StateMachineAccess; import org.springframework.statemachine.access.StateMachineAccessor; import org.springframework.statemachine.access.StateMachineFunction; import org.springframework.statemachine.listener.StateMachineListener; +import org.springframework.statemachine.monitor.StateMachineMonitor; import org.springframework.statemachine.region.Region; import org.springframework.statemachine.state.AbstractState; import org.springframework.statemachine.state.ForkPseudoState; @@ -291,6 +292,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo @Override public void transit(Transition t, StateContext ctx, Message message) { + long now = System.currentTimeMillis(); // TODO: fix above stateContext as it's not used notifyTransitionStart(buildStateContext(Stage.TRANSITION_START, message, t, getRelayStateMachine())); notifyTransition(buildStateContext(Stage.TRANSITION, message, t, getRelayStateMachine())); @@ -306,6 +308,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } // TODO: looks like events should be called here and anno processing earlier notifyTransitionEnd(buildStateContext(Stage.TRANSITION_END, message, t, getRelayStateMachine())); + notifyTransitionMonitor(getRelayStateMachine(), t, System.currentTimeMillis() - now); } }); stateMachineExecutor = executor; @@ -662,6 +665,11 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo stateMachineExecutor.addStateMachineInterceptor(interceptor); } + @Override + public void addStateMachineMonitor(StateMachineMonitor monitor) { + getStateMachineMonitor().register(monitor); + } + @Override public UUID getUuid() { return uuid; diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java index 984620fd..1d83922c 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java @@ -28,6 +28,7 @@ import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.event.StateMachineEventPublisher; import org.springframework.statemachine.listener.CompositeStateMachineListener; import org.springframework.statemachine.listener.StateMachineListener; +import org.springframework.statemachine.monitor.CompositeStateMachineMonitor; import org.springframework.statemachine.processor.StateMachineHandlerCallHelper; import org.springframework.statemachine.state.State; import org.springframework.statemachine.transition.Transition; @@ -46,6 +47,7 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup private static final Log log = LogFactory.getLog(StateMachineObjectSupport.class); private final CompositeStateMachineListener stateListener = new CompositeStateMachineListener(); + private final CompositeStateMachineMonitor stateMachineMonitor = new CompositeStateMachineMonitor(); /** Context application event publisher if exist */ private volatile StateMachineEventPublisher stateMachineEventPublisher; @@ -128,6 +130,10 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup return stateListener; } + protected CompositeStateMachineMonitor getStateMachineMonitor() { + return stateMachineMonitor; + } + protected void notifyStateChanged(StateContext stateContext) { try { stateMachineHandlerCallHelper.callOnStateChanged(getBeanName(), stateContext); @@ -304,6 +310,14 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup } } + protected void notifyTransitionMonitor(StateMachine stateMachine, Transition transition, long duration) { + try { + stateMachineMonitor.transition(stateMachine, transition, duration); + } catch (Exception e) { + log.warn("Error during notifyTransitionMonitor", e); + } + } + protected void stateChangedInRelay() { // TODO: this is a temporary tweak to know when state is // changed in a submachine/regions order to give diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/access/StateMachineAccessTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/access/StateMachineAccessTests.java index b7ae2370..590b8648 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/access/StateMachineAccessTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/access/StateMachineAccessTests.java @@ -29,6 +29,7 @@ import org.springframework.statemachine.ExtendedState; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.StateMachineContext; import org.springframework.statemachine.listener.StateMachineListener; +import org.springframework.statemachine.monitor.StateMachineMonitor; import org.springframework.statemachine.state.State; import org.springframework.statemachine.support.StateMachineInterceptor; import org.springframework.statemachine.transition.Transition; @@ -96,6 +97,10 @@ public class StateMachineAccessTests { public void addStateMachineInterceptor(StateMachineInterceptor interceptor) { } + @Override + public void addStateMachineMonitor(StateMachineMonitor monitor) { + } + @Override public void setRelay(StateMachine stateMachine) { this.relay = stateMachine; diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/model/StateMachineModelTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/model/StateMachineModelTests.java index 16c6db23..85edce0e 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/model/StateMachineModelTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/model/StateMachineModelTests.java @@ -56,7 +56,7 @@ public class StateMachineModelTests { ConfigurationData configurationData = new ConfigurationData<>(beanFactory, taskExecutor, taskScheduler, autoStart, ensemble, listeners, securityEnabled, transitionSecurityAccessDecisionManager, eventSecurityAccessDecisionManager, - eventSecurityRule, transitionSecurityRule, verifierEnabled, verifier, null); + eventSecurityRule, transitionSecurityRule, verifierEnabled, verifier, null, null); Collection> stateData = new ArrayList<>(); StateData stateData1 = new StateData(null, null, "S1", null, null, null); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/monitor/StateMachineMonitorTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/monitor/StateMachineMonitorTests.java new file mode 100644 index 00000000..46df1b77 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/monitor/StateMachineMonitorTests.java @@ -0,0 +1,123 @@ +/* + * 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.monitor; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.AbstractStateMachineTests; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.StateMachineSystemConstants; +import org.springframework.statemachine.config.EnableStateMachine; +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.statemachine.transition.Transition; + +public class StateMachineMonitorTests extends AbstractStateMachineTests { + + @SuppressWarnings({ "unchecked" }) + @Test + public void testSimpleMonitor() throws Exception { + context.register(Config1.class); + context.refresh(); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + + TestStateMachineMonitor monitor = context.getBean(TestStateMachineMonitor.class); + + machine.start(); + assertThat(machine.getState().getIds(), contains("S1")); + machine.sendEvent("E1"); + assertThat(machine.getState().getIds(), contains("S2")); + assertThat(monitor.transition, notNullValue()); + assertThat(monitor.duration, notNullValue()); + monitor.reset(); + machine.sendEvent("E2"); + assertThat(machine.getState().getIds(), contains("S1")); + assertThat(monitor.transition, notNullValue()); + assertThat(monitor.duration, notNullValue()); + } + + @Configuration + @EnableStateMachine + public static class Config1 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withMonitoring() + .monitor(stateMachineMonitor()); + } + + @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") + .and() + .withExternal() + .source("S2") + .target("S1") + .event("E2"); + } + + @Bean + public StateMachineMonitor stateMachineMonitor() { + return new TestStateMachineMonitor(); + } + + } + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + + private static class TestStateMachineMonitor extends AbstractStateMachineMonitor { + + Transition transition; + Long duration; + + @Override + public void transition(StateMachine stateMachine, Transition transition, long duration) { + this.transition = transition; + this.duration = duration; + } + + void reset() { + transition = null; + duration = null; + } + } +} diff --git a/spring-statemachine-samples/build.gradle b/spring-statemachine-samples/build.gradle index 95bddd95..f7d69378 100644 --- a/spring-statemachine-samples/build.gradle +++ b/spring-statemachine-samples/build.gradle @@ -120,3 +120,11 @@ project('spring-statemachine-samples-datajpa') { } } +project('spring-statemachine-samples-monitoring') { + description = 'Spring State Machine Monitoring Sample' + dependencies { + compile project(":spring-statemachine-boot") + compile("org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion") + } +} + diff --git a/spring-statemachine-samples/monitoring/src/main/java/demo/monitoring/Application.java b/spring-statemachine-samples/monitoring/src/main/java/demo/monitoring/Application.java new file mode 100644 index 00000000..d286e367 --- /dev/null +++ b/spring-statemachine-samples/monitoring/src/main/java/demo/monitoring/Application.java @@ -0,0 +1,29 @@ +/* + * 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 demo.monitoring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +//tag::snippetA[] +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +//end::snippetA[] diff --git a/spring-statemachine-samples/monitoring/src/main/java/demo/monitoring/StateMachineConfig.java b/spring-statemachine-samples/monitoring/src/main/java/demo/monitoring/StateMachineConfig.java new file mode 100644 index 00000000..836077a8 --- /dev/null +++ b/spring-statemachine-samples/monitoring/src/main/java/demo/monitoring/StateMachineConfig.java @@ -0,0 +1,54 @@ +/* + * 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 demo.monitoring; + +import org.springframework.context.annotation.Configuration; +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; + +@Configuration +public class StateMachineConfig { + +//tag::snippetA[] + @Configuration + @EnableStateMachine + public static class Config extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("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"); + } + } +//end::snippetA[] +} diff --git a/spring-statemachine-samples/monitoring/src/main/java/demo/monitoring/StateMachineController.java b/spring-statemachine-samples/monitoring/src/main/java/demo/monitoring/StateMachineController.java new file mode 100644 index 00000000..6e94f13e --- /dev/null +++ b/spring-statemachine-samples/monitoring/src/main/java/demo/monitoring/StateMachineController.java @@ -0,0 +1,63 @@ +/* + * 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 demo.monitoring; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.statemachine.StateMachine; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Controller +public class StateMachineController { + + @Autowired + private StateMachine stateMachine; + + @RequestMapping("/") + public String home() { + return "redirect:/state"; + } + + @RequestMapping("/state") + public String feedAndGetStates(@RequestParam(value = "events", required = false) List events, Model model) throws Exception { + StateMachineLogListener listener = new StateMachineLogListener(); + stateMachine.addStateListener(listener); + stateMachine.start(); + if (events != null) { + for (String event : events) { + stateMachine.sendEvent(event); + } + } + stateMachine.stop(); + model.addAttribute("allEvents", new String[]{"E1", "E2"}); + model.addAttribute("messages", createMessages(listener.getMessages())); + return "states"; + } + + private String createMessages(List messages) { + StringBuilder buf = new StringBuilder(); + for (String message : messages) { + buf.append(message); + buf.append("\n"); + } + return buf.toString(); + } + +} diff --git a/spring-statemachine-samples/monitoring/src/main/java/demo/monitoring/StateMachineLogListener.java b/spring-statemachine-samples/monitoring/src/main/java/demo/monitoring/StateMachineLogListener.java new file mode 100644 index 00000000..d77144dd --- /dev/null +++ b/spring-statemachine-samples/monitoring/src/main/java/demo/monitoring/StateMachineLogListener.java @@ -0,0 +1,49 @@ +/* + * 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 demo.monitoring; + +import java.util.LinkedList; +import java.util.List; + +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.StateContext.Stage; +import org.springframework.statemachine.listener.StateMachineListenerAdapter; + +public class StateMachineLogListener extends StateMachineListenerAdapter { + + private final LinkedList messages = new LinkedList(); + + public List getMessages() { + return messages; + } + + public void resetMessages() { + messages.clear(); + } + + @Override + public void stateContext(StateContext stateContext) { + if (stateContext.getStage() == Stage.STATE_ENTRY) { + messages.addFirst("Enter " + stateContext.getTarget().getId()); + } else if (stateContext.getStage() == Stage.STATE_EXIT) { + messages.addFirst("Exit " + stateContext.getSource().getId()); + } else if (stateContext.getStage() == Stage.STATEMACHINE_START) { + messages.addLast("Machine started"); + } else if (stateContext.getStage() == Stage.STATEMACHINE_STOP) { + messages.addFirst("Machine stopped"); + } + } +} diff --git a/spring-statemachine-samples/monitoring/src/main/resources/application.yml b/spring-statemachine-samples/monitoring/src/main/resources/application.yml new file mode 100644 index 00000000..1996f572 --- /dev/null +++ b/spring-statemachine-samples/monitoring/src/main/resources/application.yml @@ -0,0 +1,9 @@ +logging: + level: + root: INFO +management: + security: + enabled: false +security: + basic: + enabled: false diff --git a/spring-statemachine-samples/monitoring/src/main/resources/templates/states.html b/spring-statemachine-samples/monitoring/src/main/resources/templates/states.html new file mode 100644 index 00000000..fce81eaf --- /dev/null +++ b/spring-statemachine-samples/monitoring/src/main/resources/templates/states.html @@ -0,0 +1,29 @@ + + + + Spring Statemachine Demo + + + +
+
+

+

    +
  • + + +
  • +
+
+ +
+
+