diff --git a/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolutionException.java b/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolutionException.java index 3f415d3426..bc34eaef11 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolutionException.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolutionException.java @@ -19,9 +19,10 @@ package org.springframework.messaging.core; import org.springframework.messaging.MessagingException; /** - * Thrown by a ChannelResolver when it cannot resolve a channel name. + * Thrown by a {@link DestinationResolver} when it cannot resolve a destination. * * @author Mark Fisher + * @author Rossen Stoyanchev * @since 4.0 */ @SuppressWarnings("serial") diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/Header.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/Header.java new file mode 100644 index 0000000000..31ef8cb613 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/Header.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2012 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.messaging.handler.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 which indicates that a method parameter should be bound to a message header. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Header { + + /** + * The name of the request header to bind to. + */ + String value() default ""; + + /** + * Whether the header is required. + *

+ * Default is {@code true}, leading to an exception if the header missing. Switch this + * to {@code false} if you prefer a {@code null} in case of the header missing. + */ + boolean required() default true; + + /** + * The default value to use as a fallback. Supplying a default value implicitly + * sets {@link #required} to {@code false}. + */ + String defaultValue() default ValueConstants.DEFAULT_NONE; + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageBody.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/Headers.java similarity index 68% rename from spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageBody.java rename to spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/Headers.java index e233be9ba6..460ef8b70d 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageBody.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/Headers.java @@ -22,12 +22,11 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.springframework.messaging.Message; - /** - * Annotation indicating a method parameter should be bound to the body of a - * {@link Message}. + * Annotation which indicates that a method parameter should be bound to the headers of a + * message. The annotated parameter must be assignable to {@link java.util.Map} with + * String keys and Object values. * * @author Rossen Stoyanchev * @since 4.0 @@ -35,14 +34,6 @@ import org.springframework.messaging.Message; @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Documented -public @interface MessageBody { - - /** - * Whether body content is required. - *

Default is {@code true}, leading to an exception thrown in case - * there is no body content. Switch this to {@code false} if you prefer - * {@code null} to be passed when the body content is {@code null}. - */ - boolean required() default true; +public @interface Headers { } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/Payload.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/Payload.java new file mode 100644 index 0000000000..dfc41afaab --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/Payload.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2012 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.messaging.handler.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; + +import org.springframework.messaging.support.converter.MessageConverter; + + +/** + * Annotation that binds a method parameter to the payload of a message. The payload may + * be passed through a {@link MessageConverter} to convert it from serialized form with a + * specific MIME type to an Object matching the target method parameter. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Payload { + + /** + * Whether payload content is required. + *

+ * Default is {@code true}, leading to an exception if there is no payload. Switch to + * {@code false} to have {@code null} passed when there is no payload. + */ + boolean required() default true; + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/ValueConstants.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/ValueConstants.java new file mode 100644 index 0000000000..962b97cb7b --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/ValueConstants.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2012 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.messaging.handler.annotation; + +/** + * Common annotation value constants. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public interface ValueConstants { + + /** + * Constant defining a value for no default - as a replacement for {@code null} which + * we cannot use in annotation attributes. + *

+ * This is an artificial arrangement of 16 unicode characters, with its sole purpose + * being to never match user-declared values. + * + * @see Header#defaultValue() + */ + String DEFAULT_NONE = "\n\t\t\n\t\t\n\uE000\uE001\uE002\n\t\t\t\t\n"; + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java new file mode 100644 index 0000000000..4f17df9276 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java @@ -0,0 +1,240 @@ +/* + * Copyright 2002-2013 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.messaging.handler.annotation.support; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.BeanExpressionResolver; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.annotation.ValueConstants; +import org.springframework.messaging.handler.method.HandlerMethodArgumentResolver; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + + +/** + * Abstract base class for resolving method arguments from a named value. Message headers, + * and path variables are examples of named values. Each may have a name, a required flag, + * and a default value. + *

+ * Subclasses define how to do the following: + *

+ *

+ * A default value string can contain ${...} placeholders and Spring Expression Language + * #{...} expressions. For this to work a {@link ConfigurableBeanFactory} must be supplied + * to the class constructor. + *

+ * A {@link ConversionService} may be used to apply type conversion to the resolved + * argument value if it doesn't match the method parameter type. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver { + + private final ConfigurableBeanFactory configurableBeanFactory; + + private final BeanExpressionContext expressionContext; + + private Map namedValueInfoCache = + new ConcurrentHashMap(256); + + private ConversionService conversionService; + + + /** + * Constructor with a {@link ConversionService} and a {@link BeanFactory}. + * + * @param cs conversion service for converting values to match the + * target method parameter type + * @param beanFactory a bean factory to use for resolving ${...} placeholder and + * #{...} SpEL expressions in default values, or {@code null} if default values + * are not expected to contain expressions + */ + protected AbstractNamedValueMethodArgumentResolver(ConversionService cs, ConfigurableBeanFactory beanFactory) { + this.conversionService = (cs != null) ? cs : new DefaultConversionService(); + this.configurableBeanFactory = beanFactory; + this.expressionContext = (beanFactory != null) ? new BeanExpressionContext(beanFactory, null) : null; + } + + + @Override + public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { + + Class paramType = parameter.getParameterType(); + NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); + + Object value = resolveArgumentInternal(parameter, message, namedValueInfo.name); + if (value == null) { + if (namedValueInfo.defaultValue != null) { + value = resolveDefaultValue(namedValueInfo.defaultValue); + } + else if (namedValueInfo.required) { + handleMissingValue(namedValueInfo.name, parameter, message); + } + value = handleNullValue(namedValueInfo.name, value, paramType); + } + else if ("".equals(value) && (namedValueInfo.defaultValue != null)) { + value = resolveDefaultValue(namedValueInfo.defaultValue); + } + + if (!ClassUtils.isAssignableValue(paramType, value)) { + value = this.conversionService.convert(value, + TypeDescriptor.valueOf(value.getClass()), new TypeDescriptor(parameter)); + } + + handleResolvedValue(value, namedValueInfo.name, parameter, message); + + return value; + } + + /** + * Obtain the named value for the given method parameter. + */ + private NamedValueInfo getNamedValueInfo(MethodParameter parameter) { + NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter); + if (namedValueInfo == null) { + namedValueInfo = createNamedValueInfo(parameter); + namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo); + this.namedValueInfoCache.put(parameter, namedValueInfo); + } + return namedValueInfo; + } + + /** + * Create the {@link NamedValueInfo} object for the given method parameter. Implementations typically + * retrieve the method annotation by means of {@link MethodParameter#getParameterAnnotation(Class)}. + * @param parameter the method parameter + * @return the named value information + */ + protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter); + + /** + * Create a new NamedValueInfo based on the given NamedValueInfo with sanitized values. + */ + private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) { + String name = info.name; + if (info.name.length() == 0) { + name = parameter.getParameterName(); + Assert.notNull(name, "Name for argument type [" + parameter.getParameterType().getName() + + "] not available, and parameter name information not found in class file either."); + } + String defaultValue = ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue; + return new NamedValueInfo(name, info.required, defaultValue); + } + + /** + * Resolves the given parameter type and value name into an argument value. + * @param parameter the method parameter to resolve to an argument value + * @param message the current request + * @param name the name of the value being resolved + * + * @return the resolved argument. May be {@code null} + * @throws Exception in case of errors + */ + protected abstract Object resolveArgumentInternal(MethodParameter parameter, Message message, String name) + throws Exception; + + /** + * Resolves the given default value into an argument value. + */ + private Object resolveDefaultValue(String defaultValue) { + if (this.configurableBeanFactory == null) { + return defaultValue; + } + String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(defaultValue); + BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver(); + if (exprResolver == null) { + return defaultValue; + } + return exprResolver.evaluate(placeholdersResolved, this.expressionContext); + } + + /** + * Invoked when a named value is required, but + * {@link #resolveArgumentInternal(MethodParameter, Message, String)} returned {@code null} and + * there is no default value. Subclasses typically throw an exception in this case. + * + * @param name the name for the value + * @param parameter the method parameter + * @param message the message being processed + */ + protected abstract void handleMissingValue(String name, MethodParameter parameter, Message message); + + /** + * A {@code null} results in a {@code false} value for {@code boolean}s or an + * exception for other primitives. + */ + private Object handleNullValue(String name, Object value, Class paramType) { + if (value == null) { + if (Boolean.TYPE.equals(paramType)) { + return Boolean.FALSE; + } + else if (paramType.isPrimitive()) { + throw new IllegalStateException("Optional " + paramType + " parameter '" + name + + "' is present but cannot be translated into a null value due to being " + + "declared as a primitive type. Consider declaring it as object wrapper " + + "for the corresponding primitive type."); + } + } + return value; + } + + /** + * Invoked after a value is resolved. + * + * @param arg the resolved argument value + * @param name the argument name + * @param parameter the argument parameter type + * @param message the message + */ + protected void handleResolvedValue(Object arg, String name, MethodParameter parameter, Message message) { + } + + + /** + * Represents the information about a named value, including name, whether it's + * required and a default value. + */ + protected static class NamedValueInfo { + + private final String name; + + private final boolean required; + + private final String defaultValue; + + protected NamedValueInfo(String name, boolean required, String defaultValue) { + this.name = name; + this.required = required; + this.defaultValue = defaultValue; + } + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolver.java new file mode 100644 index 0000000000..c3b7a7153f --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolver.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2013 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.messaging.handler.annotation.support; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.annotation.Header; + + +/** + * Resolves method parameters annotated with {@link Header @Header}. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class HeaderMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + + public HeaderMethodArgumentResolver(ConversionService cs, ConfigurableBeanFactory beanFactory) { + super(cs, beanFactory); + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(Header.class); + } + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + Header annotation = parameter.getParameterAnnotation(Header.class); + return new HeaderNamedValueInfo(annotation); + } + + @Override + protected Object resolveArgumentInternal(MethodParameter parameter, Message message, + String name) throws Exception { + + return message.getHeaders().get(name); + } + + @Override + protected void handleMissingValue(String headerName, MethodParameter parameter, Message message) { + throw new MessageHandlingException(message, "Missing header '" + headerName + + "' for method parameter type [" + parameter.getParameterType() + "]"); + } + + + private static class HeaderNamedValueInfo extends NamedValueInfo { + + private HeaderNamedValueInfo(Header annotation) { + super(annotation.value(), annotation.required(), annotation.defaultValue()); + } + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/HeadersMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/HeadersMethodArgumentResolver.java new file mode 100644 index 0000000000..3be9f9a48b --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/HeadersMethodArgumentResolver.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2013 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.messaging.handler.annotation.support; + +import java.lang.reflect.Method; +import java.util.Map; + +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.messaging.handler.annotation.Headers; +import org.springframework.messaging.handler.method.HandlerMethodArgumentResolver; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + + +/** + * Resolves the following method parameters: + *

+ * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class HeadersMethodArgumentResolver implements HandlerMethodArgumentResolver { + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + Class paramType = parameter.getParameterType(); + return ((parameter.hasParameterAnnotation(Headers.class) && Map.class.isAssignableFrom(paramType)) || + MessageHeaders.class.equals(paramType) || + MessageHeaderAccessor.class.isAssignableFrom(paramType)); + } + + @Override + public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { + + Class paramType = parameter.getParameterType(); + + if (Map.class.isAssignableFrom(paramType)) { + return message.getHeaders(); + } + else if (MessageHeaderAccessor.class.equals(paramType)) { + return new MessageHeaderAccessor(message); + } + else if (MessageHeaderAccessor.class.isAssignableFrom(paramType)) { + Method factoryMethod = ClassUtils.getMethod(paramType, "wrap", Message.class); + return ReflectionUtils.invokeMethod(factoryMethod, null, message); + } + else { + throw new IllegalStateException("Unexpected method parameter type " + + paramType + "in method " + parameter.getMethod() + ". " + + "@Headers method arguments must be assignable to java.util.Map."); + } + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageBodyMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageBodyMethodArgumentResolver.java deleted file mode 100644 index ceed5b7028..0000000000 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageBodyMethodArgumentResolver.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2002-2013 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.messaging.handler.annotation.support; - -import org.springframework.core.MethodParameter; -import org.springframework.messaging.Message; -import org.springframework.messaging.handler.annotation.MessageBody; -import org.springframework.messaging.handler.method.HandlerMethodArgumentResolver; -import org.springframework.messaging.support.converter.MessageConverter; -import org.springframework.util.Assert; - - -/** - * A resolver for extracting the body of a message. - * - *

This {@link HandlerMethodArgumentResolver} should be ordered last as it supports all - * types and does not require the {@link MessageBody} annotation. - * - * @author Rossen Stoyanchev - * @since 4.0 - */ -public class MessageBodyMethodArgumentResolver implements HandlerMethodArgumentResolver { - - private final MessageConverter converter; - - - public MessageBodyMethodArgumentResolver(MessageConverter converter) { - Assert.notNull(converter, "converter is required"); - this.converter = converter; - } - - @Override - public boolean supportsParameter(MethodParameter parameter) { - return true; - } - - @Override - public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { - - Object arg = null; - - MessageBody annot = parameter.getParameterAnnotation(MessageBody.class); - - if (annot == null || annot.required()) { - Class sourceClass = message.getPayload().getClass(); - Class targetClass = parameter.getParameterType(); - if (targetClass.isAssignableFrom(sourceClass)) { - return message.getPayload(); - } - else { - return this.converter.fromMessage(message, targetClass); - } - } - - return arg; - } - -} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageHandlingException.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageHandlingException.java new file mode 100644 index 0000000000..b3de5cb9ad --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageHandlingException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2013 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.messaging.handler.annotation.support; + +import org.springframework.messaging.Message; +import org.springframework.messaging.MessagingException; + + +/** + * Thrown when the handling of a message results in an unrecoverable exception. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class MessageHandlingException extends MessagingException { + + + public MessageHandlingException(Message message, String description, Throwable cause) { + super(message, description, cause); + } + + public MessageHandlingException(Message message, String description) { + super(message, description); + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolver.java new file mode 100644 index 0000000000..ebff786da9 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolver.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2013 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.messaging.handler.annotation.support; + +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.handler.method.HandlerMethodArgumentResolver; +import org.springframework.messaging.support.converter.MessageConverter; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + + +/** + * A resolver to extract and convert the payload of a message using a + * {@link MessageConverter}. + * + *

+ * This {@link HandlerMethodArgumentResolver} should be ordered last as it supports all + * types and does not require the {@link Payload} annotation. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class PayloadArgumentResolver implements HandlerMethodArgumentResolver { + + private final MessageConverter converter; + + + public PayloadArgumentResolver(MessageConverter messageConverter) { + Assert.notNull(messageConverter, "converter is required"); + this.converter = messageConverter; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return true; + } + + @Override + public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { + + Class sourceClass = message.getPayload().getClass(); + Class targetClass = parameter.getParameterType(); + + if (ClassUtils.isAssignable(targetClass,sourceClass)) { + return message.getPayload(); + } + + Payload annot = parameter.getParameterAnnotation(Payload.class); + + if (isEmptyPayload(message)) { + if ((annot != null) && !annot.required()) { + return null; + } + } + + return this.converter.fromMessage(message, targetClass); + } + + protected boolean isEmptyPayload(Message message) { + Object payload = message.getPayload(); + if (payload instanceof byte[]) { + return ((byte[]) message.getPayload()).length == 0; + } + else if (payload instanceof String) { + return ((String) payload).trim().equals(""); + } + else { + return false; + } + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandler.java index 0bc6f0d254..df54a07557 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandler.java @@ -31,10 +31,14 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.convert.ConversionService; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; @@ -42,8 +46,10 @@ import org.springframework.messaging.MessagingException; import org.springframework.messaging.core.AbstractMessageSendingTemplate; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.support.ExceptionHandlerMethodResolver; -import org.springframework.messaging.handler.annotation.support.MessageBodyMethodArgumentResolver; +import org.springframework.messaging.handler.annotation.support.HeaderMethodArgumentResolver; +import org.springframework.messaging.handler.annotation.support.HeadersMethodArgumentResolver; import org.springframework.messaging.handler.annotation.support.MessageMethodArgumentResolver; +import org.springframework.messaging.handler.annotation.support.PayloadArgumentResolver; import org.springframework.messaging.handler.method.HandlerMethod; import org.springframework.messaging.handler.method.HandlerMethodArgumentResolver; import org.springframework.messaging.handler.method.HandlerMethodArgumentResolverComposite; @@ -61,7 +67,10 @@ import org.springframework.messaging.simp.annotation.support.PrincipalMethodArgu import org.springframework.messaging.simp.annotation.support.SendToMethodReturnValueHandler; import org.springframework.messaging.simp.annotation.support.SubscriptionMethodReturnValueHandler; import org.springframework.messaging.support.MessageBuilder; +import org.springframework.messaging.support.converter.ByteArrayMessageConverter; +import org.springframework.messaging.support.converter.CompositeMessageConverter; import org.springframework.messaging.support.converter.MessageConverter; +import org.springframework.messaging.support.converter.StringMessageConverter; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -85,6 +94,8 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati private MessageConverter messageConverter; + private ConversionService conversionService = new DefaultFormattingConversionService(); + private ApplicationContext applicationContext; private Map messageMethods = new HashMap(); @@ -116,6 +127,11 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati Assert.notNull(webSocketResponseChannel, "webSocketReplyChannel is required"); this.brokerTemplate = brokerTemplate; this.webSocketResponseTemplate = new SimpMessagingTemplate(webSocketResponseChannel); + + Collection converters = new ArrayList(); + converters.add(new StringMessageConverter()); + converters.add(new ByteArrayMessageConverter()); + this.messageConverter = new CompositeMessageConverter(converters); } /** @@ -147,6 +163,14 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati return this.destinationPrefixes; } + /** + * Configure a {@link MessageConverter} to use to convert the payload of a message + * from serialize form with a specific MIME type to an Object matching the target + * method parameter. The converter is also used when sending message to the message + * broker. + * + * @see CompositeMessageConverter + */ public void setMessageConverter(MessageConverter converter) { this.messageConverter = converter; if (converter != null) { @@ -154,6 +178,30 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati } } + /** + * Return the configured {@link MessageConverter}. + */ + public MessageConverter getMessageConverter() { + return this.messageConverter; + } + + /** + * Configure a {@link ConversionService} to use when resolving method arguments, for + * example message header values. + *

+ * By default an instance of {@link DefaultFormattingConversionService} is used. + */ + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + /** + * The configured {@link ConversionService}. + */ + public ConversionService getConversionService() { + return this.conversionService; + } + /** * Sets the list of custom {@code HandlerMethodArgumentResolver}s that will be used * after resolvers for supported argument type. @@ -176,20 +224,25 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati this.customReturnValueHandlers = customReturnValueHandlers; } - public MessageConverter getMessageConverter() { - return this.messageConverter; - } - @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } + @Override public void afterPropertiesSet() { initHandlerMethods(); + ConfigurableBeanFactory beanFactory = + (ClassUtils.isAssignableValue(ConfigurableApplicationContext.class, this.applicationContext)) ? + ((ConfigurableApplicationContext) this.applicationContext).getBeanFactory() : null; + + // Annotation-based argument resolution + this.argumentResolvers.addResolver(new HeaderMethodArgumentResolver(this.conversionService, beanFactory)); + this.argumentResolvers.addResolver(new HeadersMethodArgumentResolver()); + // Type-based argument resolution this.argumentResolvers.addResolver(new PrincipalMethodArgumentResolver()); this.argumentResolvers.addResolver(new MessageMethodArgumentResolver()); @@ -198,7 +251,7 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati this.argumentResolvers.addResolvers(this.customArgumentResolvers); // catch-all argument resolver - this.argumentResolvers.addResolver(new MessageBodyMethodArgumentResolver(this.messageConverter)); + this.argumentResolvers.addResolver(new PayloadArgumentResolver(this.messageConverter)); // Annotation-based return value types this.returnValueHandlers.addHandler(new SendToMethodReturnValueHandler(this.brokerTemplate, true)); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java index 54ed13c3e8..db4f8de95a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java @@ -49,19 +49,18 @@ public class NativeMessageHeaderAccessor extends MessageHeaderAccessor { /** * A constructor for creating new headers, accepting an optional native header map. */ - public NativeMessageHeaderAccessor(Map> nativeHeaders) { + protected NativeMessageHeaderAccessor(Map> nativeHeaders) { this.originalNativeHeaders = nativeHeaders; } /** * A constructor for accessing and modifying existing message headers. */ - public NativeMessageHeaderAccessor(Message message) { + protected NativeMessageHeaderAccessor(Message message) { super(message); this.originalNativeHeaders = initNativeHeaders(message); } - private static Map> initNativeHeaders(Message message) { if (message != null) { @SuppressWarnings("unchecked") @@ -73,6 +72,13 @@ public class NativeMessageHeaderAccessor extends MessageHeaderAccessor { return null; } + /** + * Create {@link NativeMessageHeaderAccessor} from the headers of an existing message. + */ + public static NativeMessageHeaderAccessor wrap(Message message) { + return new NativeMessageHeaderAccessor(message); + } + @Override public Map toMap() { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java new file mode 100644 index 0000000000..b54cbf7580 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2013 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.messaging.handler.annotation.support; + +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.messaging.support.MessageBuilder; + +import static org.junit.Assert.*; + + +/** + * Test fixture for {@link HeaderMethodArgumentResolver} tests. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class HeaderMethodArgumentResolverTests { + + private HeaderMethodArgumentResolver resolver; + + private MethodParameter paramRequired; + private MethodParameter paramNamedDefaultValueStringHeader; + private MethodParameter paramSystemProperty; + private MethodParameter paramNotAnnotated; + + + @Before + public void setup() throws Exception { + @SuppressWarnings("resource") + GenericApplicationContext cxt = new GenericApplicationContext(); + cxt.refresh(); + this.resolver = new HeaderMethodArgumentResolver(new DefaultConversionService(), cxt.getBeanFactory()); + + Method method = getClass().getDeclaredMethod("handleMessage", + String.class, String.class, String.class, String.class); + this.paramRequired = new MethodParameter(method, 0); + this.paramNamedDefaultValueStringHeader = new MethodParameter(method, 1); + this.paramSystemProperty = new MethodParameter(method, 2); + this.paramNotAnnotated = new MethodParameter(method, 3); + + this.paramRequired.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); + GenericTypeResolver.resolveParameterType(this.paramRequired, HeaderMethodArgumentResolver.class); + } + + @Test + public void supportsParameter() { + assertTrue(resolver.supportsParameter(paramNamedDefaultValueStringHeader)); + assertFalse(resolver.supportsParameter(paramNotAnnotated)); + } + + @Test + public void resolveArgument() throws Exception { + Message message = MessageBuilder.withPayload(new byte[0]).setHeader("param1", "foo").build(); + this.resolver.resolveArgument(this.paramRequired, message); + } + + @Test(expected = MessageHandlingException.class) + public void resolveArgumentNotFound() throws Exception { + Message message = MessageBuilder.withPayload(new byte[0]).build(); + this.resolver.resolveArgument(this.paramRequired, message); + } + + @Test + public void resolveArgumentDefaultValue() throws Exception { + Message message = MessageBuilder.withPayload(new byte[0]).build(); + Object result = this.resolver.resolveArgument(this.paramNamedDefaultValueStringHeader, message); + + assertEquals("bar", result); + } + + @Test + public void resolveDefaultValueSystemProperty() throws Exception { + System.setProperty("systemProperty", "sysbar"); + try { + Message message = MessageBuilder.withPayload(new byte[0]).build(); + Object result = resolver.resolveArgument(paramSystemProperty, message); + assertEquals("sysbar", result); + } + finally { + System.clearProperty("systemProperty"); + } + } + + + @SuppressWarnings("unused") + private void handleMessage( + @Header String param1, + @Header(value = "name", defaultValue = "bar") String param2, + @Header(value = "name", defaultValue="#{systemProperties.systemProperty}") String param3, + String param4) { + } + +} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeadersMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeadersMethodArgumentResolverTests.java new file mode 100644 index 0000000000..0f6b32ae04 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeadersMethodArgumentResolverTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2013 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.messaging.handler.annotation.support; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.handler.annotation.Headers; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.messaging.support.NativeMessageHeaderAccessor; + +import static org.junit.Assert.*; + + +/** + * Test fixture for {@link HeadersMethodArgumentResolver} tests. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class HeadersMethodArgumentResolverTests { + + private HeadersMethodArgumentResolver resolver; + + private MethodParameter paramAnnotated; + private MethodParameter paramAnnotatedNotMap; + private MethodParameter paramMessageHeaders; + private MethodParameter paramMessageHeaderAccessor; + private MethodParameter paramMessageHeaderAccessorSubclass; + + private Message message; + + + @Before + public void setup() throws Exception { + this.resolver = new HeadersMethodArgumentResolver(); + + Method method = getClass().getDeclaredMethod("handleMessage", Map.class, String.class, + MessageHeaders.class, MessageHeaderAccessor.class, TestMessageHeaderAccessor.class); + + this.paramAnnotated = new MethodParameter(method, 0); + this.paramAnnotatedNotMap = new MethodParameter(method, 1); + this.paramMessageHeaders = new MethodParameter(method, 2); + this.paramMessageHeaderAccessor = new MethodParameter(method, 3); + this.paramMessageHeaderAccessorSubclass = new MethodParameter(method, 4); + + Map headers = new HashMap(); + headers.put("foo", "bar"); + this.message = MessageBuilder.withPayload(new byte[0]).copyHeaders(headers).build(); + } + + @Test + public void supportsParameter() { + assertTrue(this.resolver.supportsParameter(this.paramAnnotated)); + assertFalse(this.resolver.supportsParameter(this.paramAnnotatedNotMap)); + assertTrue(this.resolver.supportsParameter(this.paramMessageHeaders)); + assertTrue(this.resolver.supportsParameter(this.paramMessageHeaderAccessor)); + assertTrue(this.resolver.supportsParameter(this.paramMessageHeaderAccessorSubclass)); + } + + @Test + public void resolveArgumentAnnotated() throws Exception { + Object resolved = this.resolver.resolveArgument(this.paramAnnotated, this.message); + + assertTrue(resolved instanceof Map); + @SuppressWarnings("unchecked") + Map headers = (Map) resolved; + assertEquals("bar", headers.get("foo")); + } + + @Test(expected=IllegalStateException.class) + public void resolveArgumentAnnotatedNotMap() throws Exception { + this.resolver.resolveArgument(this.paramAnnotatedNotMap, this.message); + } + + @Test + public void resolveArgumentMessageHeaders() throws Exception { + Object resolved = this.resolver.resolveArgument(this.paramMessageHeaders, this.message); + + assertTrue(resolved instanceof MessageHeaders); + MessageHeaders headers = (MessageHeaders) resolved; + assertEquals("bar", headers.get("foo")); + } + + @Test + public void resolveArgumentMessageHeaderAccessor() throws Exception { + Object resolved = this.resolver.resolveArgument(this.paramMessageHeaderAccessor, this.message); + + assertTrue(resolved instanceof MessageHeaderAccessor); + MessageHeaderAccessor headers = (MessageHeaderAccessor) resolved; + assertEquals("bar", headers.getHeader("foo")); + } + + @Test + public void resolveArgumentMessageHeaderAccessorSubclass() throws Exception { + Object resolved = this.resolver.resolveArgument(this.paramMessageHeaderAccessorSubclass, this.message); + + assertTrue(resolved instanceof TestMessageHeaderAccessor); + TestMessageHeaderAccessor headers = (TestMessageHeaderAccessor) resolved; + assertEquals("bar", headers.getHeader("foo")); + } + + + @SuppressWarnings("unused") + private void handleMessage( + @Headers Map param1, + @Headers String param2, + MessageHeaders param3, + MessageHeaderAccessor param4, + TestMessageHeaderAccessor param5) { + } + + + public static class TestMessageHeaderAccessor extends NativeMessageHeaderAccessor { + + protected TestMessageHeaderAccessor(Message message) { + super(message); + } + + public static TestMessageHeaderAccessor wrap(Message message) { + return new TestMessageHeaderAccessor(message); + } + } + +} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolverTests.java new file mode 100644 index 0000000000..f14b6708b2 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolverTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2013 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.messaging.handler.annotation.support; + +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.messaging.support.converter.MessageConverter; +import org.springframework.messaging.support.converter.StringMessageConverter; + +import static org.junit.Assert.*; + + +/** + * Test fixture for {@link PayloadArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class PayloadArgumentResolverTests { + + private PayloadArgumentResolver resolver; + + private MethodParameter param; + private MethodParameter paramNotRequired; + + + @Before + public void setup() throws Exception { + + MessageConverter messageConverter = new StringMessageConverter(); + this.resolver = new PayloadArgumentResolver(messageConverter ); + + Method method = PayloadArgumentResolverTests.class.getDeclaredMethod("handleMessage", + String.class, String.class); + + this.param = new MethodParameter(method , 0); + this.paramNotRequired = new MethodParameter(method , 1); + } + + + @Test + public void resolveRequired() throws Exception { + Message message = MessageBuilder.withPayload("ABC".getBytes()).build(); + Object actual = this.resolver.resolveArgument(this.param, message); + + assertEquals("ABC", actual); + } + + @Test + public void resolveNotRequired() throws Exception { + + Message emptyByteArrayMessage = MessageBuilder.withPayload(new byte[0]).build(); + assertNull(this.resolver.resolveArgument(this.paramNotRequired, emptyByteArrayMessage)); + + Message notEmptyMessage = MessageBuilder.withPayload("ABC".getBytes()).build(); + assertEquals("ABC", this.resolver.resolveArgument(this.paramNotRequired, notEmptyMessage)); + } + + + @SuppressWarnings("unused") + private void handleMessage( + @Payload String param, + @Payload(required=false) String paramNotRequired) { + } + +} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandlerTests.java index a875448cbc..ab31216a2b 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandlerTests.java @@ -16,15 +16,26 @@ package org.springframework.messaging.simp.handler; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.springframework.context.support.StaticApplicationContext; +import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.messaging.handler.annotation.Headers; import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessageSendingOperations; import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.messaging.support.MessageBuilder; import org.springframework.stereotype.Controller; +import static org.junit.Assert.*; + /** * Test fixture for {@link AnnotationMethodMessageHandler}. @@ -32,24 +43,76 @@ import org.springframework.stereotype.Controller; */ public class AnnotationMethodMessageHandlerTests { + private TestAnnotationMethodMessageHandler messageHandler; + + private TestController testController; + + + @Before + public void setup() { + MessageChannel channel = Mockito.mock(MessageChannel.class); + SimpMessageSendingOperations brokerTemplate = new SimpMessagingTemplate(channel); + this.messageHandler = new TestAnnotationMethodMessageHandler(brokerTemplate, channel); + this.messageHandler.setApplicationContext(new StaticApplicationContext()); + this.messageHandler.afterPropertiesSet(); + + testController = new TestController(); + this.messageHandler.registerHandler(testController); + } + + + @SuppressWarnings("unchecked") + @Test + public void headerArgumentResolution() { + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(); + headers.setDestination("/headers"); + headers.setHeader("foo", "bar"); + Message message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build(); + this.messageHandler.handleMessage(message); + + assertEquals("headers", this.testController.method); + assertEquals("bar", this.testController.arguments.get("foo")); + assertEquals("bar", ((Map) this.testController.arguments.get("headers")).get("foo")); + } @Test(expected=IllegalStateException.class) public void duplicateMappings() { + this.messageHandler.registerHandler(new DuplicateMappingController()); + } - StaticApplicationContext cxt = new StaticApplicationContext(); - cxt.registerSingleton("d", DuplicateMappingController.class); - cxt.refresh(); - MessageChannel channel = Mockito.mock(MessageChannel.class); - SimpMessageSendingOperations brokerTemplate = new SimpMessagingTemplate(channel); - AnnotationMethodMessageHandler mh = new AnnotationMethodMessageHandler(brokerTemplate, channel); - mh.setApplicationContext(cxt); - mh.afterPropertiesSet(); + private static class TestAnnotationMethodMessageHandler extends AnnotationMethodMessageHandler { + + public TestAnnotationMethodMessageHandler(SimpMessageSendingOperations brokerTemplate, + MessageChannel webSocketResponseChannel) { + + super(brokerTemplate, webSocketResponseChannel); + } + + public void registerHandler(Object handler) { + super.detectHandlerMethods(handler); + } } @Controller - static class DuplicateMappingController { + private static class TestController { + + private String method; + + private Map arguments = new LinkedHashMap(); + + + @MessageMapping("/headers") + public void headers(@Header String foo, @Headers Map headers) { + this.method = "headers"; + this.arguments.put("foo", foo); + this.arguments.put("headers", headers); + } + } + + @Controller + private static class DuplicateMappingController { @MessageMapping(value="/duplicate") public void handle1() { } diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java index fa511ad3f0..dfd4fe7252 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java @@ -16,12 +16,6 @@ package org.springframework.web.method.annotation; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - import java.lang.reflect.Method; import java.util.Map; @@ -37,7 +31,8 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.support.GenericWebApplicationContext; -import org.springframework.web.method.annotation.RequestHeaderMethodArgumentResolver; + +import static org.junit.Assert.*; /** * Test fixture with {@link org.springframework.web.method.annotation.RequestHeaderMethodArgumentResolver}. @@ -143,7 +138,6 @@ public class RequestHeaderMethodArgumentResolverTests { @Test(expected = ServletRequestBindingException.class) public void notFound() throws Exception { resolver.resolveArgument(paramNamedValueStringArray, null, webRequest, null); - fail("Expected exception"); } public void params(@RequestHeader(value = "name", defaultValue = "bar") String param1,