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:
Janne Valkealahti
2019-03-17 08:08:13 +00:00
parent 53d6ddefa9
commit 7df664d306
6 changed files with 172 additions and 10 deletions

View File

@@ -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`.

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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

View File

@@ -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,