diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/action/SpelExpressionAction.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/action/SpelExpressionAction.java new file mode 100644 index 00000000..92a96aa6 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/action/SpelExpressionAction.java @@ -0,0 +1,52 @@ +/* + * 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.action; + +import org.springframework.expression.Expression; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.support.StateContextExpressionMethods; +import org.springframework.util.Assert; + +/** + * {@link Action} which uses Spring SpEL expression for action execution. + * + * @author Janne Valkealahti + * + */ +public class SpelExpressionAction implements Action { + + private final StateContextExpressionMethods methods; + + private final Expression expression; + + /** + * Instantiates a new spel expression action. + * + * @param expression the expression + */ + public SpelExpressionAction(Expression expression) { + Assert.notNull(expression, "Expression cannot be null"); + this.expression = expression; + this.methods = new StateContextExpressionMethods(new StandardEvaluationContext()); + } + + @Override + public void execute(StateContext context) { + methods.getValue(expression, context, Object.class); + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/guard/SpelExpressionGuard.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/guard/SpelExpressionGuard.java index 1a83718d..52e0675f 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/guard/SpelExpressionGuard.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/guard/SpelExpressionGuard.java @@ -18,18 +18,21 @@ package org.springframework.statemachine.guard; import org.springframework.expression.Expression; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.support.StateContextExpressionMethods; import org.springframework.util.Assert; /** * {@link Guard} which uses Spring SpEL expression for condition evaluation. - * + * * @author Janne Valkealahti * */ public class SpelExpressionGuard implements Guard { - + + private final StateContextExpressionMethods methods; + private final Expression expression; - + /** * Instantiates a new spel expression guard. * @@ -38,12 +41,12 @@ public class SpelExpressionGuard implements Guard { public SpelExpressionGuard(Expression expression) { Assert.notNull(expression, "Expression cannot be null"); this.expression = expression; + this.methods = new StateContextExpressionMethods(new StandardEvaluationContext()); } @Override public boolean evaluate(StateContext context) { - StandardEvaluationContext evaluationContext = new StandardEvaluationContext(context); - return expression.getValue(evaluationContext, Boolean.class); + return methods.getValue(expression, context, Boolean.class); } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateContextExpressionMethods.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateContextExpressionMethods.java new file mode 100644 index 00000000..7c0c4ce1 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateContextExpressionMethods.java @@ -0,0 +1,60 @@ +/* + * 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.support; + +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.statemachine.StateContext; +import org.springframework.util.Assert; + +/** + * Helper class to work with a spel expressions and {@link StateContext}. + * + * @author Janne Valkealahti + * + */ +public class StateContextExpressionMethods { + + private final StandardEvaluationContext context; + + /** + * Instantiates a new state context expression methods + * with a {@link StandardEvaluationContext}. + * + * @param evaluationContext the spel evaluation context + */ + public StateContextExpressionMethods(StandardEvaluationContext evaluationContext) { + this.context = evaluationContext; + } + + /** + * Gets the value. + * + * @param the generic type + * @param expression the expression + * @param stateContext the state context + * @param desiredResultType the desired result type + * @return the value + * @throws EvaluationException the evaluation exception + */ + public T getValue(Expression expression, StateContext stateContext, Class desiredResultType) + throws EvaluationException { + Assert.notNull(expression, "Expression cannot be null"); + return expression.getValue(context, stateContext, desiredResultType); + } + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java index fb726d60..7a6c0459 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java @@ -226,10 +226,10 @@ public abstract class AbstractStateMachineTests { } } - protected static class TestStateMachineListener extends StateMachineListenerAdapter { + public static class TestStateMachineListener extends StateMachineListenerAdapter { - volatile CountDownLatch stateChangedLatch = new CountDownLatch(0); - volatile CountDownLatch stateMachineStartedLatch = new CountDownLatch(3); + public volatile CountDownLatch stateChangedLatch = new CountDownLatch(0); + public volatile CountDownLatch stateMachineStartedLatch = new CountDownLatch(3); @Override public void stateChanged(State from, State to) { @@ -241,7 +241,7 @@ public abstract class AbstractStateMachineTests { stateMachineStartedLatch.countDown(); } - void reset(int c1, int c2) { + public void reset(int c1, int c2) { stateChangedLatch = new CountDownLatch(c1); stateMachineStartedLatch = new CountDownLatch(c2); } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/action/SpelExpressionActionTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/action/SpelExpressionActionTests.java new file mode 100644 index 00000000..7fec1e05 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/action/SpelExpressionActionTests.java @@ -0,0 +1,107 @@ +/* + * 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.action; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.TimeUnit; + +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.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.messaging.support.MessageBuilder; +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.EnumStateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +public class SpelExpressionActionTests extends AbstractStateMachineTests { + + @SuppressWarnings({ "unchecked" }) + @Test + public void testSpelActionSendsEvent() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config1.class); + assertTrue(ctx.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + StateMachine machine = + ctx.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestStateMachineListener listener = new TestStateMachineListener(); + machine.addStateListener(listener); + machine.start(); + listener.reset(2, 0); + + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + assertThat(listener.stateChangedLatch.await(5, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), contains(TestStates.S3)); + ctx.close(); + } + + private static class TestSpelAction extends SpelExpressionAction { + + public TestSpelAction(Expression expression) { + super(expression); + } + + } + + @Configuration + @EnableStateMachine + static class Config1 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S1, TestEvents.E2) + .state(TestStates.S2) + .state(TestStates.S3); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S2) + .event(TestEvents.E1) + .action(testAction1()) + .and() + .withExternal() + .source(TestStates.S2) + .target(TestStates.S3) + .event(TestEvents.E2); + } + + @Bean + public TestSpelAction testAction1() { + ExpressionParser parser = new SpelExpressionParser(); + return new TestSpelAction( + parser.parseExpression("stateMachine.sendEvent(T(org.springframework.statemachine.AbstractStateMachineTests.TestEvents).E2)")); + } + + } + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateContextExpressionMethodsTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateContextExpressionMethodsTests.java new file mode 100644 index 00000000..37eb81f3 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateContextExpressionMethodsTests.java @@ -0,0 +1,193 @@ +/* + * 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.support; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.statemachine.ExtendedState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.action.Action; +import org.springframework.statemachine.listener.StateMachineListener; +import org.springframework.statemachine.state.EnumState; +import org.springframework.statemachine.state.State; +import org.springframework.statemachine.transition.Transition; +import org.springframework.statemachine.transition.TransitionKind; +import org.springframework.statemachine.trigger.Trigger; + +public class StateContextExpressionMethodsTests { + + @Test + public void testGuardBooleanExpressions() { + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + StateContextExpressionMethods methods = new StateContextExpressionMethods(evaluationContext); + StateContext stateContext = mockStateContext(null); + + assertThat(methods.getValue(parser.parseExpression("true"), stateContext, Boolean.class), is(true)); + assertThat(methods.getValue(parser.parseExpression("event.toString().equals('E1')"), stateContext, Boolean.class), is(true)); + assertThat(methods.getValue(parser.parseExpression("event==T(org.springframework.statemachine.support.StateContextExpressionMethodsTests.SpelEvents).E1"), stateContext, Boolean.class), is(true)); + assertThat(methods.getValue(parser.parseExpression("getExtendedState().getVariables().get('boolean1')"), stateContext, Boolean.class), is(true)); + assertThat(methods.getValue(parser.parseExpression("extendedState.variables.get('boolean1')"), stateContext, Boolean.class), is(true)); + assertThat(methods.getValue(parser.parseExpression("extendedState.variables.get('boolean1')&&!extendedState.variables.get('boolean2')"), stateContext, Boolean.class), is(true)); + assertThat(methods.getValue(parser.parseExpression("extendedState.variables.get('boolean3')==NULL"), stateContext, Boolean.class), is(true)); + assertThat(methods.getValue(parser.parseExpression("transition.source.id.toString().equals('S1')"), stateContext, Boolean.class), is(true)); + } + + @Test + public void testSendEvent() { + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + StateContextExpressionMethods methods = new StateContextExpressionMethods(evaluationContext); + MockStatemachine stateMachine = new MockStatemachine(); + StateContext stateContext = mockStateContext(stateMachine); + + assertThat(methods.getValue(parser.parseExpression("stateMachine.sendEvent(T(org.springframework.statemachine.support.StateContextExpressionMethodsTests.SpelEvents).E1)"), stateContext, Boolean.class), is(true)); + assertThat(stateMachine.events.size(), is(1)); + } + + enum SpelStates { + SI,S1,S2,S3,S4,SF,SH + } + + public enum SpelEvents { + E1,E2,E3,E4,EF + } + + private StateContext mockStateContext(StateMachine stateMachine) { + Map headers = new HashMap(); + headers.put("foo", "bar"); + MessageHeaders messageHeaders = new MessageHeaders(headers); + ExtendedState extendedState = new DefaultExtendedState(); + extendedState.getVariables().put("key1", "val1"); + extendedState.getVariables().put("boolean1", true); + extendedState.getVariables().put("boolean2", false); + StateContext stateContext = new DefaultStateContext( + SpelEvents.E1, messageHeaders, extendedState, new MockTransition(), stateMachine); + return stateContext; + } + + private static class MockTransition implements Transition { + + @Override + public boolean transit(StateContext context) { + return false; + } + + @Override + public State getSource() { + return new EnumState(SpelStates.S1); + } + + @Override + public State getTarget() { + return new EnumState(SpelStates.S2); + } + + @Override + public Collection> getActions() { + return null; + } + + @Override + public Trigger getTrigger() { + return null; + } + + @Override + public TransitionKind getKind() { + return null; + } + + } + + private static class MockStatemachine implements StateMachine { + + ArrayList> events = new ArrayList>(); + + @Override + public void start() { + } + + @Override + public void stop() { + } + + @Override + public boolean sendEvent(Message event) { + events.add(event); + return true; + } + + @Override + public boolean sendEvent(SpelEvents event) { + return sendEvent(MessageBuilder.createMessage(event, new MessageHeaders(new HashMap()))); + } + + @Override + public State getState() { + return null; + } + + @Override + public Collection> getStates() { + return null; + } + + @Override + public Collection> getTransitions() { + return null; + } + + @Override + public boolean isComplete() { + return false; + } + + @Override + public void addStateListener(StateMachineListener listener) { + } + + @Override + public void removeStateListener(StateMachineListener listener) { + } + + @Override + public State getInitialState() { + return null; + } + + @Override + public ExtendedState getExtendedState() { + return null; + } + + } + +}