Add monitoring subsystem

- This is a preliminary work.
- New monitoring and tracing conceps around StateMachineMonitor.
- Adds hooks internally to better calculate transition times.
- Add new annotation configurer for monitors.
- New boot module which autoconfigures monitoring for boot's metrics
  and trancing repos.
- New monitoring sample.
- Relates to #149
This commit is contained in:
Janne Valkealahti
2016-10-18 06:54:47 +01:00
parent 543c906c35
commit 79f0280990
31 changed files with 1115 additions and 4 deletions

View File

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

View File

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

View File

@@ -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 <S> the type of state
* @param <E> the type of event
*/
public class BootStateMachineMonitor<S, E> extends AbstractStateMachineMonitor<S, E> {
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<S, E> stateMachine, Transition<S, E> transition, long duration) {
String transitionName = transitionToName(transition);
this.counterService.increment(METRIC_BASE + "." + transitionName + ".transit");
this.gaugeService.submit(METRIC_BASE + "." + transitionName + ".duration", duration);
Map<String, Object> traceInfo = new HashMap<>();
traceInfo.put("transition", transitionToName(transition));
traceInfo.put("duration", duration);
traceInfo.put("machine", stateMachine.getId());
traceRepository.add(traceInfo);
}
private static <S, E> String transitionToName(Transition<S, E> 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 <S, E> String nullStateId(State<S, E> state) {
if (state == null) {
return null;
}
S id = state.getId();
return id != null ? id.toString() : null;
}
}

View File

@@ -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<CounterService> counterServiceProvider,
ObjectProvider<GaugeService> gaugeServiceProvider,
ObjectProvider<TraceRepository> traceRepositoryProvider) {
this.counterService = counterServiceProvider.getIfAvailable();
this.gaugeService = gaugeServiceProvider.getIfAvailable();
this.traceRepository = traceRepositoryProvider.getIfAvailable();
}
@Bean
public BootStateMachineMonitor<?, ?> bootStateMachineMonitor() {
return new BootStateMachineMonitor<>(counterService, gaugeService, traceRepository);
}
}
}

View File

@@ -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 <em>spring.statemachine</em>.
*
* @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;
}
}
}

View File

@@ -0,0 +1,3 @@
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.statemachine.boot.StateMachineAutoConfiguration

View File

@@ -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<Object> 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<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states)
throws Exception {
states
.withStates()
.initial("S1")
.state("S2")
.state("S3");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions)
throws Exception {
transitions
.withExternal()
.source("S1").target("S2").event("E1")
.and()
.withExternal()
.source("S2").target("S3").event("E2");
}
}
}

View File

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

View File

@@ -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<S, E> {
*/
void addStateMachineInterceptor(StateMachineInterceptor<S, E> interceptor);
/**
* Adds the state machine monitor.
*
* @param monitor the monitor
*/
void addStateMachineMonitor(StateMachineMonitor<S, E> monitor);
/**
* Sets if initial state is enabled when a state machine is
* using sub states.

View File

@@ -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<S, E> extends LifecycleObjectS
private String beanName;
private StateMachineMonitor<S, E> defaultStateMachineMonitor;
/**
* Instantiates a new abstract state machine factory.
*
@@ -265,6 +268,23 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
}
});
// add monitoring hooks
final StateMachineMonitor<S, E> stateMachineMonitor = stateMachineModel.getConfigurationData().getStateMachineMonitor();
if (stateMachineMonitor != null || defaultStateMachineMonitor != null) {
fmachine.getStateMachineAccessor().doWithRegion(new StateMachineFunction<StateMachineAccess<S ,E>>() {
@Override
public void apply(StateMachineAccess<S, E> 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<S, E> securityInterceptor = new StateMachineSecurityInterceptor<S, E>(
@@ -324,6 +344,15 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
this.contextEvents = contextEvents;
}
/**
* Sett state machine monitor.
*
* @param stateMachineMonitor the state machine monitor
*/
public void setStateMachineMonitor(StateMachineMonitor<S, E> stateMachineMonitor) {
this.defaultStateMachineMonitor = stateMachineMonitor;
}
private StateMachine<S, E> delegateAutoStartup(StateMachine<S, E> delegate) {
if (handleAutostartup && delegate instanceof SmartLifecycle && ((SmartLifecycle) delegate).isAutoStartup()) {
((SmartLifecycle)delegate).start();

View File

@@ -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<S, E>
private AccessDecisionManager eventSecurityAccessDecisionManager;
private SecurityRule eventSecurityRule;
private SecurityRule transitionSecurityRule;
private StateMachineMonitor<S, E> stateMachineMonitor;
/**
* Instantiates a new state machine configuration builder.
@@ -114,11 +118,16 @@ public class StateMachineConfigurationBuilder<S, E>
return apply(new DefaultVerifierConfigurer<S, E>());
}
@Override
public MonitoringConfigurer<S, E> withMonitoring() throws Exception {
return apply(new DefaultMonitoringConfigurer<S, E>());
}
@Override
protected ConfigurationData<S, E> performBuild() throws Exception {
return new ConfigurationData<S, E>(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<S, E>
this.verifierEnabled = verifierEnabled;
}
/**
* Sets the state machine monitor.
*
* @param stateMachineMonitor the state machine monitor
*/
public void setStateMachineMonitor(StateMachineMonitor<S, E> stateMachineMonitor) {
this.stateMachineMonitor = stateMachineMonitor;
}
/**
* Sets the security transition access decision manager.
*

View File

@@ -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<S, E> {
* @throws Exception if configuration error happens
*/
VerifierConfigurer<S, E> withVerifier() throws Exception;
/**
* Gets a configurer for state machine monitoring.
*
* @return {@link MonitoringConfigurer} for chaining
* @throws Exception if configuration error happens
*/
MonitoringConfigurer<S, E> withMonitoring() throws Exception;
}

View File

@@ -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<S, E> extends
private SmartLifecycle lifecycle;
private DisposableBean disposableBean;
private String beanName;
private StateMachineMonitor<S, E> stateMachineMonitor;
public StateMachineDelegatingFactoryBean(StateMachineConfigBuilder<S, E> builder, Class<StateMachine<S, E>> clazz,
String clazzName, Boolean contextEvents) {
@@ -160,6 +163,9 @@ public class StateMachineConfiguration<S, E> extends
stateMachineFactory.setContextEventsEnabled(contextEvents);
stateMachineFactory.setBeanName(beanName);
stateMachineFactory.setHandleAutostartup(stateMachineConfigurationConfig.isAutoStart());
if (stateMachineMonitor != null) {
stateMachineFactory.setStateMachineMonitor(stateMachineMonitor);
}
StateMachine<S, E> stateMachine = stateMachineFactory.getStateMachine();
this.lifecycle = (SmartLifecycle) stateMachine;
this.disposableBean = (DisposableBean) stateMachine;
@@ -201,6 +207,10 @@ public class StateMachineConfiguration<S, E> extends
lifecycle.stop(callback);
}
@Autowired(required = false)
public void setStateMachineMonitor(StateMachineMonitor<S, E> stateMachineMonitor) {
this.stateMachineMonitor = stateMachineMonitor;
}
}
}

View File

@@ -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 <S> the type of state
* @param <E> the type of event
*/
public class DefaultMonitoringConfigurer <S, E>
extends AnnotationConfigurerAdapter<ConfigurationData<S, E>, StateMachineConfigurationConfigurer<S, E>, StateMachineConfigurationBuilder<S, E>>
implements MonitoringConfigurer<S, E> {
private StateMachineMonitor<S, E> monitor;
@Override
public void configure(StateMachineConfigurationBuilder<S, E> builder) throws Exception {
builder.setStateMachineMonitor(monitor);
}
@Override
public MonitoringConfigurer<S, E> monitor(StateMachineMonitor<S, E> monitor) {
this.monitor = monitor;
return this;
}
}

View File

@@ -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 <S> the type of state
* @param <E> the type of event
*/
public interface MonitoringConfigurer <S, E> extends
AnnotationConfigurerBuilder<StateMachineConfigurationConfigurer<S, E>> {
/**
* Specify a state machine monitor.
*
* @param monitor the state machine monitor
* @return configurer for chaining
*/
MonitoringConfigurer<S, E> monitor(StateMachineMonitor<S, E> monitor);
}

View File

@@ -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<S, E> {
private final AccessDecisionManager eventSecurityAccessDecisionManager;
private final SecurityRule eventSecurityRule;
private final SecurityRule transitionSecurityRule;
private final StateMachineMonitor<S, E> stateMachineMonitor;
/**
* Instantiates a new state machine configuration config data.
*/
public ConfigurationData() {
this(null, new SyncTaskExecutor(), new ConcurrentTaskScheduler(), false, null, new ArrayList<StateMachineListener<S, E>>(), false,
null, null, null, null, true, new DefaultStateMachineModelVerifier<S, E>(), null);
null, null, null, null, true, new DefaultStateMachineModelVerifier<S, E>(), null, null);
}
/**
@@ -87,7 +89,7 @@ public class ConfigurationData<S, E> {
List<StateMachineListener<S, E>> listeners, boolean securityEnabled,
AccessDecisionManager transitionSecurityAccessDecisionManager, AccessDecisionManager eventSecurityAccessDecisionManager,
SecurityRule eventSecurityRule, SecurityRule transitionSecurityRule, boolean verifierEnabled,
StateMachineModelVerifier<S, E> verifier, String machineId) {
StateMachineModelVerifier<S, E> verifier, String machineId, StateMachineMonitor<S, E> stateMachineMonitor) {
this.beanFactory = beanFactory;
this.taskExecutor = taskExecutor;
this.taskScheduler = taskScheduler;
@@ -102,6 +104,7 @@ public class ConfigurationData<S, E> {
this.verifierEnabled = verifierEnabled;
this.verifier = verifier;
this.machineId = machineId;
this.stateMachineMonitor = stateMachineMonitor;
}
public String getMachineId() {
@@ -189,6 +192,15 @@ public class ConfigurationData<S, E> {
return verifier;
}
/**
* Gets the state machine monitor.
*
* @return the state machine monitor
*/
public StateMachineMonitor<S, E> getStateMachineMonitor() {
return stateMachineMonitor;
}
/**
* Gets the transition security access decision manager.
*

View File

@@ -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 <S> the type of state
* @param <E> the type of event
*/
public abstract class AbstractStateMachineMonitor<S, E> implements StateMachineMonitor<S, E> {
@Override
public void transition(StateMachine<S, E> stateMachine, Transition<S, E> transition, long duration) {
}
}

View File

@@ -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 <S> the type of state
* @param <E> the type of event
*/
public class CompositeStateMachineMonitor<S, E> extends AbstractCompositeItems<StateMachineMonitor<S, E>>
implements StateMachineMonitor<S, E> {
@Override
public void transition(StateMachine<S, E> stateMachine, Transition<S, E> transition, long duration) {
for (Iterator<StateMachineMonitor<S, E>> iterator = getItems().reverse(); iterator.hasNext();) {
StateMachineMonitor<S, E> monitor = iterator.next();
monitor.transition(stateMachine, transition, duration);
}
}
}

View File

@@ -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 <S> the type of state
* @param <E> the type of event
*/
public interface StateMachineMonitor<S, E> {
/**
* Notified duration of a particular transition.
*
* @param stateMachine the state machine
* @param transition the transition
* @param duration the transition duration
*/
void transition(StateMachine<S, E> stateMachine, Transition<S, E> transition, long duration);
}

View File

@@ -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<S, E> extends StateMachineObjectSuppo
@Override
public void transit(Transition<S, E> t, StateContext<S, E> ctx, Message<E> 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<S, E> 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<S, E> extends StateMachineObjectSuppo
stateMachineExecutor.addStateMachineInterceptor(interceptor);
}
@Override
public void addStateMachineMonitor(StateMachineMonitor<S, E> monitor) {
getStateMachineMonitor().register(monitor);
}
@Override
public UUID getUuid() {
return uuid;

View File

@@ -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<S, E> extends LifecycleObjectSup
private static final Log log = LogFactory.getLog(StateMachineObjectSupport.class);
private final CompositeStateMachineListener<S, E> stateListener = new CompositeStateMachineListener<S, E>();
private final CompositeStateMachineMonitor<S, E> stateMachineMonitor = new CompositeStateMachineMonitor<S, E>();
/** Context application event publisher if exist */
private volatile StateMachineEventPublisher stateMachineEventPublisher;
@@ -128,6 +130,10 @@ public abstract class StateMachineObjectSupport<S, E> extends LifecycleObjectSup
return stateListener;
}
protected CompositeStateMachineMonitor<S, E> getStateMachineMonitor() {
return stateMachineMonitor;
}
protected void notifyStateChanged(StateContext<S, E> stateContext) {
try {
stateMachineHandlerCallHelper.callOnStateChanged(getBeanName(), stateContext);
@@ -304,6 +310,14 @@ public abstract class StateMachineObjectSupport<S, E> extends LifecycleObjectSup
}
}
protected void notifyTransitionMonitor(StateMachine<S, E> stateMachine, Transition<S, E> 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

View File

@@ -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<String, String> interceptor) {
}
@Override
public void addStateMachineMonitor(StateMachineMonitor<String, String> monitor) {
}
@Override
public void setRelay(StateMachine<String, String> stateMachine) {
this.relay = stateMachine;

View File

@@ -56,7 +56,7 @@ public class StateMachineModelTests {
ConfigurationData<String, String> 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<String, String>> stateData = new ArrayList<>();
StateData<String, String> stateData1 = new StateData<String, String>(null, null, "S1", null, null, null);

View File

@@ -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<String, String> 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<String, String> {
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config)
throws Exception {
config
.withMonitoring()
.monitor(stateMachineMonitor());
}
@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")
.and()
.withExternal()
.source("S2")
.target("S1")
.event("E2");
}
@Bean
public StateMachineMonitor<String, String> stateMachineMonitor() {
return new TestStateMachineMonitor();
}
}
@Override
protected AnnotationConfigApplicationContext buildContext() {
return new AnnotationConfigApplicationContext();
}
private static class TestStateMachineMonitor extends AbstractStateMachineMonitor<String, String> {
Transition<String, String> transition;
Long duration;
@Override
public void transition(StateMachine<String, String> stateMachine, Transition<String, String> transition, long duration) {
this.transition = transition;
this.duration = duration;
}
void reset() {
transition = null;
duration = null;
}
}
}

View File

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

View File

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

View File

@@ -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<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states)
throws Exception {
states
.withStates()
.initial("S1")
.state("S2")
.state("S3");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions)
throws Exception {
transitions
.withExternal()
.source("S1").target("S2").event("E1")
.and()
.withExternal()
.source("S2").target("S3").event("E2");
}
}
//end::snippetA[]
}

View File

@@ -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<String, String> stateMachine;
@RequestMapping("/")
public String home() {
return "redirect:/state";
}
@RequestMapping("/state")
public String feedAndGetStates(@RequestParam(value = "events", required = false) List<String> 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<String> messages) {
StringBuilder buf = new StringBuilder();
for (String message : messages) {
buf.append(message);
buf.append("\n");
}
return buf.toString();
}
}

View File

@@ -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<String, String> {
private final LinkedList<String> messages = new LinkedList<String>();
public List<String> getMessages() {
return messages;
}
public void resetMessages() {
messages.clear();
}
@Override
public void stateContext(StateContext<String, String> 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");
}
}
}

View File

@@ -0,0 +1,9 @@
logging:
level:
root: INFO
management:
security:
enabled: false
security:
basic:
enabled: false

View File

@@ -0,0 +1,29 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring Statemachine Demo</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<form action="#" data-th-action="@{/state}" data-th-object="${model}" method="post">
<div>
<p th:text="'Choose events'"/>
<ul>
<li th:each="ty : ${allEvents}">
<input type="checkbox" name="events" th:value="${ty}" />
<label th:text="${ty}">replaced</label>
</li>
</ul>
</div>
<button type="submit">Send Events</button>
</form>
<div>
<textarea th:text="${messages}" rows="20" cols="100"/>
</div>
<div>
<form action="#" data-th-action="@{/state}" method="get">
<button type="submit">Refresh</button>
</form>
</div>
</body>
</html>