diff --git a/spring-integration-core/src/main/java/org/springframework/integration/util/MessagingMethodInvokerHelper.java b/spring-integration-core/src/main/java/org/springframework/integration/util/MessagingMethodInvokerHelper.java index 0c4a58815e..43f4a44c09 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/util/MessagingMethodInvokerHelper.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/util/MessagingMethodInvokerHelper.java @@ -262,7 +262,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator this.displayString = sb.toString() + "]"; } - private void prepareEvaluationContext() { + private void prepareEvaluationContext() throws Exception { StandardEvaluationContext context = getEvaluationContext(false); Class targetType = AopUtils.getTargetClass(this.targetObject); if (this.method != null) { @@ -282,6 +282,8 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator context.registerMethodFilter(targetType, filter); } context.setVariable("target", this.targetObject); + context.registerFunction("requiredHeader", ParametersWrapper.class.getDeclaredMethod("getHeader", + Map.class, String.class)); } private boolean canReturnExpectedType(AnnotatedMethodFilter filter, Class targetType, @@ -845,12 +847,16 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator + "disabled or header name is not explicitly provided via @Header annotation."); String headerRetrievalExpression = "headers['" + headerName + "']"; String fullHeaderExpression = headerRetrievalExpression + relativeExpression; - String fallbackExpression = (annotationAttributes.getBoolean("required") - && !methodParameter.getParameterType().getName().equals("java.util.Optional")) - ? "T(org.springframework.util.Assert).isTrue(false, 'required header not available: " - + headerName + "')" - : "null"; - return headerRetrievalExpression + " != null ? " + fullHeaderExpression + " : " + fallbackExpression; + 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"; + } } private synchronized void setExclusiveTargetParameterType(TypeDescriptor targetParameterType, @@ -871,7 +877,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator } } - public class ParametersWrapper { + public static class ParametersWrapper { private final Object payload; @@ -895,6 +901,21 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator this.message = null; } + /** + * SpEL Function to retrieve a required header. + * @param headers the headers. + * @param header the header name + * @return the header + * @throws IllegalArgumentException if the header does not exist + */ + 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 Object getPayload() { Assert.state(this.payload != null, "Invalid method parameter for payload: was expecting collection."); return this.payload; diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java index 549b5e5da2..edc421c89e 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java @@ -16,6 +16,7 @@ package org.springframework.integration.handler; +import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -40,12 +41,16 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.BeanFactory; +import org.springframework.expression.spel.SpelCompilerMode; import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelParserConfiguration; import org.springframework.integration.annotation.ServiceActivator; import org.springframework.integration.gateway.GatewayProxyFactoryBean; import org.springframework.integration.gateway.RequestReplyExchanger; import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.test.util.TestUtils; import org.springframework.integration.util.MessagingMethodInvokerHelper; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandlingException; @@ -62,7 +67,7 @@ import org.springframework.messaging.support.GenericMessage; * @author Gunnar Hillert * @author Artem Bilan */ -@SuppressWarnings({"rawtypes", "unchecked"}) +@SuppressWarnings({ "rawtypes", "unchecked" }) public class MethodInvokingMessageProcessorTests { private static final Log logger = LogFactory.getLog(MethodInvokingMessageProcessorTests.class); @@ -73,6 +78,7 @@ public class MethodInvokingMessageProcessorTests { @Test public void testHandlerInheritanceMethodImplInSuper() { class A { + @SuppressWarnings("unused") public Message myMethod(final Message msg) { return MessageBuilder.fromMessage(msg).setHeader("A", "A").build(); @@ -80,10 +86,12 @@ public class MethodInvokingMessageProcessorTests { } class B extends A { + } @SuppressWarnings("unused") class C extends B { + } MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(new B(), "myMethod"); @@ -94,6 +102,7 @@ public class MethodInvokingMessageProcessorTests { @Test public void testHandlerInheritanceMethodImplInLatestSuper() { class A { + @SuppressWarnings("unused") public Message myMethod(Message msg) { return MessageBuilder.fromMessage(msg).setHeader("A", "A").build(); @@ -101,6 +110,7 @@ public class MethodInvokingMessageProcessorTests { } class B extends A { + @Override public Message myMethod(Message msg) { return MessageBuilder.fromMessage(msg).setHeader("B", "B").build(); @@ -109,6 +119,7 @@ public class MethodInvokingMessageProcessorTests { @SuppressWarnings("unused") class C extends B { + } MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(new B(), "myMethod"); @@ -118,6 +129,7 @@ public class MethodInvokingMessageProcessorTests { public void testHandlerInheritanceMethodImplInSubClass() { class A { + @SuppressWarnings("unused") public Message myMethod(Message msg) { return MessageBuilder.fromMessage(msg).setHeader("A", "A").build(); @@ -125,6 +137,7 @@ public class MethodInvokingMessageProcessorTests { } class B extends A { + @Override public Message myMethod(Message msg) { return MessageBuilder.fromMessage(msg).setHeader("B", "B").build(); @@ -132,6 +145,7 @@ public class MethodInvokingMessageProcessorTests { } class C extends B { + @Override public Message myMethod(Message msg) { return MessageBuilder.fromMessage(msg).setHeader("C", "C").build(); @@ -145,6 +159,7 @@ public class MethodInvokingMessageProcessorTests { public void testHandlerInheritanceMethodImplInSubClassAndSuper() { class A { + @SuppressWarnings("unused") public Message myMethod(Message msg) { return MessageBuilder.fromMessage(msg).setHeader("A", "A").build(); @@ -152,9 +167,11 @@ public class MethodInvokingMessageProcessorTests { } class B extends A { + } class C extends B { + @Override public Message myMethod(Message msg) { return MessageBuilder.fromMessage(msg).setHeader("C", "C").build(); @@ -341,6 +358,106 @@ public class MethodInvokingMessageProcessorTests { assertEquals("bar-42", result); } + @Test + public void optionalAndRequiredWithAnnotatedMethod() throws Exception { + AnnotatedTestService service = new AnnotatedTestService(); + Method method = service.getClass().getMethod("optionalAndRequiredHeader", String.class, Integer.class); + MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(service, method); + optionalAndRequiredWithAnnotatedMethodGuts(processor, false); + } + + @Test + public void compiledOptionalAndRequiredWithAnnotatedMethod() throws Exception { + AnnotatedTestService service = new AnnotatedTestService(); + Method method = service.getClass().getMethod("optionalAndRequiredHeader", String.class, Integer.class); + MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(service, method); + DirectFieldAccessor compilerConfigAccessor = compileImmediate(processor); + optionalAndRequiredWithAnnotatedMethodGuts(processor, true); + assertNotNull(TestUtils.getPropertyValue(processor, "delegate.handlerMethod.expression.compiledAst")); + optionalAndRequiredWithAnnotatedMethodGuts(processor, true); + compilerConfigAccessor.setPropertyValue("compilerMode", SpelCompilerMode.OFF); + } + + private void optionalAndRequiredWithAnnotatedMethodGuts(MethodInvokingMessageProcessor processor, + boolean compiled) { + Message message = MessageBuilder.withPayload("foo") + .setHeader("num", 42) + .build(); + Object result = processor.processMessage(message); + assertEquals("null42", result); + message = MessageBuilder.withPayload("foo") + .setHeader("prop", "bar") + .setHeader("num", 42) + .build(); + result = processor.processMessage(message); + assertEquals("bar42", result); + message = MessageBuilder.withPayload("foo") + .setHeader("prop", "bar") + .build(); + try { + result = processor.processMessage(message); + fail("Expected MessageHandlingException"); + } + catch (MessageHandlingException e) { + if (compiled) { + assertThat(e.getCause().getMessage(), equalTo("required header not available: num")); + } + else { + assertThat(e.getCause().getCause().getMessage(), equalTo("required header not available: num")); + } + } + } + + @Test + public void optionalAndRequiredDottedWithAnnotatedMethod() throws Exception { + AnnotatedTestService service = new AnnotatedTestService(); + Method method = service.getClass().getMethod("optionalAndRequiredDottedHeader", String.class, Integer.class); + MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(service, method); + optionalAndRequiredDottedWithAnnotatedMethodGuts(processor, false); + } + + @Test + public void compiledOptionalAndRequiredDottedWithAnnotatedMethod() throws Exception { + AnnotatedTestService service = new AnnotatedTestService(); + Method method = service.getClass().getMethod("optionalAndRequiredDottedHeader", String.class, Integer.class); + MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(service, method); + DirectFieldAccessor compilerConfigAccessor = compileImmediate(processor); + optionalAndRequiredDottedWithAnnotatedMethodGuts(processor, true); + assertNotNull(TestUtils.getPropertyValue(processor, "delegate.handlerMethod.expression.compiledAst")); + optionalAndRequiredDottedWithAnnotatedMethodGuts(processor, true); + compilerConfigAccessor.setPropertyValue("compilerMode", SpelCompilerMode.OFF); + } + + private void optionalAndRequiredDottedWithAnnotatedMethodGuts(MethodInvokingMessageProcessor processor, + boolean compiled) { + Message message = MessageBuilder.withPayload("hello") + .setHeader("dot2", new DotBean()) + .build(); + Object result = processor.processMessage(message); + assertEquals("null42", result); + message = MessageBuilder.withPayload("hello") + .setHeader("dot1", new DotBean()) + .setHeader("dot2", new DotBean()) + .build(); + result = processor.processMessage(message); + assertEquals("bar42", result); + message = MessageBuilder.withPayload("hello") + .setHeader("dot1", new DotBean()) + .build(); + try { + result = processor.processMessage(message); + fail("Expected MessageHandlingException"); + } + catch (MessageHandlingException e) { + if (compiled) { + assertThat(e.getCause().getMessage(), equalTo("required header not available: dot2")); + } + else { // interpreted + assertThat(e.getCause().getCause().getMessage(), equalTo("required header not available: dot2")); + } + } + } + @Test public void testOverloadedNonVoidReturningMethodsWithExactMatchForType() { AmbiguousMethodBean bean = new AmbiguousMethodBean(); @@ -527,7 +644,7 @@ public class MethodInvokingMessageProcessorTests { @SuppressWarnings("unused") public void optionalHeaders(Optional foo, @Header(value = "foo", required = false) String foo1, - @Header(value = "foo") Optional foo2) { + @Header("foo") Optional foo2) { this.arguments.put("foo", (foo.isPresent() ? foo.get() : null)); this.arguments.put("foo1", foo1); this.arguments.put("foo2", (foo2.isPresent() ? foo2.get() : null)); @@ -550,6 +667,15 @@ public class MethodInvokingMessageProcessorTests { assertEquals("FOO", targetObject.arguments.get("foo2")); } + private DirectFieldAccessor compileImmediate(MethodInvokingMessageProcessor processor) { + // Update the parser configuration compiler mode + SpelParserConfiguration config = TestUtils.getPropertyValue(processor, + "delegate.handlerMethod.EXPRESSION_PARSER.configuration", SpelParserConfiguration.class); + DirectFieldAccessor accessor = new DirectFieldAccessor(config); + accessor.setPropertyValue("compilerMode", SpelCompilerMode.IMMEDIATE); + return accessor; + } + private static class ExceptionCauseMatcher extends TypeSafeMatcher { private Throwable cause; @@ -640,7 +766,7 @@ public class MethodInvokingMessageProcessorTests { } @SuppressWarnings("unused") - private static class AnnotatedTestService { + public static class AnnotatedTestService { public String messageOnly(Message message) { return (String) message.getPayload(); @@ -658,12 +784,17 @@ public class MethodInvokingMessageProcessorTests { return num; } - public Integer requiredHeader(@Header(value = "num", required = true) Integer num) { + public Integer requiredHeader(@Header("num") Integer num) { return num; } public String optionalAndRequiredHeader(@Header(required = false) String prop, - @Header(value = "num", required = true) Integer num) { + @Header("num") Integer num) { + return prop + num; + } + + public String optionalAndRequiredDottedHeader(@Header(name = "dot1.foo", required = false) String prop, + @Header(name = "dot2.baz") Integer num) { return prop + num; } @@ -743,4 +874,20 @@ public class MethodInvokingMessageProcessorTests { } + public static class DotBean { + + private final String foo = "bar"; + + private final Integer baz = 42; + + public String getFoo() { + return this.foo; + } + + public Integer getBaz() { + return this.baz; + } + + } + }