30 changed files with 1385 additions and 307 deletions
@ -0,0 +1,157 @@
@@ -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<Class<? extends Throwable>, Method> mappedMethods = |
||||
new ConcurrentHashMap<Class<? extends Throwable>, Method>(16); |
||||
|
||||
private final Map<Class<? extends Throwable>, Method> exceptionLookupCache = |
||||
new ConcurrentHashMap<Class<? extends Throwable>, 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<? extends Throwable> 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<Class<? extends Throwable>> detectExceptionMappings(Method method) { |
||||
List<Class<? extends Throwable>> result = new ArrayList<Class<? extends Throwable>>(); |
||||
|
||||
detectAnnotationExceptionMappings(method, result); |
||||
|
||||
if (result.isEmpty()) { |
||||
for (Class<?> paramType : method.getParameterTypes()) { |
||||
if (Throwable.class.isAssignableFrom(paramType)) { |
||||
result.add((Class<? extends Throwable>) paramType); |
||||
} |
||||
} |
||||
} |
||||
|
||||
Assert.notEmpty(result, "No exception types mapped to {" + method + "}"); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
protected void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) { |
||||
MessageExceptionHandler annot = AnnotationUtils.findAnnotation(method, MessageExceptionHandler.class); |
||||
result.addAll(Arrays.asList(annot.value())); |
||||
} |
||||
|
||||
private void addExceptionMapping(Class<? extends Throwable> 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<? extends Exception> 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<? extends Exception> exceptionType) { |
||||
List<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>(); |
||||
for(Class<? extends Throwable> 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; |
||||
} |
||||
}; |
||||
|
||||
} |
||||
@ -1,63 +0,0 @@
@@ -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<Class<? extends Throwable>> 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; |
||||
} |
||||
}; |
||||
} |
||||
@ -0,0 +1,285 @@
@@ -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. |
||||
* |
||||
* <p>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 extends Annotation> A getMethodAnnotation(Class<A> 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 extends Annotation> T getMethodAnnotation(Class<T> 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(); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,73 @@
@@ -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<Method> selectMethods(final Class<?> handlerType, final MethodFilter handlerMethodFilter) { |
||||
final Set<Method> handlerMethods = new LinkedHashSet<Method>(); |
||||
Set<Class<?>> handlerTypes = new LinkedHashSet<Class<?>>(); |
||||
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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,40 @@
@@ -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 {}; |
||||
|
||||
} |
||||
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
/** |
||||
* Annotations and support classes for handling messages from simple messaging |
||||
* protocols (like STOMP). |
||||
*/ |
||||
package org.springframework.messaging.simp.annotation; |
||||
@ -1,128 +0,0 @@
@@ -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}. |
||||
* |
||||
* <p>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; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,122 @@
@@ -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. |
||||
* <p> |
||||
* 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<String> messagingTemplate; |
||||
|
||||
|
||||
public ReplyToMethodReturnValueHandler(MessageSendingOperations<String> 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<String> destinations = new ArrayList<String>(); |
||||
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(); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,100 @@
@@ -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}. |
||||
* |
||||
* <p>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<String> messagingTemplate; |
||||
|
||||
|
||||
public SubscriptionMethodReturnValueHandler(MessageSendingOperations<String> 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(); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,144 @@
@@ -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() { |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,196 @@
@@ -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<Message<?>> 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<String> 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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,150 @@
@@ -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<Message<?>> 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<String> 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; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue