diff --git a/build.gradle b/build.gradle index f439a6dd12e..b5060c10fc4 100644 --- a/build.gradle +++ b/build.gradle @@ -320,7 +320,6 @@ project("spring-messaging") { compile(project(":spring-beans")) compile(project(":spring-core")) compile(project(":spring-context")) - optional(project(":spring-web")) // TODO: MediaType/HandlerMethod/EHMR optional(project(":spring-websocket")) optional("com.fasterxml.jackson.core:jackson-databind:2.2.0") optional("org.projectreactor:reactor-core:1.0.0.BUILD-SNAPSHOT") diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/ReplyTo.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/ReplyTo.java index 38fc1542262..ee620b27d02 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/ReplyTo.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/ReplyTo.java @@ -32,10 +32,9 @@ import java.lang.annotation.Target; @Documented public @interface ReplyTo { - /** - * The destination value for the reply. + * The destination for a message created from the return value of a method. */ - String value(); + String[] value() default {}; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/ExceptionHandlerMethodResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/ExceptionHandlerMethodResolver.java new file mode 100644 index 00000000000..2c98ea07505 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/ExceptionHandlerMethodResolver.java @@ -0,0 +1,157 @@ +/* + * 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.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.core.ExceptionDepthComparator; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.messaging.handler.annotation.MessageExceptionHandler; +import org.springframework.messaging.handler.method.HandlerMethodSelector; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils.MethodFilter; + + +/** + * Discovers annotated exception handling methods in a given class type, including all + * super types, and helps to resolve an Exception to a method that can handle it. The + * exception types supported by a given method can also be discovered from the method + * signature. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class ExceptionHandlerMethodResolver { + + private static final Method NO_METHOD_FOUND = ClassUtils.getMethodIfAvailable(System.class, "currentTimeMillis"); + + private final Map, Method> mappedMethods = + new ConcurrentHashMap, Method>(16); + + private final Map, Method> exceptionLookupCache = + new ConcurrentHashMap, Method>(16); + + + /** + * A constructor that finds {@link MessageExceptionHandler} methods in the given type. + * @param handlerType the type to introspect + */ + public ExceptionHandlerMethodResolver(Class handlerType) { + for (Method method : HandlerMethodSelector.selectMethods(handlerType, EXCEPTION_HANDLER_METHOD_FILTER)) { + for (Class exceptionType : detectExceptionMappings(method)) { + addExceptionMapping(exceptionType, method); + } + } + } + + /** + * Extract exception mappings from the {@code @ExceptionHandler} annotation + * first and as a fall-back from the method signature. + */ + @SuppressWarnings("unchecked") + private List> detectExceptionMappings(Method method) { + List> result = new ArrayList>(); + + detectAnnotationExceptionMappings(method, result); + + if (result.isEmpty()) { + for (Class paramType : method.getParameterTypes()) { + if (Throwable.class.isAssignableFrom(paramType)) { + result.add((Class) paramType); + } + } + } + + Assert.notEmpty(result, "No exception types mapped to {" + method + "}"); + + return result; + } + + protected void detectAnnotationExceptionMappings(Method method, List> result) { + MessageExceptionHandler annot = AnnotationUtils.findAnnotation(method, MessageExceptionHandler.class); + result.addAll(Arrays.asList(annot.value())); + } + + private void addExceptionMapping(Class exceptionType, Method method) { + Method oldMethod = this.mappedMethods.put(exceptionType, method); + if (oldMethod != null && !oldMethod.equals(method)) { + throw new IllegalStateException( + "Ambiguous @ExceptionHandler method mapped for [" + exceptionType + "]: {" + + oldMethod + ", " + method + "}."); + } + } + + /** + * Whether the contained type has any exception mappings. + */ + public boolean hasExceptionMappings() { + return (this.mappedMethods.size() > 0); + } + + /** + * Find a method to handle the given exception. + * Use {@link ExceptionDepthComparator} if more than one match is found. + * @param exception the exception + * @return a method to handle the exception or {@code null} + */ + public Method resolveMethod(Exception exception) { + Class exceptionType = exception.getClass(); + Method method = this.exceptionLookupCache.get(exceptionType); + if (method == null) { + method = getMappedMethod(exceptionType); + this.exceptionLookupCache.put(exceptionType, method != null ? method : NO_METHOD_FOUND); + } + return method != NO_METHOD_FOUND ? method : null; + } + + /** + * Return the method mapped to the given exception type or {@code null}. + */ + private Method getMappedMethod(Class exceptionType) { + List> matches = new ArrayList>(); + for(Class mappedException : this.mappedMethods.keySet()) { + if (mappedException.isAssignableFrom(exceptionType)) { + matches.add(mappedException); + } + } + if (!matches.isEmpty()) { + Collections.sort(matches, new ExceptionDepthComparator(exceptionType)); + return mappedMethods.get(matches.get(0)); + } + else { + return null; + } + } + + + /** A filter for selecting annotated exception handling methods. */ + public final static MethodFilter EXCEPTION_HANDLER_METHOD_FILTER = new MethodFilter() { + + @Override + public boolean matches(Method method) { + return AnnotationUtils.findAnnotation(method, MessageExceptionHandler.class) != null; + } + }; + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageBodyArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageBodyMethodArgumentResolver.java similarity index 81% rename from spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageBodyArgumentResolver.java rename to spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageBodyMethodArgumentResolver.java index c83fed6e0cb..2cae5cd560c 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageBodyArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageBodyMethodArgumentResolver.java @@ -19,21 +19,26 @@ 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.MessageArgumentResolver; +import org.springframework.messaging.handler.method.HandlerMethodArgumentResolver; import org.springframework.messaging.support.converter.MessageConverter; import org.springframework.util.Assert; /** + * TODO + * + *

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 MessageBodyArgumentResolver implements MessageArgumentResolver { +public class MessageBodyMethodArgumentResolver implements HandlerMethodArgumentResolver { private final MessageConverter converter; - public MessageBodyArgumentResolver(MessageConverter converter) { + public MessageBodyMethodArgumentResolver(MessageConverter converter) { Assert.notNull(converter, "converter is required"); this.converter = converter; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageExceptionHandlerMethodResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageExceptionHandlerMethodResolver.java deleted file mode 100644 index 69aaa785226..00000000000 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageExceptionHandlerMethodResolver.java +++ /dev/null @@ -1,63 +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 java.lang.reflect.Method; -import java.util.Arrays; -import java.util.List; - -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.messaging.handler.annotation.MessageExceptionHandler; -import org.springframework.util.ReflectionUtils.MethodFilter; -import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; - - -/** - * @author Rossen Stoyanchev - * @since 4.0 - */ -public class MessageExceptionHandlerMethodResolver extends ExceptionHandlerMethodResolver { - - - public MessageExceptionHandlerMethodResolver(Class handlerType) { - super(handlerType); - } - - - @Override - protected MethodFilter getExceptionHandlerMethods() { - return MESSAGE_EXCEPTION_HANDLER_METHODS; - } - - @Override - protected void detectAnnotationExceptionMappings(Method method, List> result) { - MessageExceptionHandler annotation = AnnotationUtils.findAnnotation(method, MessageExceptionHandler.class); - result.addAll(Arrays.asList(annotation.value())); - } - - - /** - * A filter for selecting {@code @ExceptionHandler} methods. - */ - public final static MethodFilter MESSAGE_EXCEPTION_HANDLER_METHODS = new MethodFilter() { - - @Override - public boolean matches(Method method) { - return AnnotationUtils.findAnnotation(method, MessageExceptionHandler.class) != null; - } - }; -} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethod.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethod.java new file mode 100644 index 00000000000..fa35b563833 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethod.java @@ -0,0 +1,285 @@ +/* + * 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.method; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Encapsulates information about a bean method consisting of a + * {@linkplain #getMethod() method} and a {@linkplain #getBean() bean}. Provides + * convenient access to method parameters, the method return value, method + * annotations. + * + *

The class may be created with a bean instance or with a bean name (e.g. lazy + * bean, prototype bean). Use {@link #createWithResolvedBean()} to obtain an + * {@link HandlerMethod} instance with a bean instance initialized through the + * bean factory. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class HandlerMethod { + + /** Logger that is available to subclasses */ + protected final Log logger = LogFactory.getLog(HandlerMethod.class); + + private final Object bean; + + private final Method method; + + private final BeanFactory beanFactory; + + private final MethodParameter[] parameters; + + private final Method bridgedMethod; + + + /** + * Create an instance from a bean instance and a method. + */ + public HandlerMethod(Object bean, Method method) { + Assert.notNull(bean, "bean is required"); + Assert.notNull(method, "method is required"); + this.bean = bean; + this.beanFactory = null; + this.method = method; + this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); + this.parameters = initMethodParameters(); + } + + private MethodParameter[] initMethodParameters() { + int count = this.bridgedMethod.getParameterTypes().length; + MethodParameter[] result = new MethodParameter[count]; + for (int i = 0; i < count; i++) { + result[i] = new HandlerMethodParameter(i); + } + return result; + } + + /** + * Create an instance from a bean instance, method name, and parameter types. + * @throws NoSuchMethodException when the method cannot be found + */ + public HandlerMethod(Object bean, String methodName, Class... parameterTypes) throws NoSuchMethodException { + Assert.notNull(bean, "bean is required"); + Assert.notNull(methodName, "method is required"); + this.bean = bean; + this.beanFactory = null; + this.method = bean.getClass().getMethod(methodName, parameterTypes); + this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); + this.parameters = initMethodParameters(); + } + + /** + * Create an instance from a bean name, a method, and a {@code BeanFactory}. + * The method {@link #createWithResolvedBean()} may be used later to + * re-create the {@code HandlerMethod} with an initialized the bean. + */ + public HandlerMethod(String beanName, BeanFactory beanFactory, Method method) { + Assert.hasText(beanName, "beanName is required"); + Assert.notNull(beanFactory, "beanFactory is required"); + Assert.notNull(method, "method is required"); + Assert.isTrue(beanFactory.containsBean(beanName), + "Bean factory [" + beanFactory + "] does not contain bean [" + beanName + "]"); + this.bean = beanName; + this.beanFactory = beanFactory; + this.method = method; + this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); + this.parameters = initMethodParameters(); + } + + /** + * Copy constructor for use in sub-classes. + */ + protected HandlerMethod(HandlerMethod handlerMethod) { + Assert.notNull(handlerMethod, "HandlerMethod is required"); + this.bean = handlerMethod.bean; + this.beanFactory = handlerMethod.beanFactory; + this.method = handlerMethod.method; + this.bridgedMethod = handlerMethod.bridgedMethod; + this.parameters = handlerMethod.parameters; + } + + /** + * Re-create HandlerMethod with the resolved handler. + */ + private HandlerMethod(HandlerMethod handlerMethod, Object handler) { + Assert.notNull(handlerMethod, "handlerMethod is required"); + Assert.notNull(handler, "handler is required"); + this.bean = handler; + this.beanFactory = handlerMethod.beanFactory; + this.method = handlerMethod.method; + this.bridgedMethod = handlerMethod.bridgedMethod; + this.parameters = handlerMethod.parameters; + } + + /** + * Returns the bean for this handler method. + */ + public Object getBean() { + return this.bean; + } + + /** + * Returns the method for this handler method. + */ + public Method getMethod() { + return this.method; + } + + /** + * Returns the type of the handler for this handler method. + * Note that if the bean type is a CGLIB-generated class, the original, user-defined class is returned. + */ + public Class getBeanType() { + Class clazz = (this.bean instanceof String) + ? this.beanFactory.getType((String) this.bean) : this.bean.getClass(); + + return ClassUtils.getUserClass(clazz); + } + + /** + * If the bean method is a bridge method, this method returns the bridged (user-defined) method. + * Otherwise it returns the same method as {@link #getMethod()}. + */ + protected Method getBridgedMethod() { + return this.bridgedMethod; + } + + /** + * Returns the method parameters for this handler method. + */ + public MethodParameter[] getMethodParameters() { + return this.parameters; + } + + /** + * Return the HandlerMethod return type. + */ + public MethodParameter getReturnType() { + return new HandlerMethodParameter(-1); + } + + /** + * Return the actual return value type. + */ + public MethodParameter getReturnValueType(Object returnValue) { + return new ReturnValueMethodParameter(returnValue); + } + + /** + * Returns {@code true} if the method return type is void, {@code false} otherwise. + */ + public boolean isVoid() { + return Void.TYPE.equals(getReturnType().getParameterType()); + } + + /** + * Returns a single annotation on the underlying method traversing its super methods if no + * annotation can be found on the given method itself. + * @param annotationType the type of annotation to introspect the method for. + * @return the annotation, or {@code null} if none found + */ + public A getMethodAnnotation(Class annotationType) { + return AnnotationUtils.findAnnotation(this.bridgedMethod, annotationType); + } + + /** + * If the provided instance contains a bean name rather than an object instance, the bean name is resolved + * before a {@link HandlerMethod} is created and returned. + */ + public HandlerMethod createWithResolvedBean() { + Object handler = this.bean; + if (this.bean instanceof String) { + String beanName = (String) this.bean; + handler = this.beanFactory.getBean(beanName); + } + return new HandlerMethod(this, handler); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o != null && o instanceof HandlerMethod) { + HandlerMethod other = (HandlerMethod) o; + return this.bean.equals(other.bean) && this.method.equals(other.method); + } + return false; + } + + @Override + public int hashCode() { + return 31 * this.bean.hashCode() + this.method.hashCode(); + } + + @Override + public String toString() { + return method.toGenericString(); + } + + /** + * A MethodParameter with HandlerMethod-specific behavior. + */ + private class HandlerMethodParameter extends MethodParameter { + + protected HandlerMethodParameter(int index) { + super(HandlerMethod.this.bridgedMethod, index); + } + + @Override + public Class getDeclaringClass() { + return HandlerMethod.this.getBeanType(); + } + + @Override + public T getMethodAnnotation(Class annotationType) { + return HandlerMethod.this.getMethodAnnotation(annotationType); + } + } + + /** + * A MethodParameter for a HandlerMethod return type based on an actual return value. + */ + private class ReturnValueMethodParameter extends HandlerMethodParameter { + + private final Object returnValue; + + public ReturnValueMethodParameter(Object returnValue) { + super(-1); + this.returnValue = returnValue; + } + + @Override + public Class getParameterType() { + return (this.returnValue != null) ? this.returnValue.getClass() : super.getParameterType(); + } + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/MessageArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodArgumentResolver.java similarity index 97% rename from spring-messaging/src/main/java/org/springframework/messaging/handler/method/MessageArgumentResolver.java rename to spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodArgumentResolver.java index a417fbd5c84..7aa53357f8c 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/MessageArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodArgumentResolver.java @@ -27,7 +27,7 @@ import org.springframework.messaging.Message; * @author Rossen Stoyanchev * @since 4.0 */ -public interface MessageArgumentResolver { +public interface HandlerMethodArgumentResolver { /** * Whether the given {@linkplain MethodParameter method parameter} is diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/MessageArgumentResolverComposite.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodArgumentResolverComposite.java similarity index 58% rename from spring-messaging/src/main/java/org/springframework/messaging/handler/method/MessageArgumentResolverComposite.java rename to spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodArgumentResolverComposite.java index 3f24151a6f4..ad9a8901ebf 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/MessageArgumentResolverComposite.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodArgumentResolverComposite.java @@ -30,32 +30,32 @@ import org.springframework.util.Assert; /** * Resolves method parameters by delegating to a list of registered - * {@link MessageArgumentResolver}. Previously resolved method parameters are cached + * {@link HandlerMethodArgumentResolver}. Previously resolved method parameters are cached * for faster lookups. * * @author Rossen Stoyanchev * @since 4.0 */ -public class MessageArgumentResolverComposite implements MessageArgumentResolver { +public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver { protected final Log logger = LogFactory.getLog(getClass()); - private final List argumentResolvers = new LinkedList(); + private final List argumentResolvers = new LinkedList(); - private final Map argumentResolverCache = - new ConcurrentHashMap(256); + private final Map argumentResolverCache = + new ConcurrentHashMap(256); /** * Return a read-only list with the contained resolvers, or an empty list. */ - public List getResolvers() { + public List getResolvers() { return Collections.unmodifiableList(this.argumentResolvers); } /** * Whether the given {@linkplain MethodParameter method parameter} is supported by any registered - * {@link MessageArgumentResolver}. + * {@link HandlerMethodArgumentResolver}. */ @Override public boolean supportsParameter(MethodParameter parameter) { @@ -63,24 +63,24 @@ public class MessageArgumentResolverComposite implements MessageArgumentResolver } /** - * Iterate over registered {@link MessageArgumentResolver}s and invoke the one that supports it. - * @exception IllegalStateException if no suitable {@link MessageArgumentResolver} is found. + * Iterate over registered {@link HandlerMethodArgumentResolver}s and invoke the one that supports it. + * @exception IllegalStateException if no suitable {@link HandlerMethodArgumentResolver} is found. */ @Override public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { - MessageArgumentResolver resolver = getArgumentResolver(parameter); + HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); Assert.notNull(resolver, "Unknown parameter type [" + parameter.getParameterType().getName() + "]"); return resolver.resolveArgument(parameter, message); } /** - * Find a registered {@link MessageArgumentResolver} that supports the given method parameter. + * Find a registered {@link HandlerMethodArgumentResolver} that supports the given method parameter. */ - private MessageArgumentResolver getArgumentResolver(MethodParameter parameter) { - MessageArgumentResolver result = this.argumentResolverCache.get(parameter); + private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { + HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); if (result == null) { - for (MessageArgumentResolver resolver : this.argumentResolvers) { + for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { if (resolver.supportsParameter(parameter)) { result = resolver; this.argumentResolverCache.put(parameter, result); @@ -92,19 +92,19 @@ public class MessageArgumentResolverComposite implements MessageArgumentResolver } /** - * Add the given {@link MessageArgumentResolver}. + * Add the given {@link HandlerMethodArgumentResolver}. */ - public MessageArgumentResolverComposite addResolver(MessageArgumentResolver argumentResolver) { + public HandlerMethodArgumentResolverComposite addResolver(HandlerMethodArgumentResolver argumentResolver) { this.argumentResolvers.add(argumentResolver); return this; } /** - * Add the given {@link MessageArgumentResolver}s. + * Add the given {@link HandlerMethodArgumentResolver}s. */ - public MessageArgumentResolverComposite addResolvers(List argumentResolvers) { + public HandlerMethodArgumentResolverComposite addResolvers(List argumentResolvers) { if (argumentResolvers != null) { - for (MessageArgumentResolver resolver : argumentResolvers) { + for (HandlerMethodArgumentResolver resolver : argumentResolvers) { this.argumentResolvers.add(resolver); } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/MessageReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodReturnValueHandler.java similarity index 97% rename from spring-messaging/src/main/java/org/springframework/messaging/handler/method/MessageReturnValueHandler.java rename to spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodReturnValueHandler.java index d343892fd4b..0999173db0e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/MessageReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodReturnValueHandler.java @@ -27,7 +27,7 @@ import org.springframework.messaging.Message; * @author Rossen Stoyanchev * @since 4.0 */ -public interface MessageReturnValueHandler { +public interface HandlerMethodReturnValueHandler { /** * Whether the given {@linkplain MethodParameter method return type} is diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/MessageReturnValueHandlerComposite.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodReturnValueHandlerComposite.java similarity index 64% rename from spring-messaging/src/main/java/org/springframework/messaging/handler/method/MessageReturnValueHandlerComposite.java rename to spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodReturnValueHandlerComposite.java index 9eea996c047..c8dd9639bd3 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/MessageReturnValueHandlerComposite.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodReturnValueHandlerComposite.java @@ -28,25 +28,25 @@ import org.springframework.util.Assert; * @author Rossen Stoyanchev * @since 4.0 */ -public class MessageReturnValueHandlerComposite implements MessageReturnValueHandler { +public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler { - private final List returnValueHandlers = new ArrayList(); + private final List returnValueHandlers = new ArrayList(); /** - * Add the given {@link MessageReturnValueHandler}. + * Add the given {@link HandlerMethodReturnValueHandler}. */ - public MessageReturnValueHandlerComposite addHandler(MessageReturnValueHandler returnValuehandler) { + public HandlerMethodReturnValueHandlerComposite addHandler(HandlerMethodReturnValueHandler returnValuehandler) { this.returnValueHandlers.add(returnValuehandler); return this; } /** - * Add the given {@link MessageReturnValueHandler}s. + * Add the given {@link HandlerMethodReturnValueHandler}s. */ - public MessageReturnValueHandlerComposite addHandlers(List handlers) { + public HandlerMethodReturnValueHandlerComposite addHandlers(List handlers) { if (handlers != null) { - for (MessageReturnValueHandler handler : handlers) { + for (HandlerMethodReturnValueHandler handler : handlers) { this.returnValueHandlers.add(handler); } } @@ -58,8 +58,8 @@ public class MessageReturnValueHandlerComposite implements MessageReturnValueHan return getReturnValueHandler(returnType) != null; } - private MessageReturnValueHandler getReturnValueHandler(MethodParameter returnType) { - for (MessageReturnValueHandler handler : this.returnValueHandlers) { + private HandlerMethodReturnValueHandler getReturnValueHandler(MethodParameter returnType) { + for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) { if (handler.supportsReturnType(returnType)) { return handler; } @@ -71,7 +71,7 @@ public class MessageReturnValueHandlerComposite implements MessageReturnValueHan public void handleReturnValue(Object returnValue, MethodParameter returnType, Message message) throws Exception { - MessageReturnValueHandler handler = getReturnValueHandler(returnType); + HandlerMethodReturnValueHandler handler = getReturnValueHandler(returnType); Assert.notNull(handler, "Unknown return value type [" + returnType.getParameterType().getName() + "]"); handler.handleReturnValue(returnValue, returnType, message); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodSelector.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodSelector.java new file mode 100644 index 00000000000..95ae629d9dc --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodSelector.java @@ -0,0 +1,73 @@ +/* + * 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.method; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.core.BridgeMethodResolver; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.MethodFilter; + +/** + * Defines the algorithm for searching handler methods exhaustively including interfaces and parent + * classes while also dealing with parameterized methods as well as interface and class-based proxies. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public abstract class HandlerMethodSelector { + + /** + * Selects handler methods for the given handler type. Callers of this method define handler methods + * of interest through the {@link MethodFilter} parameter. + * + * @param handlerType the handler type to search handler methods on + * @param handlerMethodFilter a {@link MethodFilter} to help recognize handler methods of interest + * @return the selected methods, or an empty set + */ + public static Set selectMethods(final Class handlerType, final MethodFilter handlerMethodFilter) { + final Set handlerMethods = new LinkedHashSet(); + Set> handlerTypes = new LinkedHashSet>(); + Class specificHandlerType = null; + if (!Proxy.isProxyClass(handlerType)) { + handlerTypes.add(handlerType); + specificHandlerType = handlerType; + } + handlerTypes.addAll(Arrays.asList(handlerType.getInterfaces())); + for (Class currentHandlerType : handlerTypes) { + final Class targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType); + ReflectionUtils.doWithMethods(currentHandlerType, new ReflectionUtils.MethodCallback() { + @Override + public void doWith(Method method) { + Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass); + Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); + if (handlerMethodFilter.matches(specificMethod) && + (bridgedMethod == specificMethod || !handlerMethodFilter.matches(bridgedMethod))) { + handlerMethods.add(specificMethod); + } + } + }, ReflectionUtils.USER_DECLARED_METHODS); + } + return handlerMethods; + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/InvocableMessageHandlerMethod.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/InvocableHandlerMethod.java similarity index 89% rename from spring-messaging/src/main/java/org/springframework/messaging/handler/method/InvocableMessageHandlerMethod.java rename to spring-messaging/src/main/java/org/springframework/messaging/handler/method/InvocableHandlerMethod.java index 0f78ac305fa..1a4978858f1 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/InvocableMessageHandlerMethod.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/InvocableHandlerMethod.java @@ -26,21 +26,20 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.messaging.Message; import org.springframework.util.ReflectionUtils; -import org.springframework.web.method.HandlerMethod; /** * Invokes the handler method for a given message after resolving - * its method argument values through registered {@link MessageArgumentResolver}s. + * its method argument values through registered {@link HandlerMethodArgumentResolver}s. *

- * Use {@link #setMessageMethodArgumentResolvers(MessageArgumentResolverComposite)} + * Use {@link #setMessageMethodArgumentResolvers(HandlerMethodArgumentResolverComposite)} * to customize the list of argument resolvers. * * @author Rossen Stoyanchev * @since 4.0 */ -public class InvocableMessageHandlerMethod extends HandlerMethod { +public class InvocableHandlerMethod extends HandlerMethod { - private MessageArgumentResolverComposite argumentResolvers = new MessageArgumentResolverComposite(); + private HandlerMethodArgumentResolverComposite argumentResolvers = new HandlerMethodArgumentResolverComposite(); private ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); @@ -48,14 +47,14 @@ public class InvocableMessageHandlerMethod extends HandlerMethod { /** * Create an instance from a {@code HandlerMethod}. */ - public InvocableMessageHandlerMethod(HandlerMethod handlerMethod) { + public InvocableHandlerMethod(HandlerMethod handlerMethod) { super(handlerMethod); } /** * Create an instance from a bean instance and a method. */ - public InvocableMessageHandlerMethod(Object bean, Method method) { + public InvocableHandlerMethod(Object bean, Method method) { super(bean, method); } @@ -68,17 +67,17 @@ public class InvocableMessageHandlerMethod extends HandlerMethod { * @param parameterTypes the method parameter types * @throws NoSuchMethodException when the method cannot be found */ - public InvocableMessageHandlerMethod(Object bean, String methodName, Class... parameterTypes) + public InvocableHandlerMethod(Object bean, String methodName, Class... parameterTypes) throws NoSuchMethodException { super(bean, methodName, parameterTypes); } /** - * Set {@link MessageArgumentResolver}s to use to use for resolving method + * Set {@link HandlerMethodArgumentResolver}s to use to use for resolving method * argument values. */ - public void setMessageMethodArgumentResolvers(MessageArgumentResolverComposite argumentResolvers) { + public void setMessageMethodArgumentResolvers(HandlerMethodArgumentResolverComposite argumentResolvers) { this.argumentResolvers = argumentResolvers; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java index a356d65a14f..86d5b3b06b4 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java @@ -47,6 +47,7 @@ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor { public static final String MESSAGE_TYPE = "messageType"; + // TODO public static final String PROTOCOL_MESSAGE_TYPE = "protocolMessageType"; public static final String SESSION_ID = "sessionId"; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessagingTemplate.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessagingTemplate.java index 06019d9ba99..15dcf9d5f06 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessagingTemplate.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessagingTemplate.java @@ -15,6 +15,8 @@ */ package org.springframework.messaging.simp; +import java.util.Arrays; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageDeliveryException; @@ -75,11 +77,9 @@ public class SimpMessagingTemplate extends AbstractMessageSendingTemplate Message

addDestinationToMessage(Message

message, String destination) { Assert.notNull(destination, "destination is required"); - SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE); - headers.copyHeaders(message.getHeaders()); - headers.setDestination(destination); - message = MessageBuilder.withPayload(message.getPayload()).copyHeaders(headers.toMap()).build(); - return message; + return MessageBuilder.fromMessage(message) + .setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE, SimpMessageType.MESSAGE) + .setHeader(SimpMessageHeaderAccessor.DESTINATIONS, Arrays.asList(destination)).build(); } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/ReplyToUser.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/ReplyToUser.java new file mode 100644 index 00000000000..434a71b5c3c --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/ReplyToUser.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.simp.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; + + +/** + * @author Rossen Stoyanchev + * @since 4.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ReplyToUser { + + /** + * The destination for a message based on the return value of a method. + */ + String[] value() default {}; + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/package-info.java new file mode 100644 index 00000000000..f5030addbd1 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/package-info.java @@ -0,0 +1,5 @@ +/** + * Annotations and support classes for handling messages from simple messaging + * protocols (like STOMP). + */ +package org.springframework.messaging.simp.annotation; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/DefaultMessageReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/DefaultMessageReturnValueHandler.java deleted file mode 100644 index 38c3dba0fe8..00000000000 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/DefaultMessageReturnValueHandler.java +++ /dev/null @@ -1,128 +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.simp.annotation.support; - -import java.security.Principal; - -import org.springframework.core.MethodParameter; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.handler.annotation.ReplyTo; -import org.springframework.messaging.handler.method.MessageReturnValueHandler; -import org.springframework.messaging.handler.method.MissingSessionUserException; -import org.springframework.messaging.simp.SimpMessageHeaderAccessor; -import org.springframework.messaging.support.MessageBuilder; -import org.springframework.messaging.support.converter.MessageConverter; -import org.springframework.util.Assert; - - -/** - * Expects return values to be either a {@link Message} or the payload of a message to be - * converted and sent on a {@link MessageChannel}. - * - *

This {@link MessageReturnValueHandler} should be ordered last as it supports all - * return value types. - * - * @author Rossen Stoyanchev - * @since 4.0 - */ -public class DefaultMessageReturnValueHandler implements MessageReturnValueHandler { - - private MessageChannel inboundChannel; - - private MessageChannel outboundChannel; - - private final MessageConverter converter; - - - public DefaultMessageReturnValueHandler(MessageChannel inboundChannel, MessageChannel outboundChannel, - MessageConverter converter) { - - Assert.notNull(inboundChannel, "inboundChannel is required"); - Assert.notNull(outboundChannel, "outboundChannel is required"); - Assert.notNull(converter, "converter is required"); - - this.inboundChannel = inboundChannel; - this.outboundChannel = outboundChannel; - this.converter = converter; - } - - - @Override - public boolean supportsReturnType(MethodParameter returnType) { - return true; - } - - @SuppressWarnings("unchecked") - @Override - public void handleReturnValue(Object returnValue, MethodParameter returnType, Message message) - throws Exception { - - if (returnValue == null) { - return; - } - - SimpMessageHeaderAccessor inputHeaders = SimpMessageHeaderAccessor.wrap(message); - - Message returnMessage = (returnValue instanceof Message) ? (Message) returnValue : null; - Object returnPayload = (returnMessage != null) ? returnMessage.getPayload() : returnValue; - - SimpMessageHeaderAccessor returnHeaders = (returnMessage != null) ? - SimpMessageHeaderAccessor.wrap(returnMessage) : SimpMessageHeaderAccessor.create(); - - returnHeaders.setSessionId(inputHeaders.getSessionId()); - returnHeaders.setSubscriptionId(inputHeaders.getSubscriptionId()); - - String destination = getDestination(message, returnType, inputHeaders, returnHeaders); - returnHeaders.setDestination(destination); - - returnMessage = this.converter.toMessage(returnPayload); - returnMessage = MessageBuilder.fromMessage(returnMessage).copyHeaders(returnHeaders.toMap()).build(); - - if (destination.startsWith("/user/")) { - this.inboundChannel.send(returnMessage); - } - else { - this.outboundChannel.send(returnMessage); - } - } - - protected String getDestination(Message inputMessage, MethodParameter returnType, - SimpMessageHeaderAccessor inputHeaders, SimpMessageHeaderAccessor returnHeaders) { - - ReplyTo annot = returnType.getMethodAnnotation(ReplyTo.class); - - if (returnHeaders.getDestination() != null) { - return returnHeaders.getDestination(); - } - else if (annot != null) { - Principal user = inputHeaders.getUser(); - if (user == null) { - throw new MissingSessionUserException(inputMessage); - } - return "/user/" + user.getName() + annot.value(); - } - else if (inputHeaders.getDestination() != null) { - return inputHeaders.getDestination(); - } - else { - return null; - } - - } - -} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/MissingSessionUserException.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/MissingSessionUserException.java similarity index 94% rename from spring-messaging/src/main/java/org/springframework/messaging/handler/method/MissingSessionUserException.java rename to spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/MissingSessionUserException.java index 0e13f563206..80e371f9759 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/MissingSessionUserException.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/MissingSessionUserException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.messaging.handler.method; +package org.springframework.messaging.simp.annotation.support; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/PrincipalMessageArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/PrincipalMethodArgumentResolver.java similarity index 85% rename from spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/PrincipalMessageArgumentResolver.java rename to spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/PrincipalMethodArgumentResolver.java index 3c3ed041ee6..90dbd6f4233 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/PrincipalMessageArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/PrincipalMethodArgumentResolver.java @@ -20,8 +20,7 @@ import java.security.Principal; import org.springframework.core.MethodParameter; import org.springframework.messaging.Message; -import org.springframework.messaging.handler.method.MessageArgumentResolver; -import org.springframework.messaging.handler.method.MissingSessionUserException; +import org.springframework.messaging.handler.method.HandlerMethodArgumentResolver; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; @@ -29,7 +28,7 @@ import org.springframework.messaging.simp.SimpMessageHeaderAccessor; * @author Rossen Stoyanchev * @since 4.0 */ -public class PrincipalMessageArgumentResolver implements MessageArgumentResolver { +public class PrincipalMethodArgumentResolver implements HandlerMethodArgumentResolver { @Override diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/ReplyToMethodReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/ReplyToMethodReturnValueHandler.java new file mode 100644 index 00000000000..82617a31d21 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/ReplyToMethodReturnValueHandler.java @@ -0,0 +1,122 @@ +/* + * 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.simp.annotation.support; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.core.MessagePostProcessor; +import org.springframework.messaging.core.MessageSendingOperations; +import org.springframework.messaging.handler.annotation.ReplyTo; +import org.springframework.messaging.handler.method.HandlerMethodReturnValueHandler; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.annotation.ReplyToUser; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.Assert; + + +/** + * A {@link HandlerMethodReturnValueHandler} for replying to destinations specified in a + * {@link ReplyTo} or {@link ReplyToUser} method-level annotations. + *

+ * The value returned from the method is converted, and turned to a {@link Message} and + * sent through the provided {@link MessageChannel}. The + * message is then enriched with the sessionId of the input message as well as the + * destination from the annotation(s). If multiple destinations are specified, a copy of + * the message is sent to each destination. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class ReplyToMethodReturnValueHandler implements HandlerMethodReturnValueHandler { + + private final MessageSendingOperations messagingTemplate; + + + public ReplyToMethodReturnValueHandler(MessageSendingOperations messagingTemplate) { + Assert.notNull(messagingTemplate, "messagingTemplate is required"); + this.messagingTemplate = messagingTemplate; + } + + + @Override + public boolean supportsReturnType(MethodParameter returnType) { + return ((returnType.getMethodAnnotation(ReplyTo.class) != null) + || (returnType.getMethodAnnotation(ReplyToUser.class) != null)); + } + + @Override + public void handleReturnValue(Object returnValue, MethodParameter returnType, Message inputMessage) + throws Exception { + + if (returnValue == null) { + return; + } + + ReplyTo replyTo = returnType.getMethodAnnotation(ReplyTo.class); + ReplyToUser replyToUser = returnType.getMethodAnnotation(ReplyToUser.class); + + List destinations = new ArrayList(); + if (replyTo != null) { + destinations.addAll(Arrays.asList(replyTo.value())); + } + if (replyToUser != null) { + Principal user = getUser(inputMessage); + for (String destination : replyToUser.value()) { + destinations.add("/user/" + user.getName() + destination); + } + } + + MessagePostProcessor postProcessor = new SessionIdHeaderPostProcessor(inputMessage); + + for (String destination : destinations) { + this.messagingTemplate.convertAndSend(destination, returnValue, postProcessor); + } + } + + private Principal getUser(Message inputMessage) { + SimpMessageHeaderAccessor inputHeaders = SimpMessageHeaderAccessor.wrap(inputMessage); + Principal user = inputHeaders.getUser(); + if (user == null) { + throw new MissingSessionUserException(inputMessage); + } + return user; + } + + + private final class SessionIdHeaderPostProcessor implements MessagePostProcessor { + + private final Message inputMessage; + + + public SessionIdHeaderPostProcessor(Message inputMessage) { + this.inputMessage = inputMessage; + } + + @Override + public Message postProcessMessage(Message message) { + String headerName = SimpMessageHeaderAccessor.SESSION_ID; + String sessionId = (String) this.inputMessage.getHeaders().get(headerName); + return MessageBuilder.fromMessage(message).setHeader(headerName, sessionId).build(); + } + } +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SubscriptionMethodReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SubscriptionMethodReturnValueHandler.java new file mode 100644 index 00000000000..495e8d68406 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SubscriptionMethodReturnValueHandler.java @@ -0,0 +1,100 @@ +/* + * 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.simp.annotation.support; + +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.core.MessagePostProcessor; +import org.springframework.messaging.core.MessageSendingOperations; +import org.springframework.messaging.handler.annotation.ReplyTo; +import org.springframework.messaging.handler.method.HandlerMethodReturnValueHandler; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.annotation.ReplyToUser; +import org.springframework.messaging.simp.annotation.SubscribeEvent; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.Assert; + + +/** + * A {@link HandlerMethodReturnValueHandler} for replying directly to a subscription. It + * supports methods annotated with {@link SubscribeEvent} that do not also annotated with + * neither {@link ReplyTo} nor {@link ReplyToUser}. + * + *

The value returned from the method is converted, and turned to a {@link Message} and + * then enriched with the sessionId, subscriptionId, and destination of the input message. + * The message is then sent directly back to the connected client. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class SubscriptionMethodReturnValueHandler implements HandlerMethodReturnValueHandler { + + private final MessageSendingOperations messagingTemplate; + + + public SubscriptionMethodReturnValueHandler(MessageSendingOperations messagingTemplate) { + Assert.notNull(messagingTemplate, "messagingTemplate is required"); + this.messagingTemplate = messagingTemplate; + } + + + @Override + public boolean supportsReturnType(MethodParameter returnType) { + return ((returnType.getMethodAnnotation(SubscribeEvent.class) != null) + && (returnType.getMethodAnnotation(ReplyTo.class) == null) + && (returnType.getMethodAnnotation(ReplyToUser.class) == null)); + } + + @Override + public void handleReturnValue(Object returnValue, MethodParameter returnType, Message message) + throws Exception { + + if (returnValue == null) { + return; + } + + SimpMessageHeaderAccessor inputHeaders = SimpMessageHeaderAccessor.wrap(message); + String destination = inputHeaders.getDestination(); + + Assert.state(inputHeaders.getSubscriptionId() != null, + "No subsriptiondId in input message. Add @ReplyTo or @ReplyToUser to method: " + + returnType.getMethod()); + + MessagePostProcessor postProcessor = new InputHeaderCopyingPostProcessor(inputHeaders); + this.messagingTemplate.convertAndSend(destination, returnValue, postProcessor); + } + + + private final class InputHeaderCopyingPostProcessor implements MessagePostProcessor { + + private final SimpMessageHeaderAccessor inputHeaders; + + + public InputHeaderCopyingPostProcessor(SimpMessageHeaderAccessor inputHeaders) { + this.inputHeaders = inputHeaders; + } + + @Override + public Message postProcessMessage(Message message) { + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(message); + return MessageBuilder.fromMessage(message) + .setHeader(SimpMessageHeaderAccessor.SESSION_ID, this.inputHeaders.getSessionId()) + .setHeader(SimpMessageHeaderAccessor.SUBSCRIPTION_ID, this.inputHeaders.getSubscriptionId()) + .copyHeaders(headers.toMap()).build(); + } + } +} 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 df8b83a48ca..e92a1610c49 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 @@ -38,24 +38,26 @@ import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessagingException; import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.handler.annotation.support.MessageBodyArgumentResolver; -import org.springframework.messaging.handler.annotation.support.MessageExceptionHandlerMethodResolver; -import org.springframework.messaging.handler.method.InvocableMessageHandlerMethod; -import org.springframework.messaging.handler.method.MessageArgumentResolverComposite; -import org.springframework.messaging.handler.method.MessageReturnValueHandlerComposite; +import org.springframework.messaging.handler.annotation.support.ExceptionHandlerMethodResolver; +import org.springframework.messaging.handler.annotation.support.MessageBodyMethodArgumentResolver; +import org.springframework.messaging.handler.method.HandlerMethod; +import org.springframework.messaging.handler.method.HandlerMethodArgumentResolverComposite; +import org.springframework.messaging.handler.method.HandlerMethodReturnValueHandlerComposite; +import org.springframework.messaging.handler.method.HandlerMethodSelector; +import org.springframework.messaging.handler.method.InvocableHandlerMethod; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessageType; +import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.messaging.simp.annotation.SubscribeEvent; import org.springframework.messaging.simp.annotation.UnsubscribeEvent; -import org.springframework.messaging.simp.annotation.support.DefaultMessageReturnValueHandler; -import org.springframework.messaging.simp.annotation.support.PrincipalMessageArgumentResolver; +import org.springframework.messaging.simp.annotation.support.PrincipalMethodArgumentResolver; +import org.springframework.messaging.simp.annotation.support.ReplyToMethodReturnValueHandler; +import org.springframework.messaging.simp.annotation.support.SubscriptionMethodReturnValueHandler; import org.springframework.messaging.support.converter.MessageConverter; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils.MethodFilter; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.method.HandlerMethodSelector; /** @@ -80,12 +82,12 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati private Map unsubscribeMethods = new HashMap(); - private final Map, MessageExceptionHandlerMethodResolver> exceptionHandlerCache = - new ConcurrentHashMap, MessageExceptionHandlerMethodResolver>(64); + private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = + new ConcurrentHashMap, ExceptionHandlerMethodResolver>(64); - private MessageArgumentResolverComposite argumentResolvers = new MessageArgumentResolverComposite(); + private HandlerMethodArgumentResolverComposite argumentResolvers = new HandlerMethodArgumentResolverComposite(); - private MessageReturnValueHandlerComposite returnValueHandlers = new MessageReturnValueHandlerComposite(); + private HandlerMethodReturnValueHandlerComposite returnValueHandlers = new HandlerMethodReturnValueHandlerComposite(); /** @@ -116,11 +118,17 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati initHandlerMethods(); - this.argumentResolvers.addResolver(new PrincipalMessageArgumentResolver()); - this.argumentResolvers.addResolver(new MessageBodyArgumentResolver(this.messageConverter)); + this.argumentResolvers.addResolver(new PrincipalMethodArgumentResolver()); + this.argumentResolvers.addResolver(new MessageBodyMethodArgumentResolver(this.messageConverter)); - this.returnValueHandlers.addHandler(new DefaultMessageReturnValueHandler( - this.inboundChannel, this.outboundChannel, this.messageConverter)); + SimpMessagingTemplate inboundMessagingTemplate = new SimpMessagingTemplate(this.inboundChannel); + inboundMessagingTemplate.setConverter(this.messageConverter); + + SimpMessagingTemplate outboundMessagingTemplate = new SimpMessagingTemplate(this.outboundChannel); + outboundMessagingTemplate.setConverter(this.messageConverter); + + this.returnValueHandlers.addHandler(new ReplyToMethodReturnValueHandler(inboundMessagingTemplate)); + this.returnValueHandlers.addHandler(new SubscriptionMethodReturnValueHandler(outboundMessagingTemplate)); } protected void initHandlerMethods() { @@ -213,8 +221,7 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati HandlerMethod handlerMethod = match.createWithResolvedBean(); - // TODO: avoid re-creating invocableHandlerMethod - InvocableMessageHandlerMethod invocableHandlerMethod = new InvocableMessageHandlerMethod(handlerMethod); + InvocableHandlerMethod invocableHandlerMethod = new InvocableHandlerMethod(handlerMethod); invocableHandlerMethod.setMessageMethodArgumentResolvers(this.argumentResolvers); try { @@ -237,11 +244,11 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati private void invokeExceptionHandler(Message message, HandlerMethod handlerMethod, Exception ex) { - InvocableMessageHandlerMethod exceptionHandlerMethod; + InvocableHandlerMethod exceptionHandlerMethod; Class beanType = handlerMethod.getBeanType(); - MessageExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(beanType); + ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(beanType); if (resolver == null) { - resolver = new MessageExceptionHandlerMethodResolver(beanType); + resolver = new ExceptionHandlerMethodResolver(beanType); this.exceptionHandlerCache.put(beanType, resolver); } @@ -251,7 +258,7 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati return; } - exceptionHandlerMethod = new InvocableMessageHandlerMethod(handlerMethod.getBean(), method); + exceptionHandlerMethod = new InvocableHandlerMethod(handlerMethod.getBean(), method); exceptionHandlerMethod.setMessageMethodArgumentResolvers(this.argumentResolvers); try { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/UserDestinationMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/UserDestinationMessageHandler.java index 6cc6ce001cf..819598f09de 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/UserDestinationMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/UserDestinationMessageHandler.java @@ -34,7 +34,7 @@ import org.springframework.util.StringUtils; * Supports destinations prefixed with "/user/{username}" and resolves them into a * destination to which the user is currently subscribed by appending the user session id. * For example a destination such as "/user/john/queue/trade-confirmation" would resolve - * to "/trade-confirmation/i9oqdfzo" if "i9oqdfzo" is the user's session id. + * to "/queue/trade-confirmation/i9oqdfzo" if "i9oqdfzo" is the user's session id. * * @author Rossen Stoyanchev * @since 4.0 diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/channel/TaskExecutorSubscribableChannel.java b/spring-messaging/src/main/java/org/springframework/messaging/support/channel/ExecutorSubscribableChannel.java similarity index 82% rename from spring-messaging/src/main/java/org/springframework/messaging/support/channel/TaskExecutorSubscribableChannel.java rename to spring-messaging/src/main/java/org/springframework/messaging/support/channel/ExecutorSubscribableChannel.java index 0d58885eee0..75d45c26839 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/channel/TaskExecutorSubscribableChannel.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/channel/ExecutorSubscribableChannel.java @@ -18,8 +18,8 @@ package org.springframework.messaging.support.channel; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.Executor; -import org.springframework.core.task.TaskExecutor; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.SubscribableChannel; @@ -31,28 +31,28 @@ import org.springframework.messaging.SubscribableChannel; * @author Rossen Stoyanchev * @since 4.0 */ -public class TaskExecutorSubscribableChannel extends AbstractSubscribableChannel { +public class ExecutorSubscribableChannel extends AbstractSubscribableChannel { - private final TaskExecutor executor; + private final Executor executor; private final Set handlers = new CopyOnWriteArraySet(); /** - * Create a new {@link TaskExecutorSubscribableChannel} instance where messages will be sent + * Create a new {@link ExecutorSubscribableChannel} instance where messages will be sent * in the callers thread. */ - public TaskExecutorSubscribableChannel() { + public ExecutorSubscribableChannel() { this(null); } /** - * Create a new {@link TaskExecutorSubscribableChannel} instance where messages will be sent + * Create a new {@link ExecutorSubscribableChannel} instance where messages will be sent * via the specified executor. * @param executor the executor used to send the message or {@code null} to execute in * the callers thread. */ - public TaskExecutorSubscribableChannel(TaskExecutor executor) { + public ExecutorSubscribableChannel(Executor executor) { this.executor = executor; } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/ExceptionHandlerMethodResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/ExceptionHandlerMethodResolverTests.java new file mode 100644 index 00000000000..4fb5cad5239 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/ExceptionHandlerMethodResolverTests.java @@ -0,0 +1,144 @@ +/* + * 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.io.FileNotFoundException; +import java.io.IOException; +import java.net.BindException; +import java.net.SocketException; + +import org.junit.Test; +import org.springframework.messaging.handler.annotation.MessageExceptionHandler; +import org.springframework.stereotype.Controller; +import org.springframework.util.ClassUtils; + +import static org.junit.Assert.*; + + +/** + * Test fixture for {@link ExceptionHandlerMethodResolver} tests. + * + * @author Rossen Stoyanchev + */ +public class ExceptionHandlerMethodResolverTests { + + @Test + public void resolveMethodFromAnnotation() { + ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class); + IOException exception = new IOException(); + assertEquals("handleIOException", resolver.resolveMethod(exception).getName()); + } + + @Test + public void resolveMethodFromArgument() { + ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class); + IllegalArgumentException exception = new IllegalArgumentException(); + assertEquals("handleIllegalArgumentException", resolver.resolveMethod(exception).getName()); + } + + @Test + public void resolveMethodExceptionSubType() { + ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class); + IOException ioException = new FileNotFoundException(); + assertEquals("handleIOException", resolver.resolveMethod(ioException).getName()); + SocketException bindException = new BindException(); + assertEquals("handleSocketException", resolver.resolveMethod(bindException).getName()); + } + + @Test + public void resolveMethodBestMatch() { + ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class); + SocketException exception = new SocketException(); + assertEquals("handleSocketException", resolver.resolveMethod(exception).getName()); + } + + @Test + public void resolveMethodNoMatch() { + ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class); + Exception exception = new Exception(); + assertNull("1st lookup", resolver.resolveMethod(exception)); + assertNull("2nd lookup from cache", resolver.resolveMethod(exception)); + } + + @Test + public void resolveMethodInherited() { + ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(InheritedController.class); + IOException exception = new IOException(); + assertEquals("handleIOException", resolver.resolveMethod(exception).getName()); + } + + @Test(expected = IllegalStateException.class) + public void ambiguousExceptionMapping() { + new ExceptionHandlerMethodResolver(AmbiguousController.class); + } + + @Test(expected = IllegalArgumentException.class) + public void noExceptionMapping() { + new ExceptionHandlerMethodResolver(NoExceptionController.class); + } + + @Controller + static class ExceptionController { + + public void handle() {} + + @MessageExceptionHandler(IOException.class) + public void handleIOException() { + } + + @MessageExceptionHandler(SocketException.class) + public void handleSocketException() { + } + + @MessageExceptionHandler + public void handleIllegalArgumentException(IllegalArgumentException exception) { + } + } + + @Controller + static class InheritedController extends ExceptionController { + + @Override + public void handleIOException() { + } + } + + @Controller + static class AmbiguousController { + + public void handle() {} + + @MessageExceptionHandler({BindException.class, IllegalArgumentException.class}) + public String handle1(Exception ex) throws IOException { + return ClassUtils.getShortName(ex.getClass()); + } + + @MessageExceptionHandler + public String handle2(IllegalArgumentException ex) { + return ClassUtils.getShortName(ex.getClass()); + } + } + + @Controller + static class NoExceptionController { + + @MessageExceptionHandler + public void handle() { + } + } + +} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/ReplyToMethodReturnValueHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/ReplyToMethodReturnValueHandlerTests.java new file mode 100644 index 00000000000..477b920ce4e --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/ReplyToMethodReturnValueHandlerTests.java @@ -0,0 +1,196 @@ +/* + * 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.simp.annotation.support; + +import java.lang.reflect.Method; +import java.security.Principal; + +import javax.security.auth.Subject; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.ReplyTo; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.messaging.simp.annotation.ReplyToUser; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.messaging.support.converter.MessageConverter; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + + +/** + * Test fixture for {@link ReplyToMethodReturnValueHandlerTests}. + * + * @author Rossen Stoyanchev + */ +public class ReplyToMethodReturnValueHandlerTests { + + private static final String payloadContent = "payload"; + + + private ReplyToMethodReturnValueHandler handler; + + @Mock private MessageChannel messageChannel; + + @Captor ArgumentCaptor> messageCaptor; + + @Mock private MessageConverter messageConverter; + + private MethodParameter replyToReturnType; + + private MethodParameter replyToUserReturnType; + + private MethodParameter missingReplyToReturnType; + + + @SuppressWarnings("unchecked") + @Before + public void setup() throws Exception { + + MockitoAnnotations.initMocks(this); + + Message message = MessageBuilder.withPayload(payloadContent).build(); + when(this.messageConverter.toMessage(payloadContent)).thenReturn(message); + + SimpMessagingTemplate messagingTemplate = new SimpMessagingTemplate(this.messageChannel); + messagingTemplate.setConverter(this.messageConverter); + + this.handler = new ReplyToMethodReturnValueHandler(messagingTemplate); + + Method method = this.getClass().getDeclaredMethod("handleAndReplyTo"); + this.replyToReturnType = new MethodParameter(method, -1); + + method = this.getClass().getDeclaredMethod("handleAndReplyToUser"); + this.replyToUserReturnType = new MethodParameter(method, -1); + + method = this.getClass().getDeclaredMethod("handleWithMissingReplyTo"); + this.missingReplyToReturnType = new MethodParameter(method, -1); + } + + + @Test + public void supportsReturnType() throws Exception { + assertTrue(this.handler.supportsReturnType(this.replyToReturnType)); + assertTrue(this.handler.supportsReturnType(this.replyToUserReturnType)); + assertFalse(this.handler.supportsReturnType(this.missingReplyToReturnType)); + } + + @Test + public void replyToMethod() throws Exception { + + when(this.messageChannel.send(any(Message.class))).thenReturn(true); + + String sessionId = "sess1"; + Message inputMessage = createInputMessage(sessionId, "sub1", "/dest", null); + + this.handler.handleReturnValue(payloadContent, this.replyToReturnType, inputMessage); + + verify(this.messageChannel, times(2)).send(this.messageCaptor.capture()); + + Message message = this.messageCaptor.getAllValues().get(0); + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(message); + + assertEquals(sessionId, headers.getSessionId()); + assertNull(headers.getSubscriptionId()); + assertEquals("/dest1", headers.getDestination()); + + message = this.messageCaptor.getAllValues().get(1); + headers = SimpMessageHeaderAccessor.wrap(message); + + assertEquals(sessionId, headers.getSessionId()); + assertNull(headers.getSubscriptionId()); + assertEquals("/dest2", headers.getDestination()); + } + + @Test + public void replyToUserMethod() throws Exception { + + when(this.messageChannel.send(any(Message.class))).thenReturn(true); + + String sessionId = "sess1"; + TestUser user = new TestUser(); + Message inputMessage = createInputMessage(sessionId, "sub1", "/dest", user); + + this.handler.handleReturnValue(payloadContent, this.replyToUserReturnType, inputMessage); + + verify(this.messageChannel, times(2)).send(this.messageCaptor.capture()); + + Message message = this.messageCaptor.getAllValues().get(0); + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(message); + + assertEquals(sessionId, headers.getSessionId()); + assertNull(headers.getSubscriptionId()); + assertEquals("/user/" + user.getName() + "/dest1", headers.getDestination()); + + message = this.messageCaptor.getAllValues().get(1); + headers = SimpMessageHeaderAccessor.wrap(message); + + assertEquals(sessionId, headers.getSessionId()); + assertNull(headers.getSubscriptionId()); + assertEquals("/user/" + user.getName() + "/dest2", headers.getDestination()); + } + + + private Message createInputMessage(String sessId, String subsId, String dest, Principal principal) { + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(); + headers.setSessionId(sessId); + headers.setSubscriptionId(subsId); + headers.setDestination(dest); + headers.setUser(principal); + return MessageBuilder.withPayload(new byte[0]).copyHeaders(headers.toMap()).build(); + } + + private static class TestUser implements Principal { + + public String getName() { + return "joe"; + } + + public boolean implies(Subject subject) { + return false; + } + } + + @MessageMapping("/handle") // not needed for the tests but here for completeness + public String handleWithMissingReplyTo() { + return payloadContent; + } + + @MessageMapping("/handle") // not needed for the tests but here for completeness + @ReplyTo({"/dest1", "/dest2"}) + public String handleAndReplyTo() { + return payloadContent; + } + + @MessageMapping("/handle") // not needed for the tests but here for completeness + @ReplyToUser({"/dest1", "/dest2"}) + public String handleAndReplyToUser() { + return payloadContent; + } + +} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SubscriptionMethodReturnValueHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SubscriptionMethodReturnValueHandlerTests.java new file mode 100644 index 00000000000..f4d758ad4e6 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SubscriptionMethodReturnValueHandlerTests.java @@ -0,0 +1,150 @@ +/* + * 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.simp.annotation.support; + +import java.lang.reflect.Method; +import java.security.Principal; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.ReplyTo; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.messaging.simp.annotation.SubscribeEvent; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.messaging.support.converter.MessageConverter; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + + +/** + * Test fixture for {@link SubscriptionMethodReturnValueHandler}. + * + * @author Rossen Stoyanchev + */ +public class SubscriptionMethodReturnValueHandlerTests { + + private static final String payloadContent = "payload"; + + + private SubscriptionMethodReturnValueHandler handler; + + @Mock private MessageChannel messageChannel; + + @Captor ArgumentCaptor> messageCaptor; + + @Mock private MessageConverter messageConverter; + + private MethodParameter subscribeEventReturnType; + + private MethodParameter subscribeEventReplyToReturnType; + + private MethodParameter messageMappingReturnType; + + + @SuppressWarnings("unchecked") + @Before + public void setup() throws Exception { + + MockitoAnnotations.initMocks(this); + + Message message = MessageBuilder.withPayload(payloadContent).build(); + when(this.messageConverter.toMessage(payloadContent)).thenReturn(message); + + SimpMessagingTemplate messagingTemplate = new SimpMessagingTemplate(this.messageChannel); + messagingTemplate.setConverter(this.messageConverter); + + this.handler = new SubscriptionMethodReturnValueHandler(messagingTemplate); + + Method method = this.getClass().getDeclaredMethod("getData"); + this.subscribeEventReturnType = new MethodParameter(method, -1); + + method = this.getClass().getDeclaredMethod("getDataAndReplyTo"); + this.subscribeEventReplyToReturnType = new MethodParameter(method, -1); + + method = this.getClass().getDeclaredMethod("handle"); + this.messageMappingReturnType = new MethodParameter(method, -1); + } + + + @Test + public void supportsReturnType() throws Exception { + assertTrue(this.handler.supportsReturnType(this.subscribeEventReturnType)); + assertFalse(this.handler.supportsReturnType(this.subscribeEventReplyToReturnType)); + assertFalse(this.handler.supportsReturnType(this.messageMappingReturnType)); + } + + @Test + public void subscribeEventMethod() throws Exception { + + when(this.messageChannel.send(any(Message.class))).thenReturn(true); + + String sessionId = "sess1"; + String subscriptionId = "subs1"; + String destination = "/dest"; + Message inputMessage = createInputMessage(sessionId, subscriptionId, destination, null); + + this.handler.handleReturnValue(payloadContent, this.subscribeEventReturnType, inputMessage); + + verify(this.messageChannel).send(this.messageCaptor.capture()); + assertNotNull(this.messageCaptor.getValue()); + + Message message = this.messageCaptor.getValue(); + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(message); + + assertEquals("sessionId should always be copied", sessionId, headers.getSessionId()); + assertEquals(subscriptionId, headers.getSubscriptionId()); + assertEquals(destination, headers.getDestination()); + } + + + private Message createInputMessage(String sessId, String subsId, String dest, Principal principal) { + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(); + headers.setSessionId(sessId); + headers.setSubscriptionId(subsId); + headers.setDestination(dest); + headers.setUser(principal); + return MessageBuilder.withPayload(new byte[0]).copyHeaders(headers.toMap()).build(); + } + + + @SubscribeEvent("/data") // not needed for the tests but here for completeness + private String getData() { + return payloadContent; + } + + @SubscribeEvent("/data") // not needed for the tests but here for completeness + @ReplyTo("/replyToDest") + private String getDataAndReplyTo() { + return payloadContent; + } + + @MessageMapping("/handle") // not needed for the tests but here for completeness + public String handle() { + return payloadContent; + } +} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/support/channel/PublishSubscibeChannelTests.java b/spring-messaging/src/test/java/org/springframework/messaging/support/channel/PublishSubscibeChannelTests.java index 1483e1623d5..4779d1c4da1 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/support/channel/PublishSubscibeChannelTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/support/channel/PublishSubscibeChannelTests.java @@ -36,7 +36,7 @@ import static org.mockito.BDDMockito.*; import static org.mockito.Mockito.*; /** - * Tests for {@link TaskExecutorSubscribableChannel}. + * Tests for {@link ExecutorSubscribableChannel}. * * @author Phillip Webb */ @@ -46,7 +46,7 @@ public class PublishSubscibeChannelTests { public ExpectedException thrown = ExpectedException.none(); - private TaskExecutorSubscribableChannel channel = new TaskExecutorSubscribableChannel(); + private ExecutorSubscribableChannel channel = new ExecutorSubscribableChannel(); @Mock private MessageHandler handler; @@ -71,14 +71,6 @@ public class PublishSubscibeChannelTests { this.channel.send(null); } - @Test - public void payloadMustNotBeNull() throws Exception { - Message message = mock(Message.class); - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Message payload must not be null"); - this.channel.send(message); - } - @Test public void sendWithoutExecutor() { this.channel.subscribe(this.handler); @@ -89,7 +81,7 @@ public class PublishSubscibeChannelTests { @Test public void sendWithExecutor() throws Exception { TaskExecutor executor = mock(TaskExecutor.class); - this.channel = new TaskExecutorSubscribableChannel(executor); + this.channel = new ExecutorSubscribableChannel(executor); this.channel.subscribe(this.handler); this.channel.send(this.message); verify(executor).execute(this.runnableCaptor.capture()); diff --git a/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java b/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java index aec22e4d5a3..066b273275c 100644 --- a/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java +++ b/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java @@ -206,7 +206,7 @@ public class HandlerMethod { * @return the annotation, or {@code null} if none found */ public A getMethodAnnotation(Class annotationType) { - return AnnotationUtils.findAnnotation(this.method, annotationType); + return AnnotationUtils.findAnnotation(this.bridgedMethod, annotationType); } /** diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java index 4d452ca9917..acbbdfa15f5 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -32,12 +32,13 @@ import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.method.HandlerMethodSelector; + /** - * Discovers {@linkplain ExceptionHandler @ExceptionHandler} methods in a given class - * type, including all super types, and helps to resolve an Exception to the method - * its mapped to. Exception mappings are defined through {@code @ExceptionHandler} - * annotation or by looking at the signature of an {@code @ExceptionHandler} method. - * + * Discovers annotated exception handling methods in a given class type, including all + * super types, and helps to resolve an Exception to a method that can handle it. The + * exception types supported by a given method can also be discovered from the method + * signature. + * * @author Rossen Stoyanchev * @since 3.1 */ @@ -56,17 +57,13 @@ public class ExceptionHandlerMethodResolver { * @param handlerType the type to introspect */ public ExceptionHandlerMethodResolver(Class handlerType) { - for (Method method : HandlerMethodSelector.selectMethods(handlerType, getExceptionHandlerMethods())) { + for (Method method : HandlerMethodSelector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) { for (Class exceptionType : detectExceptionMappings(method)) { addExceptionMapping(exceptionType, method); } } } - protected MethodFilter getExceptionHandlerMethods() { - return EXCEPTION_HANDLER_METHODS; - } - /** * Extract exception mappings from the {@code @ExceptionHandler} annotation * first and as a fall-back from the method signature. @@ -91,8 +88,8 @@ public class ExceptionHandlerMethodResolver { } protected void detectAnnotationExceptionMappings(Method method, List> result) { - ExceptionHandler annotation = AnnotationUtils.findAnnotation(method, ExceptionHandler.class); - result.addAll(Arrays.asList(annotation.value())); + ExceptionHandler annot = AnnotationUtils.findAnnotation(method, ExceptionHandler.class); + result.addAll(Arrays.asList(annot.value())); } private void addExceptionMapping(Class exceptionType, Method method) { @@ -146,9 +143,8 @@ public class ExceptionHandlerMethodResolver { } } - /** - * A filter for selecting {@code @ExceptionHandler} methods. - */ + + /** A filter for selecting annotated exception handling methods. */ public final static MethodFilter EXCEPTION_HANDLER_METHODS = new MethodFilter() { @Override