From 250f99a1aecc31db5e92cb08ad4c6b6508f193d4 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Sun, 8 Feb 2015 09:58:36 +0000 Subject: [PATCH] Add base support for app context event - tidy up how listener event is sent together with app context events. - resolves #12 --- .../StateMachineSystemConstants.java | 3 + .../event/AbstractStateMachineEvent.java | 44 ++++++ .../DefaultStateMachineEventPublisher.java | 59 ++++++++ .../statemachine/event/LoggingListener.java | 102 +++++++++++++ .../event/OnStateChangedEvent.java | 61 ++++++++ .../event/StateMachineEventPublisher.java | 37 +++++ ...ateMachineEventPublisherConfiguration.java | 36 +++++ .../support/AbstractStateMachine.java | 13 +- .../support/LifecycleObjectSupport.java | 29 ++++ .../support/StateMachineContextUtils.java | 17 ++- .../event/StateMachineEventTests.java | 139 ++++++++++++++++++ 11 files changed, 536 insertions(+), 4 deletions(-) create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/event/AbstractStateMachineEvent.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/event/DefaultStateMachineEventPublisher.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/event/LoggingListener.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/event/OnStateChangedEvent.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/event/StateMachineEventPublisher.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/event/StateMachineEventPublisherConfiguration.java create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/event/StateMachineEventTests.java diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineSystemConstants.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineSystemConstants.java index 38c91c5b..522c5b51 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineSystemConstants.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineSystemConstants.java @@ -29,4 +29,7 @@ public abstract class StateMachineSystemConstants { /** Default bean id for state machine factory. */ public static final String DEFAULT_ID_STATEMACHINEFACTORY = "stateMachineFactory"; + /** Default bean id for state machine event publisher. */ + public static final String DEFAULT_ID_EVENT_PUBLISHER = "stateMachineEventPublisher"; + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/event/AbstractStateMachineEvent.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/event/AbstractStateMachineEvent.java new file mode 100644 index 00000000..418fea45 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/event/AbstractStateMachineEvent.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.event; + +import org.springframework.context.ApplicationEvent; + +/** + * Base {@link ApplicationEvent} class for leader based events. All custom event + * classes should be derived from this class. + * + * @author Janne Valkealahti + * + */ +@SuppressWarnings("serial") +public abstract class AbstractStateMachineEvent extends ApplicationEvent { + + /** + * Create a new ApplicationEvent. + * + * @param source the component that published the event (never {@code null}) + */ + public AbstractStateMachineEvent(Object source) { + super(source); + } + + @Override + public String toString() { + return "AbstractLeaderEvent [source=" + source + "]"; + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/event/DefaultStateMachineEventPublisher.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/event/DefaultStateMachineEventPublisher.java new file mode 100644 index 00000000..b20ac526 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/event/DefaultStateMachineEventPublisher.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.event; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.statemachine.state.State; + +/** + * Default implementation of {@link StateMachineEventPublisher}. + * + * @author Janne Valkealahti + * + */ +public class DefaultStateMachineEventPublisher implements StateMachineEventPublisher, ApplicationEventPublisherAware { + + private ApplicationEventPublisher applicationEventPublisher; + + /** + * Instantiates a new leader event publisher. + */ + public DefaultStateMachineEventPublisher() { + } + + /** + * Instantiates a new leader event publisher. + * + * @param applicationEventPublisher the application event publisher + */ + public DefaultStateMachineEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + @Override + public void publishStateChanged(Object source, State sourceState, State targetState) { + if (applicationEventPublisher != null) { + applicationEventPublisher.publishEvent(new OnStateChangedEvent(source, sourceState, targetState)); + } + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/event/LoggingListener.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/event/LoggingListener.java new file mode 100644 index 00000000..da630f01 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/event/LoggingListener.java @@ -0,0 +1,102 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.event; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationListener; +import org.springframework.util.StringUtils; + +/** + * Simple {@link ApplicationListener} which logs all events + * based on {@link AbstractStateMachineEvent} using a log level + * set during the construction. + * + * @author Janne Valkealahti + * + */ +public class LoggingListener implements ApplicationListener { + + private static final Log log = LogFactory.getLog(LoggingListener.class); + + /** Internal enums to match the log level */ + private static enum Level { + FATAL, ERROR, WARN, INFO, DEBUG, TRACE + } + + /** Level to use */ + private final Level level; + + /** + * Constructs Logger listener with debug level. + */ + public LoggingListener() { + level = Level.DEBUG; + } + + /** + * Constructs Logger listener with given level. + * + * @param level the level string + */ + public LoggingListener(String level) { + try { + this.level = Level.valueOf(level.toUpperCase()); + } + catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid log level '" + level + + "'. The (case-insensitive) supported values are: " + + StringUtils.arrayToCommaDelimitedString(Level.values())); + } + } + + @Override + public void onApplicationEvent(AbstractStateMachineEvent event) { + switch (this.level) { + case FATAL: + if (log.isFatalEnabled()) { + log.fatal(event); + } + break; + case ERROR: + if (log.isErrorEnabled()) { + log.error(event); + } + break; + case WARN: + if (log.isWarnEnabled()) { + log.warn(event); + } + break; + case INFO: + if (log.isInfoEnabled()) { + log.info(event); + } + break; + case DEBUG: + if (log.isDebugEnabled()) { + log.debug(event); + } + break; + case TRACE: + if (log.isTraceEnabled()) { + log.trace(event); + } + break; + } + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/event/OnStateChangedEvent.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/event/OnStateChangedEvent.java new file mode 100644 index 00000000..27986ddb --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/event/OnStateChangedEvent.java @@ -0,0 +1,61 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.event; + +import org.springframework.statemachine.state.State; + +/** + * Generic event representing that state has been changed. + * + * @author Janne Valkealahti + * + */ +@SuppressWarnings("serial") +public class OnStateChangedEvent extends AbstractStateMachineEvent { + + private final State sourceState; + private final State targetState; + + /** + * Instantiates a new granted event. + * + * @param source the component that published the event (never {@code null}) + */ + public OnStateChangedEvent(Object source, State sourceState, State targetState) { + super(source); + this.sourceState = sourceState; + this.targetState = targetState; + } + + /** + * Gets the source state for this event. + * + * @return the source state + */ + public State getSourceState() { + return sourceState; + } + + /** + * Gets the target state for this event. + * + * @return the target state + */ + public State getTargetState() { + return targetState; + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/event/StateMachineEventPublisher.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/event/StateMachineEventPublisher.java new file mode 100644 index 00000000..22814270 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/event/StateMachineEventPublisher.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.event; + +import org.springframework.statemachine.state.State; + +/** + * Interface for publishing state machine based application events. + * + * @author Janne Valkealahti + * + */ +public interface StateMachineEventPublisher { + + /** + * Publish a state changed event. + * + * @param source the component generated this event + * @param sourceState the source state + * @param targetState the target state + */ + void publishStateChanged(Object source, State sourceState, State targetState); + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/event/StateMachineEventPublisherConfiguration.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/event/StateMachineEventPublisherConfiguration.java new file mode 100644 index 00000000..b2dcb7f4 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/event/StateMachineEventPublisherConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.event; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.StateMachineSystemConstants; + +/** + * Configuration for common {@link StateMachineEventPublisher}. + * + * @author Janne Valkealahti + * + */ +@Configuration +public class StateMachineEventPublisherConfiguration { + + @Bean(name = StateMachineSystemConstants.DEFAULT_ID_EVENT_PUBLISHER) + public StateMachineEventPublisher stateMachineEventPublisher() { + return new DefaultStateMachineEventPublisher(); + } + +} 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 3192a50a..989bf1a5 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 @@ -40,6 +40,7 @@ import org.springframework.statemachine.StateContext; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.annotation.OnTransition; +import org.springframework.statemachine.event.StateMachineEventPublisher; import org.springframework.statemachine.listener.CompositeStateMachineListener; import org.springframework.statemachine.listener.StateMachineListener; import org.springframework.statemachine.processor.StateMachineHandler; @@ -215,12 +216,12 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport public Collection> getTransitions() { return transitions; } - + private void switchToState(State state, Message event) { log.info("Moving into state=" + state + " from " + currentState); exitFromState(currentState, event); - stateListener.stateChanged(currentState, state); + notifyStateChanged(currentState, state); callHandlers(currentState, state, event); @@ -381,4 +382,12 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport return handlersList; } + private void notifyStateChanged(State source, State target) { + stateListener.stateChanged(source, target); + StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher(); + if (eventPublisher != null) { + eventPublisher.publishStateChanged(this, source, target); + } + } + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/LifecycleObjectSupport.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/LifecycleObjectSupport.java index ea4987c6..8ba895d0 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/LifecycleObjectSupport.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/LifecycleObjectSupport.java @@ -27,6 +27,7 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.context.SmartLifecycle; import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.TaskScheduler; +import org.springframework.statemachine.event.StateMachineEventPublisher; import org.springframework.util.Assert; /** @@ -55,6 +56,9 @@ public abstract class LifecycleObjectSupport implements InitializingBean, SmartL // to access bean factory private volatile BeanFactory beanFactory; + /** Context application event publisher if exist */ + private volatile StateMachineEventPublisher stateMachineEventPublisher; + @Override public final void afterPropertiesSet() { try { @@ -226,6 +230,31 @@ public abstract class LifecycleObjectSupport implements InitializingBean, SmartL return taskExecutor; } + /** + * Gets the state machine event publisher. + * + * @return the state machine event publisher + */ + protected StateMachineEventPublisher getStateMachineEventPublisher() { + if(stateMachineEventPublisher == null && getBeanFactory() != null) { + if(log.isDebugEnabled()) { + log.debug("getting stateMachineEventPublisher service from bean factory " + getBeanFactory()); + } + stateMachineEventPublisher = StateMachineContextUtils.getEventPublisher(getBeanFactory()); + } + return stateMachineEventPublisher; + } + + /** + * Sets the state machine event publisher. + * + * @param stateMachineEventPublisher the new state machine event publisher + */ + public void setStateMachineEventPublisher(StateMachineEventPublisher stateMachineEventPublisher) { + Assert.notNull(stateMachineEventPublisher, "StateMachineEventPublisher cannot be null"); + this.stateMachineEventPublisher = stateMachineEventPublisher; + } + /** * Subclasses may implement this for initialization logic. Called * during the {@link InitializingBean} phase. Implementor should diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineContextUtils.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineContextUtils.java index 86314df2..061db946 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineContextUtils.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineContextUtils.java @@ -20,6 +20,8 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.task.TaskExecutor; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.scheduling.TaskScheduler; +import org.springframework.statemachine.StateMachineSystemConstants; +import org.springframework.statemachine.event.StateMachineEventPublisher; import org.springframework.util.Assert; /** @@ -68,8 +70,7 @@ public class StateMachineContextUtils { * Return the {@link ConversionService} bean whose name is * "yarnConversionService" if available. * - * @param beanFactory - * BeanFactory for lookup, must not be null. + * @param beanFactory BeanFactory for lookup, must not be null. * * @return The {@link ConversionService} bean whose name is * "yarnConversionService" if available. @@ -91,6 +92,18 @@ public class StateMachineContextUtils { return getBeanOfType(beanFactory, EVALUATION_CONTEXT_BEAN_NAME, StandardEvaluationContext.class); } + /** + * Return the {@link StateMachineEventPublisher} bean whose name is "stateMachineEventPublisher" if + * available. + * + * @param beanFactory BeanFactory for lookup, must not be null. + * @return state machine event publisher + */ + public static StateMachineEventPublisher getEventPublisher(BeanFactory beanFactory) { + return getBeanOfType(beanFactory, StateMachineSystemConstants.DEFAULT_ID_EVENT_PUBLISHER, + StateMachineEventPublisher.class); + } + /** * Gets a bean from a factory with a given name and type. * diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/event/StateMachineEventTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/event/StateMachineEventTests.java new file mode 100644 index 00000000..64c0728a --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/event/StateMachineEventTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.event; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.springframework.context.ApplicationListener; +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.EnumStateMachine; +import org.springframework.statemachine.StateMachineSystemConstants; +import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +/** + * Tests from state machine app context events. + * + * @author Janne Valkealahti + * + */ +public class StateMachineEventTests extends AbstractStateMachineTests { + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + + @Test + public void testContextEvents() throws Exception { + context.register(BaseConfig.class, StateMachineEventPublisherConfiguration.class, Config1.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + TestEventListener listener = context.getBean(TestEventListener.class); + @SuppressWarnings("unchecked") + EnumStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + assertThat(machine, notNullValue()); + machine.sendEvent(TestEvents.E1); + machine.sendEvent(TestEvents.E2); + machine.sendEvent(TestEvents.E3); + machine.sendEvent(TestEvents.E4); + machine.sendEvent(TestEvents.EF); + + // 6 events instead of 5, first one is initial transition + // to SI where source state is null + assertThat(listener.onEventLatch.await(5, TimeUnit.SECONDS), is(true)); + assertThat(listener.events.size(), is(6)); + } + + @Configuration + @EnableStateMachine + static class Config1 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.SI) + .states(EnumSet.allOf(TestStates.class)) + .end(TestStates.SF); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.SI) + .target(TestStates.S1) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S1) + .target(TestStates.S2) + .event(TestEvents.E2) + .and() + .withExternal() + .source(TestStates.S2) + .target(TestStates.S3) + .event(TestEvents.E3) + .and() + .withExternal() + .source(TestStates.S3) + .target(TestStates.S4) + .event(TestEvents.E4) + .and() + .withExternal() + .source(TestStates.S4) + .target(TestStates.SF) + .event(TestEvents.EF); + } + + @Bean + public TestEventListener testEventListener() { + return new TestEventListener(); + } + + } + + static class TestEventListener implements ApplicationListener { + + CountDownLatch onEventLatch = new CountDownLatch(6); + + ArrayList events = new ArrayList(); + + @Override + public void onApplicationEvent(AbstractStateMachineEvent event) { + events.add(event); + onEventLatch.countDown(); + } + + } + +}