From 416966d943300c1155d4202196096ced535f6586 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 18 Feb 2016 18:06:13 -0500 Subject: [PATCH] Support Message argument with payload conversion The MessageMethodArgumentResolver now also supports applying a MessageConverter to the payload. This is effectively a shortcut for declaring a method with an @Payload argument + MessageHeaders and then creating a new message from the two. Issue: SPR-13288 --- .../DefaultMessageHandlerMethodFactory.java | 2 +- .../MessageMethodArgumentResolver.java | 84 +++++++++++++++++-- .../SimpAnnotationMethodMessageHandler.java | 2 +- .../MessageMethodArgumentResolverTests.java | 69 +++++++++++---- .../invocation/MethodMessageHandlerTests.java | 3 +- 5 files changed, 133 insertions(+), 27 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DefaultMessageHandlerMethodFactory.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DefaultMessageHandlerMethodFactory.java index 8c1cbdeab9..8d83f5eddf 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DefaultMessageHandlerMethodFactory.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DefaultMessageHandlerMethodFactory.java @@ -158,7 +158,7 @@ public class DefaultMessageHandlerMethodFactory implements MessageHandlerMethodF resolvers.add(new HeadersMethodArgumentResolver()); // Type-based argument resolution - resolvers.add(new MessageMethodArgumentResolver()); + resolvers.add(new MessageMethodArgumentResolver(this.messageConverter)); if (this.customArgumentResolvers != null) { resolvers.addAll(this.customArgumentResolvers); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolver.java index eec040b77a..2868ac3cc8 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolver.java @@ -21,8 +21,14 @@ import java.lang.reflect.Type; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.messaging.Message; +import org.springframework.messaging.converter.MessageConversionException; +import org.springframework.messaging.converter.MessageConverter; +import org.springframework.messaging.converter.SmartMessageConverter; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; /** * {@code HandlerMethodArgumentResolver} for {@link Message} method arguments. @@ -34,6 +40,19 @@ import org.springframework.util.ClassUtils; */ public class MessageMethodArgumentResolver implements HandlerMethodArgumentResolver { + private final MessageConverter converter; + + + /** + * Create a new instance with the given {@link MessageConverter}. + * @param converter the MessageConverter to use (required) + */ + public MessageMethodArgumentResolver(MessageConverter converter) { + Assert.notNull(converter, "MessageConverter must not be null"); + this.converter = converter; + } + + @Override public boolean supportsParameter(MethodParameter parameter) { return Message.class.isAssignableFrom(parameter.getParameterType()); @@ -41,23 +60,32 @@ public class MessageMethodArgumentResolver implements HandlerMethodArgumentResol @Override public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { - Class paramType = parameter.getParameterType(); - if (!paramType.isAssignableFrom(message.getClass())) { + + Class targetMessageType = parameter.getParameterType(); + Class targetPayloadType = getPayloadType(parameter); + + if (!targetMessageType.isAssignableFrom(message.getClass())) { String actual = ClassUtils.getQualifiedName(message.getClass()); - String expected = ClassUtils.getQualifiedName(paramType); + String expected = ClassUtils.getQualifiedName(targetMessageType); throw new MethodArgumentTypeMismatchException(message, parameter, "The actual message type " + "[" + actual + "] does not match the expected type [" + expected + "]"); } - Class targetPayloadType = getPayloadType(parameter); Object payload = message.getPayload(); - if (payload != null && !targetPayloadType.isInstance(payload)) { - throw new MethodArgumentTypeMismatchException(message, parameter, - "The expected Message payload type [" + ClassUtils.getQualifiedName(targetPayloadType) + - "] does not match the actual payload type [" + ClassUtils.getQualifiedName(payload.getClass()) + "]"); + if (payload == null || targetPayloadType.isInstance(payload)) { + return message; } - return message; + if (isEmptyPayload(payload)) { + String actual = ClassUtils.getQualifiedName(payload.getClass()); + String expected = ClassUtils.getQualifiedName(targetPayloadType); + throw new MessageConversionException(message, "Cannot convert from the " + + "expected payload type [" + expected + "] to the " + + "actual payload type [" + actual + "] when the payload is empty."); + } + + payload = convertPayload(message, parameter, targetPayloadType); + return MessageBuilder.createMessage(payload, message.getHeaders()); } private Class getPayloadType(MethodParameter parameter) { @@ -66,4 +94,42 @@ public class MessageMethodArgumentResolver implements HandlerMethodArgumentResol return resolvableType.getGeneric(0).resolve(Object.class); } + /** + * Check if the given {@code payload} is empty. + * @param payload the payload to check (can be {@code null}) + */ + protected boolean isEmptyPayload(Object payload) { + if (payload == null) { + return true; + } + else if (payload instanceof byte[]) { + return ((byte[]) payload).length == 0; + } + else if (payload instanceof String) { + return !StringUtils.hasText((String) payload); + } + else { + return false; + } + } + + private Object convertPayload(Message message, MethodParameter parameter, Class targetPayloadType) { + Object result; + if (this.converter instanceof SmartMessageConverter) { + SmartMessageConverter smartConverter = (SmartMessageConverter) this.converter; + result = smartConverter.fromMessage(message, targetPayloadType, parameter); + } + else { + result = this.converter.fromMessage(message, targetPayloadType); + } + + if (result == null) { + String actual = ClassUtils.getQualifiedName(targetPayloadType); + String expected = ClassUtils.getQualifiedName(message.getPayload().getClass()); + throw new MessageConversionException(message, "No converter found to convert payload " + + "type [" + actual + "] to expected payload type [" + expected + "]."); + } + return result; + } + } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandler.java index 706047bb7f..424e09c11d 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandler.java @@ -317,7 +317,7 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan // Type-based argument resolution resolvers.add(new PrincipalMethodArgumentResolver()); - resolvers.add(new MessageMethodArgumentResolver()); + resolvers.add(new MessageMethodArgumentResolver(this.messageConverter)); resolvers.addAll(getCustomArgumentResolvers()); resolvers.add(new PayloadArgumentResolver(this.messageConverter, this.validator)); diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolverTests.java index 34541e4b85..159207c18a 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolverTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolverTests.java @@ -26,11 +26,15 @@ import org.junit.rules.ExpectedException; import org.springframework.core.MethodParameter; import org.springframework.messaging.Message; +import org.springframework.messaging.converter.MessageConversionException; +import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.support.ErrorMessage; import org.springframework.messaging.support.GenericMessage; import org.springframework.messaging.support.MessageBuilder; import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Unit tests for @@ -43,20 +47,27 @@ public class MessageMethodArgumentResolverTests { @Rule public final ExpectedException thrown = ExpectedException.none(); - private final MessageMethodArgumentResolver resolver = new MessageMethodArgumentResolver(); + private MessageConverter converter; + + private MessageMethodArgumentResolver resolver; private Method method; @Before public void setup() throws Exception { - this.method = MessageMethodArgumentResolverTests.class.getDeclaredMethod("handleMessage", - Message.class, Message.class, Message.class, Message.class, ErrorMessage.class); + + this.method = MessageMethodArgumentResolverTests.class.getDeclaredMethod("handle", + Message.class, Message.class, Message.class, Message.class, + ErrorMessage.class); + + this.converter = mock(MessageConverter.class); + this.resolver = new MessageMethodArgumentResolver(this.converter); } @Test - public void resolveAnyPayloadType() throws Exception { + public void resolveWithPayloadTypeAsWildcard() throws Exception { Message message = MessageBuilder.withPayload("test").build(); MethodParameter parameter = new MethodParameter(this.method, 0); @@ -65,7 +76,7 @@ public class MessageMethodArgumentResolverTests { } @Test - public void resolvePayloadTypeExactType() throws Exception { + public void resolveWithMatchingPayloadType() throws Exception { Message message = MessageBuilder.withPayload(123).build(); MethodParameter parameter = new MethodParameter(this.method, 1); @@ -74,7 +85,7 @@ public class MessageMethodArgumentResolverTests { } @Test - public void resolvePayloadTypeSubClass() throws Exception { + public void resolveWithPayloadTypeSubClass() throws Exception { Message message = MessageBuilder.withPayload(123).build(); MethodParameter parameter = new MethodParameter(this.method, 2); @@ -83,19 +94,47 @@ public class MessageMethodArgumentResolverTests { } @Test - public void resolveInvalidPayloadType() throws Exception { + public void resolveWithConversion() throws Exception { + Message message = MessageBuilder.withPayload("test").build(); + MethodParameter parameter = new MethodParameter(this.method, 1); + + when(this.converter.fromMessage(message, Integer.class)).thenReturn(4); + + @SuppressWarnings("unchecked") + Message actual = (Message) this.resolver.resolveArgument(parameter, message); + + assertNotNull(actual); + assertSame(message.getHeaders(), actual.getHeaders()); + assertEquals(new Integer(4), actual.getPayload()); + } + + @Test + public void resolveWithConversionNoMatchingConverter() throws Exception { Message message = MessageBuilder.withPayload("test").build(); MethodParameter parameter = new MethodParameter(this.method, 1); assertTrue(this.resolver.supportsParameter(parameter)); - thrown.expect(MethodArgumentTypeMismatchException.class); + thrown.expect(MessageConversionException.class); thrown.expectMessage(Integer.class.getName()); thrown.expectMessage(String.class.getName()); this.resolver.resolveArgument(parameter, message); } @Test - public void resolveUpperBoundPayloadType() throws Exception { + public void resolveWithConversionEmptyPayload() throws Exception { + Message message = MessageBuilder.withPayload("").build(); + MethodParameter parameter = new MethodParameter(this.method, 1); + + assertTrue(this.resolver.supportsParameter(parameter)); + thrown.expect(MessageConversionException.class); + thrown.expectMessage("the payload is empty"); + thrown.expectMessage(Integer.class.getName()); + thrown.expectMessage(String.class.getName()); + this.resolver.resolveArgument(parameter, message); + } + + @Test + public void resolveWithPayloadTypeUpperBound() throws Exception { Message message = MessageBuilder.withPayload(123).build(); MethodParameter parameter = new MethodParameter(this.method, 3); @@ -104,19 +143,19 @@ public class MessageMethodArgumentResolverTests { } @Test - public void resolveOutOfBoundPayloadType() throws Exception { + public void resolveWithPayloadTypeOutOfBound() throws Exception { Message message = MessageBuilder.withPayload(Locale.getDefault()).build(); MethodParameter parameter = new MethodParameter(this.method, 3); assertTrue(this.resolver.supportsParameter(parameter)); - thrown.expect(MethodArgumentTypeMismatchException.class); + thrown.expect(MessageConversionException.class); thrown.expectMessage(Number.class.getName()); thrown.expectMessage(Locale.class.getName()); this.resolver.resolveArgument(parameter, message); } @Test - public void resolveMessageSubTypeExactMatch() throws Exception { + public void resolveMessageSubClassMatch() throws Exception { ErrorMessage message = new ErrorMessage(new UnsupportedOperationException()); MethodParameter parameter = new MethodParameter(this.method, 4); @@ -125,7 +164,7 @@ public class MessageMethodArgumentResolverTests { } @Test - public void resolveMessageSubTypeSubClass() throws Exception { + public void resolveWithMessageSubClassAndPayloadWildcard() throws Exception { ErrorMessage message = new ErrorMessage(new UnsupportedOperationException()); MethodParameter parameter = new MethodParameter(this.method, 0); @@ -134,7 +173,7 @@ public class MessageMethodArgumentResolverTests { } @Test - public void resolveWrongMessageType() throws Exception { + public void resolveWithWrongMessageType() throws Exception { UnsupportedOperationException ex = new UnsupportedOperationException(); Message message = new GenericMessage(ex); MethodParameter parameter = new MethodParameter(this.method, 4); @@ -148,7 +187,7 @@ public class MessageMethodArgumentResolverTests { @SuppressWarnings("unused") - private void handleMessage( + private void handle( Message wildcardPayload, Message integerPayload, Message numberPayload, diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/MethodMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/MethodMessageHandlerTests.java index 7d70c7d04b..6b471b310d 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/MethodMessageHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/MethodMessageHandlerTests.java @@ -34,6 +34,7 @@ import org.junit.Test; import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.MethodIntrospector; import org.springframework.messaging.Message; +import org.springframework.messaging.converter.SimpleMessageConverter; import org.springframework.messaging.handler.DestinationPatternsMessageCondition; import org.springframework.messaging.handler.HandlerMethod; import org.springframework.messaging.handler.annotation.support.MessageMethodArgumentResolver; @@ -196,7 +197,7 @@ public class MethodMessageHandlerTests { @Override protected List initArgumentResolvers() { List resolvers = new ArrayList(); - resolvers.add(new MessageMethodArgumentResolver()); + resolvers.add(new MessageMethodArgumentResolver(new SimpleMessageConverter())); resolvers.addAll(getCustomArgumentResolvers()); return resolvers; }