diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/handler/PayloadTypeMatchingHandlerMethodResolver.java b/org.springframework.integration/src/main/java/org/springframework/integration/handler/PayloadTypeMatchingHandlerMethodResolver.java index f79c9afd78..6f7cf1587f 100644 --- a/org.springframework.integration/src/main/java/org/springframework/integration/handler/PayloadTypeMatchingHandlerMethodResolver.java +++ b/org.springframework.integration/src/main/java/org/springframework/integration/handler/PayloadTypeMatchingHandlerMethodResolver.java @@ -18,6 +18,9 @@ package org.springframework.integration.handler; import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -46,10 +49,13 @@ public class PayloadTypeMatchingHandlerMethodResolver implements HandlerMethodRe public Method resolveHandlerMethod(Message message) { - Class payloadType = message.getPayload().getClass(); - Method method = this.methodMap.get(payloadType); + Method method = this.methodMap.get(message.getClass()); if (method == null) { - method = this.findClosestMatch(payloadType); + Class payloadType = message.getPayload().getClass(); + method = this.methodMap.get(payloadType); + if (method == null) { + method = this.findClosestMatch(payloadType); + } if (method == null) { method = this.fallbackMethod; } @@ -59,35 +65,53 @@ public class PayloadTypeMatchingHandlerMethodResolver implements HandlerMethodRe private void initMethodMap(Method[] candidates) { for (Method method : candidates) { - Class expectedPayloadType = this.determineExpectedPayloadType(method); - if (expectedPayloadType == null) { + Class expectedType = this.determineExpectedType(method); + if (expectedType == null) { Assert.isTrue(fallbackMethod == null, - "At most one method can expect only Message headers rather than a payload, " + + "At most one method can expect only Message headers rather than a Message or payload, " + "but found two: [" + method + "] and [" + this.fallbackMethod + "]"); this.fallbackMethod = method; } else { - Assert.isTrue(!this.methodMap.containsKey(expectedPayloadType), - "More than one method matches payload type [" + expectedPayloadType + + Assert.isTrue(!this.methodMap.containsKey(expectedType), + "More than one method matches type [" + expectedType + "]. Consider using annotations or providing a method name."); - this.methodMap.put(expectedPayloadType, method); + this.methodMap.put(expectedType, method); } } } - private Class determineExpectedPayloadType(Method method) { - Class expectedPayloadType = null; - Class[] parameterTypes = method.getParameterTypes(); + private Class determineExpectedType(Method method) { + Class expectedType = null; + Type[] parameterTypes = method.getGenericParameterTypes(); Annotation[][] parameterAnnotations = method.getParameterAnnotations(); for (int i = 0; i < parameterTypes.length; i++) { if (!HandlerMethodUtils.containsHeaderAnnotation(parameterAnnotations[i])) { - Assert.isTrue(expectedPayloadType == null, - "Message-handling method must only have one parameter expecting a payload. Other " + - "parameters may be included but only if they have @Header or @Headers annotations."); - expectedPayloadType = parameterTypes[i]; + Assert.isTrue(expectedType == null, + "Message-handling method must only have one parameter expecting a Message or Message payload." + + " Other parameters may be included but only if they have @Header or @Headers annotations."); + Type parameterType = parameterTypes[i]; + if (parameterType instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) parameterType; + Type rawType = parameterizedType.getRawType(); + if (rawType instanceof Class) { + Class rawTypeClass = (Class) rawType; + if (Message.class.isAssignableFrom(rawTypeClass)) { + expectedType = this.determineExpectedTypeFromParameterizedMessageType(parameterizedType); + } + else { + expectedType = rawTypeClass; + } + } + } + else if (parameterType instanceof Class) { + expectedType = (Class) parameterType; + } + Assert.notNull(expectedType, "Failed to determine expected type for parameter [" + + parameterType + "] on Method [" + method + "]"); } } - return expectedPayloadType; + return expectedType; } private Method findClosestMatch(Class payloadType) { @@ -107,6 +131,24 @@ public class PayloadTypeMatchingHandlerMethodResolver implements HandlerMethodRe return matchingMethod; } + private Class determineExpectedTypeFromParameterizedMessageType(ParameterizedType parameterizedType) { + Class expectedType = null; + Type actualType = parameterizedType.getActualTypeArguments()[0]; + if (actualType instanceof WildcardType) { + WildcardType wildcardType = (WildcardType) actualType; + if (wildcardType.getUpperBounds().length == 1) { + Type upperBound = wildcardType.getUpperBounds()[0]; + if (upperBound instanceof Class) { + expectedType = (Class) upperBound; + } + } + } + else if (actualType instanceof Class) { + expectedType = (Class) actualType; + } + return expectedType; + } + private int getTypeDifferenceWeight(Class expectedType, Class payloadType) { int result = 0; if (!ClassUtils.isAssignable(expectedType, payloadType)) { diff --git a/org.springframework.integration/src/test/java/org/springframework/integration/handler/PayloadTypeMatchingHandlerMethodResolverWithMessageParameterTests.java b/org.springframework.integration/src/test/java/org/springframework/integration/handler/PayloadTypeMatchingHandlerMethodResolverWithMessageParameterTests.java new file mode 100644 index 0000000000..cf5207d624 --- /dev/null +++ b/org.springframework.integration/src/test/java/org/springframework/integration/handler/PayloadTypeMatchingHandlerMethodResolverWithMessageParameterTests.java @@ -0,0 +1,227 @@ +/* + * 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.handler; + +import static org.junit.Assert.assertEquals; + +import java.lang.reflect.Method; +import java.util.Date; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.integration.annotation.Header; +import org.springframework.integration.core.Message; +import org.springframework.integration.message.GenericMessage; +import org.springframework.integration.message.MessageBuilder; +import org.springframework.integration.message.StringMessage; + +/** + * @author Mark Fisher + */ +public class PayloadTypeMatchingHandlerMethodResolverWithMessageParameterTests { + + private PayloadTypeMatchingHandlerMethodResolver resolver; + + + @Before + public void initResolver() { + Method[] candidates = HandlerMethodUtils.getCandidateHandlerMethods(new TestService()); + resolver = new PayloadTypeMatchingHandlerMethodResolver(candidates); + } + + + @Test + public void stringPayload() throws Exception { + Class[] types = new Class[] { Message.class }; + Method expected = TestService.class.getMethod("stringPayload", types); + Method resolved = resolver.resolveHandlerMethod(new StringMessage("foo")); + assertEquals(expected, resolved); + } + + @Test + public void exactMatch() throws Exception { + Class[] types = new Class[] { Message.class }; + Method expected = TestService.class.getMethod("fooImpl1Payload", types); + Method resolved = resolver.resolveHandlerMethod(new GenericMessage(new TestFooImpl1())); + assertEquals(expected, resolved); + } + + @Test + public void interfaceMatch() throws Exception { + Class[] types = new Class[] { Message.class }; + Method expected = TestService.class.getMethod("fooInterfacePayload", types); + Method resolved = resolver.resolveHandlerMethod(new GenericMessage(new TestFooImpl2())); + assertEquals(expected, resolved); + } + + @Test + public void superclassMatch() throws Exception { + Class[] types = new Class[] { Message.class }; + Method expected = TestService.class.getMethod("fooImpl1Payload", types); + Method resolved = resolver.resolveHandlerMethod(new GenericMessage(new TestFooImpl1Subclass())); + assertEquals(expected, resolved); + } + + @Test + public void interfaceOfSuperclassMatch() throws Exception { + Class[] types = new Class[] { Message.class }; + Method expected = TestService.class.getMethod("fooInterfacePayload", types); + Method resolved = resolver.resolveHandlerMethod(new GenericMessage(new TestFooImpl2Subclass())); + assertEquals(expected, resolved); + } + + @Test + public void numberSuperclassMatch() throws Exception { + Class[] types = new Class[] { Message.class }; + Method expected = TestService.class.getMethod("numberPayload", types); + Method resolved = resolver.resolveHandlerMethod(new GenericMessage(new Long(99))); + assertEquals(expected, resolved); + } + + @Test + public void payloadAndHeaderMethod() throws Exception { + Class[] types = new Class[] { Message.class, String.class }; + Method expected = TestService.class.getMethod("integerPayloadAndHeader", types); + Method resolved = resolver.resolveHandlerMethod(new GenericMessage(new Integer(123))); + assertEquals(expected, resolved); + } + + @Test + public void fallbackToHeaderOnlyMethod() throws Exception { + Class[] types = new Class[] { String.class }; + Method expected = TestService.class.getMethod("headerOnlyMethod", types); + Method resolved = resolver.resolveHandlerMethod(new GenericMessage(new Date())); + assertEquals(expected, resolved); + } + + @Test + public void testStringMessageTypedParameter() throws Exception { + Object service = new TestServiceWithMessageTypes(); + Method[] candidates = HandlerMethodUtils.getCandidateHandlerMethods(service); + PayloadTypeMatchingHandlerMethodResolver methodResovler + = new PayloadTypeMatchingHandlerMethodResolver(candidates); + Class[] types = new Class[] { StringMessage.class }; + Method expected = TestServiceWithMessageTypes.class.getMethod("stringMessage", types); + Message message = new StringMessage("foo"); + Method resolved = methodResovler.resolveHandlerMethod(message); + assertEquals(expected, resolved); + assertEquals("foo", resolved.invoke(service, message)); + } + + @Test + public void testMessageParameterizedWithString() throws Exception { + Object service = new TestServiceWithMessageTypes(); + Method[] candidates = HandlerMethodUtils.getCandidateHandlerMethods(service); + PayloadTypeMatchingHandlerMethodResolver methodResovler + = new PayloadTypeMatchingHandlerMethodResolver(candidates); + Class[] types = new Class[] { Message.class }; + Method expected = TestServiceWithMessageTypes.class.getMethod("stringParameterizedMessage", types); + Message message = new GenericMessage("foo"); + Method resolved = methodResovler.resolveHandlerMethod(message); + assertEquals(expected, resolved); + assertEquals("foo", resolved.invoke(service, message)); + } + + @Test + public void testUnboundedWildcard() throws Exception { + Object service = new TestServiceWithMessageTypes(); + Method[] candidates = HandlerMethodUtils.getCandidateHandlerMethods(service); + PayloadTypeMatchingHandlerMethodResolver methodResovler + = new PayloadTypeMatchingHandlerMethodResolver(candidates); + Class[] types = new Class[] { Message.class }; + Method expected = TestServiceWithMessageTypes.class.getMethod("unboundedWildcardMessage", types); + Date date = new Date(); + Message message = new GenericMessage(date); + Method resolved = methodResovler.resolveHandlerMethod(message); + assertEquals(expected, resolved); + assertEquals(date, resolved.invoke(service, message)); + } + + @Test + public void testBoundedWildcard() throws Exception { + Object service = new TestServiceWithMessageTypes(); + Method[] candidates = HandlerMethodUtils.getCandidateHandlerMethods(service); + PayloadTypeMatchingHandlerMethodResolver methodResovler + = new PayloadTypeMatchingHandlerMethodResolver(candidates); + Class[] types = new Class[] { Message.class }; + Method expected = TestServiceWithMessageTypes.class.getMethod("boundedWildcardMessage", types); + Message message = MessageBuilder.withPayload(new Integer(123)).build(); + Method resolved = methodResovler.resolveHandlerMethod(message); + assertEquals(expected, resolved); + assertEquals(new Integer(123), resolved.invoke(service, message)); + } + + + public static class TestService { + + public void stringPayload(Message message) { + } + + public void fooInterfacePayload(Message message) { + } + + public void fooImpl1Payload(Message message) { + } + + public void numberPayload(Message message) { + } + + public void headerOnlyMethod(@Header("testHeader") String s) { + } + + public void integerPayloadAndHeader(Message message, @Header("testHeader") String s) { + } + } + + + public static class TestServiceWithMessageTypes { + + public String stringMessage(StringMessage message) { + return message.getPayload(); + } + + public String stringParameterizedMessage(Message message) { + return message.getPayload(); + } + + public Object unboundedWildcardMessage(Message message) { + return message.getPayload(); + } + + public Number boundedWildcardMessage(Message message) { + return message.getPayload(); + } + } + + + public interface TestFoo { + } + + public class TestFooImpl1 implements TestFoo { + } + + public class TestFooImpl2 implements TestFoo { + } + + public class TestFooImpl1Subclass extends TestFooImpl1 { + } + + public class TestFooImpl2Subclass extends TestFooImpl2 { + } + +}