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
This commit is contained in:
parent
4892a27016
commit
b8809daf5f
|
|
@ -21,7 +21,7 @@ import org.springframework.util.ObjectUtils;
|
|||
/**
|
||||
* Simple test entity for use with caching tests.
|
||||
*
|
||||
* @author Michael Plšd
|
||||
* @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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.messaging.handler.annotation.support;
|
||||
|
||||
import 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;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -1,157 +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.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;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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();
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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.springframework.core.ExceptionDepthComparator;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
|
||||
/**
|
||||
* 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 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> exceptionLookupCache = new ConcurrentHashMap<Class<? extends Throwable>, Method>(16);
|
||||
|
||||
|
||||
/**
|
||||
* Protected constructor accepting exception-to-method mappings.
|
||||
*/
|
||||
protected AbstractExceptionHandlerMethodResolver(Map<Class<? extends Throwable>, Method> mappedMethods) {
|
||||
Assert.notNull(mappedMethods, "'mappedMethods' is required");
|
||||
this.mappedMethods.putAll(mappedMethods);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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")
|
||||
protected static List<Class<? extends Throwable>> getExceptionsFromMethodSignature(Method method) {
|
||||
List<Class<? extends Throwable>> result = new ArrayList<Class<? extends Throwable>>();
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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}
|
||||
*/
|
||||
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 this.mappedMethods.get(matches.get(0));
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,537 @@
|
|||
/*
|
||||
* 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);
|
||||
|
|
|
|||
|
|
@ -1,575 +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.handler;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
import org.springframework.messaging.core.AbstractMessageSendingTemplate;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.handler.annotation.support.ExceptionHandlerMethodResolver;
|
||||
import org.springframework.messaging.handler.annotation.support.HeaderMethodArgumentResolver;
|
||||
import org.springframework.messaging.handler.annotation.support.HeadersMethodArgumentResolver;
|
||||
import org.springframework.messaging.handler.annotation.support.MessageMethodArgumentResolver;
|
||||
import org.springframework.messaging.handler.annotation.support.PathVariableMethodArgumentResolver;
|
||||
import org.springframework.messaging.handler.annotation.support.PayloadArgumentResolver;
|
||||
import org.springframework.messaging.handler.method.HandlerMethod;
|
||||
import org.springframework.messaging.handler.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.messaging.handler.method.HandlerMethodArgumentResolverComposite;
|
||||
import org.springframework.messaging.handler.method.HandlerMethodReturnValueHandler;
|
||||
import org.springframework.messaging.handler.method.HandlerMethodReturnValueHandlerComposite;
|
||||
import org.springframework.messaging.handler.method.HandlerMethodSelector;
|
||||
import org.springframework.messaging.handler.method.InvocableHandlerMethod;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.simp.SimpMessageSendingOperations;
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.messaging.simp.annotation.SubscribeEvent;
|
||||
import org.springframework.messaging.simp.annotation.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.CollectionUtils;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.util.ReflectionUtils.MethodFilter;
|
||||
|
||||
|
||||
/**
|
||||
* A handler for messages that delegates to {@link SubscribeEvent @SubscribeEvent} and
|
||||
* {@link MessageMapping @MessageMapping} annotated methods.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 4.0
|
||||
*/
|
||||
public class AnnotationMethodMessageHandler implements MessageHandler, ApplicationContextAware, InitializingBean {
|
||||
|
||||
public static final String PATH_TEMPLATE_VARIABLES_HEADER =
|
||||
AnnotationMethodMessageHandler.class.getSimpleName() + ".templateVariables";
|
||||
|
||||
public static final String BEST_MATCHING_PATTERN_HEADER =
|
||||
AnnotationMethodMessageHandler.class.getSimpleName() + ".bestMatchingPattern";
|
||||
|
||||
private static final Log logger = LogFactory.getLog(AnnotationMethodMessageHandler.class);
|
||||
|
||||
|
||||
private final PathMatcher pathMatcher = new AntPathMatcher();
|
||||
|
||||
private final SimpMessageSendingOperations brokerTemplate;
|
||||
|
||||
private final SimpMessageSendingOperations webSocketResponseTemplate;
|
||||
|
||||
private Collection<String> destinationPrefixes = new ArrayList<String>();
|
||||
|
||||
private MessageConverter messageConverter;
|
||||
|
||||
private ConversionService conversionService = new DefaultFormattingConversionService();
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private Map<MappingInfo, HandlerMethod> messageMethods = new HashMap<MappingInfo, HandlerMethod>();
|
||||
|
||||
private Map<MappingInfo, HandlerMethod> subscribeMethods = new HashMap<MappingInfo, HandlerMethod>();
|
||||
|
||||
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache =
|
||||
new ConcurrentHashMap<Class<?>, ExceptionHandlerMethodResolver>(64);
|
||||
|
||||
private List<HandlerMethodArgumentResolver> customArgumentResolvers = new ArrayList<HandlerMethodArgumentResolver>();
|
||||
|
||||
private List<HandlerMethodReturnValueHandler> customReturnValueHandlers = new ArrayList<HandlerMethodReturnValueHandler>();
|
||||
|
||||
private HandlerMethodArgumentResolverComposite argumentResolvers = new HandlerMethodArgumentResolverComposite();
|
||||
|
||||
private HandlerMethodReturnValueHandlerComposite returnValueHandlers = new HandlerMethodReturnValueHandlerComposite();
|
||||
|
||||
|
||||
/**
|
||||
* @param brokerTemplate a messaging template to sending messages to the broker
|
||||
* @param webSocketResponseChannel the channel for messages to WebSocket clients
|
||||
*/
|
||||
public AnnotationMethodMessageHandler(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 one or more prefixes to filter destinations targeting annotated
|
||||
* application methods. For example destinations prefixed with "/app" may be processed
|
||||
* by annotated application methods while other destinations may target the message
|
||||
* broker (e.g. "/topic", "/queue").
|
||||
* <p>
|
||||
* When messages are processed, the matching prefix is removed from the destination in
|
||||
* order to form the lookup path. This means annotations should not contain the
|
||||
* destination prefix.
|
||||
* <p>
|
||||
* Prefixes that do not have a trailing slash will have one automatically appended.
|
||||
*/
|
||||
public void setDestinationPrefixes(Collection<String> destinationPrefixes) {
|
||||
this.destinationPrefixes.clear();
|
||||
if (destinationPrefixes != null) {
|
||||
for (String prefix : destinationPrefixes) {
|
||||
prefix = prefix.trim();
|
||||
if (!prefix.endsWith("/")) {
|
||||
prefix += "/";
|
||||
}
|
||||
this.destinationPrefixes.add(prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Collection<String> getDestinationPrefixes() {
|
||||
return this.destinationPrefixes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link MessageConverter} to use to convert the payload of a message
|
||||
* from serialize form with a specific MIME type to an Object matching the target
|
||||
* method parameter. The converter is also used when sending message to the message
|
||||
* broker.
|
||||
*
|
||||
* @see CompositeMessageConverter
|
||||
*/
|
||||
public void setMessageConverter(MessageConverter converter) {
|
||||
this.messageConverter = converter;
|
||||
if (converter != null) {
|
||||
((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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
|
||||
initHandlerMethods();
|
||||
|
||||
ConfigurableBeanFactory beanFactory =
|
||||
(ClassUtils.isAssignableValue(ConfigurableApplicationContext.class, this.applicationContext)) ?
|
||||
((ConfigurableApplicationContext) this.applicationContext).getBeanFactory() : null;
|
||||
|
||||
// Annotation-based argument resolution
|
||||
this.argumentResolvers.addResolver(new HeaderMethodArgumentResolver(this.conversionService, beanFactory));
|
||||
this.argumentResolvers.addResolver(new HeadersMethodArgumentResolver());
|
||||
this.argumentResolvers.addResolver(new PathVariableMethodArgumentResolver(this.conversionService));
|
||||
|
||||
// Type-based argument resolution
|
||||
this.argumentResolvers.addResolver(new PrincipalMethodArgumentResolver());
|
||||
this.argumentResolvers.addResolver(new MessageMethodArgumentResolver());
|
||||
|
||||
// custom arguments
|
||||
this.argumentResolvers.addResolvers(this.customArgumentResolvers);
|
||||
|
||||
// catch-all argument resolver
|
||||
this.argumentResolvers.addResolver(new PayloadArgumentResolver(this.messageConverter));
|
||||
|
||||
// Annotation-based return value types
|
||||
this.returnValueHandlers.addHandler(new SendToMethodReturnValueHandler(this.brokerTemplate, true));
|
||||
this.returnValueHandlers.addHandler(new SubscriptionMethodReturnValueHandler(this.webSocketResponseTemplate));
|
||||
|
||||
// custom return value types
|
||||
this.returnValueHandlers.addHandlers(this.customReturnValueHandlers);
|
||||
|
||||
// catch-all
|
||||
this.returnValueHandlers.addHandler(new SendToMethodReturnValueHandler(this.brokerTemplate, false));
|
||||
}
|
||||
|
||||
protected final void initHandlerMethods() {
|
||||
String[] beanNames = this.applicationContext.getBeanNamesForType(Object.class);
|
||||
for (String beanName : beanNames) {
|
||||
if (isHandler(this.applicationContext.getType(beanName))){
|
||||
detectHandlerMethods(beanName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the given bean type should be introspected for messaging handling methods.
|
||||
*/
|
||||
protected boolean isHandler(Class<?> beanType) {
|
||||
return (AnnotationUtils.findAnnotation(beanType, Controller.class) != null);
|
||||
}
|
||||
|
||||
protected final void detectHandlerMethods(Object handler) {
|
||||
|
||||
Class<?> handlerType = (handler instanceof String) ?
|
||||
this.applicationContext.getType((String) handler) : handler.getClass();
|
||||
|
||||
handlerType = ClassUtils.getUserClass(handlerType);
|
||||
|
||||
initHandlerMethods(handler, handlerType, MessageMapping.class, this.messageMethods);
|
||||
initHandlerMethods(handler, handlerType, SubscribeEvent.class, this.subscribeMethods);
|
||||
}
|
||||
|
||||
private <A extends Annotation> void initHandlerMethods(Object handler, Class<?> handlerType,
|
||||
final Class<A> annotationType, Map<MappingInfo, HandlerMethod> handlerMethods) {
|
||||
|
||||
Set<Method> methods = HandlerMethodSelector.selectMethods(handlerType, new MethodFilter() {
|
||||
@Override
|
||||
public boolean matches(Method method) {
|
||||
return AnnotationUtils.findAnnotation(method, annotationType) != null;
|
||||
}
|
||||
});
|
||||
|
||||
for (Method method : methods) {
|
||||
A annotation = AnnotationUtils.findAnnotation(method, annotationType);
|
||||
String[] destinations = (String[]) AnnotationUtils.getValue(annotation);
|
||||
MappingInfo mapping = new MappingInfo(destinations);
|
||||
|
||||
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.");
|
||||
}
|
||||
handlerMethods.put(mapping, newHandlerMethod);
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Mapped \"@" + annotationType.getSimpleName()
|
||||
+ " " + mapping + "\" onto " + newHandlerMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message<?> message) throws MessagingException {
|
||||
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(message);
|
||||
SimpMessageType messageType = headers.getMessageType();
|
||||
|
||||
if (SimpMessageType.MESSAGE.equals(messageType)) {
|
||||
handleMessageInternal(message, this.messageMethods);
|
||||
}
|
||||
else if (SimpMessageType.SUBSCRIBE.equals(messageType)) {
|
||||
handleMessageInternal(message, this.subscribeMethods);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMessageInternal(Message<?> message, Map<MappingInfo, HandlerMethod> handlerMethods) {
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Message " + message);
|
||||
}
|
||||
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(message);
|
||||
String destinationToMatch = getDestinationToMatch(headers.getDestination());
|
||||
if (destinationToMatch == null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Ignoring message with destination=" + headers.getDestination());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Match match = getMatchingHandlerMethod(destinationToMatch, handlerMethods);
|
||||
if (match == null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("No matching handler method for destination=" + destinationToMatch);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String matchedPattern = match.getMatchedPattern();
|
||||
HandlerMethod handlerMethod = match.getHandlerMethod().createWithResolvedBean();
|
||||
|
||||
InvocableHandlerMethod invocableHandlerMethod = new InvocableHandlerMethod(handlerMethod);
|
||||
invocableHandlerMethod.setMessageMethodArgumentResolvers(this.argumentResolvers);
|
||||
|
||||
try {
|
||||
headers.setDestination(destinationToMatch);
|
||||
headers.setHeader(BEST_MATCHING_PATTERN_HEADER, matchedPattern);
|
||||
Map<String, String> vars = this.pathMatcher.extractUriTemplateVariables(matchedPattern, destinationToMatch);
|
||||
headers.setHeader(PATH_TEMPLATE_VARIABLES_HEADER, vars);
|
||||
message = MessageBuilder.withPayload(message.getPayload()).setHeaders(headers).build();
|
||||
|
||||
Object returnValue = invocableHandlerMethod.invoke(message);
|
||||
|
||||
MethodParameter returnType = handlerMethod.getReturnType();
|
||||
if (void.class.equals(returnType.getParameterType())) {
|
||||
return;
|
||||
}
|
||||
this.returnValueHandlers.handleReturnValue(returnValue, returnType, message);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
invokeExceptionHandler(message, handlerMethod, ex);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
logger.error("Error while processing message " + message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the destination against the list the configured destination prefixes, if any,
|
||||
* and return a destination with the matched prefix removed.
|
||||
*/
|
||||
private String getDestinationToMatch(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;
|
||||
}
|
||||
|
||||
private void invokeExceptionHandler(Message<?> message, HandlerMethod handlerMethod, Exception ex) {
|
||||
|
||||
InvocableHandlerMethod exceptionHandlerMethod;
|
||||
Class<?> beanType = handlerMethod.getBeanType();
|
||||
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(beanType);
|
||||
if (resolver == null) {
|
||||
resolver = new ExceptionHandlerMethodResolver(beanType);
|
||||
this.exceptionHandlerCache.put(beanType, resolver);
|
||||
}
|
||||
|
||||
Method method = resolver.resolveMethod(ex);
|
||||
if (method == null) {
|
||||
logger.error("Unhandled exception", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
exceptionHandlerMethod = new InvocableHandlerMethod(handlerMethod.getBean(), method);
|
||||
exceptionHandlerMethod.setMessageMethodArgumentResolvers(this.argumentResolvers);
|
||||
|
||||
try {
|
||||
Object returnValue = exceptionHandlerMethod.invoke(message, ex);
|
||||
|
||||
MethodParameter returnType = exceptionHandlerMethod.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 Match getMatchingHandlerMethod(String destination, Map<MappingInfo, HandlerMethod> handlerMethods) {
|
||||
List<Match> matches = new ArrayList<Match>(4);
|
||||
for (MappingInfo key : handlerMethods.keySet()) {
|
||||
for (String pattern : key.getDestinationPatterns()) {
|
||||
if (this.pathMatcher.match(pattern, destination)) {
|
||||
matches.add(new Match(pattern, handlerMethods.get(key)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (matches.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
else if (matches.size() == 1) {
|
||||
return matches.get(0);
|
||||
}
|
||||
else {
|
||||
Comparator<Match> comparator = getMatchComparator(destination, this.pathMatcher);
|
||||
Collections.sort(matches, comparator);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Found " + matches.size() +
|
||||
" matching mapping(s) for [" + destination + "] : " + 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 Message destination '" + destination + "': {" +
|
||||
m1 + ", " + m2 + "}");
|
||||
}
|
||||
}
|
||||
return bestMatch;
|
||||
}
|
||||
}
|
||||
|
||||
private Comparator<Match> getMatchComparator(final String destination, final PathMatcher pathMatcher) {
|
||||
return new Comparator<Match>() {
|
||||
@Override
|
||||
public int compare(Match one, Match other) {
|
||||
Comparator<String> patternComparator = pathMatcher.getPatternComparator(destination);
|
||||
return patternComparator.compare(one.destinationPattern, other.destinationPattern);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static class MappingInfo {
|
||||
|
||||
private final String[] destinationPatterns;
|
||||
|
||||
|
||||
public MappingInfo(String[] destinationPatterns) {
|
||||
Assert.notNull(destinationPatterns, "No destination patterns");
|
||||
this.destinationPatterns = destinationPatterns;
|
||||
}
|
||||
|
||||
public String[] getDestinationPatterns() {
|
||||
return this.destinationPatterns;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(this.destinationPatterns);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o != null && getClass().equals(o.getClass())) {
|
||||
MappingInfo other = (MappingInfo) o;
|
||||
return Arrays.equals(destinationPatterns, other.getDestinationPatterns());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[destinationPatters=" + Arrays.toString(this.destinationPatterns) + "]";
|
||||
}
|
||||
}
|
||||
|
||||
private static class Match {
|
||||
|
||||
private final String destinationPattern;
|
||||
|
||||
private final HandlerMethod handlerMethod;
|
||||
|
||||
public Match(String destinationPattern, HandlerMethod handlerMethod) {
|
||||
this.destinationPattern = destinationPattern;
|
||||
this.handlerMethod = handlerMethod;
|
||||
}
|
||||
|
||||
public String getMatchedPattern() {
|
||||
return this.destinationPattern;
|
||||
}
|
||||
|
||||
public HandlerMethod getHandlerMethod() {
|
||||
return this.handlerMethod;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* 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;
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue