diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/annotation/Headers.java b/org.springframework.integration/src/main/java/org/springframework/integration/annotation/Headers.java new file mode 100644 index 0000000000..5c2fdaf93b --- /dev/null +++ b/org.springframework.integration/src/main/java/org/springframework/integration/annotation/Headers.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2008 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.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; + +/** + * Annotation indicating that a method parameter's value should be mapped to or + * from the message headers. The annotated parameter must be assignable to + * {@link java.util.Map}, and all of the Map's keys must be Strings. + * + * @author Mark Fisher + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Headers { + +} diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/message/MethodParameterMessageMapper.java b/org.springframework.integration/src/main/java/org/springframework/integration/message/MethodParameterMessageMapper.java index 2286abc2a7..929e01793a 100644 --- a/org.springframework.integration/src/main/java/org/springframework/integration/message/MethodParameterMessageMapper.java +++ b/org.springframework.integration/src/main/java/org/springframework/integration/message/MethodParameterMessageMapper.java @@ -18,6 +18,7 @@ package org.springframework.integration.message; import java.lang.reflect.Method; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.Properties; @@ -26,6 +27,7 @@ import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.integration.annotation.Header; +import org.springframework.integration.annotation.Headers; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -70,6 +72,7 @@ public class MethodParameterMessageMapper implements MessageMapper { Assert.isTrue(parameters.length == this.parameterMetadata.length, "wrong number of parameters: expected " + this.parameterMetadata.length + ", received " + parameters.length); + Message message = null; Object payload = null; Map headers = new HashMap(); for (int i = 0; i < parameters.length; i++) { @@ -77,18 +80,33 @@ public class MethodParameterMessageMapper implements MessageMapper { MethodParameterMetadata metadata = this.parameterMetadata[i]; Class expectedType = metadata.type; if (expectedType.equals(Header.class)) { - if (metadata.required) { - Assert.notNull(value, "header '" + metadata.key + "' is required"); + if (value != null) { + headers.put(metadata.key, value); + } + else { + Assert.isTrue(!metadata.required, "header '" + metadata.key + "' is required"); } - headers.put(metadata.key, value); } - // TODO: add checks for Map and Properties to be used as headers + else if (expectedType.equals(Headers.class)) { + if (value != null) { + this.addHeadersAnnotatedParameterToMap(value, headers); + } + } + else if (expectedType.equals(Message.class)) { + Assert.isNull(message, "more than one Message argument received"); + message = (Message) value; + } else { Assert.isNull(payload, "unable to determine a single payload object, found: " + "[" + payload + "] and [" + value + "]s"); payload = value; } } + if (message != null) { + Assert.isNull(payload, "cannot handle payload object [" + payload + + "] since a Message-typed parameter has also been provided"); + return MessageBuilder.fromMessage(message).copyHeadersIfAbsent(headers).build(); + } Assert.notNull(payload, "payload object must not be null"); return MessageBuilder.withPayload(payload).copyHeaders(headers).build(); } @@ -142,15 +160,19 @@ public class MethodParameterMessageMapper implements MessageMapper { methodParam.initParameterNameDiscovery(this.parameterNameDiscoverer); GenericTypeResolver.resolveParameterType(methodParam, this.method.getDeclaringClass()); Object[] paramAnnotations = methodParam.getParameterAnnotations(); - String headerName = null; for (int j = 0; j < paramAnnotations.length; j++) { if (Header.class.isInstance(paramAnnotations[j])) { Header headerAnnotation = (Header) paramAnnotations[j]; - headerName = this.resolveParameterNameIfNecessary(headerAnnotation.value(), methodParam); + String headerName = this.resolveParameterNameIfNecessary(headerAnnotation.value(), methodParam); parameterMetadata[i] = new MethodParameterMetadata(Header.class, headerName, headerAnnotation.required()); } + else if (Headers.class.isInstance(paramAnnotations[j])) { + Assert.isAssignable(Map.class, methodParam.getParameterType(), + "parameter with the @Headers annotation must be assignable to java.util.Map"); + parameterMetadata[i] = new MethodParameterMetadata(Headers.class, null, false); + } } - if (headerName == null) { + if (parameterMetadata[i] == null) { parameterMetadata[i] = new MethodParameterMetadata(methodParam.getParameterType(), null, false); } } @@ -178,6 +200,17 @@ public class MethodParameterMessageMapper implements MessageMapper { return paramName; } + @SuppressWarnings("unchecked") + private void addHeadersAnnotatedParameterToMap(Object value, Map headers) { + Map map = (Map) value; + for (Iterator iter = map.entrySet().iterator(); iter.hasNext();) { + Map.Entry entry = (Map.Entry) iter.next(); + Assert.isTrue(entry.getKey() instanceof String, + "Map annotated with @Headers must have String-typed keys"); + headers.put((String) entry.getKey(), entry.getValue()); + } + } + private static class MethodParameterMetadata { diff --git a/org.springframework.integration/src/test/java/org/springframework/integration/message/MethodParameterMessageMapperToMessageTests.java b/org.springframework.integration/src/test/java/org/springframework/integration/message/MethodParameterMessageMapperToMessageTests.java index 83be2430c9..56468c5367 100644 --- a/org.springframework.integration/src/test/java/org/springframework/integration/message/MethodParameterMessageMapperToMessageTests.java +++ b/org.springframework.integration/src/test/java/org/springframework/integration/message/MethodParameterMessageMapperToMessageTests.java @@ -20,10 +20,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; import org.junit.Test; import org.springframework.integration.annotation.Header; +import org.springframework.integration.annotation.Headers; /** * @author Mark Fisher @@ -90,6 +93,86 @@ public class MethodParameterMessageMapperToMessageTests { assertNull(message.getHeaders().get("foo")); } + @Test + public void toMessageWithPayloadAndHeadersMap() throws Exception { + Method method = TestService.class.getMethod( + "sendPayloadAndHeadersMap", String.class, Map.class); + MethodParameterMessageMapper mapper = new MethodParameterMessageMapper(method); + Map headers = new HashMap(); + headers.put("abc", 123); + headers.put("def", 456); + Message message = mapper.toMessage(new Object[] { "test", headers }); + assertEquals("test", message.getPayload()); + assertEquals(123, message.getHeaders().get("abc")); + assertEquals(456, message.getHeaders().get("def")); + } + + @Test + public void toMessageWithPayloadAndNullHeadersMap() throws Exception { + Method method = TestService.class.getMethod( + "sendPayloadAndHeadersMap", String.class, Map.class); + MethodParameterMessageMapper mapper = new MethodParameterMessageMapper(method); + Message message = mapper.toMessage(new Object[] { "test", null }); + assertEquals("test", message.getPayload()); + } + + @Test(expected = IllegalArgumentException.class) + public void toMessageWithPayloadAndHeadersMapWithNonStringKey() throws Exception { + Method method = TestService.class.getMethod( + "sendPayloadAndHeadersMap", String.class, Map.class); + MethodParameterMessageMapper mapper = new MethodParameterMessageMapper(method); + Map headers = new HashMap(); + headers.put(123, "abc"); + mapper.toMessage(new Object[] { "test", headers }); + } + + @Test + public void toMessageWithMessageParameter() throws Exception { + Method method = TestService.class.getMethod("sendMessage", Message.class); + MethodParameterMessageMapper mapper = new MethodParameterMessageMapper(method); + Message inputMessage = MessageBuilder.withPayload("test message").build(); + Message message = mapper.toMessage(new Object[] { inputMessage }); + assertEquals("test message", message.getPayload()); + } + + @Test + public void toMessageWithMessageParameterAndHeader() throws Exception { + Method method = TestService.class.getMethod("sendMessageAndHeader", Message.class, String.class); + MethodParameterMessageMapper mapper = new MethodParameterMessageMapper(method); + Message inputMessage = MessageBuilder.withPayload("test message").build(); + Message message = mapper.toMessage(new Object[] { inputMessage, "bar" }); + assertEquals("test message", message.getPayload()); + assertEquals("bar", message.getHeaders().get("foo")); + } + + @Test(expected = IllegalArgumentException.class) + public void toMessageWithMessageParameterAndRequiredHeaderButNullValue() throws Exception { + Method method = TestService.class.getMethod("sendMessageAndHeader", Message.class, String.class); + MethodParameterMessageMapper mapper = new MethodParameterMessageMapper(method); + Message inputMessage = MessageBuilder.withPayload("test message").build(); + mapper.toMessage(new Object[] { inputMessage, null }); + } + + @Test + public void toMessageWithMessageParameterAndOptionalHeaderWithValue() throws Exception { + Method method = TestService.class.getMethod("sendMessageAndOptionalHeader", Message.class, String.class); + MethodParameterMessageMapper mapper = new MethodParameterMessageMapper(method); + Message inputMessage = MessageBuilder.withPayload("test message").build(); + Message message = mapper.toMessage(new Object[] { inputMessage, "bar" }); + assertEquals("test message", message.getPayload()); + assertEquals("bar", message.getHeaders().get("foo")); + } + + @Test + public void toMessageWithMessageParameterAndOptionalHeaderWithNull() throws Exception { + Method method = TestService.class.getMethod("sendMessageAndOptionalHeader", Message.class, String.class); + MethodParameterMessageMapper mapper = new MethodParameterMessageMapper(method); + Message inputMessage = MessageBuilder.withPayload("test message").build(); + Message message = mapper.toMessage(new Object[] { inputMessage, null }); + assertEquals("test message", message.getPayload()); + assertNull(message.getHeaders().get("foo")); + } + private static interface TestService { @@ -99,6 +182,14 @@ public class MethodParameterMessageMapperToMessageTests { void sendPayloadAndOptionalHeader(String payload, @Header(value="foo", required=false) String foo); + void sendPayloadAndHeadersMap(String payload, @Headers Map headers); + + void sendMessage(Message message); + + void sendMessageAndHeader(Message message, @Header("foo") String foo); + + void sendMessageAndOptionalHeader(Message message, @Header(value="foo", required=false) String foo); + } }