Fixes for annotation method handling

- adding generics
- tidy up annotation handling for methods
- add EventHeaders annotation
This commit is contained in:
Janne Valkealahti
2015-03-07 15:15:46 +00:00
parent ed81b1b578
commit b30ad73e00
10 changed files with 217 additions and 132 deletions

View File

@@ -0,0 +1,28 @@
/*
* 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.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EventHeaders {
}

View File

@@ -25,25 +25,27 @@ import java.lang.reflect.Method;
* @author Janne Valkealahti
*
* @param <T> the return type
* @param <S> the type of state
* @param <E> the type of event
*/
public class MethodInvokingStateMachineRuntimeProcessor<T> implements StateMachineRuntimeProcessor<T> {
public class MethodInvokingStateMachineRuntimeProcessor<T, S, E> implements StateMachineRuntimeProcessor<T, S, E> {
private final StateMachineMethodInvokerHelper<T> delegate;
private final StateMachineMethodInvokerHelper<T, S, E> delegate;
public MethodInvokingStateMachineRuntimeProcessor(Object targetObject, Method method) {
delegate = new StateMachineMethodInvokerHelper<T>(targetObject, method);
delegate = new StateMachineMethodInvokerHelper<T, S, E>(targetObject, method);
}
public MethodInvokingStateMachineRuntimeProcessor(Object targetObject, String methodName) {
delegate = new StateMachineMethodInvokerHelper<T>(targetObject, methodName);
delegate = new StateMachineMethodInvokerHelper<T, S, E>(targetObject, methodName);
}
public MethodInvokingStateMachineRuntimeProcessor(Object targetObject, Class<? extends Annotation> annotationType) {
delegate = new StateMachineMethodInvokerHelper<T>(targetObject, annotationType);
delegate = new StateMachineMethodInvokerHelper<T, S, E>(targetObject, annotationType);
}
@Override
public T process(StateMachineRuntime stateMachineRuntime) {
public T process(StateMachineRuntime<S, E> stateMachineRuntime) {
try {
return delegate.process(stateMachineRuntime);
} catch (Exception e) {

View File

@@ -40,8 +40,8 @@ public class StateMachineActivatorAnnotationPostProcessor implements MethodAnnot
@Override
public Object postProcess(Object bean, String beanName, Method method, OnTransition annotation) {
StateMachineHandler handler = new StateMachineOnTransitionHandler(bean, method, annotation);
StateMachineHandler<Object, Object> handler = new StateMachineOnTransitionHandler<Object, Object>(bean, method, annotation);
Integer order = findOrder(bean, method);
if (order != null) {
handler.setOrder(order);

View File

@@ -28,22 +28,16 @@ import org.springframework.statemachine.annotation.WithStateMachine;
*
* @author Janne Valkealahti
*
* @param <T> the return type
* @param <S> the type of state
* @param <E> the type of event
*/
public class StateMachineHandler implements Ordered {
public class StateMachineHandler<S, E> implements Ordered {
private final StateMachineRuntimeProcessor<?> processor;
private final StateMachineRuntimeProcessor<?, S, E> processor;
private int order = Ordered.LOWEST_PRECEDENCE;
/**
* Instantiates a new container handler.
*
* @param target the target bean
*/
// public StateMachineHandler(Object target) {
// this(new MethodInvokingStateMachineRuntimeProcessor<Object>(target, OnTransition.class));
// }
/**
* Instantiates a new container handler.
*
@@ -51,7 +45,7 @@ public class StateMachineHandler implements Ordered {
* @param method the method
*/
public StateMachineHandler(Object target, Method method) {
this(new MethodInvokingStateMachineRuntimeProcessor<Object>(target, method));
this(new MethodInvokingStateMachineRuntimeProcessor<Object, S, E>(target, method));
}
/**
@@ -61,7 +55,7 @@ public class StateMachineHandler implements Ordered {
* @param methodName the method name
*/
public StateMachineHandler(Object target, String methodName) {
this(new MethodInvokingStateMachineRuntimeProcessor<Object>(target, methodName));
this(new MethodInvokingStateMachineRuntimeProcessor<Object, S, E>(target, methodName));
}
/**
@@ -70,7 +64,7 @@ public class StateMachineHandler implements Ordered {
* @param <T> the generic type
* @param processor the processor
*/
public <T> StateMachineHandler(MethodInvokingStateMachineRuntimeProcessor<T> processor) {
public <T> StateMachineHandler(MethodInvokingStateMachineRuntimeProcessor<T, S, E> processor) {
this.processor = processor;
}
@@ -95,7 +89,7 @@ public class StateMachineHandler implements Ordered {
* @param stateMachineRuntime the state machine runtime
* @return the result value
*/
public Object handle(StateMachineRuntime stateMachineRuntime) {
public Object handle(StateMachineRuntime<S, E> stateMachineRuntime) {
return processor.process(stateMachineRuntime);
}

View File

@@ -29,9 +29,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.EvaluationException;
@@ -39,7 +37,9 @@ import org.springframework.expression.Expression;
import org.springframework.expression.TypeConverter;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.statemachine.ExtendedState;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.annotation.EventHeaders;
import org.springframework.statemachine.support.AbstractExpressionEvaluator;
import org.springframework.statemachine.support.AnnotatedMethodFilter;
import org.springframework.statemachine.support.FixedMethodFilter;
@@ -56,7 +56,7 @@ import org.springframework.util.ReflectionUtils.MethodFilter;
*
* @param <T> the return type
*/
public class StateMachineMethodInvokerHelper<T> extends AbstractExpressionEvaluator {
public class StateMachineMethodInvokerHelper<T, S, E> extends AbstractExpressionEvaluator {
private static final String CANDIDATE_METHODS = "CANDIDATE_METHODS";
@@ -105,8 +105,8 @@ public class StateMachineMethodInvokerHelper<T> extends AbstractExpressionEvalua
this(targetObject, annotationType, (String) null, expectedType);
}
public T process(StateMachineRuntime stateMachineRuntime) throws Exception {
ParametersWrapper wrapper = new ParametersWrapper(stateMachineRuntime.getStateContext());
public T process(StateMachineRuntime<S, E> stateMachineRuntime) throws Exception {
ParametersWrapper<S, E> wrapper = new ParametersWrapper<S, E>(stateMachineRuntime.getStateContext());
return processInternal(wrapper);
}
@@ -214,7 +214,7 @@ public class StateMachineMethodInvokerHelper<T> extends AbstractExpressionEvalua
return false;
}
private T processInternal(ParametersWrapper parameters) throws Exception {
private T processInternal(ParametersWrapper<S, E> parameters) throws Exception {
HandlerMethod candidate = this.findHandlerMethodForParameters(parameters);
Assert.notNull(candidate, "No candidate methods found for messages.");
Expression expression = candidate.getExpression();
@@ -347,7 +347,7 @@ public class StateMachineMethodInvokerHelper<T> extends AbstractExpressionEvalua
return targetClass;
}
private HandlerMethod findHandlerMethodForParameters(ParametersWrapper parameters) {
private HandlerMethod findHandlerMethodForParameters(ParametersWrapper<S, E> parameters) {
if (this.handlerMethod != null) {
return this.handlerMethod;
} else {
@@ -378,7 +378,7 @@ public class StateMachineMethodInvokerHelper<T> extends AbstractExpressionEvalua
private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new LocalVariableTableParameterNameDiscoverer();
// private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new LocalVariableTableParameterNameDiscoverer();
private final Method method;
@@ -429,19 +429,12 @@ public class StateMachineMethodInvokerHelper<T> extends AbstractExpressionEvalua
if (mappingAnnotation != null) {
Class<? extends Annotation> annotationType = mappingAnnotation.annotationType();
// if (annotationType.equals(YarnEnvironments.class)) {
// sb.append("environment");
// } else if (annotationType.equals(YarnEnvironment.class)) {
// YarnEnvironment headerAnnotation = (YarnEnvironment) mappingAnnotation;
// sb.append(this.determineEnvironmentExpression(headerAnnotation, methodParameter));
// } else if (annotationType.equals(YarnParameters.class)) {
// Assert.isTrue(Map.class.isAssignableFrom(parameterType),
// "The @YarnParameters annotation can only be applied to a Map-typed parameter.");
// sb.append("parameters");
// } else if (annotationType.equals(YarnParameter.class)) {
// YarnParameter headerAnnotation = (YarnParameter) mappingAnnotation;
// sb.append(this.determineParameterExpression(headerAnnotation, methodParameter));
// }
if (annotationType.equals(EventHeaders.class)) {
sb.append("headers");
}
} else if (ExtendedState.class.isAssignableFrom(parameterType)) {
sb.append("extendedState");
}
}
if (hasUnqualifiedMapParameter) {
@@ -465,89 +458,44 @@ public class StateMachineMethodInvokerHelper<T> extends AbstractExpressionEvalua
Annotation match = null;
for (Annotation annotation : annotations) {
Class<? extends Annotation> type = annotation.annotationType();
// if (type.equals(YarnParameters.class) || type.equals(YarnParameter.class)
// || type.equals(YarnEnvironments.class) || type.equals(YarnEnvironment.class)) {
// if (match != null) {
// throw new IllegalArgumentException(
// "At most one parameter annotation can be provided for message mapping, "
// + "but found two: [" + match.annotationType().getName() + "] and ["
// + annotation.annotationType().getName() + "]");
// }
// match = annotation;
// }
if (type.equals(EventHeaders.class)) {
if (match != null) {
throw new IllegalArgumentException(
"At most one parameter annotation can be provided for message mapping, "
+ "but found two: [" + match.annotationType().getName() + "] and ["
+ annotation.annotationType().getName() + "]");
}
match = annotation;
}
}
return match;
}
// private String determineParameterExpression(YarnParameter parameterAnnotation, MethodParameter methodParameter) {
// methodParameter.initParameterNameDiscovery(PARAMETER_NAME_DISCOVERER);
// String headerName = null;
// String relativeExpression = "";
// String valueAttribute = parameterAnnotation.value();
// if (!StringUtils.hasText(valueAttribute)) {
// headerName = methodParameter.getParameterName();
// } else if (valueAttribute.indexOf('.') != -1) {
// String tokens[] = valueAttribute.split("\\.", 2);
// headerName = tokens[0];
// if (StringUtils.hasText(tokens[1])) {
// relativeExpression = "." + tokens[1];
// }
// } else {
// headerName = valueAttribute;
// }
// Assert.notNull(headerName, "Cannot determine parameter name. Possible reasons: -debug is "
// + "disabled or header name is not explicitly provided via @YarnParameter annotation.");
// String headerRetrievalExpression = "parameters['" + headerName + "']";
// String fullHeaderExpression = headerRetrievalExpression + relativeExpression;
// String fallbackExpression = (parameterAnnotation.required()) ? "T(org.springframework.util.Assert).isTrue(false, 'required parameter not available: "
// + headerName + "')"
// : "null";
// return headerRetrievalExpression + " != null ? " + fullHeaderExpression + " : " + fallbackExpression;
// }
// private String determineEnvironmentExpression(YarnEnvironment environmentAnnotation, MethodParameter methodParameter) {
// methodParameter.initParameterNameDiscovery(PARAMETER_NAME_DISCOVERER);
// String headerName = null;
// String relativeExpression = "";
// String valueAttribute = environmentAnnotation.value();
// if (!StringUtils.hasText(valueAttribute)) {
// headerName = methodParameter.getParameterName();
// } else if (valueAttribute.indexOf('.') != -1) {
// String tokens[] = valueAttribute.split("\\.", 2);
// headerName = tokens[0];
// if (StringUtils.hasText(tokens[1])) {
// relativeExpression = "." + tokens[1];
// }
// } else {
// headerName = valueAttribute;
// }
// Assert.notNull(headerName, "Cannot determine parameter name. Possible reasons: -debug is "
// + "disabled or header name is not explicitly provided via @YarnEnvironment annotation.");
// String headerRetrievalExpression = "environment['" + headerName + "']";
// String fullHeaderExpression = headerRetrievalExpression + relativeExpression;
// String fallbackExpression = (environmentAnnotation.required()) ? "T(org.springframework.util.Assert).isTrue(false, 'required parameter not available: "
// + headerName + "')"
// : "null";
// return headerRetrievalExpression + " != null ? " + fullHeaderExpression + " : " + fallbackExpression;
// }
}
/**
* Wrapping everything we need to work with spel.
*/
public class ParametersWrapper {
public class ParametersWrapper<SS, EE> {
private final StateContext stateContext;
public ParametersWrapper(StateContext stateContext) {
private final StateContext<SS, EE> stateContext;
public ParametersWrapper(StateContext<SS, EE> stateContext) {
this.stateContext = stateContext;
}
public StateContext getStateContext() {
public StateContext<SS, EE> getStateContext() {
return stateContext;
}
public Map<String, ?> getHeaders() {
return stateContext.getMessageHeaders();
}
public ExtendedState getExtendedState() {
return stateContext.getExtendedState();
}
}
}

View File

@@ -19,15 +19,35 @@ import java.lang.reflect.Method;
import org.springframework.statemachine.annotation.OnTransition;
public class StateMachineOnTransitionHandler extends StateMachineHandler {
/**
* Transition specific {@link StateMachineHandler}.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public class StateMachineOnTransitionHandler<S, E> extends StateMachineHandler<S, E> {
private OnTransition annotation;
private final OnTransition annotation;
/**
* Instantiates a new state machine on transition handler.
*
* @param target the target
* @param method the method
* @param annotation the annotation
*/
public StateMachineOnTransitionHandler(Object target, Method method, OnTransition annotation) {
super(target, method);
this.annotation = annotation;
}
/**
* Gets the annotation.
*
* @return the annotation
*/
public OnTransition getAnnotation() {
return annotation;
}

View File

@@ -22,9 +22,16 @@ import org.springframework.statemachine.StateContext;
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public interface StateMachineRuntime {
public interface StateMachineRuntime<S, E> {
StateContext getStateContext();
/**
* Gets the state context.
*
* @return the state context
*/
StateContext<S, E> getStateContext();
}

View File

@@ -21,9 +21,11 @@ package org.springframework.statemachine.processor;
*
* @author Janne Valkealahti
*
* @param <T> type
* @param <T> the return type
* @param <S> the type of state
* @param <E> the type of event
*/
public interface StateMachineRuntimeProcessor<T> {
public interface StateMachineRuntimeProcessor<T, S, E> {
/**
* Process the container based on information available
@@ -32,6 +34,6 @@ public interface StateMachineRuntimeProcessor<T> {
* @param stateMachineRuntime the yarn container runtime
* @return the result
*/
T process(StateMachineRuntime stateMachineRuntime);
T process(StateMachineRuntime<S, E> stateMachineRuntime);
}

View File

@@ -87,6 +87,10 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
private volatile Runnable task;
private final Map<String, StateMachineOnTransitionHandler<S, E>> handlers = new HashMap<String, StateMachineOnTransitionHandler<S,E>>();
private volatile boolean handlersInitialized;
/**
* Instantiates a new abstract state machine.
*
@@ -109,7 +113,7 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
*/
public AbstractStateMachine(Collection<State<S, E>> states, Collection<Transition<S, E>> transitions,
State<S, E> initialState, State<S, E> endState) {
this(states, transitions, initialState, endState, null, null);
this(states, transitions, initialState, endState, null, new DefaultExtendedState());
}
/**
@@ -366,6 +370,9 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
}
private void callHandlers(State<S,E> sourceState, State<S,E> targetState, Message<E> event) {
if (sourceState != null && targetState != null) {
MessageHeaders messageHeaders = event != null ? event.getHeaders() : new MessageHeaders(
new HashMap<String, Object>());
@@ -375,21 +382,21 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
}
private List<Object> getStateMachineHandlerResults(List<StateMachineHandler> stateMachineHandlers, final StateContext<S, E> stateContext) {
StateMachineRuntime runtime = new StateMachineRuntime() {
private List<Object> getStateMachineHandlerResults(List<StateMachineHandler<S, E>> stateMachineHandlers, final StateContext<S, E> stateContext) {
StateMachineRuntime<S, E> runtime = new StateMachineRuntime<S, E>() {
@Override
public StateContext<S, E> getStateContext() {
return stateContext;
}
};
List<Object> results = new ArrayList<Object>();
for (StateMachineHandler handler : stateMachineHandlers) {
for (StateMachineHandler<S, E> handler : stateMachineHandlers) {
results.add(handler.handle(runtime));
}
return results;
}
private List<StateMachineHandler> getStateMachineHandlers(State<S, E> sourceState, State<S, E> targetState) {
private synchronized List<StateMachineHandler<S, E>> getStateMachineHandlers(State<S, E> sourceState, State<S, E> targetState) {
BeanFactory beanFactory = getBeanFactory();
// TODO think how to handle null bf
@@ -397,11 +404,19 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
return Collections.emptyList();
}
Assert.state(beanFactory instanceof ListableBeanFactory, "Bean factory must be instance of ListableBeanFactory");
Map<String, StateMachineOnTransitionHandler> handlers = ((ListableBeanFactory) beanFactory)
.getBeansOfType(StateMachineOnTransitionHandler.class);
List<StateMachineHandler> handlersList = new ArrayList<StateMachineHandler>();
for (Entry<String, StateMachineOnTransitionHandler> entry : handlers.entrySet()) {
if (!handlersInitialized) {
Map<String, StateMachineOnTransitionHandler> handlersx = ((ListableBeanFactory) beanFactory)
.getBeansOfType(StateMachineOnTransitionHandler.class);
for (Entry<String, StateMachineOnTransitionHandler> entry : handlersx.entrySet()) {
handlers.put(entry.getKey(), entry.getValue());
}
handlersInitialized = true;
}
List<StateMachineHandler<S, E>> handlersList = new ArrayList<StateMachineHandler<S, E>>();
for (Entry<String, StateMachineOnTransitionHandler<S, E>> entry : handlers.entrySet()) {
OnTransition annotation = entry.getValue().getAnnotation();
String source = annotation.source();
String target = annotation.target();

View File

@@ -16,9 +16,11 @@
package org.springframework.statemachine.annotation;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.junit.Assert.assertThat;
import java.util.EnumSet;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -29,6 +31,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.AbstractStateMachineTests;
import org.springframework.statemachine.EnumStateMachine;
import org.springframework.statemachine.ExtendedState;
import org.springframework.statemachine.StateMachineSystemConstants;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
@@ -59,6 +62,34 @@ public class MethodAnnotationTests extends AbstractStateMachineTests {
context.close();
}
@Test
@SuppressWarnings("unchecked")
public void testMethodAnnotations2() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BaseConfig.class, AnnoConfig.class, BeanConfig2.class, Config1.class);
EnumStateMachine<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class);
assertThat(context.containsBean("fooMachine"), is(true));
machine.start();
Bean2 bean2 = context.getBean(Bean2.class);
// this event should cause 'method1' to get called
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).setHeader("foo", "jee").build());
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E2).build());
assertThat(bean2.onMethod1Latch.await(2, TimeUnit.SECONDS), is(true));
assertThat(bean2.headers, notNullValue());
assertThat((String)bean2.headers.get("foo"), is("jee"));
assertThat(bean2.extendedState, notNullValue());
assertThat(bean2.onMethod2Latch.await(2, TimeUnit.SECONDS), is(true));
assertThat(bean2.variable, notNullValue());
assertThat((String)bean2.variable, is("jee"));
context.close();
}
@WithStateMachine(name = StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)
static class Bean1 {
@@ -75,6 +106,30 @@ public class MethodAnnotationTests extends AbstractStateMachineTests {
onOnTransitionFromS2ToS3Latch.countDown();
}
}
@WithStateMachine(name = StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)
static class Bean2 {
CountDownLatch onMethod1Latch = new CountDownLatch(1);
CountDownLatch onMethod2Latch = new CountDownLatch(1);
Map<String, Object> headers;
ExtendedState extendedState;
Object variable;
@OnTransition(source = "S1", target = "S2")
public void method1(@EventHeaders Map<String, Object> headers, ExtendedState extendedState) {
this.headers = headers;
extendedState.getVariables().put("foo", "jee");
this.extendedState = extendedState;
onMethod1Latch.countDown();
}
@OnTransition(source = "S2", target = "S3")
public void method2(@EventHeaders Map<String, Object> headers, ExtendedState extendedState) {
variable = extendedState.getVariables().get("foo");
onMethod2Latch.countDown();
}
}
@@ -88,6 +143,15 @@ public class MethodAnnotationTests extends AbstractStateMachineTests {
}
@Configuration
static class BeanConfig2 {
@Bean
public Bean2 bean2() {
return new Bean2();
}
}
@Configuration
static class AnnoConfig {
@@ -124,7 +188,12 @@ public class MethodAnnotationTests extends AbstractStateMachineTests {
.withExternal()
.source(TestStates.S2)
.target(TestStates.S3)
.event(TestEvents.E2);
.event(TestEvents.E2)
.and()
.withExternal()
.source(TestStates.S3)
.target(TestStates.S4)
.event(TestEvents.E3);
}
@Bean