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 1662797414..62e9d4e713 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,9 +18,11 @@ package org.springframework.integration.message; import java.lang.reflect.Method; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Properties; +import java.util.Set; import org.springframework.core.GenericTypeResolver; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; @@ -36,41 +38,80 @@ import org.springframework.util.StringUtils; /** * Prepares arguments for handler methods. The method parameters are matched - * against the Message payload as well as its headers. If a method parameter - * is annotated with {@link Header @Header}, the annotation's value will be - * used as a header name. If such an annotation contains no value, then the - * parameter name will be used as long as the information is available in the - * class file (requires compilation with debug settings for parameter names). - * If the {@link Header @Header} annotation is not present, then the parameter - * will typically match the Message payload. However, if a Map or Properties - * object is expected, and the paylaod is not itself assignable to that type, - * then the MessageHeaders' values will be passed in the case of a Map-typed - * parameter, or the MessageHeaders' String-based values will be passed in the - * case of a Properties-typed parameter. + * against the Message payload as well as its headers. If a method parameter is + * annotated with {@link Header @Header}, the annotation's value will be used as + * a header name. If such an annotation contains no value, then the parameter + * name will be used as long as the information is available in the class file + * (requires compilation with debug settings for parameter names). If the + * {@link Header @Header} annotation is not present, then the parameter will + * typically match the Message payload. However, if a Map or Properties object + * is expected, and the paylaod is not itself assignable to that type, then the + * MessageHeaders' values will be passed in the case of a Map-typed parameter, + * or the MessageHeaders' String-based values will be passed in the case of a + * Properties-typed parameter. * * @author Mark Fisher + * @author Iwein Fuld */ public class MethodParameterMessageMapper implements InboundMessageMapper, OutboundMessageMapper { private final Method method; - private volatile MethodParameterMetadata[] parameterMetadata; + private final MethodParameterMetadata[] parameterMetadata; + + private final MethodParameterMetadata payloadParameterMetadata; private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); - public MethodParameterMessageMapper(Method method) { Assert.notNull(method, "method must not be null"); this.method = method; - this.initializeParameterMetadata(); + parameterMetadata = this.initializeParameterMetadata(); + payloadParameterMetadata = getPayloadParameterFrom(parameterMetadata); } + private MethodParameterMetadata getPayloadParameterFrom(MethodParameterMetadata[] parameterMetadata) { + Set payloadCandidates = new HashSet(); + int messagesFound = 0; + for (MethodParameterMetadata metadata : parameterMetadata) { + if (metadata.getHeaderAnnotation() == null && !metadata.hasHeadersAnnotation() + && !Message.class.isAssignableFrom(metadata.getParameterType())) { + payloadCandidates.add(metadata); + } else if (Message.class.isAssignableFrom(metadata.getParameterType())){ + messagesFound++; + } + } + if (payloadCandidates.size() > 1) { + Iterator iterator = payloadCandidates.iterator(); + while (iterator.hasNext()) { + Class type = iterator.next().getParameterType(); + if (Map.class.isAssignableFrom(type) || Properties.class.isAssignableFrom(type)) { + iterator.remove(); + } + } + } + Assert.isTrue(payloadCandidates.size()+messagesFound <= 1, "Could not find at most one message or payload parameter among [" + + parameterMetadata + "] ended up with the candidates [" + payloadCandidates + "]"); + MethodParameterMetadata methodParameterMetadata = payloadCandidates.toArray(new MethodParameterMetadata[1])[0]; + return methodParameterMetadata; + } + + private MethodParameterMetadata[] initializeParameterMetadata() { + Class[] paramTypes = this.method.getParameterTypes(); + MethodParameterMetadata[] parameterMetadata = new MethodParameterMetadata[paramTypes.length]; + for (int i = 0; i < paramTypes.length; i++) { + MethodParameterMetadata metadata = new MethodParameterMetadata(this.method, i); + metadata.initParameterNameDiscovery(this.parameterNameDiscoverer); + GenericTypeResolver.resolveParameterType(metadata, this.method.getDeclaringClass()); + parameterMetadata[i] = metadata; + } + return parameterMetadata; + } public Message toMessage(Object[] parameters) { Assert.isTrue(!ObjectUtils.isEmpty(parameters), "parameter array is required"); - Assert.isTrue(parameters.length == this.parameterMetadata.length, - "wrong number of parameters: expected " + this.parameterMetadata.length - + ", received " + parameters.length); + 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(); @@ -78,7 +119,11 @@ public class MethodParameterMessageMapper implements InboundMessageMapper) value; } else { - Assert.notNull(value, "payload object must not be null"); - payload = value; + Assert.isTrue(metadata.isMapOrProperties()); + this.addHeadersAnnotatedParameterToMap(value, headers); } } if (message != null) { @@ -121,51 +166,35 @@ public class MethodParameterMessageMapper implements InboundMessageMapper expectedType = metadata.getParameterType(); Header headerAnnotation = metadata.getHeaderAnnotation(); - if (headerAnnotation != null) { + if (metadata == payloadParameterMetadata) { + args[i] = message.getPayload(); + } + else if (headerAnnotation != null) { String headerName = metadata.getHeaderName(); Object value = message.getHeaders().get(headerName); if (value == null && headerAnnotation.required()) { - throw new MessageHandlingException(message, - "required header '" + headerName + "' not available"); + throw new MessageHandlingException(message, "required header '" + headerName + "' not available"); } args[i] = value; } - else if (metadata.hasHeadersAnnotation()) { + else if (metadata.isMapOrProperties()) { if (Properties.class.isAssignableFrom(expectedType)) { - args[i] = this.getStringTypedHeaders(message); + args[i] = this.getStringTypedHeaders(message); } else { - args[i] = message.getHeaders(); + args[i] = message.getHeaders(); } } - else if (expectedType.isAssignableFrom(message.getClass()) - && Message.class.isAssignableFrom(expectedType)) { - args[i] = message; - } else { - args[i] = message.getPayload(); + Assert.isTrue(expectedType.isAssignableFrom(message.getClass()) + && Message.class.isAssignableFrom(expectedType), + "Argument is neither header or payload, so it should be of type message."); + args[i] = message; } } return args; } - private void initializeParameterMetadata() { - boolean foundMessageOrPayload = false; - Class[] paramTypes = this.method.getParameterTypes(); - this.parameterMetadata = new MethodParameterMetadata[paramTypes.length]; - for (int i = 0; i < parameterMetadata.length; i++) { - MethodParameterMetadata metadata = new MethodParameterMetadata(this.method, i); - metadata.initParameterNameDiscovery(this.parameterNameDiscoverer); - GenericTypeResolver.resolveParameterType(metadata, this.method.getDeclaringClass()); - if (metadata.getHeaderAnnotation() == null && !metadata.hasHeadersAnnotation()) { - // this is either a Message or the Object to be used as a Message payload - Assert.isTrue(!foundMessageOrPayload, "only one Message or payload parameter is allowed"); - foundMessageOrPayload = true; - } - parameterMetadata[i] = metadata; - } - } - private Properties getStringTypedHeaders(Message message) { Properties properties = new Properties(); MessageHeaders headers = message.getHeaders(); @@ -183,38 +212,45 @@ public class MethodParameterMessageMapper implements InboundMessageMapper message = MessageBuilder.withPayload("test") + .setHeader("prop1", "foo").setHeader("prop2", "bar").build(); + Object[] args = mapper.fromMessage(message); + Properties result = (Properties) args[0]; + assertEquals(2, result.size()); + assertEquals("foo", result.getProperty("prop1")); + assertEquals("bar", result.getProperty("prop2")); + assertEquals("test", args[1]); + } + + @SuppressWarnings("unchecked") + @Test + public void fromMessageWithMapAndObjectMethod() throws Exception { + Method method = TestService.class.getMethod("mapHeadersAndPayload", Map.class, Object.class); + MethodParameterMessageMapper mapper = new MethodParameterMessageMapper(method); + Message message = MessageBuilder.withPayload("test") + .setHeader("prop1", "foo").setHeader("prop2", "bar").build(); + Object[] args = mapper.fromMessage(message); + Map result = (Map) args[0]; + //Map also contains id and timestamp + assertEquals(4, result.size()); + assertEquals("foo", result.get("prop1")); + assertEquals("bar", result.get("prop2")); + assertEquals("test", args[1]); + } @Test public void fromMessageWithPropertiesMethodAndPropertiesPayload() throws Exception { @@ -192,6 +222,10 @@ public class MethodParameterMessageMapperFromMessageTests { public Properties propertiesHeaders(@Headers Properties properties) { return properties; } + + public Object propertiesHeadersAndPayload(Properties headers, Object payload) { + return payload; + } @SuppressWarnings("unchecked") public Map mapPayload(Map map) { @@ -202,11 +236,14 @@ public class MethodParameterMessageMapperFromMessageTests { public Map mapHeaders(@Headers Map map) { return map; } + + @SuppressWarnings("unchecked") + public Object mapHeadersAndPayload(Map headers, Object payload) { + return payload; + } public Integer integerMethod(Integer i) { return i; } } - - }