提交 b8809daf 编写于 作者: R Rossen Stoyanchev

Refactor HandlerMethod support in spring-messaging

Introduce base class AbstractMethodMessageHandler for
HandlerMethod-based message handling.

Add MessageCondition interface for mapping conditions to messages
with support for combining type- and method-level annotation
conditions, the ability to match conditions to messages, and also
comparing matches to select the best match.

Issue: SPR-11024
上级 4892a270
......@@ -21,7 +21,7 @@ import org.springframework.util.ObjectUtils;
/**
* Simple test entity for use with caching tests.
*
* @author Michael Pld
* @author Michael Plod
*/
public class TestEntity {
......
......@@ -32,7 +32,7 @@ import org.springframework.messaging.Message;
* @author Rossen Stoyanchev
* @since 4.0
*
* @see org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler
* @see org.springframework.messaging.simp.handler.SimpAnnotationMethodMessageHandler
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
......
......@@ -32,7 +32,7 @@ import java.lang.annotation.Target;
*
* @author Brian Clozel
* @see org.springframework.messaging.handler.annotation.MessageMapping
* @see org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler
* @see org.springframework.messaging.simp.handler.SimpAnnotationMethodMessageHandler
*
* @since 4.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.*;
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.AbstractExceptionHandlerMethodResolver;
import org.springframework.messaging.handler.method.HandlerMethodSelector;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils.MethodFilter;
/**
* A sub-class of {@link AbstractExceptionHandlerMethodResolver} that looks for
* {@link MessageExceptionHandler}-annotated methods in a given class. The actual
* exception types handled are extracted either from the annotation, if present,
* or from the method signature as a fallback option.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class AnnotationExceptionHandlerMethodResolver extends AbstractExceptionHandlerMethodResolver {
/**
* A constructor that finds {@link MessageExceptionHandler} methods in the given type.
* @param handlerType the type to introspect
*/
public AnnotationExceptionHandlerMethodResolver(Class<?> handlerType) {
super(initExceptionMappings(handlerType));
}
private static Map<Class<? extends Throwable>, Method> initExceptionMappings(Class<?> handlerType) {
Map<Class<? extends Throwable>, Method> result = new HashMap<Class<? extends Throwable>, Method>();
for (Method method : HandlerMethodSelector.selectMethods(handlerType, EXCEPTION_HANDLER_METHOD_FILTER)) {
for (Class<? extends Throwable> exceptionType : getMappedExceptions(method)) {
Method oldMethod = result.put(exceptionType, method);
if (oldMethod != null && !oldMethod.equals(method)) {
throw new IllegalStateException(
"Ambiguous @ExceptionHandler method mapped for [" + exceptionType + "]: {" +
oldMethod + ", " + method + "}.");
}
}
}
return result;
}
private static List<Class<? extends Throwable>> getMappedExceptions(Method method) {
List<Class<? extends Throwable>> result = new ArrayList<Class<? extends Throwable>>();
MessageExceptionHandler annot = AnnotationUtils.findAnnotation(method, MessageExceptionHandler.class);
result.addAll(Arrays.asList(annot.value()));
if (result.isEmpty()) {
result.addAll(getExceptionsFromMethodSignature(method));
}
return result;
}
/** 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;
}
};
}
......@@ -23,7 +23,6 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.PathVariable;
import org.springframework.messaging.handler.annotation.ValueConstants;
import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler;
/**
* Resolves method parameters annotated with {@link PathVariable @PathVariable}.
......@@ -39,6 +38,9 @@ import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler
*/
public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
public static final String PATH_TEMPLATE_VARIABLES_HEADER =
PathVariableMethodArgumentResolver.class.getSimpleName() + ".templateVariables";
public PathVariableMethodArgumentResolver(ConversionService cs) {
super(cs, null);
......@@ -57,9 +59,8 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod
@Override
protected Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name) throws Exception {
String headerName = AnnotationMethodMessageHandler.PATH_TEMPLATE_VARIABLES_HEADER;
@SuppressWarnings("unchecked")
Map<String, String> vars = (Map<String, String>) message.getHeaders().get(headerName);
Map<String, String> vars = (Map<String, String>) message.getHeaders().get(PATH_TEMPLATE_VARIABLES_HEADER);
return (vars != null) ? vars.get(name) : null;
}
......
/*
* 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.condition;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import java.util.Collection;
import java.util.Iterator;
/**
* A base class for {@link MessageCondition} types providing implementations of
* {@link #equals(Object)}, {@link #hashCode()}, and {@link #toString()}.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public abstract class AbstractMessageCondition<T extends AbstractMessageCondition<T>> implements MessageCondition<T> {
/**
* @return the collection of objects the message condition is composed of
* (e.g. destination patterns), never {@code null}
*/
protected abstract Collection<?> getContent();
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o != null && getClass().equals(o.getClass())) {
AbstractMessageCondition<?> other = (AbstractMessageCondition<?>) o;
return getContent().equals(other.getContent());
}
return false;
}
@Override
public int hashCode() {
return getContent().hashCode();
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("[");
for (Iterator<?> iterator = getContent().iterator(); iterator.hasNext();) {
Object expression = iterator.next();
builder.append(expression.toString());
if (iterator.hasNext()) {
builder.append(getToStringInfix());
}
}
builder.append("]");
return builder.toString();
}
/**
* The notation to use when printing discrete items of content.
* For example " || " for URL patterns or " && " for param expressions.
*/
protected abstract String getToStringInfix();
}
/*
* 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.condition;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.method.AbstractMethodMessageHandler;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import java.util.*;
/**
* A {@link MessageCondition} for matching the destination of a Message against one or
* more destination patterns using a {@link PathMatcher}.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public final class DestinationPatternsMessageCondition
extends AbstractMessageCondition<DestinationPatternsMessageCondition> {
private final Set<String> patterns;
private final PathMatcher pathMatcher;
/**
* Creates a new instance with the given destination patterns.
* Each pattern that is not empty and does not start with "/" is prepended with "/".
* @param patterns 0 or more URL patterns; if 0 the condition will match to every request.
*/
public DestinationPatternsMessageCondition(String... patterns) {
this(patterns, null);
}
/**
* Additional constructor with flags for using suffix pattern (.*) and
* trailing slash matches.
*
* @param patterns the URL patterns to use; if 0, the condition will match to every request.
* @param pathMatcher for path matching with patterns
*/
public DestinationPatternsMessageCondition(String[] patterns,PathMatcher pathMatcher) {
this(asList(patterns), pathMatcher);
}
private DestinationPatternsMessageCondition(Collection<String> patterns, PathMatcher pathMatcher) {
this.patterns = Collections.unmodifiableSet(prependLeadingSlash(patterns));
this.pathMatcher = (pathMatcher != null) ? pathMatcher : new AntPathMatcher();
}
private static List<String> asList(String... patterns) {
return patterns != null ? Arrays.asList(patterns) : Collections.<String>emptyList();
}
private static Set<String> prependLeadingSlash(Collection<String> patterns) {
if (patterns == null) {
return Collections.emptySet();
}
Set<String> result = new LinkedHashSet<String>(patterns.size());
for (String pattern : patterns) {
if (StringUtils.hasLength(pattern) && !pattern.startsWith("/")) {
pattern = "/" + pattern;
}
result.add(pattern);
}
return result;
}
public Set<String> getPatterns() {
return this.patterns;
}
@Override
protected Collection<String> getContent() {
return this.patterns;
}
@Override
protected String getToStringInfix() {
return " || ";
}
/**
* Returns a new instance with URL patterns from the current instance ("this") and
* the "other" instance as follows:
* <ul>
* <li>If there are patterns in both instances, combine the patterns in "this" with
* the patterns in "other" using {@link org.springframework.util.PathMatcher#combine(String, String)}.
* <li>If only one instance has patterns, use them.
* <li>If neither instance has patterns, use an empty String (i.e. "").
* </ul>
*/
@Override
public DestinationPatternsMessageCondition combine(DestinationPatternsMessageCondition other) {
Set<String> result = new LinkedHashSet<String>();
if (!this.patterns.isEmpty() && !other.patterns.isEmpty()) {
for (String pattern1 : this.patterns) {
for (String pattern2 : other.patterns) {
result.add(this.pathMatcher.combine(pattern1, pattern2));
}
}
}
else if (!this.patterns.isEmpty()) {
result.addAll(this.patterns);
}
else if (!other.patterns.isEmpty()) {
result.addAll(other.patterns);
}
else {
result.add("");
}
return new DestinationPatternsMessageCondition(result, this.pathMatcher);
}
/**
* Check if any of the patterns match the given Message destination and return an instance
* that is guaranteed to contain matching patterns, sorted via
* {@link org.springframework.util.PathMatcher#getPatternComparator(String)}.
*
* @param message the message to match to
*
* @return the same instance if the condition contains no patterns;
* or a new condition with sorted matching patterns;
* or {@code null} either if a destination can not be extracted or there is no match
*/
@Override
public DestinationPatternsMessageCondition getMatchingCondition(Message<?> message) {
String destination = (String) message.getHeaders().get(AbstractMethodMessageHandler.LOOKUP_DESTINATION_HEADER);
if (destination == null) {
return null;
}
if (this.patterns.isEmpty()) {
return this;
}
List<String> matches = new ArrayList<String>();
for (String pattern : patterns) {
if (pattern.equals(destination) || this.pathMatcher.match(pattern, destination)) {
matches.add(pattern);
}
}
if (matches.isEmpty()) {
return null;
}
Collections.sort(matches, this.pathMatcher.getPatternComparator(destination));
return new DestinationPatternsMessageCondition(matches, this.pathMatcher);
}
/**
* Compare the two conditions based on the destination patterns they contain.
* Patterns are compared one at a time, from top to bottom via
* {@link org.springframework.util.PathMatcher#getPatternComparator(String)}.
* If all compared patterns match equally, but one instance has more patterns,
* it is considered a closer match.
*
* <p>It is assumed that both instances have been obtained via
* {@link #getMatchingCondition(Message)} to ensure they
* contain only patterns that match the request and are sorted with
* the best matches on top.
*/
@Override
public int compareTo(DestinationPatternsMessageCondition other, Message<?> message) {
String destination = (String) message.getHeaders().get(AbstractMethodMessageHandler.LOOKUP_DESTINATION_HEADER);
Comparator<String> patternComparator = this.pathMatcher.getPatternComparator(destination);
Iterator<String> iterator = patterns.iterator();
Iterator<String> iteratorOther = other.patterns.iterator();
while (iterator.hasNext() && iteratorOther.hasNext()) {
int result = patternComparator.compare(iterator.next(), iteratorOther.next());
if (result != 0) {
return result;
}
}
if (iterator.hasNext()) {
return -1;
}
else if (iteratorOther.hasNext()) {
return 1;
}
else {
return 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.condition;
import org.springframework.messaging.Message;
/**
* Contract for mapping conditions to messages.
*
* <p>Message conditions can be combined (e.g. type + method-level conditions),
* matched to a specific Message, as well as compared to each other in the
* context of a Message to determine which one matches a request more closely.
*
* @param <T> The kind of condition that this condition can be combined
* with or compared to
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public interface MessageCondition<T> {
/**
* Define the rules for combining this condition with another.
* For example combining type- and method-level conditions.
*
* @param other the condition to combine with
* @return the resulting message condition
*/
T combine(T other);
/**
* Check if this condition matches the given Message and returns a
* potentially new condition with content tailored to the current message.
* For example a condition with destination patterns might return a new
* condition with sorted, matching patterns only.
*
* @return a condition instance in case of a match;
* or {@code null} if there is no match.
*/
T getMatchingCondition(Message<?> message);
/**
* Compare this condition to another in the context of a specific message.
* It is assumed both instances have been obtained via
* {@link #getMatchingCondition(Message)} to ensure they have content
* relevant to current message only.
*/
int compareTo(T other, Message<?> message);
}
......@@ -14,94 +14,59 @@
* 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;
package org.springframework.messaging.handler.method;
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;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 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.
* Cache exception handling method mappings and provide options to look up a method
* that should handle an exception. If multiple methods match, they are sorted using
* {@link ExceptionDepthComparator} and the top match is returned.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class ExceptionHandlerMethodResolver {
public abstract class AbstractExceptionHandlerMethodResolver {
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> mappedMethods = new ConcurrentHashMap<Class<? extends Throwable>, Method>(16);
private final Map<Class<? extends Throwable>, Method> exceptionLookupCache =
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
* Protected constructor accepting exception-to-method mappings.
*/
public ExceptionHandlerMethodResolver(Class<?> handlerType) {
for (Method method : HandlerMethodSelector.selectMethods(handlerType, EXCEPTION_HANDLER_METHOD_FILTER)) {
for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
addExceptionMapping(exceptionType, method);
}
}
protected AbstractExceptionHandlerMethodResolver(Map<Class<? extends Throwable>, Method> mappedMethods) {
Assert.notNull(mappedMethods, "'mappedMethods' is required");
this.mappedMethods.putAll(mappedMethods);
}
/**
* Extract exception mappings from the {@code @ExceptionHandler} annotation
* first and as a fall-back from the method signature.
* Extract the exceptions this method handles.This implementation looks for
* sub-classes of Throwable in the method signature.
* The method is static to ensure safe use from sub-class constructors.
*/
@SuppressWarnings("unchecked")
private List<Class<? extends Throwable>> detectExceptionMappings(Method method) {
protected static List<Class<? extends Throwable>> getExceptionsFromMethodSignature(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);
}
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.
*/
......@@ -111,7 +76,7 @@ public class ExceptionHandlerMethodResolver {
/**
* Find a method to handle the given exception.
* Use {@link ExceptionDepthComparator} if more than one match is found.
* Use {@link org.springframework.core.ExceptionDepthComparator} if more than one match is found.
* @param exception the exception
* @return a method to handle the exception or {@code null}
*/
......@@ -137,21 +102,11 @@ public class ExceptionHandlerMethodResolver {
}
if (!matches.isEmpty()) {
Collections.sort(matches, new ExceptionDepthComparator(exceptionType));
return mappedMethods.get(matches.get(0));
return this.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;
}
};
}
/*
* 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 org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.MethodParameter;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.*;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Abstract base class for HandlerMethod-based message handling. Provides most of
* the logic required to discover handler methods at startup, find a matching handler
* method at runtime for a given message and invoke it.
* <p>
* Also supports discovering and invoking exception handling methods to process
* exceptions raised during message handling.
*
* @param <T> the type of the Object that contains information mapping
* a {@link HandlerMethod} to incoming messages
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public abstract class AbstractMethodMessageHandler<T>
implements MessageHandler, ApplicationContextAware, InitializingBean {
protected final Log logger = LogFactory.getLog(getClass());
public static final String LOOKUP_DESTINATION_HEADER = "lookupDestination";
private Collection<String> destinationPrefixes = new ArrayList<String>();
private List<HandlerMethodArgumentResolver> customArgumentResolvers = new ArrayList<HandlerMethodArgumentResolver>();
private List<HandlerMethodReturnValueHandler> customReturnValueHandlers = new ArrayList<HandlerMethodReturnValueHandler>();
private HandlerMethodArgumentResolverComposite argumentResolvers = new HandlerMethodArgumentResolverComposite();
private HandlerMethodReturnValueHandlerComposite returnValueHandlers =new HandlerMethodReturnValueHandlerComposite();
private ApplicationContext applicationContext;
private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<T, HandlerMethod>();
private final MultiValueMap<String, T> destinationLookup = new LinkedMultiValueMap<String, T>();
private final Map<Class<?>, AbstractExceptionHandlerMethodResolver> exceptionHandlerCache =
new ConcurrentHashMap<Class<?>, AbstractExceptionHandlerMethodResolver>(64);
/**
* Configure one or more prefixes to match to the destinations of handled messages.
* Messages whose destination does not start with one of the configured prefixes
* are ignored. When a destination matches one of the configured prefixes, the
* matching part is removed from destination before performing a lookup for a matching
* message handling method. Prefixes without a trailing slash will have one appended
* automatically.
* <p>
* By default the list of prefixes is empty in which case all destinations match.
*/
public void setDestinationPrefixes(Collection<String> prefixes) {
this.destinationPrefixes.clear();
if (prefixes != null) {
for (String prefix : prefixes) {
prefix = prefix.trim();
if (!prefix.endsWith("/")) {
prefix += "/";
}
this.destinationPrefixes.add(prefix);
}
}
}
public Collection<String> getDestinationPrefixes() {
return this.destinationPrefixes;
}
/**
* Sets the list of custom {@code HandlerMethodArgumentResolver}s that will be used
* after resolvers for supported argument type.
*
* @param customArgumentResolvers the list of resolvers; never {@code null}.
*/
public void setCustomArgumentResolvers(List<HandlerMethodArgumentResolver> customArgumentResolvers) {
Assert.notNull(customArgumentResolvers, "The 'customArgumentResolvers' cannot be null.");
this.customArgumentResolvers = customArgumentResolvers;
}
public List<HandlerMethodArgumentResolver> getCustomArgumentResolvers() {
return this.customArgumentResolvers;
}
/**
* Set the list of custom {@code HandlerMethodReturnValueHandler}s that will be used
* after return value handlers for known types.
*
* @param customReturnValueHandlers the list of custom return value handlers, never {@code null}.
*/
public void setCustomReturnValueHandlers(List<HandlerMethodReturnValueHandler> customReturnValueHandlers) {
Assert.notNull(customReturnValueHandlers, "The 'customReturnValueHandlers' cannot be null.");
this.customReturnValueHandlers = customReturnValueHandlers;
}
public List<HandlerMethodReturnValueHandler> getCustomReturnValueHandlers() {
return this.customReturnValueHandlers;
}
/**
* Configure the complete list of supported argument types effectively overriding
* the ones configured by default. This is an advanced option. For most use cases
* it should be sufficient to use {@link #setCustomArgumentResolvers(java.util.List)}.
*/
public void setArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
if (argumentResolvers == null) {
this.argumentResolvers.clear();
return;
}
this.argumentResolvers.addResolvers(argumentResolvers);
}
public List<HandlerMethodArgumentResolver> getArgumentResolvers() {
return this.argumentResolvers.getResolvers();
}
/**
* Configure the complete list of supported return value types effectively overriding
* the ones configured by default. This is an advanced option. For most use cases
* it should be sufficient to use {@link #setCustomReturnValueHandlers(java.util.List)}
*/
public void setReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
if (returnValueHandlers == null) {
this.returnValueHandlers.clear();
return;
}
this.returnValueHandlers.addHandlers(returnValueHandlers);
}
public List<HandlerMethodReturnValueHandler> getReturnValueHandlers() {
return this.returnValueHandlers.getReturnValueHandlers();
}
/**
* Return a map with all handler methods and their mappings.
*/
public Map<T, HandlerMethod> getHandlerMethods() {
return Collections.unmodifiableMap(this.handlerMethods);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public ApplicationContext getApplicationContext() {
return this.applicationContext;
}
@Override
public void afterPropertiesSet() {
if (this.argumentResolvers.getResolvers().isEmpty()) {
this.argumentResolvers.addResolvers(initArgumentResolvers());
}
if (this.returnValueHandlers.getReturnValueHandlers().isEmpty()) {
this.returnValueHandlers.addHandlers(initReturnValueHandlers());
}
for (String beanName : this.applicationContext.getBeanNamesForType(Object.class)) {
if (isHandler(this.applicationContext.getType(beanName))){
detectHandlerMethods(beanName);
}
}
}
/**
* Return the list of argument resolvers to use. Invoked only if the resolvers
* have not already been set via {@link #setArgumentResolvers(java.util.List)}.
* <p>
* Sub-classes should also take into account custom argument types configured via
* {@link #setCustomArgumentResolvers(java.util.List)}.
*/
protected abstract List<? extends HandlerMethodArgumentResolver> initArgumentResolvers();
/**
* Return the list of return value handlers to use. Invoked only if the return
* value handlers have not already been set via {@link #setReturnValueHandlers(java.util.List)}.
* <p>
* Sub-classes should also take into account custom return value types configured via
* {@link #setCustomReturnValueHandlers(java.util.List)}.
*/
protected abstract List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers();
/**
* Whether the given bean type should be introspected for messaging handling methods.
*/
protected abstract boolean isHandler(Class<?> beanType);
/**
* Detect if the given handler has any methods that can handle messages and if
* so register it with the extracted mapping information.
*
* @param handler the handler to check, either an instance of a Spring bean name
*/
protected final void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String) ?
this.applicationContext.getType((String) handler) : handler.getClass();
final Class<?> userType = ClassUtils.getUserClass(handlerType);
Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new ReflectionUtils.MethodFilter() {
@Override
public boolean matches(Method method) {
return getMappingForMethod(method, userType) != null;
}
});
for (Method method : methods) {
T mapping = getMappingForMethod(method, userType);
registerHandlerMethod(handler, method, mapping);
}
}
/**
* Provide the mapping for a handler method.
*
* @param method the method to provide a mapping for
* @param handlerType the handler type, possibly a sub-type of the method's declaring class
*
* @return the mapping, or {@code null} if the method is not mapped
*/
protected abstract T getMappingForMethod(Method method, Class<?> handlerType);
/**
* Register a handler method and its unique mapping.
*
* @param handler the bean name of the handler or the handler instance
* @param method the method to register
* @param mapping the mapping conditions associated with the handler method
*
* @throws IllegalStateException if another method was already registered
* under the same mapping
*/
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
HandlerMethod newHandlerMethod = createHandlerMethod(handler, method);
HandlerMethod oldHandlerMethod = handlerMethods.get(mapping);
if (oldHandlerMethod != null && !oldHandlerMethod.equals(newHandlerMethod)) {
throw new IllegalStateException("Ambiguous mapping found. Cannot map '" + newHandlerMethod.getBean()
+ "' bean method \n" + newHandlerMethod + "\nto " + mapping + ": There is already '"
+ oldHandlerMethod.getBean() + "' bean method\n" + oldHandlerMethod + " mapped.");
}
this.handlerMethods.put(mapping, newHandlerMethod);
if (logger.isInfoEnabled()) {
logger.info("Mapped \"" + mapping + "\" onto " + newHandlerMethod);
}
for (String pattern : getDirectLookupDestinations(mapping)) {
this.destinationLookup.add(pattern, mapping);
}
}
/**
* Create a HandlerMethod instance from an Object handler that is either a handler
* instance or a String-based bean name.
*/
protected HandlerMethod createHandlerMethod(Object handler, Method method) {
HandlerMethod handlerMethod;
if (handler instanceof String) {
String beanName = (String) handler;
handlerMethod = new HandlerMethod(beanName, this.applicationContext, method);
}
else {
handlerMethod = new HandlerMethod(handler, method);
}
return handlerMethod;
}
/**
* Return destinations contained in the mapping that are not patterns and are
* therefore suitable for direct lookups.
*/
protected abstract Set<String> getDirectLookupDestinations(T mapping);
@Override
public void handleMessage(Message<?> message) throws MessagingException {
String destination = getDestination(message);
String lookupDestination = getLookupDestination(destination);
if (lookupDestination == null) {
if (logger.isTraceEnabled()) {
logger.trace("Ignoring message with destination=" + destination);
}
return;
}
if (logger.isTraceEnabled()) {
logger.trace("Handling message " + message);
}
message = MessageBuilder.fromMessage(message).setHeader(LOOKUP_DESTINATION_HEADER, lookupDestination).build();
handleMessageInternal(message, lookupDestination);
}
protected abstract String getDestination(Message<?> message);
/**
* Find if the given destination matches any of the configured allowed destination
* prefixes and if a match is found return the destination with the prefix removed.
* <p>
* If no destination prefixes are configured, the destination is returned as is.
*
* @return the destination to use to find matching message handling methods
* or {@code null} if the destination does not match
*/
protected String getLookupDestination(String destination) {
if (destination == null) {
return null;
}
if (CollectionUtils.isEmpty(this.destinationPrefixes)) {
return destination;
}
for (String prefix : this.destinationPrefixes) {
if (destination.startsWith(prefix)) {
return destination.substring(prefix.length() - 1);
}
}
return null;
}
protected void handleMessageInternal(Message<?> message, String lookupDestination) {
List<Match> matches = new ArrayList<Match>();
List<T> mappingsByUrl = this.destinationLookup.get(lookupDestination);
if (mappingsByUrl != null) {
addMatchesToCollection(mappingsByUrl, message, matches);
}
if (matches.isEmpty()) {
// No direct hits, go through all mappings
Set<T> allMappings = this.handlerMethods.keySet();
addMatchesToCollection(allMappings, message, matches);
}
if (matches.isEmpty()) {
handleNoMatch(handlerMethods.keySet(), lookupDestination, message);
return;
}
Comparator<Match> comparator = new MatchComparator(getMappingComparator(message));
Collections.sort(matches, comparator);
if (logger.isTraceEnabled()) {
logger.trace("Found " + matches.size() + " matching mapping(s) for ["
+ lookupDestination + "] : " + matches);
}
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
throw new IllegalStateException(
"Ambiguous handler methods mapped for destination '"
+ lookupDestination + "': {" + m1 + ", " + m2 + "}");
}
}
handleMatch(bestMatch.mapping, bestMatch.handlerMethod, lookupDestination, message);
}
private void addMatchesToCollection(Collection<T> mappingsToCheck, Message<?> message, List<Match> matches) {
for (T mapping : mappingsToCheck) {
T match = getMatchingMapping(mapping, message);
if (match != null) {
matches.add(new Match(match, handlerMethods.get(mapping)));
}
}
}
/**
* Check if a mapping matches the current message and return a possibly
* new mapping with conditions relevant to the current request.
*
* @param mapping the mapping to get a match for
* @param message the message being handled
*
* @return the match or {@code null} if there is no match
*/
protected abstract T getMatchingMapping(T mapping, Message<?> message);
/**
* Return a comparator for sorting matching mappings.
* The returned comparator should sort 'better' matches higher.
*
* @param message the current Message
* @return the comparator, never {@code null}
*/
protected abstract Comparator<T> getMappingComparator(Message<?> message);
protected void handleMatch(T mapping, HandlerMethod handlerMethod, String lookupDestination, Message<?> message) {
InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod.createWithResolvedBean());
invocable.setMessageMethodArgumentResolvers(this.argumentResolvers);
try {
Object returnValue = invocable.invoke(message);
MethodParameter returnType = handlerMethod.getReturnType();
if (void.class.equals(returnType.getParameterType())) {
return;
}
this.returnValueHandlers.handleReturnValue(returnValue, returnType, message);
}
catch (Exception ex) {
processHandlerMethodException(handlerMethod, ex, message);
}
catch (Throwable ex) {
logger.error("Error while processing message " + message, ex);
}
}
protected void processHandlerMethodException(HandlerMethod handlerMethod, Exception ex, Message<?> message) {
Class<?> beanType = handlerMethod.getBeanType();
AbstractExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(beanType);
if (resolver == null) {
resolver = createExceptionHandlerMethodResolverFor(beanType);
this.exceptionHandlerCache.put(beanType, resolver);
}
Method method = resolver.resolveMethod(ex);
if (method == null) {
logger.error("Unhandled exception", ex);
return;
}
InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod.getBean(), method);
invocable.setMessageMethodArgumentResolvers(this.argumentResolvers);
try {
Object returnValue = invocable.invoke(message, ex);
MethodParameter returnType = invocable.getReturnType();
if (void.class.equals(returnType.getParameterType())) {
return;
}
this.returnValueHandlers.handleReturnValue(returnValue, returnType, message);
}
catch (Throwable t) {
logger.error("Error while handling exception", t);
return;
}
}
protected abstract AbstractExceptionHandlerMethodResolver createExceptionHandlerMethodResolverFor(Class<?> beanType);
protected abstract void handleNoMatch(Set<T> ts, String lookupDestination, Message<?> message);
/**
* A thin wrapper around a matched HandlerMethod and its matched mapping for
* the purpose of comparing the best match with a comparator in the context
* of a message.
*/
private class Match {
private final T mapping;
private final HandlerMethod handlerMethod;
private Match(T mapping, HandlerMethod handlerMethod) {
this.mapping = mapping;
this.handlerMethod = handlerMethod;
}
@Override
public String toString() {
return this.mapping.toString();
}
}
private class MatchComparator implements Comparator<Match> {
private final Comparator<T> comparator;
public MatchComparator(Comparator<T> comparator) {
this.comparator = comparator;
}
@Override
public int compare(Match match1, Match match2) {
return this.comparator.compare(match1.mapping, match2.mapping);
}
}
}
......@@ -40,7 +40,7 @@ public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgu
protected final Log logger = LogFactory.getLog(getClass());
private final List<HandlerMethodArgumentResolver> argumentResolvers = new LinkedList<HandlerMethodArgumentResolver>();
private final List<HandlerMethodArgumentResolver> argumentResolvers = new LinkedList<HandlerMethodArgumentResolver>();
private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache =
new ConcurrentHashMap<MethodParameter, HandlerMethodArgumentResolver>(256);
......@@ -53,6 +53,13 @@ public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgu
return Collections.unmodifiableList(this.argumentResolvers);
}
/**
* Clear the list of configured resolvers.
*/
public void clear() {
this.argumentResolvers.clear();
}
/**
* Whether the given {@linkplain MethodParameter method parameter} is supported by any registered
* {@link HandlerMethodArgumentResolver}.
......
......@@ -17,6 +17,7 @@
package org.springframework.messaging.handler.method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.logging.Log;
......@@ -37,6 +38,20 @@ public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodRe
private final List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList<HandlerMethodReturnValueHandler>();
/**
* Return a read-only list with the configured handlers.
*/
public List<HandlerMethodReturnValueHandler> getReturnValueHandlers() {
return Collections.unmodifiableList(this.returnValueHandlers);
}
/**
* Clear the list of configured handlers.
*/
public void clear() {
this.returnValueHandlers.clear();
}
/**
* Add the given {@link HandlerMethodReturnValueHandler}.
*/
......
......@@ -39,17 +39,17 @@ import org.springframework.util.Assert;
*/
public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor {
public static final String CONNECT_MESSAGE_HEADER = "connectMessage";
public static final String CONNECT_MESSAGE_HEADER = "simpConnectMessage";
public static final String DESTINATION_HEADER = "destination";
public static final String DESTINATION_HEADER = "simpDestination";
public static final String MESSAGE_TYPE_HEADER = "messageType";
public static final String MESSAGE_TYPE_HEADER = "simpMessageType";
public static final String SESSION_ID_HEADER = "sessionId";
public static final String SESSION_ID_HEADER = "simpSessionId";
public static final String SUBSCRIPTION_ID_HEADER = "subscriptionId";
public static final String SUBSCRIPTION_ID_HEADER = "simpSubscriptionId";
public static final String USER_HEADER = "user";
public static final String USER_HEADER = "simpUser";
/**
......
......@@ -25,11 +25,8 @@ import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.handler.websocket.SubProtocolWebSocketHandler;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.handler.AbstractBrokerMessageHandler;
import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler;
import org.springframework.messaging.simp.handler.MutableUserQueueSuffixResolver;
import org.springframework.messaging.simp.handler.SimpleUserQueueSuffixResolver;
import org.springframework.messaging.simp.handler.UserDestinationMessageHandler;
import org.springframework.messaging.simp.handler.*;
import org.springframework.messaging.simp.handler.SimpAnnotationMethodMessageHandler;
import org.springframework.messaging.support.channel.ExecutorSubscribableChannel;
import org.springframework.messaging.support.converter.ByteArrayMessageConverter;
import org.springframework.messaging.support.converter.CompositeMessageConverter;
......@@ -139,9 +136,9 @@ public abstract class WebSocketMessageBrokerConfigurationSupport {
// Handling of messages by the application
@Bean
public AnnotationMethodMessageHandler annotationMethodMessageHandler() {
AnnotationMethodMessageHandler handler =
new AnnotationMethodMessageHandler(brokerMessagingTemplate(), webSocketResponseChannel());
public SimpAnnotationMethodMessageHandler annotationMethodMessageHandler() {
SimpAnnotationMethodMessageHandler handler =
new SimpAnnotationMethodMessageHandler(brokerMessagingTemplate(), webSocketResponseChannel());
handler.setDestinationPrefixes(getMessageBrokerConfigurer().getAnnotationMethodDestinationPrefixes());
handler.setMessageConverter(simpMessageConverter());
webSocketRequestChannel().subscribe(handler);
......
/*
* 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.handler;
import java.lang.reflect.Method;
import java.util.*;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.core.AbstractMessageSendingTemplate;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.support.*;
import org.springframework.messaging.handler.condition.DestinationPatternsMessageCondition;
import org.springframework.messaging.handler.method.*;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SubscribeEvent;
import org.springframework.messaging.simp.annotation.support.PrincipalMethodArgumentResolver;
import org.springframework.messaging.simp.annotation.support.SendToMethodReturnValueHandler;
import org.springframework.messaging.simp.annotation.support.SubscriptionMethodReturnValueHandler;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.messaging.support.converter.ByteArrayMessageConverter;
import org.springframework.messaging.support.converter.CompositeMessageConverter;
import org.springframework.messaging.support.converter.MessageConverter;
import org.springframework.messaging.support.converter.StringMessageConverter;
import org.springframework.stereotype.Controller;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.PathMatcher;
/**
* A handler for messages delegating to {@link SubscribeEvent @SubscribeEvent} and
* {@link MessageMapping @MessageMapping} annotated methods.
* <p>
* Supports Ant-style path patterns as well as URI template variables in destinations.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 4.0
*/
public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHandler<SimpMessageMappingInfo> {
private final SimpMessageSendingOperations brokerTemplate;
private final SimpMessageSendingOperations webSocketResponseTemplate;
private MessageConverter messageConverter;
private ConversionService conversionService = new DefaultFormattingConversionService();
private PathMatcher pathMatcher = new AntPathMatcher();
/**
* @param brokerTemplate a messaging template to send application messages to the broker
* @param webSocketResponseChannel the channel for messages to WebSocket clients
*/
public SimpAnnotationMethodMessageHandler(SimpMessageSendingOperations brokerTemplate,
MessageChannel webSocketResponseChannel) {
Assert.notNull(brokerTemplate, "brokerTemplate is required");
Assert.notNull(webSocketResponseChannel, "webSocketReplyChannel is required");
this.brokerTemplate = brokerTemplate;
this.webSocketResponseTemplate = new SimpMessagingTemplate(webSocketResponseChannel);
Collection<MessageConverter> converters = new ArrayList<MessageConverter>();
converters.add(new StringMessageConverter());
converters.add(new ByteArrayMessageConverter());
this.messageConverter = new CompositeMessageConverter(converters);
}
/**
* Configure a {@link MessageConverter} to use to convert the payload of a message
* from serialize form with a specific MIME type to an Object matching the target
* method parameter. The converter is also used when sending message to the message
* broker.
*
* @see CompositeMessageConverter
*/
public void setMessageConverter(MessageConverter converter) {
this.messageConverter = converter;
if (converter != null) {
((AbstractMessageSendingTemplate<?>) this.webSocketResponseTemplate).setMessageConverter(converter);
}
}
/**
* Return the configured {@link MessageConverter}.
*/
public MessageConverter getMessageConverter() {
return this.messageConverter;
}
/**
* Configure a {@link ConversionService} to use when resolving method arguments, for
* example message header values.
* <p>
* By default an instance of {@link DefaultFormattingConversionService} is used.
*/
public void setConversionService(ConversionService conversionService) {
this.conversionService = conversionService;
}
/**
* The configured {@link ConversionService}.
*/
public ConversionService getConversionService() {
return this.conversionService;
}
/**
* Set the PathMatcher implementation to use for matching destinations
* against configured destination patterns.
* <p>
* By default AntPathMatcher is used
*/
public void setPathMatcher(PathMatcher pathMatcher) {
Assert.notNull(pathMatcher, "PathMatcher must not be null");
this.pathMatcher = pathMatcher;
}
/**
* Return the PathMatcher implementation to use for matching destinations
*/
public PathMatcher getPathMatcher() {
return this.pathMatcher;
}
protected List<HandlerMethodArgumentResolver> initArgumentResolvers() {
ConfigurableBeanFactory beanFactory =
(ClassUtils.isAssignableValue(ConfigurableApplicationContext.class, getApplicationContext())) ?
((ConfigurableApplicationContext) getApplicationContext()).getBeanFactory() : null;
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();
// Annotation-based argument resolution
resolvers.add(new HeaderMethodArgumentResolver(this.conversionService, beanFactory));
resolvers.add(new HeadersMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver(this.conversionService));
// Type-based argument resolution
resolvers.add(new PrincipalMethodArgumentResolver());
resolvers.add(new MessageMethodArgumentResolver());
resolvers.addAll(getCustomArgumentResolvers());
resolvers.add(new PayloadArgumentResolver(this.messageConverter));
return resolvers;
}
@Override
protected List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers() {
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<HandlerMethodReturnValueHandler>();
// Annotation-based return value types
handlers.add(new SendToMethodReturnValueHandler(this.brokerTemplate, true));
handlers.add(new SubscriptionMethodReturnValueHandler(this.webSocketResponseTemplate));
// custom return value types
handlers.addAll(getCustomReturnValueHandlers());
// catch-all
handlers.add(new SendToMethodReturnValueHandler(this.brokerTemplate, false));
return handlers;
}
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotationUtils.findAnnotation(beanType, Controller.class) != null);
}
@Override
protected SimpMessageMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
MessageMapping messageMappingAnnot = AnnotationUtils.findAnnotation(method, MessageMapping.class);
if (messageMappingAnnot != null) {
SimpMessageMappingInfo result = createMessageMappingCondition(messageMappingAnnot);
MessageMapping typeAnnot = AnnotationUtils.findAnnotation(handlerType, MessageMapping.class);
if (typeAnnot != null) {
result = createMessageMappingCondition(typeAnnot).combine(result);
}
return result;
}
SubscribeEvent subsribeAnnot = AnnotationUtils.findAnnotation(method, SubscribeEvent.class);
if (subsribeAnnot != null) {
SimpMessageMappingInfo result = createSubscribeCondition(subsribeAnnot);
SubscribeEvent typeAnnot = AnnotationUtils.findAnnotation(handlerType, SubscribeEvent.class);
if (typeAnnot != null) {
result = createSubscribeCondition(typeAnnot).combine(result);
}
return result;
}
return null;
}
private SimpMessageMappingInfo createMessageMappingCondition(MessageMapping annotation) {
return new SimpMessageMappingInfo(SimpMessageTypeMessageCondition.MESSAGE,
new DestinationPatternsMessageCondition(annotation.value()));
}
private SimpMessageMappingInfo createSubscribeCondition(SubscribeEvent annotation) {
return new SimpMessageMappingInfo(SimpMessageTypeMessageCondition.SUBSCRIBE,
new DestinationPatternsMessageCondition(annotation.value()));
}
@Override
protected Set<String> getDirectLookupDestinations(SimpMessageMappingInfo mapping) {
Set<String> result = new LinkedHashSet<String>();
for (String s : mapping.getDestinationConditions().getPatterns()) {
if (!this.pathMatcher.isPattern(s)) {
result.add(s);
}
}
return result;
}
@Override
protected String getDestination(Message<?> message) {
return (String) message.getHeaders().get(SimpMessageHeaderAccessor.DESTINATION_HEADER);
}
@Override
protected SimpMessageMappingInfo getMatchingMapping(SimpMessageMappingInfo mapping, Message<?> message) {
return mapping.getMatchingCondition(message);
}
@Override
protected Comparator getMappingComparator(final Message<?> message) {
return new Comparator<SimpMessageMappingInfo>() {
@Override
public int compare(SimpMessageMappingInfo info1, SimpMessageMappingInfo info2) {
return info1.compareTo(info2, message);
}
};
}
@Override
protected void handleMatch(SimpMessageMappingInfo mapping, HandlerMethod handlerMethod,
String lookupDestination, Message<?> message) {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(message);
String matchedPattern = mapping.getDestinationConditions().getPatterns().iterator().next();
Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(matchedPattern, lookupDestination);
headers.setDestination(lookupDestination);
headers.setHeader(PathVariableMethodArgumentResolver.PATH_TEMPLATE_VARIABLES_HEADER, vars);
message = MessageBuilder.withPayload(message.getPayload()).setHeaders(headers).build();
super.handleMatch(mapping, handlerMethod, lookupDestination, message);
}
@Override
protected void handleNoMatch(Set<SimpMessageMappingInfo> set, String lookupDestination, Message<?> message) {
if (logger.isTraceEnabled()) {
logger.trace("No match for " + lookupDestination);
}
}
@Override
protected AbstractExceptionHandlerMethodResolver createExceptionHandlerMethodResolverFor(Class<?> beanType) {
return new AnnotationExceptionHandlerMethodResolver(beanType);
}
}
\ No newline at end of file
/*
* 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.handler;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.condition.DestinationPatternsMessageCondition;
import org.springframework.messaging.handler.condition.MessageCondition;
/**
* Encapsulates the following request mapping conditions:
* <ol>
* <li>{@link SimpMessageTypeMessageCondition}
* <li>{@link DestinationPatternsMessageCondition}
* </ol>
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class SimpMessageMappingInfo implements MessageCondition<SimpMessageMappingInfo> {
private final SimpMessageTypeMessageCondition messageTypeMessageCondition;
private final DestinationPatternsMessageCondition destinationConditions;
private int hash;
public SimpMessageMappingInfo(SimpMessageTypeMessageCondition messageTypeMessageCondition,
DestinationPatternsMessageCondition destinationConditions) {
this.messageTypeMessageCondition = messageTypeMessageCondition;
this.destinationConditions = destinationConditions;
}
public SimpMessageTypeMessageCondition getMessageTypeMessageCondition() {
return this.messageTypeMessageCondition;
}
public DestinationPatternsMessageCondition getDestinationConditions() {
return this.destinationConditions;
}
@Override
public SimpMessageMappingInfo combine(SimpMessageMappingInfo other) {
SimpMessageTypeMessageCondition typeCond =
this.getMessageTypeMessageCondition().combine(other.getMessageTypeMessageCondition());
DestinationPatternsMessageCondition destCond =
this.destinationConditions.combine(other.getDestinationConditions());
return new SimpMessageMappingInfo(typeCond, destCond);
}
@Override
public SimpMessageMappingInfo getMatchingCondition(Message<?> message) {
SimpMessageTypeMessageCondition typeCond = this.messageTypeMessageCondition.getMatchingCondition(message);
if (typeCond == null) {
return null;
}
DestinationPatternsMessageCondition destCond = this.destinationConditions.getMatchingCondition(message);
if (destCond == null) {
return null;
}
return new SimpMessageMappingInfo(typeCond, destCond);
}
@Override
public int compareTo(SimpMessageMappingInfo other, Message<?> message) {
int result = this.messageTypeMessageCondition.compareTo(other.messageTypeMessageCondition, message);
if (result != 0) {
return result;
}
result = this.destinationConditions.compareTo(other.destinationConditions, message);
if (result != 0) {
return result;
}
return 0;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj != null && obj instanceof SimpMessageMappingInfo) {
SimpMessageMappingInfo other = (SimpMessageMappingInfo) obj;
return this.destinationConditions.equals(other.destinationConditions);
}
return false;
}
@Override
public int hashCode() {
int result = hash;
if (result == 0) {
result = destinationConditions.hashCode();
result = 31 * result + messageTypeMessageCondition.hashCode();
hash = result;
}
return result;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("{");
builder.append(this.destinationConditions);
builder.append(",messageType=").append(this.messageTypeMessageCondition);
builder.append('}');
return builder.toString();
}
}
/*
* 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.handler;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.condition.AbstractMessageCondition;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.util.Assert;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
/**
* A message condition that checks the message type.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class SimpMessageTypeMessageCondition extends AbstractMessageCondition<SimpMessageTypeMessageCondition> {
public static final SimpMessageTypeMessageCondition MESSAGE =
new SimpMessageTypeMessageCondition(SimpMessageType.MESSAGE);
public static final SimpMessageTypeMessageCondition SUBSCRIBE =
new SimpMessageTypeMessageCondition(SimpMessageType.SUBSCRIBE);
private final SimpMessageType messageType;
/**
* A constructor accepting a message type.
*/
public SimpMessageTypeMessageCondition() {
this.messageType = null;
}
/**
* A constructor accepting a message type.
*
* @param messageType the message type to match messages to
*/
public SimpMessageTypeMessageCondition(SimpMessageType messageType) {
Assert.notNull(messageType, "'messageType' is required");
this.messageType = messageType;
}
public SimpMessageType getMessageType() {
return this.messageType;
}
@Override
protected Collection<?> getContent() {
return (this.messageType != null) ? Arrays.asList(messageType) : Collections.emptyList();
}
@Override
protected String getToStringInfix() {
return " || ";
}
@Override
public SimpMessageTypeMessageCondition combine(SimpMessageTypeMessageCondition other) {
return (this.messageType != null) ? this : other;
}
@Override
public SimpMessageTypeMessageCondition getMatchingCondition(Message<?> message) {
Object actualMessageType = message.getHeaders().get(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER);
if (actualMessageType == null) {
return null;
}
return ((this.messageType != null) && this.messageType.equals(actualMessageType)) ? this : null;
}
@Override
public int compareTo(SimpMessageTypeMessageCondition other, Message<?> message) {
if ((this.messageType == null) && (other.messageType == null)) {
return 0;
}
if (this.messageType == null) {
return 1;
}
if (other.messageType == null) {
return -1;
}
return 0;
}
}
......@@ -30,29 +30,29 @@ import static org.junit.Assert.*;
/**
* Test fixture for {@link ExceptionHandlerMethodResolver} tests.
* Test fixture for {@link AnnotationExceptionHandlerMethodResolver} tests.
*
* @author Rossen Stoyanchev
*/
public class ExceptionHandlerMethodResolverTests {
public class AnnotationExceptionHandlerMethodResolverTests {
@Test
public void resolveMethodFromAnnotation() {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class);
IOException exception = new IOException();
assertEquals("handleIOException", resolver.resolveMethod(exception).getName());
}
@Test
public void resolveMethodFromArgument() {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class);
IllegalArgumentException exception = new IllegalArgumentException();
assertEquals("handleIllegalArgumentException", resolver.resolveMethod(exception).getName());
}
@Test
public void resolveMethodExceptionSubType() {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class);
IOException ioException = new FileNotFoundException();
assertEquals("handleIOException", resolver.resolveMethod(ioException).getName());
SocketException bindException = new BindException();
......@@ -61,14 +61,14 @@ public class ExceptionHandlerMethodResolverTests {
@Test
public void resolveMethodBestMatch() {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class);
SocketException exception = new SocketException();
assertEquals("handleSocketException", resolver.resolveMethod(exception).getName());
}
@Test
public void resolveMethodNoMatch() {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class);
Exception exception = new Exception();
assertNull("1st lookup", resolver.resolveMethod(exception));
assertNull("2nd lookup from cache", resolver.resolveMethod(exception));
......@@ -76,19 +76,19 @@ public class ExceptionHandlerMethodResolverTests {
@Test
public void resolveMethodInherited() {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(InheritedController.class);
AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(InheritedController.class);
IOException exception = new IOException();
assertEquals("handleIOException", resolver.resolveMethod(exception).getName());
}
@Test(expected = IllegalStateException.class)
public void ambiguousExceptionMapping() {
new ExceptionHandlerMethodResolver(AmbiguousController.class);
new AnnotationExceptionHandlerMethodResolver(AmbiguousController.class);
}
@Test(expected = IllegalArgumentException.class)
public void noExceptionMapping() {
new ExceptionHandlerMethodResolver(NoExceptionController.class);
new AnnotationExceptionHandlerMethodResolver(NoExceptionController.class);
}
@Controller
......
......@@ -28,7 +28,7 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.PathVariable;
import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler;
import org.springframework.messaging.simp.handler.SimpAnnotationMethodMessageHandler;
import org.springframework.messaging.support.MessageBuilder;
import static org.junit.Assert.*;
......@@ -74,7 +74,7 @@ public class PathVariableMethodArgumentResolverTests {
pathParams.put("foo","bar");
pathParams.put("name","value");
Message<byte[]> message = MessageBuilder.withPayload(new byte[0])
.setHeader(AnnotationMethodMessageHandler.PATH_TEMPLATE_VARIABLES_HEADER, pathParams).build();
.setHeader(PathVariableMethodArgumentResolver.PATH_TEMPLATE_VARIABLES_HEADER, pathParams).build();
Object result = this.resolver.resolveArgument(this.paramAnnotated, message);
assertEquals("bar",result);
result = this.resolver.resolveArgument(this.paramAnnotatedValue, message);
......
/*
* 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.condition;
import org.junit.Test;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.method.AbstractMethodMessageHandler;
import org.springframework.messaging.support.MessageBuilder;
import static org.junit.Assert.*;
/**
* Unit tests for DestinationPatternsMessageCondition.
*
* @author Rossen Stoyanchev
*/
public class DestinationPatternsMessageConditionTests {
@Test
public void prependSlash() {
DestinationPatternsMessageCondition c = condition("foo");
assertEquals("/foo", c.getPatterns().iterator().next());
}
// SPR-8255
@Test
public void prependNonEmptyPatternsOnly() {
DestinationPatternsMessageCondition c = condition("");
assertEquals("", c.getPatterns().iterator().next());
}
@Test
public void combineEmptySets() {
DestinationPatternsMessageCondition c1 = condition();
DestinationPatternsMessageCondition c2 = condition();
assertEquals(condition(""), c1.combine(c2));
}
@Test
public void combineOnePatternWithEmptySet() {
DestinationPatternsMessageCondition c1 = condition("/type1", "/type2");
DestinationPatternsMessageCondition c2 = condition();
assertEquals(condition("/type1", "/type2"), c1.combine(c2));
c1 = condition();
c2 = condition("/method1", "/method2");
assertEquals(condition("/method1", "/method2"), c1.combine(c2));
}
@Test
public void combineMultiplePatterns() {
DestinationPatternsMessageCondition c1 = condition("/t1", "/t2");
DestinationPatternsMessageCondition c2 = condition("/m1", "/m2");
assertEquals(new DestinationPatternsMessageCondition(
"/t1/m1", "/t1/m2", "/t2/m1", "/t2/m2"), c1.combine(c2));
}
@Test
public void matchDirectPath() {
DestinationPatternsMessageCondition condition = condition("/foo");
DestinationPatternsMessageCondition match = condition.getMatchingCondition(messageTo("/foo"));
assertNotNull(match);
}
@Test
public void matchPattern() {
DestinationPatternsMessageCondition condition = condition("/foo/*");
DestinationPatternsMessageCondition match = condition.getMatchingCondition(messageTo("/foo/bar"));
assertNotNull(match);
}
@Test
public void matchSortPatterns() {
DestinationPatternsMessageCondition condition = condition("/**", "/foo/bar", "/foo/*");
DestinationPatternsMessageCondition match = condition.getMatchingCondition(messageTo("/foo/bar"));
DestinationPatternsMessageCondition expected = condition("/foo/bar", "/foo/*", "/**");
assertEquals(expected, match);
}
@Test
public void compareEqualPatterns() {
DestinationPatternsMessageCondition c1 = condition("/foo*");
DestinationPatternsMessageCondition c2 = condition("/foo*");
assertEquals(0, c1.compareTo(c2, messageTo("/foo")));
}
@Test
public void comparePatternSpecificity() {
DestinationPatternsMessageCondition c1 = condition("/fo*");
DestinationPatternsMessageCondition c2 = condition("/foo");
assertEquals(1, c1.compareTo(c2, messageTo("/foo")));
}
@Test
public void compareNumberOfMatchingPatterns() throws Exception {
Message<?> message = messageTo("/foo");
DestinationPatternsMessageCondition c1 = condition("/foo", "bar");
DestinationPatternsMessageCondition c2 = condition("/foo", "f*");
DestinationPatternsMessageCondition match1 = c1.getMatchingCondition(message);
DestinationPatternsMessageCondition match2 = c2.getMatchingCondition(message);
assertEquals(1, match1.compareTo(match2, message));
}
private DestinationPatternsMessageCondition condition(String... patterns) {
return new DestinationPatternsMessageCondition(patterns);
}
private Message<?> messageTo(String destination) {
return MessageBuilder.withPayload(new byte[0]).setHeader(
AbstractMethodMessageHandler.LOOKUP_DESTINATION_HEADER, destination).build();
}
}
......@@ -34,7 +34,7 @@ import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.handler.websocket.SubProtocolWebSocketHandler;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.annotation.SubscribeEvent;
import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler;
import org.springframework.messaging.simp.handler.SimpAnnotationMethodMessageHandler;
import org.springframework.messaging.simp.handler.MutableUserQueueSuffixResolver;
import org.springframework.messaging.simp.handler.SimpleBrokerMessageHandler;
import org.springframework.messaging.simp.handler.UserDestinationMessageHandler;
......@@ -103,7 +103,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
List<MessageHandler> values = captor.getAllValues();
assertEquals(3, values.size());
assertTrue(values.contains(cxtSimpleBroker.getBean(AnnotationMethodMessageHandler.class)));
assertTrue(values.contains(cxtSimpleBroker.getBean(SimpAnnotationMethodMessageHandler.class)));
assertTrue(values.contains(cxtSimpleBroker.getBean(UserDestinationMessageHandler.class)));
assertTrue(values.contains(cxtSimpleBroker.getBean(SimpleBrokerMessageHandler.class)));
}
......@@ -117,7 +117,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
List<MessageHandler> values = captor.getAllValues();
assertEquals(3, values.size());
assertTrue(values.contains(cxtStompBroker.getBean(AnnotationMethodMessageHandler.class)));
assertTrue(values.contains(cxtStompBroker.getBean(SimpAnnotationMethodMessageHandler.class)));
assertTrue(values.contains(cxtStompBroker.getBean(UserDestinationMessageHandler.class)));
assertTrue(values.contains(cxtStompBroker.getBean(StompBrokerRelayMessageHandler.class)));
}
......@@ -152,7 +152,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
public void webSocketResponseChannelUsedByAnnotatedMethod() {
SubscribableChannel channel = this.cxtSimpleBroker.getBean("webSocketResponseChannel", SubscribableChannel.class);
AnnotationMethodMessageHandler messageHandler = this.cxtSimpleBroker.getBean(AnnotationMethodMessageHandler.class);
SimpAnnotationMethodMessageHandler messageHandler = this.cxtSimpleBroker.getBean(SimpAnnotationMethodMessageHandler.class);
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SUBSCRIBE);
headers.setSessionId("sess1");
......@@ -235,7 +235,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
@Test
public void brokerChannelUsedByAnnotatedMethod() {
SubscribableChannel channel = this.cxtSimpleBroker.getBean("brokerChannel", SubscribableChannel.class);
AnnotationMethodMessageHandler messageHandler = this.cxtSimpleBroker.getBean(AnnotationMethodMessageHandler.class);
SimpAnnotationMethodMessageHandler messageHandler = this.cxtSimpleBroker.getBean(SimpAnnotationMethodMessageHandler.class);
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND);
headers.setDestination("/foo");
......
......@@ -62,7 +62,7 @@ import static org.springframework.messaging.simp.stomp.StompTextMessageBuilder.*
* @author Rossen Stoyanchev
*/
@RunWith(Parameterized.class)
public class AnnotationMethodIntegrationTests extends AbstractWebSocketIntegrationTests {
public class SimpAnnotationMethodIntegrationTests extends AbstractWebSocketIntegrationTests {
@Parameters
public static Iterable<Object[]> arguments() {
......@@ -190,7 +190,7 @@ public class AnnotationMethodIntegrationTests extends AbstractWebSocketIntegrati
}
@Configuration
@ComponentScan(basePackageClasses=AnnotationMethodIntegrationTests.class,
@ComponentScan(basePackageClasses=SimpAnnotationMethodIntegrationTests.class,
useDefaultFilters=false,
includeFilters=@ComponentScan.Filter(IntegrationTestController.class))
static class TestMessageBrokerConfigurer implements WebSocketMessageBrokerConfigurer {
......
......@@ -41,13 +41,13 @@ import static org.junit.Assert.*;
/**
* Test fixture for {@link AnnotationMethodMessageHandler}.
* Test fixture for {@link SimpAnnotationMethodMessageHandler}.
* @author Rossen Stoyanchev
* @author Brian Clozel
*/
public class AnnotationMethodMessageHandlerTests {
public class SimpAnnotationMethodMessageHandlerTests {
private TestAnnotationMethodMessageHandler messageHandler;
private TestSimpAnnotationMethodMessageHandler messageHandler;
private TestController testController;
......@@ -56,7 +56,7 @@ public class AnnotationMethodMessageHandlerTests {
public void setup() {
MessageChannel channel = Mockito.mock(MessageChannel.class);
SimpMessageSendingOperations brokerTemplate = new SimpMessagingTemplate(channel);
this.messageHandler = new TestAnnotationMethodMessageHandler(brokerTemplate, channel);
this.messageHandler = new TestSimpAnnotationMethodMessageHandler(brokerTemplate, channel);
this.messageHandler.setApplicationContext(new StaticApplicationContext());
this.messageHandler.afterPropertiesSet();
......@@ -69,7 +69,7 @@ public class AnnotationMethodMessageHandlerTests {
@Test
public void headerArgumentResolution() {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
headers.setDestination("/headers");
headers.setDestination("/pre/headers");
headers.setHeader("foo", "bar");
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
this.messageHandler.handleMessage(message);
......@@ -87,7 +87,7 @@ public class AnnotationMethodMessageHandlerTests {
@Test
public void messageMappingPathVariableResolution() {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
headers.setDestination("/message/bar/value");
headers.setDestination("/pre/message/bar/value");
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
this.messageHandler.handleMessage(message);
......@@ -99,7 +99,7 @@ public class AnnotationMethodMessageHandlerTests {
@Test
public void subscribeEventPathVariableResolution() {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.SUBSCRIBE);
headers.setDestination("/sub/bar/value");
headers.setDestination("/pre/sub/bar/value");
Message<?> message = MessageBuilder.withPayload(new byte[0])
.copyHeaders(headers.toMap()).build();
this.messageHandler.handleMessage(message);
......@@ -112,7 +112,7 @@ public class AnnotationMethodMessageHandlerTests {
@Test
public void antPatchMatchWildcard() {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
headers.setDestination("/pathmatch/wildcard/test");
headers.setDestination("/pre/pathmatch/wildcard/test");
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
this.messageHandler.handleMessage(message);
......@@ -122,7 +122,7 @@ public class AnnotationMethodMessageHandlerTests {
@Test
public void bestMatchWildcard() {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
headers.setDestination("/bestmatch/bar/path");
headers.setDestination("/pre/bestmatch/bar/path");
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
this.messageHandler.handleMessage(message);
......@@ -133,7 +133,7 @@ public class AnnotationMethodMessageHandlerTests {
@Test
public void simpleBinding() {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
headers.setDestination("/binding/id/12");
headers.setDestination("/pre/binding/id/12");
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
this.messageHandler.handleMessage(message);
......@@ -142,9 +142,10 @@ public class AnnotationMethodMessageHandlerTests {
assertEquals(12L, this.testController.arguments.get("id"));
}
private static class TestAnnotationMethodMessageHandler extends AnnotationMethodMessageHandler {
public TestAnnotationMethodMessageHandler(SimpMessageSendingOperations brokerTemplate,
private static class TestSimpAnnotationMethodMessageHandler extends SimpAnnotationMethodMessageHandler {
public TestSimpAnnotationMethodMessageHandler(SimpMessageSendingOperations brokerTemplate,
MessageChannel webSocketResponseChannel) {
super(brokerTemplate, webSocketResponseChannel);
......@@ -157,6 +158,8 @@ public class AnnotationMethodMessageHandlerTests {
@Controller
@MessageMapping("/pre")
@SubscribeEvent("/pre")
private static class TestController {
private String method;
......
/*
* 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.handler;
import org.junit.Test;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.condition.DestinationPatternsMessageCondition;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.support.MessageBuilder;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static org.junit.Assert.assertEquals;
/**
* Unit tests for SimpMessageTypeMessageCondition.
*
* @author Rossen Stoyanchev
*/
public class SimpMessageTypeMessageConditionTests {
@Test
public void combineEmptySets() {
SimpMessageTypeMessageCondition c1 = condition();
SimpMessageTypeMessageCondition c2 = condition();
assertNull(c1.combine(c2).getMessageType());
}
@Test
public void combine() {
SimpMessageType actual = condition().combine(condition()).getMessageType();
assertNull(actual);
actual = condition().combine(condition(SimpMessageType.SUBSCRIBE)).getMessageType();
assertEquals(SimpMessageType.SUBSCRIBE, actual);
actual = condition(SimpMessageType.SUBSCRIBE).combine(condition()).getMessageType();
assertEquals(SimpMessageType.SUBSCRIBE, actual);
actual = condition(SimpMessageType.SUBSCRIBE).combine(condition(SimpMessageType.SUBSCRIBE)).getMessageType();
assertEquals(SimpMessageType.SUBSCRIBE, actual);
}
@Test
public void getMatchingCondition() {
Message<?> message = message(SimpMessageType.MESSAGE);
SimpMessageTypeMessageCondition condition = condition(SimpMessageType.MESSAGE);
SimpMessageTypeMessageCondition actual = condition.getMatchingCondition(message);
assertNotNull(actual);
assertEquals(SimpMessageType.MESSAGE, actual.getMessageType());
}
@Test
public void getMatchingConditionNoMessageType() {
Message<?> message = message(null);
SimpMessageTypeMessageCondition condition = condition(SimpMessageType.MESSAGE);
assertNull(condition.getMatchingCondition(message));
}
@Test
public void compareTo() {
Message<byte[]> message = message(null);
assertEquals(1, condition().compareTo(condition(SimpMessageType.MESSAGE), message));
assertEquals(-1, condition(SimpMessageType.MESSAGE).compareTo(condition(), message));
assertEquals(0, condition(SimpMessageType.MESSAGE).compareTo(condition(SimpMessageType.MESSAGE), message));
}
private Message<byte[]> message(SimpMessageType messageType) {
MessageBuilder<byte[]> builder = MessageBuilder.withPayload(new byte[0]);
if (messageType != null) {
builder.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, messageType);
}
return builder.build();
}
private SimpMessageTypeMessageCondition condition() {
return new SimpMessageTypeMessageCondition();
}
private SimpMessageTypeMessageCondition condition(SimpMessageType messageType) {
return new SimpMessageTypeMessageCondition(messageType);
}
}
......@@ -337,7 +337,9 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
/**
* A temporary container for a mapping matched to a request.
* A thin wrapper around a matched HandlerMethod and its matched mapping for
* the purpose of comparing the best match with a comparator in the context
* of the current request.
*/
private class Match {
......
......@@ -98,7 +98,6 @@ public final class PatternsRequestCondition extends AbstractRequestCondition<Pat
/**
* Private constructor accepting a collection of patterns.
* @param fileExtensionResolver
*/
private PatternsRequestCondition(Collection<String> patterns, UrlPathHelper urlPathHelper,
PathMatcher pathMatcher, boolean useSuffixPatternMatch, boolean useTrailingSlashMatch,
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册