Add better spel support for actions and guards

- Add new StateContextExpressionMethods support class.
- Change SpelExpressionGuard to user methods class.
- Add new SpelExpressionAction which can take expression
  to execute action.
This commit is contained in:
Janne Valkealahti
2015-05-30 17:00:34 +01:00
parent 7d26f505a1
commit aaa20e6b26
6 changed files with 424 additions and 9 deletions

View File

@@ -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<S, E> implements Action<S, E> {
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<S, E> context) {
methods.getValue(expression, context, Object.class);
}
}

View File

@@ -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<S, E> implements Guard<S, E> {
private final StateContextExpressionMethods methods;
private final Expression expression;
/**
* Instantiates a new spel expression guard.
*
@@ -38,12 +41,12 @@ public class SpelExpressionGuard<S, E> implements Guard<S, E> {
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<S, E> context) {
StandardEvaluationContext evaluationContext = new StandardEvaluationContext(context);
return expression.getValue(evaluationContext, Boolean.class);
return methods.getValue(expression, context, Boolean.class);
}
}

View File

@@ -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 <T> 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> T getValue(Expression expression, StateContext<?, ?> stateContext, Class<T> desiredResultType)
throws EvaluationException {
Assert.notNull(expression, "Expression cannot be null");
return expression.getValue(context, stateContext, desiredResultType);
}
}

View File

@@ -226,10 +226,10 @@ public abstract class AbstractStateMachineTests {
}
}
protected static class TestStateMachineListener extends StateMachineListenerAdapter<TestStates, TestEvents> {
public static class TestStateMachineListener extends StateMachineListenerAdapter<TestStates, TestEvents> {
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<TestStates, TestEvents> from, State<TestStates, TestEvents> 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);
}

View File

@@ -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<TestStates,TestEvents> 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<TestStates, TestEvents> {
public TestSpelAction(Expression expression) {
super(expression);
}
}
@Configuration
@EnableStateMachine
static class Config1 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
states
.withStates()
.initial(TestStates.S1)
.state(TestStates.S1, TestEvents.E2)
.state(TestStates.S2)
.state(TestStates.S3);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> 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)"));
}
}
}

View File

@@ -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<SpelStates, SpelEvents> 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<SpelStates, SpelEvents> 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<SpelStates, SpelEvents> mockStateContext(StateMachine<SpelStates, SpelEvents> stateMachine) {
Map<String, Object> headers = new HashMap<String, Object>();
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<SpelStates, SpelEvents> stateContext = new DefaultStateContext<SpelStates, SpelEvents>(
SpelEvents.E1, messageHeaders, extendedState, new MockTransition(), stateMachine);
return stateContext;
}
private static class MockTransition implements Transition<SpelStates, SpelEvents> {
@Override
public boolean transit(StateContext<SpelStates, SpelEvents> context) {
return false;
}
@Override
public State<SpelStates, SpelEvents> getSource() {
return new EnumState<SpelStates, SpelEvents>(SpelStates.S1);
}
@Override
public State<SpelStates, SpelEvents> getTarget() {
return new EnumState<SpelStates, SpelEvents>(SpelStates.S2);
}
@Override
public Collection<Action<SpelStates, SpelEvents>> getActions() {
return null;
}
@Override
public Trigger<SpelStates, SpelEvents> getTrigger() {
return null;
}
@Override
public TransitionKind getKind() {
return null;
}
}
private static class MockStatemachine implements StateMachine<SpelStates, SpelEvents> {
ArrayList<Message<SpelEvents>> events = new ArrayList<Message<SpelEvents>>();
@Override
public void start() {
}
@Override
public void stop() {
}
@Override
public boolean sendEvent(Message<SpelEvents> event) {
events.add(event);
return true;
}
@Override
public boolean sendEvent(SpelEvents event) {
return sendEvent(MessageBuilder.createMessage(event, new MessageHeaders(new HashMap<String, Object>())));
}
@Override
public State<SpelStates, SpelEvents> getState() {
return null;
}
@Override
public Collection<State<SpelStates, SpelEvents>> getStates() {
return null;
}
@Override
public Collection<Transition<SpelStates, SpelEvents>> getTransitions() {
return null;
}
@Override
public boolean isComplete() {
return false;
}
@Override
public void addStateListener(StateMachineListener<SpelStates, SpelEvents> listener) {
}
@Override
public void removeStateListener(StateMachineListener<SpelStates, SpelEvents> listener) {
}
@Override
public State<SpelStates, SpelEvents> getInitialState() {
return null;
}
@Override
public ExtendedState getExtendedState() {
return null;
}
}
}