From 31970fcf11d9d24090775cf1e2371769b68f3ea6 Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Sat, 1 May 2010 21:45:55 +0000 Subject: [PATCH] INT-1111. Added support for enriching headers when mapping gateway methods --- .../integration/config/xml/GatewayParser.java | 43 ++++-- .../gateway/GatewayMethodDefinition.java | 10 ++ .../gateway/GatewayProxyFactoryBean.java | 18 +-- .../handler/ArgumentArrayMessageMapper.java | 63 +++------ .../config/xml/spring-integration-2.0.xsd | 8 ++ .../HeaderEnrichedGatewayTests-context.xml | 33 +++++ ...derEnrichedGatewayTests-failed-context.xml | 17 +++ .../gateway/HeaderEnrichedGatewayTests.java | 122 ++++++++++++++++++ ...umentArrayMessageMapperToMessageTests.java | 23 ++++ 9 files changed, 273 insertions(+), 64 deletions(-) create mode 100644 org.springframework.integration/src/test/java/org/springframework/integration/gateway/HeaderEnrichedGatewayTests-context.xml create mode 100644 org.springframework.integration/src/test/java/org/springframework/integration/gateway/HeaderEnrichedGatewayTests-failed-context.xml create mode 100644 org.springframework.integration/src/test/java/org/springframework/integration/gateway/HeaderEnrichedGatewayTests.java diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/config/xml/GatewayParser.java b/org.springframework.integration/src/main/java/org/springframework/integration/config/xml/GatewayParser.java index c6d1d8f26b..ebd839298b 100644 --- a/org.springframework.integration/src/main/java/org/springframework/integration/config/xml/GatewayParser.java +++ b/org.springframework.integration/src/main/java/org/springframework/integration/config/xml/GatewayParser.java @@ -17,11 +17,15 @@ package org.springframework.integration.config.xml; import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.ManagedMap; import org.springframework.beans.factory.xml.AbstractSimpleBeanDefinitionParser; +import org.springframework.integration.core.MessageHeaders; import org.springframework.integration.gateway.GatewayMethodDefinition; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; @@ -30,6 +34,7 @@ import org.w3c.dom.Element; * Parser for the <gateway/> element. * * @author Mark Fisher + * @author Oleg Zhurakousky */ public class GatewayParser extends AbstractSimpleBeanDefinitionParser { @@ -76,20 +81,40 @@ public class GatewayParser extends AbstractSimpleBeanDefinitionParser { IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, attributeName); } List elements = DomUtils.getChildElementsByTagName(element, "method"); - ManagedMap methodToChannelMap = null; + + ManagedMap methodToChannelMap = null; if (elements != null && elements.size() > 0){ - methodToChannelMap = new ManagedMap(); + methodToChannelMap = new ManagedMap(); } for (Element methodElement : elements) { String methodName = methodElement.getAttribute("name"); - GatewayMethodDefinition gatewayDefinition = new GatewayMethodDefinition(); - gatewayDefinition.setRequestChannelName(methodElement.getAttribute("request-channel")); - gatewayDefinition.setReplyChannelName(methodElement.getAttribute("reply-channel")); - gatewayDefinition.setRequestTimeout(methodElement.getAttribute("request-timeout")); - gatewayDefinition.setReplyTimeout(methodElement.getAttribute("reply-timeout")); - methodToChannelMap.put(methodName, gatewayDefinition); + BeanDefinitionBuilder gatewayDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(GatewayMethodDefinition.class); + gatewayDefinitionBuilder.addPropertyValue("requestChannelName", methodElement.getAttribute("request-channel")); + gatewayDefinitionBuilder.addPropertyValue("replyChannelName", methodElement.getAttribute("reply-channel")); + gatewayDefinitionBuilder.addPropertyValue("requestTimeout", methodElement.getAttribute("request-timeout")); + gatewayDefinitionBuilder.addPropertyValue("replyTimeout", methodElement.getAttribute("reply-timeout")); + List invocationHeaders = DomUtils.getChildElementsByTagName(methodElement, "header"); + if (!CollectionUtils.isEmpty(invocationHeaders)){ + this.setMethodInvocationHeaders(gatewayDefinitionBuilder, invocationHeaders); + } + methodToChannelMap.put(methodName, gatewayDefinitionBuilder.getBeanDefinition()); } builder.addPropertyValue("methodToChannelMap", methodToChannelMap); } - + /* + * + */ + private void setMethodInvocationHeaders(BeanDefinitionBuilder gatewayDefinitionBuilder, List invocationHeaders){ + Map methodInvocationHeaders = new ManagedMap(); + for (Element headerElement : invocationHeaders) { + String name = headerElement.getAttribute("name"); + if (name.startsWith(MessageHeaders.PREFIX)){ + throw new IllegalArgumentException("Attempting to set header: " + name + ". Prefix: '" + + MessageHeaders.PREFIX + "' is reservered for SI internal use"); + } else { + methodInvocationHeaders.put(name, headerElement.getAttribute("value")); + } + } + gatewayDefinitionBuilder.addPropertyValue("staticHeaders", methodInvocationHeaders); + } } diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/gateway/GatewayMethodDefinition.java b/org.springframework.integration/src/main/java/org/springframework/integration/gateway/GatewayMethodDefinition.java index edef9f6914..c810ee39a8 100644 --- a/org.springframework.integration/src/main/java/org/springframework/integration/gateway/GatewayMethodDefinition.java +++ b/org.springframework.integration/src/main/java/org/springframework/integration/gateway/GatewayMethodDefinition.java @@ -15,6 +15,9 @@ */ package org.springframework.integration.gateway; +import java.util.HashMap; +import java.util.Map; + /** * Represents the definition of Gateway methods, when using multiple methos per * Gateway interface
@@ -29,6 +32,13 @@ public class GatewayMethodDefinition { private String replyChannelName; private String requestTimeout; private String replyTimeout; + private Map staticHeaders = new HashMap(); + public Map getStaticHeaders() { + return staticHeaders; + } + public void setStaticHeaders(Map staticHeaders) { + this.staticHeaders = staticHeaders; + } public String getRequestChannelName() { return requestChannelName; } diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/gateway/GatewayProxyFactoryBean.java b/org.springframework.integration/src/main/java/org/springframework/integration/gateway/GatewayProxyFactoryBean.java index 33bdaf2aac..efdf58db05 100644 --- a/org.springframework.integration/src/main/java/org/springframework/integration/gateway/GatewayProxyFactoryBean.java +++ b/org.springframework.integration/src/main/java/org/springframework/integration/gateway/GatewayProxyFactoryBean.java @@ -252,17 +252,12 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint implements Factory } private SimpleMessagingGateway createGatewayForMethod(Method method) { - SimpleMessagingGateway gateway = new SimpleMessagingGateway( - new ArgumentArrayMessageMapper(method), new SimpleMessageMapper()); - if (this.getTaskScheduler() != null) { - gateway.setTaskScheduler(this.getTaskScheduler()); - } - gateway.setBeanName(this.getComponentName()); Gateway gatewayAnnotation = method.getAnnotation(Gateway.class); MessageChannel requestChannel = this.defaultRequestChannel; MessageChannel replyChannel = this.defaultReplyChannel; long requestTimeout = this.defaultRequestTimeout; long replyTimeout = this.defaultReplyTimeout; + Map staticHeaders = null; if (gatewayAnnotation != null) { Assert.state(this.getChannelResolver() != null, "ChannelResolver is required"); String requestChannelName = gatewayAnnotation.requestChannel(); @@ -272,10 +267,11 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint implements Factory requestTimeout = gatewayAnnotation.requestTimeout(); replyTimeout = gatewayAnnotation.replyTimeout(); } - else if (methodToChannelMap != null && methodToChannelMap.size() > 0) { + else if (methodToChannelMap != null && methodToChannelMap.size() > 0) { Assert.state(this.getChannelResolver() != null, "ChannelResolver is required"); - GatewayMethodDefinition gatewayDefinition = methodToChannelMap.get(method.getName()); + GatewayMethodDefinition gatewayDefinition = methodToChannelMap.get(method.getName()); if (gatewayDefinition != null) { + staticHeaders = gatewayDefinition.getStaticHeaders(); String requestChannelName = gatewayDefinition.getRequestChannelName(); requestChannel = this.resolveChannel(requestChannel, requestChannelName); String replyChannelName = gatewayDefinition.getReplyChannelName(); @@ -290,6 +286,12 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint implements Factory } } } + ArgumentArrayMessageMapper messageMapper = new ArgumentArrayMessageMapper(method, staticHeaders); + SimpleMessagingGateway gateway = new SimpleMessagingGateway(messageMapper, new SimpleMessageMapper()); + if (this.getTaskScheduler() != null) { + gateway.setTaskScheduler(this.getTaskScheduler()); + } + gateway.setBeanName(this.getComponentName()); gateway.setRequestChannel(requestChannel); gateway.setReplyChannel(replyChannel); gateway.setRequestTimeout(requestTimeout); diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/handler/ArgumentArrayMessageMapper.java b/org.springframework.integration/src/main/java/org/springframework/integration/handler/ArgumentArrayMessageMapper.java index 4acb954d94..2d4808658f 100644 --- a/org.springframework.integration/src/main/java/org/springframework/integration/handler/ArgumentArrayMessageMapper.java +++ b/org.springframework.integration/src/main/java/org/springframework/integration/handler/ArgumentArrayMessageMapper.java @@ -18,28 +18,24 @@ package org.springframework.integration.handler; import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Properties; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.converter.ConverterRegistry; -import org.springframework.core.convert.support.ConversionServiceFactory; import org.springframework.integration.annotation.Header; import org.springframework.integration.annotation.Headers; import org.springframework.integration.annotation.Payload; import org.springframework.integration.core.Message; +import org.springframework.integration.core.MessageHeaders; import org.springframework.integration.core.MessagingException; import org.springframework.integration.message.InboundMessageMapper; import org.springframework.integration.message.MessageBuilder; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** @@ -101,34 +97,23 @@ import org.springframework.util.StringUtils; */ public class ArgumentArrayMessageMapper implements InboundMessageMapper { - private static ConversionService conversionService; - - static { // see INT-829 - conversionService = ConversionServiceFactory.createDefaultConversionService(); - ConverterRegistry registry = (ConverterRegistry) conversionService; - registry.removeConvertible(Object.class, Map.class); - registry.removeConvertible(Map.class, Object.class); - registry.removeConvertible(Object.class, String.class); - registry.addConverter(new Converter() { - public String convert(Number source) { return source.toString(); } - }); - registry.addConverter(new Converter() { - public String convert(Date source) { return source.toString(); } - }); - } - + private Map staticHeaders; private final Method method; private final List parameterList; - public ArgumentArrayMessageMapper(Method method) { + this(method, null); + } + + public ArgumentArrayMessageMapper(Method method, Map staticHeaders) { Assert.notNull(method, "method must not be null"); this.method = method; + this.staticHeaders = staticHeaders; this.parameterList = this.getMethodParameterList(method); } - + public Message toMessage(Object[] arguments) { Assert.notNull(arguments, "cannot map null arguments to Message"); if (arguments.length != this.parameterList.size()) { @@ -163,6 +148,10 @@ public class ArgumentArrayMessageMapper implements InboundMessageMapper) messageOrPayload) : MessageBuilder.withPayload(messageOrPayload); builder.copyHeadersIfAbsent(headers); + if (!CollectionUtils.isEmpty(staticHeaders)){ + builder.copyHeaders(staticHeaders); + } return builder.build(); } @@ -259,27 +251,4 @@ public class ArgumentArrayMessageMapper implements InboundMessageMapper message) { - // Validate against a Map with no annotations - if (message.getPayload() instanceof Map) { - boolean foundOneMatch = false; - for (MethodParameter parameter : this.parameterList) { - String name = parameter.getParameterName(); - Class type = parameter.getParameterType(); - if (parameter.getParameterAnnotations().length == 0 && - !(name.equals("payload") || name.equals("headers")) && - (type.isAssignableFrom(Properties.class) || type.isAssignableFrom(Map.class))) { - if (foundOneMatch) { - throw new IllegalArgumentException("Ambiguous parameters. " + - "Cannot determine parameter mappings between Method: [" + method + "] and Message: " + message + - ". Try annotating individual parameters with @Payload, @Header, or @Headers"); - } - foundOneMatch = true; - } - } - } - } - } diff --git a/org.springframework.integration/src/main/resources/org/springframework/integration/config/xml/spring-integration-2.0.xsd b/org.springframework.integration/src/main/resources/org/springframework/integration/config/xml/spring-integration-2.0.xsd index fd4a14451b..a19567d5f8 100644 --- a/org.springframework.integration/src/main/resources/org/springframework/integration/config/xml/spring-integration-2.0.xsd +++ b/org.springframework.integration/src/main/resources/org/springframework/integration/config/xml/spring-integration-2.0.xsd @@ -311,6 +311,14 @@ + + + + + + + + diff --git a/org.springframework.integration/src/test/java/org/springframework/integration/gateway/HeaderEnrichedGatewayTests-context.xml b/org.springframework.integration/src/test/java/org/springframework/integration/gateway/HeaderEnrichedGatewayTests-context.xml new file mode 100644 index 0000000000..26ab655b12 --- /dev/null +++ b/org.springframework.integration/src/test/java/org/springframework/integration/gateway/HeaderEnrichedGatewayTests-context.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.springframework.integration/src/test/java/org/springframework/integration/gateway/HeaderEnrichedGatewayTests-failed-context.xml b/org.springframework.integration/src/test/java/org/springframework/integration/gateway/HeaderEnrichedGatewayTests-failed-context.xml new file mode 100644 index 0000000000..5ff5c76419 --- /dev/null +++ b/org.springframework.integration/src/test/java/org/springframework/integration/gateway/HeaderEnrichedGatewayTests-failed-context.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/org.springframework.integration/src/test/java/org/springframework/integration/gateway/HeaderEnrichedGatewayTests.java b/org.springframework.integration/src/test/java/org/springframework/integration/gateway/HeaderEnrichedGatewayTests.java new file mode 100644 index 0000000000..eb30d44390 --- /dev/null +++ b/org.springframework.integration/src/test/java/org/springframework/integration/gateway/HeaderEnrichedGatewayTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2010 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.integration.gateway; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.integration.annotation.Header; +import org.springframework.integration.channel.DirectChannel; +import org.springframework.integration.core.Message; +import org.springframework.integration.core.MessageHeaders; +import org.springframework.integration.message.MessageHandler; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Oleg Zhurakousky + * @since 2.0 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class HeaderEnrichedGatewayTests { + @Autowired + @Qualifier("gateway") + private SampleGateway gateway; + @Autowired + @Qualifier("input") + private DirectChannel input; + + private Object testPayload; + + @Test + public void validateStaticHeaderMappings() throws Exception { + MessageHandler handler = Mockito.mock(MessageHandler.class); + input.subscribe(handler); + + this.prepareHandlerForTest(handler); + testPayload = "hello"; + gateway.sendString((String) testPayload); + Mockito.verify(handler, Mockito.times(1)).handleMessage(Mockito.any(Message.class)); + + this.prepareHandlerForTest(handler); + testPayload = 123; + gateway.sendInteger((Integer) testPayload); + Mockito.verify(handler, Mockito.times(1)).handleMessage(Mockito.any(Message.class)); + + this.prepareHandlerForTest(handler); + testPayload = "withAnnotatedHeaders"; + gateway.sendStringWithParameterHeaders((String) testPayload, "headerA", "headerB"); + Mockito.verify(handler, Mockito.times(1)).handleMessage(Mockito.any(Message.class)); + } + @Test(expected=BeanDefinitionStoreException.class) + public void validateFailedGatewayHeaders() throws Exception { + new ClassPathXmlApplicationContext("HeaderEnrichedGatewayTests-failed-context.xml", HeaderEnrichedGatewayTests.class); + } + + public static class Foo{ + private String bar; + + public String getBar() { + return bar; + } + + public void setBar(String bar) { + this.bar = bar; + } + } + + /* + * + */ + @SuppressWarnings("unchecked") + private void prepareHandlerForTest(MessageHandler handler){ + Mockito.reset(handler); + Mockito.doAnswer(new Answer() { + public Object answer(InvocationOnMock invocation) { + Message message = (Message) invocation.getArguments()[0]; + assertEquals(testPayload, message.getPayload()); + assertEquals("foo", message.getHeaders().get("foo")); + assertEquals("bar", message.getHeaders().get("bar")); + assertNull(message.getHeaders().get(MessageHeaders.PREFIX + "baz")); + if (message.getPayload().equals("withAnnotatedHeaders")){ + assertEquals("headerA", message.getHeaders().get("headerA")); + assertEquals("headerB", message.getHeaders().get("headerB")); + } + return null; + }}) + .when(handler).handleMessage(Mockito.any(Message.class)); + } + /* + * + */ + public static interface SampleGateway { + public void sendString(String value); + + public void sendInteger(Integer value); + + public void sendStringWithParameterHeaders(String value, + @Header("headerA") String headerA, @Header("headerB") String headerB); + } +} diff --git a/org.springframework.integration/src/test/java/org/springframework/integration/handler/ArgumentArrayMessageMapperToMessageTests.java b/org.springframework.integration/src/test/java/org/springframework/integration/handler/ArgumentArrayMessageMapperToMessageTests.java index 6b655f2a44..ba70f14226 100644 --- a/org.springframework.integration/src/test/java/org/springframework/integration/handler/ArgumentArrayMessageMapperToMessageTests.java +++ b/org.springframework.integration/src/test/java/org/springframework/integration/handler/ArgumentArrayMessageMapperToMessageTests.java @@ -28,6 +28,7 @@ import org.junit.Test; import org.springframework.integration.annotation.Header; import org.springframework.integration.annotation.Headers; import org.springframework.integration.core.Message; +import org.springframework.integration.core.MessageHeaders; import org.springframework.integration.handler.ArgumentArrayMessageMapper; import org.springframework.integration.message.MessageBuilder; @@ -189,6 +190,26 @@ public class ArgumentArrayMessageMapperToMessageTests { ArgumentArrayMessageMapper mapper = new ArgumentArrayMessageMapper(method); mapper.toMessage(new Object[] { "abc", "def" }); } + + @Test + public void toMessageWithPayloadAndStaticHeaders() throws Exception { + Method method = TestService.class.getMethod("sendPayload", String.class); + Map headers = new HashMap(); + headers.put("foo", "foo"); + headers.put("bar", "bar"); + headers.put(MessageHeaders.PREFIX + "baz", "hello"); + ArgumentArrayMessageMapper mapper = new ArgumentArrayMessageMapper(method, headers); + Message message = mapper.toMessage(new Object[] { "test" }); + assertEquals("test", message.getPayload()); + assertEquals("foo", message.getHeaders().get("foo")); + assertEquals("bar", message.getHeaders().get("bar")); + } + @Test(expected=IllegalArgumentException.class) + public void toMessageWithPayloadAndIllegalHeader() throws Exception { + Method method = TestService.class.getMethod("sendPayloadAndIllegalHeader", String.class, String.class); + ArgumentArrayMessageMapper mapper = new ArgumentArrayMessageMapper(method); + Message message = mapper.toMessage(new Object[] { "test", "foo"}); + } private static interface TestService { @@ -196,6 +217,8 @@ public class ArgumentArrayMessageMapperToMessageTests { void sendPayload(String payload); void sendPayloadAndHeader(String payload, @Header("foo") String foo); + + void sendPayloadAndIllegalHeader(String payload, @Header(MessageHeaders.PREFIX + "foo") String foo); void sendPayloadAndOptionalHeader(String payload, @Header(value="foo", required=false) String foo);