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.
|
* Simple test entity for use with caching tests.
|
||||||
*
|
*
|
||||||
* @author Michael Plšd
|
* @author Michael Plod
|
||||||
*/
|
*/
|
||||||
public class TestEntity {
|
public class TestEntity {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import org.springframework.messaging.Message;
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @since 4.0
|
* @since 4.0
|
||||||
*
|
*
|
||||||
* @see org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler
|
* @see org.springframework.messaging.simp.handler.SimpAnnotationMethodMessageHandler
|
||||||
*/
|
*/
|
||||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import java.lang.annotation.Target;
|
||||||
*
|
*
|
||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
* @see org.springframework.messaging.handler.annotation.MessageMapping
|
* @see org.springframework.messaging.handler.annotation.MessageMapping
|
||||||
* @see org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler
|
* @see org.springframework.messaging.simp.handler.SimpAnnotationMethodMessageHandler
|
||||||
*
|
*
|
||||||
* @since 4.0
|
* @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.Message;
|
||||||
import org.springframework.messaging.handler.annotation.PathVariable;
|
import org.springframework.messaging.handler.annotation.PathVariable;
|
||||||
import org.springframework.messaging.handler.annotation.ValueConstants;
|
import org.springframework.messaging.handler.annotation.ValueConstants;
|
||||||
import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves method parameters annotated with {@link PathVariable @PathVariable}.
|
* 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 class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
|
||||||
|
|
||||||
|
public static final String PATH_TEMPLATE_VARIABLES_HEADER =
|
||||||
|
PathVariableMethodArgumentResolver.class.getSimpleName() + ".templateVariables";
|
||||||
|
|
||||||
|
|
||||||
public PathVariableMethodArgumentResolver(ConversionService cs) {
|
public PathVariableMethodArgumentResolver(ConversionService cs) {
|
||||||
super(cs, null);
|
super(cs, null);
|
||||||
|
|
@ -57,9 +59,8 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name) throws Exception {
|
protected Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name) throws Exception {
|
||||||
String headerName = AnnotationMethodMessageHandler.PATH_TEMPLATE_VARIABLES_HEADER;
|
|
||||||
@SuppressWarnings("unchecked")
|
@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;
|
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());
|
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 =
|
private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache =
|
||||||
new ConcurrentHashMap<MethodParameter, HandlerMethodArgumentResolver>(256);
|
new ConcurrentHashMap<MethodParameter, HandlerMethodArgumentResolver>(256);
|
||||||
|
|
@ -53,6 +53,13 @@ public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgu
|
||||||
return Collections.unmodifiableList(this.argumentResolvers);
|
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
|
* Whether the given {@linkplain MethodParameter method parameter} is supported by any registered
|
||||||
* {@link HandlerMethodArgumentResolver}.
|
* {@link HandlerMethodArgumentResolver}.
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package org.springframework.messaging.handler.method;
|
package org.springframework.messaging.handler.method;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
|
|
@ -37,6 +38,20 @@ public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodRe
|
||||||
private final List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList<HandlerMethodReturnValueHandler>();
|
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}.
|
* Add the given {@link HandlerMethodReturnValueHandler}.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -39,17 +39,17 @@ import org.springframework.util.Assert;
|
||||||
*/
|
*/
|
||||||
public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor {
|
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.handler.websocket.SubProtocolWebSocketHandler;
|
||||||
import org.springframework.messaging.simp.SimpMessageSendingOperations;
|
import org.springframework.messaging.simp.SimpMessageSendingOperations;
|
||||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
import org.springframework.messaging.simp.handler.AbstractBrokerMessageHandler;
|
import org.springframework.messaging.simp.handler.*;
|
||||||
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.SimpleUserQueueSuffixResolver;
|
|
||||||
import org.springframework.messaging.simp.handler.UserDestinationMessageHandler;
|
|
||||||
import org.springframework.messaging.support.channel.ExecutorSubscribableChannel;
|
import org.springframework.messaging.support.channel.ExecutorSubscribableChannel;
|
||||||
import org.springframework.messaging.support.converter.ByteArrayMessageConverter;
|
import org.springframework.messaging.support.converter.ByteArrayMessageConverter;
|
||||||
import org.springframework.messaging.support.converter.CompositeMessageConverter;
|
import org.springframework.messaging.support.converter.CompositeMessageConverter;
|
||||||
|
|
@ -139,9 +136,9 @@ public abstract class WebSocketMessageBrokerConfigurationSupport {
|
||||||
// Handling of messages by the application
|
// Handling of messages by the application
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public AnnotationMethodMessageHandler annotationMethodMessageHandler() {
|
public SimpAnnotationMethodMessageHandler annotationMethodMessageHandler() {
|
||||||
AnnotationMethodMessageHandler handler =
|
SimpAnnotationMethodMessageHandler handler =
|
||||||
new AnnotationMethodMessageHandler(brokerMessagingTemplate(), webSocketResponseChannel());
|
new SimpAnnotationMethodMessageHandler(brokerMessagingTemplate(), webSocketResponseChannel());
|
||||||
handler.setDestinationPrefixes(getMessageBrokerConfigurer().getAnnotationMethodDestinationPrefixes());
|
handler.setDestinationPrefixes(getMessageBrokerConfigurer().getAnnotationMethodDestinationPrefixes());
|
||||||
handler.setMessageConverter(simpMessageConverter());
|
handler.setMessageConverter(simpMessageConverter());
|
||||||
webSocketRequestChannel().subscribe(handler);
|
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
|
* @author Rossen Stoyanchev
|
||||||
*/
|
*/
|
||||||
public class ExceptionHandlerMethodResolverTests {
|
public class AnnotationExceptionHandlerMethodResolverTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void resolveMethodFromAnnotation() {
|
public void resolveMethodFromAnnotation() {
|
||||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
|
AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class);
|
||||||
IOException exception = new IOException();
|
IOException exception = new IOException();
|
||||||
assertEquals("handleIOException", resolver.resolveMethod(exception).getName());
|
assertEquals("handleIOException", resolver.resolveMethod(exception).getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void resolveMethodFromArgument() {
|
public void resolveMethodFromArgument() {
|
||||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
|
AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class);
|
||||||
IllegalArgumentException exception = new IllegalArgumentException();
|
IllegalArgumentException exception = new IllegalArgumentException();
|
||||||
assertEquals("handleIllegalArgumentException", resolver.resolveMethod(exception).getName());
|
assertEquals("handleIllegalArgumentException", resolver.resolveMethod(exception).getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void resolveMethodExceptionSubType() {
|
public void resolveMethodExceptionSubType() {
|
||||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
|
AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class);
|
||||||
IOException ioException = new FileNotFoundException();
|
IOException ioException = new FileNotFoundException();
|
||||||
assertEquals("handleIOException", resolver.resolveMethod(ioException).getName());
|
assertEquals("handleIOException", resolver.resolveMethod(ioException).getName());
|
||||||
SocketException bindException = new BindException();
|
SocketException bindException = new BindException();
|
||||||
|
|
@ -61,14 +61,14 @@ public class ExceptionHandlerMethodResolverTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void resolveMethodBestMatch() {
|
public void resolveMethodBestMatch() {
|
||||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
|
AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class);
|
||||||
SocketException exception = new SocketException();
|
SocketException exception = new SocketException();
|
||||||
assertEquals("handleSocketException", resolver.resolveMethod(exception).getName());
|
assertEquals("handleSocketException", resolver.resolveMethod(exception).getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void resolveMethodNoMatch() {
|
public void resolveMethodNoMatch() {
|
||||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
|
AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class);
|
||||||
Exception exception = new Exception();
|
Exception exception = new Exception();
|
||||||
assertNull("1st lookup", resolver.resolveMethod(exception));
|
assertNull("1st lookup", resolver.resolveMethod(exception));
|
||||||
assertNull("2nd lookup from cache", resolver.resolveMethod(exception));
|
assertNull("2nd lookup from cache", resolver.resolveMethod(exception));
|
||||||
|
|
@ -76,19 +76,19 @@ public class ExceptionHandlerMethodResolverTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void resolveMethodInherited() {
|
public void resolveMethodInherited() {
|
||||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(InheritedController.class);
|
AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(InheritedController.class);
|
||||||
IOException exception = new IOException();
|
IOException exception = new IOException();
|
||||||
assertEquals("handleIOException", resolver.resolveMethod(exception).getName());
|
assertEquals("handleIOException", resolver.resolveMethod(exception).getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = IllegalStateException.class)
|
@Test(expected = IllegalStateException.class)
|
||||||
public void ambiguousExceptionMapping() {
|
public void ambiguousExceptionMapping() {
|
||||||
new ExceptionHandlerMethodResolver(AmbiguousController.class);
|
new AnnotationExceptionHandlerMethodResolver(AmbiguousController.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = IllegalArgumentException.class)
|
@Test(expected = IllegalArgumentException.class)
|
||||||
public void noExceptionMapping() {
|
public void noExceptionMapping() {
|
||||||
new ExceptionHandlerMethodResolver(NoExceptionController.class);
|
new AnnotationExceptionHandlerMethodResolver(NoExceptionController.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
|
|
@ -28,7 +28,7 @@ import org.springframework.core.MethodParameter;
|
||||||
import org.springframework.core.convert.support.DefaultConversionService;
|
import org.springframework.core.convert.support.DefaultConversionService;
|
||||||
import org.springframework.messaging.Message;
|
import org.springframework.messaging.Message;
|
||||||
import org.springframework.messaging.handler.annotation.PathVariable;
|
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 org.springframework.messaging.support.MessageBuilder;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
@ -74,7 +74,7 @@ public class PathVariableMethodArgumentResolverTests {
|
||||||
pathParams.put("foo","bar");
|
pathParams.put("foo","bar");
|
||||||
pathParams.put("name","value");
|
pathParams.put("name","value");
|
||||||
Message<byte[]> message = MessageBuilder.withPayload(new byte[0])
|
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);
|
Object result = this.resolver.resolveArgument(this.paramAnnotated, message);
|
||||||
assertEquals("bar",result);
|
assertEquals("bar",result);
|
||||||
result = this.resolver.resolveArgument(this.paramAnnotatedValue, message);
|
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.handler.websocket.SubProtocolWebSocketHandler;
|
||||||
import org.springframework.messaging.simp.SimpMessageType;
|
import org.springframework.messaging.simp.SimpMessageType;
|
||||||
import org.springframework.messaging.simp.annotation.SubscribeEvent;
|
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.MutableUserQueueSuffixResolver;
|
||||||
import org.springframework.messaging.simp.handler.SimpleBrokerMessageHandler;
|
import org.springframework.messaging.simp.handler.SimpleBrokerMessageHandler;
|
||||||
import org.springframework.messaging.simp.handler.UserDestinationMessageHandler;
|
import org.springframework.messaging.simp.handler.UserDestinationMessageHandler;
|
||||||
|
|
@ -103,7 +103,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
|
||||||
List<MessageHandler> values = captor.getAllValues();
|
List<MessageHandler> values = captor.getAllValues();
|
||||||
assertEquals(3, values.size());
|
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(UserDestinationMessageHandler.class)));
|
||||||
assertTrue(values.contains(cxtSimpleBroker.getBean(SimpleBrokerMessageHandler.class)));
|
assertTrue(values.contains(cxtSimpleBroker.getBean(SimpleBrokerMessageHandler.class)));
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +117,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
|
||||||
|
|
||||||
List<MessageHandler> values = captor.getAllValues();
|
List<MessageHandler> values = captor.getAllValues();
|
||||||
assertEquals(3, values.size());
|
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(UserDestinationMessageHandler.class)));
|
||||||
assertTrue(values.contains(cxtStompBroker.getBean(StompBrokerRelayMessageHandler.class)));
|
assertTrue(values.contains(cxtStompBroker.getBean(StompBrokerRelayMessageHandler.class)));
|
||||||
}
|
}
|
||||||
|
|
@ -152,7 +152,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
|
||||||
public void webSocketResponseChannelUsedByAnnotatedMethod() {
|
public void webSocketResponseChannelUsedByAnnotatedMethod() {
|
||||||
|
|
||||||
SubscribableChannel channel = this.cxtSimpleBroker.getBean("webSocketResponseChannel", SubscribableChannel.class);
|
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);
|
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SUBSCRIBE);
|
||||||
headers.setSessionId("sess1");
|
headers.setSessionId("sess1");
|
||||||
|
|
@ -235,7 +235,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
|
||||||
@Test
|
@Test
|
||||||
public void brokerChannelUsedByAnnotatedMethod() {
|
public void brokerChannelUsedByAnnotatedMethod() {
|
||||||
SubscribableChannel channel = this.cxtSimpleBroker.getBean("brokerChannel", SubscribableChannel.class);
|
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);
|
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND);
|
||||||
headers.setDestination("/foo");
|
headers.setDestination("/foo");
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ import static org.springframework.messaging.simp.stomp.StompTextMessageBuilder.*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
*/
|
*/
|
||||||
@RunWith(Parameterized.class)
|
@RunWith(Parameterized.class)
|
||||||
public class AnnotationMethodIntegrationTests extends AbstractWebSocketIntegrationTests {
|
public class SimpAnnotationMethodIntegrationTests extends AbstractWebSocketIntegrationTests {
|
||||||
|
|
||||||
@Parameters
|
@Parameters
|
||||||
public static Iterable<Object[]> arguments() {
|
public static Iterable<Object[]> arguments() {
|
||||||
|
|
@ -190,7 +190,7 @@ public class AnnotationMethodIntegrationTests extends AbstractWebSocketIntegrati
|
||||||
}
|
}
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ComponentScan(basePackageClasses=AnnotationMethodIntegrationTests.class,
|
@ComponentScan(basePackageClasses=SimpAnnotationMethodIntegrationTests.class,
|
||||||
useDefaultFilters=false,
|
useDefaultFilters=false,
|
||||||
includeFilters=@ComponentScan.Filter(IntegrationTestController.class))
|
includeFilters=@ComponentScan.Filter(IntegrationTestController.class))
|
||||||
static class TestMessageBrokerConfigurer implements WebSocketMessageBrokerConfigurer {
|
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 Rossen Stoyanchev
|
||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
*/
|
*/
|
||||||
public class AnnotationMethodMessageHandlerTests {
|
public class SimpAnnotationMethodMessageHandlerTests {
|
||||||
|
|
||||||
private TestAnnotationMethodMessageHandler messageHandler;
|
private TestSimpAnnotationMethodMessageHandler messageHandler;
|
||||||
|
|
||||||
private TestController testController;
|
private TestController testController;
|
||||||
|
|
||||||
|
|
@ -56,7 +56,7 @@ public class AnnotationMethodMessageHandlerTests {
|
||||||
public void setup() {
|
public void setup() {
|
||||||
MessageChannel channel = Mockito.mock(MessageChannel.class);
|
MessageChannel channel = Mockito.mock(MessageChannel.class);
|
||||||
SimpMessageSendingOperations brokerTemplate = new SimpMessagingTemplate(channel);
|
SimpMessageSendingOperations brokerTemplate = new SimpMessagingTemplate(channel);
|
||||||
this.messageHandler = new TestAnnotationMethodMessageHandler(brokerTemplate, channel);
|
this.messageHandler = new TestSimpAnnotationMethodMessageHandler(brokerTemplate, channel);
|
||||||
this.messageHandler.setApplicationContext(new StaticApplicationContext());
|
this.messageHandler.setApplicationContext(new StaticApplicationContext());
|
||||||
this.messageHandler.afterPropertiesSet();
|
this.messageHandler.afterPropertiesSet();
|
||||||
|
|
||||||
|
|
@ -69,7 +69,7 @@ public class AnnotationMethodMessageHandlerTests {
|
||||||
@Test
|
@Test
|
||||||
public void headerArgumentResolution() {
|
public void headerArgumentResolution() {
|
||||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
|
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
|
||||||
headers.setDestination("/headers");
|
headers.setDestination("/pre/headers");
|
||||||
headers.setHeader("foo", "bar");
|
headers.setHeader("foo", "bar");
|
||||||
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
|
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
|
||||||
this.messageHandler.handleMessage(message);
|
this.messageHandler.handleMessage(message);
|
||||||
|
|
@ -87,7 +87,7 @@ public class AnnotationMethodMessageHandlerTests {
|
||||||
@Test
|
@Test
|
||||||
public void messageMappingPathVariableResolution() {
|
public void messageMappingPathVariableResolution() {
|
||||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
|
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();
|
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
|
||||||
this.messageHandler.handleMessage(message);
|
this.messageHandler.handleMessage(message);
|
||||||
|
|
||||||
|
|
@ -99,7 +99,7 @@ public class AnnotationMethodMessageHandlerTests {
|
||||||
@Test
|
@Test
|
||||||
public void subscribeEventPathVariableResolution() {
|
public void subscribeEventPathVariableResolution() {
|
||||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.SUBSCRIBE);
|
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.SUBSCRIBE);
|
||||||
headers.setDestination("/sub/bar/value");
|
headers.setDestination("/pre/sub/bar/value");
|
||||||
Message<?> message = MessageBuilder.withPayload(new byte[0])
|
Message<?> message = MessageBuilder.withPayload(new byte[0])
|
||||||
.copyHeaders(headers.toMap()).build();
|
.copyHeaders(headers.toMap()).build();
|
||||||
this.messageHandler.handleMessage(message);
|
this.messageHandler.handleMessage(message);
|
||||||
|
|
@ -112,7 +112,7 @@ public class AnnotationMethodMessageHandlerTests {
|
||||||
@Test
|
@Test
|
||||||
public void antPatchMatchWildcard() {
|
public void antPatchMatchWildcard() {
|
||||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
|
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();
|
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
|
||||||
this.messageHandler.handleMessage(message);
|
this.messageHandler.handleMessage(message);
|
||||||
|
|
||||||
|
|
@ -122,7 +122,7 @@ public class AnnotationMethodMessageHandlerTests {
|
||||||
@Test
|
@Test
|
||||||
public void bestMatchWildcard() {
|
public void bestMatchWildcard() {
|
||||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
|
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();
|
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
|
||||||
this.messageHandler.handleMessage(message);
|
this.messageHandler.handleMessage(message);
|
||||||
|
|
||||||
|
|
@ -133,7 +133,7 @@ public class AnnotationMethodMessageHandlerTests {
|
||||||
@Test
|
@Test
|
||||||
public void simpleBinding() {
|
public void simpleBinding() {
|
||||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
|
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();
|
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
|
||||||
this.messageHandler.handleMessage(message);
|
this.messageHandler.handleMessage(message);
|
||||||
|
|
||||||
|
|
@ -142,9 +142,10 @@ public class AnnotationMethodMessageHandlerTests {
|
||||||
assertEquals(12L, this.testController.arguments.get("id"));
|
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) {
|
MessageChannel webSocketResponseChannel) {
|
||||||
|
|
||||||
super(brokerTemplate, webSocketResponseChannel);
|
super(brokerTemplate, webSocketResponseChannel);
|
||||||
|
|
@ -157,6 +158,8 @@ public class AnnotationMethodMessageHandlerTests {
|
||||||
|
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
|
@MessageMapping("/pre")
|
||||||
|
@SubscribeEvent("/pre")
|
||||||
private static class TestController {
|
private static class TestController {
|
||||||
|
|
||||||
private String method;
|
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 {
|
private class Match {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,6 @@ public final class PatternsRequestCondition extends AbstractRequestCondition<Pat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private constructor accepting a collection of patterns.
|
* Private constructor accepting a collection of patterns.
|
||||||
* @param fileExtensionResolver
|
|
||||||
*/
|
*/
|
||||||
private PatternsRequestCondition(Collection<String> patterns, UrlPathHelper urlPathHelper,
|
private PatternsRequestCondition(Collection<String> patterns, UrlPathHelper urlPathHelper,
|
||||||
PathMatcher pathMatcher, boolean useSuffixPatternMatch, boolean useTrailingSlashMatch,
|
PathMatcher pathMatcher, boolean useSuffixPatternMatch, boolean useTrailingSlashMatch,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue