diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/annotation/MessageMapping.java b/org.springframework.integration/src/main/java/org/springframework/integration/annotation/MessageMapping.java index 8760f1b9a3..9f15c2c77c 100644 --- a/org.springframework.integration/src/main/java/org/springframework/integration/annotation/MessageMapping.java +++ b/org.springframework.integration/src/main/java/org/springframework/integration/annotation/MessageMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 the original author or authors. + * Copyright 2002-2009 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.integration.annotation; import java.lang.annotation.Documented; @@ -22,11 +23,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * This annotation allows you to specify EL expression indicating that a method - * parameter's value should be mapped to the result of expression processing. + * This annotation allows you to specify a SpEL expression indicating that a method + * parameter's value should be mapped from the result of expression processing. * The annotated parameter must be of the required type. - * Example: void foo(@MessageMapping(expression="headers.day")String arg) - will map the value of - * the 'day' header to the 'arg' + * Example: void foo(@MessageMapping("headers.day") String arg) - will map the value of + * the 'day' header to 'arg'. * * @author Oleg Zhurakousky * @since 2.0 @@ -35,5 +36,7 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MessageMapping { - String expression(); + + String value(); + } 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 43cbb0afcb..85711c71ef 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 @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.integration.handler; import java.lang.annotation.Annotation; @@ -47,6 +48,7 @@ import org.springframework.integration.message.MessageHandlingException; import org.springframework.integration.message.OutboundMessageMapper; import org.springframework.util.Assert; import org.springframework.util.StringUtils; + /** * A Message Mapper implementation that supports mapping from a Message * to an argument array when invoking handler methods, and mapping to a @@ -105,11 +107,10 @@ import org.springframework.util.StringUtils; * @since 2.0 */ public class ArgumentArrayMessageMapper implements InboundMessageMapper, OutboundMessageMapper { - private final ExpressionParser expressionParser = new SpelExpressionParser(); - private final Method method; - private List parameterList; + private static GenericConversionService conversionService; - static{ // see INT-829 + + static { // see INT-829 conversionService = new DefaultConversionService(); conversionService.removeConvertible(Object.class, Map.class); conversionService.removeConvertible(Map.class, Object.class); @@ -121,191 +122,194 @@ public class ArgumentArrayMessageMapper implements InboundMessageMapper parameterList; + + + public ArgumentArrayMessageMapper(Method method) { Assert.notNull(method, "Can not create an instance of " + this.getClass().getName() + " with 'null' value"); this.method = method; parameterList = this.getMethodParameterList(method); } - /** - * - * @param message - * @param method - * @return - */ - public Object[] fromMessage(Message message){ + + public Object[] fromMessage(Message message) { Assert.notNull(message, "Can not map Message with 'null' value"); this.validateMessageMapppings(message); Map messageArgumentsMap = this.mapMessageToArguments(parameterList, message); return messageArgumentsMap.values().toArray(); } - /** - * - * @param parameterMap - * @param message - * @return - */ - private Map mapMessageToArguments(List parameterList, Message message){ + + private Map mapMessageToArguments(List parameterList, Message message) { Map messageArgumentsMap = new java.util.LinkedHashMap(); for (MethodParameter methodParameter : parameterList) { String parameterName = methodParameter.getParameterName(); String[] expressions = null; - Annotation[] annotations = methodParameter.getParameterAnnotations(); + Annotation mappingAnnotation = null; Object value = null; - if (annotations.length == 0){ - if ("headers".equals(parameterName) || "payload".equals(parameterName)){ - expressions = new String[]{parameterName}; - } else if ("message".equals(parameterName)){ - expressions = new String[]{"#this", "payload"}; // just in case if 'parameterName' is 'message' but type is not Message - } else { - expressions = new String[]{"payload."+parameterName, "headers."+parameterName, "payload", "headers", "#this"}; + mappingAnnotation = this.findMappingAnnotation(methodParameter.getParameterAnnotations()); + if (mappingAnnotation == null) { + if ("headers".equals(parameterName) || "payload".equals(parameterName)) { + expressions = new String[] { parameterName }; + } + else if ("message".equals(parameterName)) { + // just in case 'parameterName' is 'message' but type is not Message + expressions = new String[] { "#this", "payload" }; + } + else { + expressions = new String[] { "payload." + parameterName, "headers." + parameterName, "payload", "headers", "#this" }; } value = this.getValueFromMessageBasedOnEL(message, methodParameter.getParameterType(), false, expressions); - } else { - // for now support only single annotation per parameter - if (annotations[0].annotationType().isAssignableFrom(Header.class)){ - value = this.mapHeaderThruAnnotation(annotations[0], message, methodParameter, null)[1]; - } else if (annotations[0].annotationType().isAssignableFrom(MessageMapping.class)){ - expressions = new String[]{(String) AnnotationUtils.getAnnotationAttributes(annotations[0]).get("expression")}; + } + else { + if (mappingAnnotation.annotationType().isAssignableFrom(Header.class)) { + value = this.mapHeaderThruAnnotation(mappingAnnotation, message, methodParameter, null)[1]; + } + else if (mappingAnnotation.annotationType().isAssignableFrom(MessageMapping.class)) { + expressions = new String[] { (String) AnnotationUtils.getValue(mappingAnnotation) }; value = this.getValueFromMessageBasedOnEL(message, methodParameter.getParameterType(), true, expressions); - } else if (annotations[0].annotationType().isAssignableFrom(Headers.class)){ + } + else if (mappingAnnotation.annotationType().isAssignableFrom(Headers.class)) { expressions = new String[]{"headers"}; value = this.getValueFromMessageBasedOnEL(message, methodParameter.getParameterType(), false, expressions); - } else { - throw new IllegalArgumentException("unknown or unsupported annotation: " + annotations[0]); + } + else { + throw new IllegalArgumentException("unknown or unsupported annotation: " + mappingAnnotation); } } messageArgumentsMap.put(methodParameter.getParameterIndex()+":"+parameterName, value); } return messageArgumentsMap; } - /** - * - * @param arguments - * @return - */ - public Message toMessage(Object[] arguments){ - Assert.notNull(arguments, "Can not map to 'null' arguments to Message"); - if (arguments.length > parameterList.size()) { + + public Message toMessage(Object[] arguments) { + Assert.notNull(arguments, "Can not map 'null' arguments to Message"); + if (arguments.length > this.parameterList.size()) { throw new IllegalArgumentException("Too many parameters provided for: " + method); - } else if (arguments.length < parameterList.size()){ + } + else if (arguments.length < this.parameterList.size()) { throw new IllegalArgumentException("Not enough parameters provided for: " + method); - } else { + } + else { Map messageArgumentsMap = this.mapArgumentsToMessage(arguments, null); return this.buildMessageFromArgumentMap(messageArgumentsMap); } } - /** - * - * @param arguments - * @return - */ + + private Annotation findMappingAnnotation(Annotation[] annotations) { + if (annotations == null || annotations.length == 0) { + return null; + } + Annotation match = null; + for (Annotation annotation : annotations) { + Class type = annotation.annotationType(); + if (type.equals(MessageMapping.class) || type.equals(Header.class) || type.equals(Headers.class)) { + if (match != null) { + throw new IllegalArgumentException("at most one parameter annotation can be provided for message mapping, " + + "but found two [" + match.annotationType().getName() + "] and [" + annotation.annotationType().getName() + "]"); + } + match = annotation; + } + } + return match; + } + @SuppressWarnings("unchecked") - private Map mapArgumentsToMessage(Object[] arguments, Message message){ + private Map mapArgumentsToMessage(Object[] arguments, Message message) { boolean payloadExist = false; Map messageArgumentsMap = new LinkedHashMap(); - for (int i = 0; i < parameterList.size(); i++) { + for (int i = 0; i < this.parameterList.size(); i++) { Object argumentValue = arguments[i]; - MethodParameter methodParam = (MethodParameter)parameterList.get(i); + MethodParameter methodParam = (MethodParameter) this.parameterList.get(i); Annotation annotation = methodParam.getParameterAnnotations().length == 0 ? null : (methodParam.getParameterAnnotations()[0]); - if (annotation == null && !payloadExist){ + if (annotation == null && !payloadExist) { if (argumentValue instanceof Message) { messageArgumentsMap.put("message", argumentValue); - } else { + } + else { messageArgumentsMap.put("payload", argumentValue); } payloadExist = true; - } else if (annotation.annotationType().equals(Headers.class)) { - if (argumentValue != null){ + } + else if (annotation.annotationType().equals(Headers.class)) { + if (argumentValue != null) { messageArgumentsMap.putAll(((Map)argumentValue)); for (Object key : ((Map)argumentValue).keySet()) { Assert.isInstanceOf(String.class, key, "Header names must be of type String: " + key); Object value = ((Map)argumentValue).get(key); messageArgumentsMap.put((String) key, value); } - } - } else if (annotation.annotationType().equals(Header.class)) { + } + } + else if (annotation.annotationType().equals(Header.class)) { Object[] header = this.mapHeaderThruAnnotation(annotation, message, methodParam, argumentValue); messageArgumentsMap.put((String) header[0], header[1]); - } else if (annotation.annotationType().equals(MessageMapping.class)) { - throw new IllegalArgumentException("@MessageMapping is not allowed when mapping from method to Message"); // need to clarify what to do here } - } + else if (annotation.annotationType().equals(MessageMapping.class)) { + // need to clarify what to do here + throw new IllegalArgumentException("@MessageMapping is not allowed when mapping from method to Message"); + } + } Assert.isTrue(payloadExist, "Payload can not be determined from method: " + method); return messageArgumentsMap; } - /** - * - * @param payload - * @param headers - * @return - */ - private Message buildMessageFromArgumentMap(Map messageArgumentsMap){ + + private Message buildMessageFromArgumentMap(Map messageArgumentsMap) { MessageBuilder builder = null; Map headers = null; Message message = (Message) messageArgumentsMap.get("message"); - if (message != null){ + if (message != null) { Object payload = message.getPayload(); headers = message.getHeaders(); builder = MessageBuilder.withPayload(payload).copyHeaders(headers); - } else { + } + else { builder = MessageBuilder.withPayload(messageArgumentsMap.get("payload")); } for (Object headerName : messageArgumentsMap.keySet()) { - if (!headerName.equals("payload") && !headerName.equals("message")){ // everything else is a header + if (!headerName.equals("payload") && !headerName.equals("message")) { // everything else is a header builder.setHeader((String) headerName, messageArgumentsMap.get(headerName)); } } return builder.build(); } + /** - * * @param header * @param message * @param methodParameter * @param headerValue = will be present when mapping from Arg to Message and will be null the other way * @return */ - private Object[] mapHeaderThruAnnotation(Annotation header, Message message, MethodParameter methodParameter, Object headerValue){ + private Object[] mapHeaderThruAnnotation(Annotation header, Message message, MethodParameter methodParameter, Object headerValue) { String valueAttribute = (String) AnnotationUtils.getValue(header); String headerName = StringUtils.hasText(valueAttribute) ? valueAttribute : methodParameter.getParameterName(); - Assert.notNull(headerName, "Can not determine header name. Possible reasons: -debug is being " + - "disabled or header name is not explicitely provided via @Header annotation"); - if (message != null){ + Assert.notNull(headerName, "Can not determine header name. Possible reasons: -debug is " + + "disabled or header name is not explicitly provided via @Header annotation"); + if (message != null) { headerValue = this.getValueFromMessageBasedOnEL(message, methodParameter.getParameterType(), false, "headers." + headerName); } - this.evauateHeader(header, headerName, headerValue, message); + this.evaluateHeader(header, headerName, headerValue, message); return new Object[]{headerName, headerValue}; } - /** - * - * @param headerAnnotation - * @param headerName - * @param headerValue - * @param message - */ - private void evauateHeader(Annotation headerAnnotation, String headerName, Object headerValue, Message message){ + + private void evaluateHeader(Annotation headerAnnotation, String headerName, Object headerValue, Message message) { boolean required = ((Boolean) AnnotationUtils.getAnnotationAttributes(headerAnnotation).get("required")).booleanValue(); - if (required && headerValue == null){ + if (required && headerValue == null) { if (message != null) { throw new MessageHandlingException(message, "Message is missing required header: '" + headerName + "'"); } throw new IllegalArgumentException("Argument is missing required header: '" + headerName + "'"); } } - /** - * - * @param expression - * @param contextTarget - * @return - */ + @SuppressWarnings("unchecked") - private Object getValueFromMessageBasedOnEL(Message message, Class targetType, boolean rethrowException, String... expressions){ + private Object getValueFromMessageBasedOnEL(Message message, Class targetType, boolean rethrowException, String... expressions) { Object value = null; targetType = new TypeDescriptor(targetType).getObjectType(); //converts primitives to Object wrappers for (String expression : expressions) { @@ -317,23 +321,21 @@ public class ArgumentArrayMessageMapper implements InboundMessageMapperProperties conversion break; - } else { + } + else { value = null; } - } catch (Throwable e) { - if (rethrowException){ + } + catch (Throwable e) { + if (rethrowException) { throw new MessageHandlingException(message, e); } } } return value; } - /** - * - * @param method - * @return - */ - private List getMethodParameterList(Method method){ + + private List getMethodParameterList(Method method) { List parameterList = new LinkedList(); ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); int parameterCount = method.getParameterTypes().length; @@ -341,39 +343,34 @@ public class ArgumentArrayMessageMapper implements InboundMessageMapper message){ // validate against maps with no annotations + public void validateMessageMapppings(Message message) { // validate against maps with no annotations int counterOfUnnamedMapAttributes = 0; - if (message.getPayload() instanceof Map){ - for (MethodParameter parameter : parameterList) { + if (message.getPayload() instanceof Map) { + for (MethodParameter parameter : this.parameterList) { if ( parameter.getParameterAnnotations().length == 0 && (parameter.getParameterType().isAssignableFrom(Properties.class) || parameter.getParameterType().isAssignableFrom(Map.class)) && !(parameter.getParameterName().equals("payload") || parameter.getParameterName().equals("headers")) ) { counterOfUnnamedMapAttributes++; } } - if (counterOfUnnamedMapAttributes > 1){ - throw new IllegalArgumentException("Ambiguate parameters. Can not determine parameter mappings between Method: " + method + + if (counterOfUnnamedMapAttributes > 1) { + throw new IllegalArgumentException("Ambiguous parameters. Can not determine parameter mappings between Method: " + method + " and Message: " + message + ". Try annotating individual parameters with @Headers, @Header or @MessageMapping"); } } } -} \ No newline at end of file + +} diff --git a/org.springframework.integration/src/test/java/org/springframework/integration/handler/ArgumentArrayMessageMapperFromMessageTests.java b/org.springframework.integration/src/test/java/org/springframework/integration/handler/ArgumentArrayMessageMapperFromMessageTests.java index 476b4802ac..81d23670c1 100644 --- a/org.springframework.integration/src/test/java/org/springframework/integration/handler/ArgumentArrayMessageMapperFromMessageTests.java +++ b/org.springframework.integration/src/test/java/org/springframework/integration/handler/ArgumentArrayMessageMapperFromMessageTests.java @@ -26,6 +26,7 @@ import java.util.Properties; import org.junit.Assert; import org.junit.Test; + import org.springframework.integration.annotation.Header; import org.springframework.integration.annotation.Headers; import org.springframework.integration.annotation.MessageMapping; @@ -173,7 +174,6 @@ public class ArgumentArrayMessageMapperFromMessageTests { assertEquals(new Integer(123), result.get("attrib1")); assertEquals(new Integer(456), result.get("attrib2")); } - @Test @SuppressWarnings("unchecked") @@ -192,6 +192,7 @@ public class ArgumentArrayMessageMapperFromMessageTests { assertEquals(new Integer(88), result.get("attrib1")); assertEquals(new Integer(99), result.get("attrib2")); } + @Test public void fromMessageToMessageMappingAnnotation() throws Exception { Message message = this.getMessage(); @@ -202,13 +203,16 @@ public class ArgumentArrayMessageMapperFromMessageTests { Assert.assertTrue(parameters.length == 1); Assert.assertTrue(parameters[0].equals("monday")); } - @Test(expected=IllegalArgumentException.class) - public void fromMessageUnsupportedAnnotation() throws Exception { - Message message = this.getMessage(); - - ArgumentArrayMessageMapper mapper = new ArgumentArrayMessageMapper(TestService.class.getMethod("fromMessageUnsupportedAnnotation", String.class)); - mapper.fromMessage(message); + + @Test + public void fromMessageIrrelevantAnnotation() throws Exception { + Message message = MessageBuilder.withPayload("foo").build(); + ArgumentArrayMessageMapper mapper = new ArgumentArrayMessageMapper(TestService.class.getMethod("fromMessageIrrelevantAnnotation", String.class)); + Object args[] = mapper.fromMessage(message); + assertEquals(1, args.length); + assertEquals("foo", args[0]); } + @Test public void fromMessageToMessageMappingAnnotationMultiArguments() throws Exception { Message message = this.getMessage(); @@ -231,6 +235,7 @@ public class ArgumentArrayMessageMapperFromMessageTests { Assert.assertTrue(parameters[4].equals("oleg")); Assert.assertTrue(parameters[5] instanceof Map); } + @Test public void fromMessageToPayload() throws Exception { Method method = TestService.class.getMethod("payloadOnly", Map.class); @@ -240,6 +245,7 @@ public class ArgumentArrayMessageMapperFromMessageTests { Assert.assertTrue(args[0] instanceof Map); Assert.assertTrue(((Map)args[0]).get("number").equals("jkl")); } + @Test public void fromMessageToPayloadArg() throws Exception { Method method = TestService.class.getMethod("payloadOnlyPayloadArg", String.class); @@ -249,6 +255,7 @@ public class ArgumentArrayMessageMapperFromMessageTests { Assert.assertTrue(args[0] instanceof String); Assert.assertTrue(args[0].equals("oleg")); } + @Test public void fromMessageToPayloadArgs() throws Exception { Method method = TestService.class.getMethod("payloadOnlyPayloadArgs", String.class, String.class); @@ -260,6 +267,7 @@ public class ArgumentArrayMessageMapperFromMessageTests { Assert.assertTrue(args[1] instanceof String); Assert.assertTrue(args[1].equals("zhurakousky")); } + @Test public void fromMessageToPayloadArgsHeaderArgs() throws Exception { Method method = TestService.class.getMethod("payloadOnlyPayloadArgsHeaderArg", String.class, String.class); @@ -271,6 +279,23 @@ public class ArgumentArrayMessageMapperFromMessageTests { Assert.assertTrue(args[1] instanceof String); Assert.assertTrue(args[1].equals("monday")); } + + @Test(expected = IllegalArgumentException.class) + public void fromMessageInvalidMethodWithMultipleMappingAnnotations() throws Exception { + Method method = MultipleMappingAnnotationTestBean.class.getMethod("test", String.class); + ArgumentArrayMessageMapper mapper = new ArgumentArrayMessageMapper(method); + Message message = MessageBuilder.withPayload("payload").setHeader("foo", "bar").build(); + mapper.fromMessage(message); + } + + + @SuppressWarnings("unused") + private static class MultipleMappingAnnotationTestBean { + public void test(@MessageMapping("payload") @Header("foo") String s) { + } + } + + @SuppressWarnings("unused") private static class TestService { @@ -333,37 +358,52 @@ public class ArgumentArrayMessageMapperFromMessageTests { public Integer integerMethod(Integer i) { return i; } - public void fromMessageToArgWithConversion(@MessageMapping(expression="headers.number")String sArg){} // - public void fromMessageToArgWithConversion(@MessageMapping(expression="headers.number")Integer iArg){} // - public void fromMessageToArgWithConversion(@Header("numberA")Integer valueA, @Header("numberB")Integer valueB){} // - public void fromMessageToMessageMappingAnnotation(@MessageMapping(expression="headers.day")String value){} // - public void fromMessageToMessageMappingAnnotationMultiArguments(@MessageMapping(expression="headers.day")String argA, - @MessageMapping(expression="headers.month")String argB, - @MessageMapping(expression="#this")Message message, - @MessageMapping(expression="payload")Employee payloadArg, - @MessageMapping(expression="payload.fname")String value, - @MessageMapping(expression="headers")Map headers){} // - public void fromMessageUnsupportedAnnotation(@BogusAnnotation() String value){} // + + public void fromMessageToArgWithConversion(@MessageMapping("headers.number") String sArg) {} // + + public void fromMessageToArgWithConversion(@MessageMapping("headers.number") Integer iArg) {} // + + public void fromMessageToArgWithConversion(@Header("numberA")Integer valueA, @Header("numberB") Integer valueB) {} // + + public void fromMessageToMessageMappingAnnotation(@MessageMapping("headers.day") String value) {} // + + public void fromMessageToMessageMappingAnnotationMultiArguments(@MessageMapping("headers.day") String argA, + @MessageMapping("headers.month") String argB, + @MessageMapping("#this") Message message, + @MessageMapping("payload") Employee payloadArg, + @MessageMapping("payload.fname") String value, + @MessageMapping("headers") Map headers){} // + + public void fromMessageIrrelevantAnnotation(@BogusAnnotation() String value){} // } - private Message getMessage(){ + + private Message getMessage() { MessageBuilder builder = MessageBuilder.withPayload(employee); builder.setHeader("day", "monday"); builder.setHeader("month", "September"); Message message = builder.build(); return message; } - public static class Employee{ + + + public static class Employee { + private String fname; + private String lname; - public Employee(String fname, String lname){ + + public Employee(String fname, String lname) { this.fname = fname; this.lname = lname; } + public String getFname() { return fname; } + public String getLname() { return lname; } } + }