diff --git a/docs/src/reference/asciidoc/sm.adoc b/docs/src/reference/asciidoc/sm.adoc
index 07ca3637..4ff11607 100644
--- a/docs/src/reference/asciidoc/sm.adoc
+++ b/docs/src/reference/asciidoc/sm.adoc
@@ -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`.
diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/EventHeader.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/EventHeader.java
new file mode 100644
index 00000000..7c6747a2
--- /dev/null
+++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/EventHeader.java
@@ -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.
+ *
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;
+}
diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/EventHeaders.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/EventHeaders.java
index e5111acb..65bec928 100644
--- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/EventHeaders.java
+++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/annotation/EventHeaders.java
@@ -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
diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineMethodInvokerHelper.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineMethodInvokerHelper.java
index e35774fc..ccb81877 100644
--- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineMethodInvokerHelper.java
+++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineMethodInvokerHelper.java
@@ -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 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 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 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 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 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 extends AbstractExpression
this.expression = this.generateExpression(method);
}
-
Expression getExpression() {
return this.expression;
}
@@ -435,6 +449,8 @@ public class StateMachineMethodInvokerHelper 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 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 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 {
+ public static class ParametersWrapper {
private final StateContext stateContext;
@@ -513,6 +562,14 @@ public class StateMachineMethodInvokerHelper 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 getHeaders() {
return stateContext.getMessageHeaders();
}
diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/annotation/MethodAnnotationTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/annotation/MethodAnnotationTests.java
index 19fcc3c5..14205a2b 100644
--- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/annotation/MethodAnnotationTests.java
+++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/annotation/MethodAnnotationTests.java
@@ -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 headers;
ExtendedState extendedState;
Object variable;
+ Object fooHeader;
+ Object bar1Header;
+ Object bar2Header;
+ String bar3Header;
@OnTransition(source = "S1", target = "S2")
public void method1(@EventHeaders Map 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
diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests4.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests4.java
index 4f8ba008..cae2b2ed 100644
--- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests4.java
+++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests4.java
@@ -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 headers,
+ @EventHeader("myheader1") Object myheader1,
+ @EventHeader(name = "myheader2", required = false) String myheader2,
ExtendedState extendedState,
StateMachine stateMachine,
Message message,