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:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user