Add support for @EventHeader annotation
- Add @EventHeader which can be used to bound as single event headers instead of all headers @EventHeaders. - Fixes #638
This commit is contained in:
@@ -1300,6 +1300,12 @@ Number of parameters or order of those doesn't matter.
|
||||
include::samples/DocsConfigurationSampleTests4.java[tags=snippetBB]
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Instead of getting all event headers with `@EventHeaders` you can use
|
||||
`@EventHeader` which can bound to a single header.
|
||||
====
|
||||
|
||||
=== Transition Annotations
|
||||
Annotations for transitions are `OnTransition`, `OnTransitionStart`
|
||||
and `OnTransitionEnd`.
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2019 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;
|
||||
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* Annotation which indicates that a method parameter should be bound to a event header.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*
|
||||
*/
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface EventHeader {
|
||||
|
||||
/**
|
||||
* Alias for {@link #name}.
|
||||
*
|
||||
* @return the header name
|
||||
*/
|
||||
@AliasFor("name")
|
||||
String value() default "";
|
||||
|
||||
/**
|
||||
* The name of the request header to bind to.
|
||||
*
|
||||
* @return the header name
|
||||
*/
|
||||
@AliasFor("value")
|
||||
String name() default "";
|
||||
|
||||
/**
|
||||
* Whether the header is required.
|
||||
* <p>Default is {@code true}, leading to an exception if the header is
|
||||
* missing. Switch this to {@code false} if you prefer a {@code null}
|
||||
* value in case of a header missing.
|
||||
*
|
||||
* @return the required flag
|
||||
*/
|
||||
boolean required() default true;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
* Copyright 2015-2019 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.
|
||||
@@ -21,6 +21,14 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation which indicates that a method parameter should be bound to the
|
||||
* event headers of a message. The annotated parameter must be assignable to
|
||||
* {@link java.util.Map} with String keys and Object values.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*
|
||||
*/
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
|
||||
@@ -29,7 +29,9 @@ 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.AnnotationAttributes;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
@@ -42,6 +44,8 @@ import org.springframework.messaging.Message;
|
||||
import org.springframework.statemachine.ExtendedState;
|
||||
import org.springframework.statemachine.StateContext;
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
import org.springframework.statemachine.StateMachineException;
|
||||
import org.springframework.statemachine.annotation.EventHeader;
|
||||
import org.springframework.statemachine.annotation.EventHeaders;
|
||||
import org.springframework.statemachine.annotation.ExtendedStateVariable;
|
||||
import org.springframework.statemachine.support.AbstractExpressionEvaluator;
|
||||
@@ -52,6 +56,7 @@ import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.ReflectionUtils.MethodCallback;
|
||||
import org.springframework.util.ReflectionUtils.MethodFilter;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A helper class using spel to execute target methods.
|
||||
@@ -134,7 +139,11 @@ public class StateMachineMethodInvokerHelper<T, S, E> extends AbstractExpression
|
||||
this.handlerMethods = null;
|
||||
this.handlerMessageMethods = null;
|
||||
this.handlerMethodsList = null;
|
||||
this.prepareEvaluationContext(this.getEvaluationContext(false), method, annotationType);
|
||||
try {
|
||||
this.prepareEvaluationContext(this.getEvaluationContext(false), method, annotationType);
|
||||
} catch (Exception e) {
|
||||
throw new StateMachineException("Unable to prepare evaluation context", e);
|
||||
}
|
||||
this.setDisplayString(targetObject, method);
|
||||
}
|
||||
|
||||
@@ -169,7 +178,11 @@ public class StateMachineMethodInvokerHelper<T, S, E> extends AbstractExpression
|
||||
this.handlerMethodsList.add(this.handlerMethods);
|
||||
this.handlerMethodsList.add(this.handlerMessageMethods);
|
||||
}
|
||||
this.prepareEvaluationContext(this.getEvaluationContext(false), methodName, annotationType);
|
||||
try {
|
||||
this.prepareEvaluationContext(this.getEvaluationContext(false), methodName, annotationType);
|
||||
} catch (Exception e) {
|
||||
throw new StateMachineException("Unable to prepare evaluation context", e);
|
||||
}
|
||||
this.setDisplayString(targetObject, methodName);
|
||||
}
|
||||
|
||||
@@ -184,7 +197,7 @@ public class StateMachineMethodInvokerHelper<T, S, E> extends AbstractExpression
|
||||
}
|
||||
|
||||
private void prepareEvaluationContext(StandardEvaluationContext context, Object method,
|
||||
Class<? extends Annotation> annotationType) {
|
||||
Class<? extends Annotation> annotationType) throws Exception {
|
||||
Class<?> targetType = AopUtils.getTargetClass(this.targetObject);
|
||||
if (method instanceof Method) {
|
||||
context.registerMethodFilter(targetType, new FixedMethodFilter((Method) method));
|
||||
@@ -203,6 +216,8 @@ public class StateMachineMethodInvokerHelper<T, S, E> extends AbstractExpression
|
||||
context.registerMethodFilter(targetType, filter);
|
||||
}
|
||||
context.setVariable("target", targetObject);
|
||||
context.registerFunction("requiredHeader", ParametersWrapper.class.getDeclaredMethod("getHeader",
|
||||
Map.class, String.class));
|
||||
}
|
||||
|
||||
private boolean canReturnExpectedType(AnnotatedMethodFilter filter, Class<?> targetType, TypeConverter typeConverter) {
|
||||
@@ -382,7 +397,7 @@ public class StateMachineMethodInvokerHelper<T, S, E> extends AbstractExpression
|
||||
|
||||
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;
|
||||
|
||||
@@ -399,7 +414,6 @@ public class StateMachineMethodInvokerHelper<T, S, E> extends AbstractExpression
|
||||
this.expression = this.generateExpression(method);
|
||||
}
|
||||
|
||||
|
||||
Expression getExpression() {
|
||||
return this.expression;
|
||||
}
|
||||
@@ -435,6 +449,8 @@ public class StateMachineMethodInvokerHelper<T, S, E> extends AbstractExpression
|
||||
|
||||
if (annotationType.equals(EventHeaders.class)) {
|
||||
sb.append("headers");
|
||||
} else if (annotationType.equals(EventHeader.class)) {
|
||||
sb.append(this.determineHeaderExpression(mappingAnnotation, methodParameter));
|
||||
} else if (annotationType.equals(ExtendedStateVariable.class)) {
|
||||
AnnotationAttributes annotationAttributes = AnnotationAttributes
|
||||
.fromMap(AnnotationUtils.getAnnotationAttributes(mappingAnnotation));
|
||||
@@ -483,6 +499,8 @@ public class StateMachineMethodInvokerHelper<T, S, E> extends AbstractExpression
|
||||
+ annotation.annotationType().getName() + "]");
|
||||
}
|
||||
match = annotation;
|
||||
} else if (type.equals(EventHeader.class)) {
|
||||
match = annotation;
|
||||
} else if (type.equals(ExtendedStateVariable.class)) {
|
||||
if (match != null) {
|
||||
throw new IllegalArgumentException(
|
||||
@@ -496,12 +514,43 @@ public class StateMachineMethodInvokerHelper<T, S, E> extends AbstractExpression
|
||||
return match;
|
||||
}
|
||||
|
||||
private String determineHeaderExpression(Annotation headerAnnotation, MethodParameter methodParameter) {
|
||||
methodParameter.initParameterNameDiscovery(PARAMETER_NAME_DISCOVERER);
|
||||
String headerName = null;
|
||||
String relativeExpression = "";
|
||||
AnnotationAttributes annotationAttributes = (AnnotationAttributes) AnnotationUtils
|
||||
.getAnnotationAttributes(headerAnnotation);
|
||||
String valueAttribute = annotationAttributes.getString(AnnotationUtils.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 header name. Possible reasons: -debug is "
|
||||
+ "disabled or header name is not explicitly provided via @EventHeader annotation.");
|
||||
String headerRetrievalExpression = "headers['" + headerName + "']";
|
||||
String fullHeaderExpression = headerRetrievalExpression + relativeExpression;
|
||||
if (annotationAttributes.getBoolean("required")
|
||||
&& !methodParameter.getParameterType().getName().equals("java.util.Optional")) {
|
||||
return "#requiredHeader(headers, '" + headerName + "')" + relativeExpression;
|
||||
} else if (!StringUtils.hasLength(relativeExpression)) {
|
||||
return headerRetrievalExpression + " ?: null";
|
||||
} else {
|
||||
return headerRetrievalExpression + " != null ? " + fullHeaderExpression + " : null";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapping everything we need to work with spel.
|
||||
*/
|
||||
public class ParametersWrapper<SS, EE> {
|
||||
public static class ParametersWrapper<SS, EE> {
|
||||
|
||||
private final StateContext<SS, EE> stateContext;
|
||||
|
||||
@@ -513,6 +562,14 @@ public class StateMachineMethodInvokerHelper<T, S, E> extends AbstractExpression
|
||||
return stateContext;
|
||||
}
|
||||
|
||||
public static Object getHeader(Map<?, ?> headers, String header) {
|
||||
Object object = headers.get(header);
|
||||
if (object == null) {
|
||||
throw new IllegalArgumentException("required header not available: " + header);
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
public Map<String, ?> getHeaders() {
|
||||
return stateContext.getMessageHeaders();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
* Copyright 2015-2019 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.
|
||||
@@ -177,7 +177,12 @@ public class MethodAnnotationTests extends AbstractStateMachineTests {
|
||||
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.E1)
|
||||
.setHeader("foo", "jee")
|
||||
.setHeader("bar1", "jee1")
|
||||
.setHeader("bar2", "jee2")
|
||||
.setHeader("bar3", "jee3")
|
||||
.build());
|
||||
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E2).build());
|
||||
|
||||
assertThat(bean2.onMethod1Latch.await(2, TimeUnit.SECONDS), is(true));
|
||||
@@ -188,6 +193,11 @@ public class MethodAnnotationTests extends AbstractStateMachineTests {
|
||||
assertThat(bean2.onMethod2Latch.await(2, TimeUnit.SECONDS), is(true));
|
||||
assertThat(bean2.variable, notNullValue());
|
||||
assertThat((String)bean2.variable, is("jee"));
|
||||
|
||||
assertThat((String)bean2.fooHeader, is("jee"));
|
||||
assertThat((String)bean2.bar1Header, is("jee1"));
|
||||
assertThat((String)bean2.bar2Header, is("jee2"));
|
||||
assertThat(bean2.bar3Header, is("jee3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -460,6 +470,10 @@ public class MethodAnnotationTests extends AbstractStateMachineTests {
|
||||
Map<String, Object> headers;
|
||||
ExtendedState extendedState;
|
||||
Object variable;
|
||||
Object fooHeader;
|
||||
Object bar1Header;
|
||||
Object bar2Header;
|
||||
String bar3Header;
|
||||
|
||||
@OnTransition(source = "S1", target = "S2")
|
||||
public void method1(@EventHeaders Map<String, Object> headers, ExtendedState extendedState) {
|
||||
@@ -475,6 +489,18 @@ public class MethodAnnotationTests extends AbstractStateMachineTests {
|
||||
onMethod2Latch.countDown();
|
||||
}
|
||||
|
||||
@OnTransition(source = "S1", target = "S2")
|
||||
public void method3(@EventHeader(name = "foo") Object header) {
|
||||
this.fooHeader = header;
|
||||
}
|
||||
|
||||
@OnTransition(source = "S1", target = "S2")
|
||||
public void method4(@EventHeader(name = "bar1") Object header1, @EventHeader(value = "bar2") Object header2,
|
||||
@EventHeader(value = "bar3") String header3) {
|
||||
this.bar1Header = header1;
|
||||
this.bar2Header = header2;
|
||||
this.bar3Header = header3;
|
||||
}
|
||||
}
|
||||
|
||||
@WithStateMachine
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
* Copyright 2015-2019 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.
|
||||
@@ -27,6 +27,7 @@ import org.springframework.statemachine.AbstractStateMachineTests;
|
||||
import org.springframework.statemachine.ExtendedState;
|
||||
import org.springframework.statemachine.StateContext;
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
import org.springframework.statemachine.annotation.EventHeader;
|
||||
import org.springframework.statemachine.annotation.EventHeaders;
|
||||
import org.springframework.statemachine.annotation.OnEventNotAccepted;
|
||||
import org.springframework.statemachine.annotation.OnExtendedStateChanged;
|
||||
@@ -131,6 +132,8 @@ public class DocsConfigurationSampleTests4 extends AbstractStateMachineTests {
|
||||
@OnTransition
|
||||
public void anyTransition(
|
||||
@EventHeaders Map<String, Object> headers,
|
||||
@EventHeader("myheader1") Object myheader1,
|
||||
@EventHeader(name = "myheader2", required = false) String myheader2,
|
||||
ExtendedState extendedState,
|
||||
StateMachine<String, String> stateMachine,
|
||||
Message<String> message,
|
||||
|
||||
Reference in New Issue
Block a user