From b8809daf5fb6d0608334c2fcbfd3091d5122263a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 24 Oct 2013 21:50:49 -0400 Subject: [PATCH] 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 --- .../cache/config/TestEntity.java | 2 +- .../handler/annotation/MessageMapping.java | 2 +- .../handler/annotation/PathVariable.java | 2 +- ...otationExceptionHandlerMethodResolver.java | 88 +++ .../ExceptionHandlerMethodResolver.java | 157 ----- .../PathVariableMethodArgumentResolver.java | 7 +- .../condition/AbstractMessageCondition.java | 78 +++ .../DestinationPatternsMessageCondition.java | 207 +++++++ .../handler/condition/MessageCondition.java | 65 ++ ...bstractExceptionHandlerMethodResolver.java | 112 ++++ .../method/AbstractMethodMessageHandler.java | 537 ++++++++++++++++ ...andlerMethodArgumentResolverComposite.java | 9 +- ...dlerMethodReturnValueHandlerComposite.java | 15 + .../simp/SimpMessageHeaderAccessor.java | 12 +- ...cketMessageBrokerConfigurationSupport.java | 13 +- .../AnnotationMethodMessageHandler.java | 575 ------------------ .../SimpAnnotationMethodMessageHandler.java | 295 +++++++++ .../simp/handler/SimpMessageMappingInfo.java | 130 ++++ .../SimpMessageTypeMessageCondition.java | 108 ++++ ...nExceptionHandlerMethodResolverTests.java} | 20 +- ...thVariableMethodArgumentResolverTests.java | 4 +- ...tinationPatternsMessageConditionTests.java | 142 +++++ ...essageBrokerConfigurationSupportTests.java | 10 +- ...SimpAnnotationMethodIntegrationTests.java} | 4 +- ...pAnnotationMethodMessageHandlerTests.java} | 27 +- .../SimpMessageTypeMessageConditionTests.java | 105 ++++ .../handler/AbstractHandlerMethodMapping.java | 4 +- .../condition/PatternsRequestCondition.java | 1 - 28 files changed, 1945 insertions(+), 786 deletions(-) create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AnnotationExceptionHandlerMethodResolver.java delete mode 100644 spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/ExceptionHandlerMethodResolver.java create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/handler/condition/AbstractMessageCondition.java create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/handler/condition/DestinationPatternsMessageCondition.java create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/handler/condition/MessageCondition.java create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/handler/method/AbstractExceptionHandlerMethodResolver.java create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/handler/method/AbstractMethodMessageHandler.java delete mode 100644 spring-messaging/src/main/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandler.java create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/simp/handler/SimpAnnotationMethodMessageHandler.java create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/simp/handler/SimpMessageMappingInfo.java create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/simp/handler/SimpMessageTypeMessageCondition.java rename spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/{ExceptionHandlerMethodResolverTests.java => AnnotationExceptionHandlerMethodResolverTests.java} (77%) create mode 100644 spring-messaging/src/test/java/org/springframework/messaging/handler/condition/DestinationPatternsMessageConditionTests.java rename spring-messaging/src/test/java/org/springframework/messaging/simp/handler/{AnnotationMethodIntegrationTests.java => SimpAnnotationMethodIntegrationTests.java} (97%) rename spring-messaging/src/test/java/org/springframework/messaging/simp/handler/{AnnotationMethodMessageHandlerTests.java => SimpAnnotationMethodMessageHandlerTests.java} (89%) create mode 100644 spring-messaging/src/test/java/org/springframework/messaging/simp/handler/SimpMessageTypeMessageConditionTests.java diff --git a/spring-context/src/test/java/org/springframework/cache/config/TestEntity.java b/spring-context/src/test/java/org/springframework/cache/config/TestEntity.java index 4c430071582..b0557464b83 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/TestEntity.java +++ b/spring-context/src/test/java/org/springframework/cache/config/TestEntity.java @@ -21,7 +21,7 @@ import org.springframework.util.ObjectUtils; /** * Simple test entity for use with caching tests. * - * @author Michael Plšd + * @author Michael Plod */ public class TestEntity { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMapping.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMapping.java index a14dac5e0fd..62b3b889a64 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMapping.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMapping.java @@ -32,7 +32,7 @@ import org.springframework.messaging.Message; * @author Rossen Stoyanchev * @since 4.0 * - * @see org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler + * @see org.springframework.messaging.simp.handler.SimpAnnotationMethodMessageHandler */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/PathVariable.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/PathVariable.java index 52eb2fbf3c4..df22081b694 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/PathVariable.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/PathVariable.java @@ -32,7 +32,7 @@ import java.lang.annotation.Target; * * @author Brian Clozel * @see org.springframework.messaging.handler.annotation.MessageMapping - * @see org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler + * @see org.springframework.messaging.simp.handler.SimpAnnotationMethodMessageHandler * * @since 4.0 */ diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AnnotationExceptionHandlerMethodResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AnnotationExceptionHandlerMethodResolver.java new file mode 100644 index 00000000000..fd52abddcdc --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AnnotationExceptionHandlerMethodResolver.java @@ -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, Method> initExceptionMappings(Class handlerType) { + Map, Method> result = new HashMap, Method>(); + for (Method method : HandlerMethodSelector.selectMethods(handlerType, EXCEPTION_HANDLER_METHOD_FILTER)) { + for (Class 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> getMappedExceptions(Method method) { + List> result = new ArrayList>(); + 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; + } + }; + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/ExceptionHandlerMethodResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/ExceptionHandlerMethodResolver.java deleted file mode 100644 index 2c98ea07505..00000000000 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/ExceptionHandlerMethodResolver.java +++ /dev/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, Method> mappedMethods = - new ConcurrentHashMap, Method>(16); - - private final Map, Method> exceptionLookupCache = - new ConcurrentHashMap, Method>(16); - - - /** - * A constructor that finds {@link MessageExceptionHandler} methods in the given type. - * @param handlerType the type to introspect - */ - public ExceptionHandlerMethodResolver(Class handlerType) { - for (Method method : HandlerMethodSelector.selectMethods(handlerType, EXCEPTION_HANDLER_METHOD_FILTER)) { - for (Class exceptionType : detectExceptionMappings(method)) { - addExceptionMapping(exceptionType, method); - } - } - } - - /** - * Extract exception mappings from the {@code @ExceptionHandler} annotation - * first and as a fall-back from the method signature. - */ - @SuppressWarnings("unchecked") - private List> detectExceptionMappings(Method method) { - List> result = new ArrayList>(); - - detectAnnotationExceptionMappings(method, result); - - if (result.isEmpty()) { - for (Class paramType : method.getParameterTypes()) { - if (Throwable.class.isAssignableFrom(paramType)) { - result.add((Class) paramType); - } - } - } - - Assert.notEmpty(result, "No exception types mapped to {" + method + "}"); - - return result; - } - - protected void detectAnnotationExceptionMappings(Method method, List> result) { - MessageExceptionHandler annot = AnnotationUtils.findAnnotation(method, MessageExceptionHandler.class); - result.addAll(Arrays.asList(annot.value())); - } - - private void addExceptionMapping(Class exceptionType, Method method) { - Method oldMethod = this.mappedMethods.put(exceptionType, method); - if (oldMethod != null && !oldMethod.equals(method)) { - throw new IllegalStateException( - "Ambiguous @ExceptionHandler method mapped for [" + exceptionType + "]: {" + - oldMethod + ", " + method + "}."); - } - } - - /** - * Whether the contained type has any exception mappings. - */ - public boolean hasExceptionMappings() { - return (this.mappedMethods.size() > 0); - } - - /** - * Find a method to handle the given exception. - * Use {@link ExceptionDepthComparator} if more than one match is found. - * @param exception the exception - * @return a method to handle the exception or {@code null} - */ - public Method resolveMethod(Exception exception) { - Class exceptionType = exception.getClass(); - Method method = this.exceptionLookupCache.get(exceptionType); - if (method == null) { - method = getMappedMethod(exceptionType); - this.exceptionLookupCache.put(exceptionType, method != null ? method : NO_METHOD_FOUND); - } - return method != NO_METHOD_FOUND ? method : null; - } - - /** - * Return the method mapped to the given exception type or {@code null}. - */ - private Method getMappedMethod(Class exceptionType) { - List> matches = new ArrayList>(); - for(Class mappedException : this.mappedMethods.keySet()) { - if (mappedException.isAssignableFrom(exceptionType)) { - matches.add(mappedException); - } - } - if (!matches.isEmpty()) { - Collections.sort(matches, new ExceptionDepthComparator(exceptionType)); - return mappedMethods.get(matches.get(0)); - } - else { - return null; - } - } - - - /** A filter for selecting annotated exception handling methods. */ - public final static MethodFilter EXCEPTION_HANDLER_METHOD_FILTER = new MethodFilter() { - - @Override - public boolean matches(Method method) { - return AnnotationUtils.findAnnotation(method, MessageExceptionHandler.class) != null; - } - }; - -} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PathVariableMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PathVariableMethodArgumentResolver.java index d2eca952396..53f2cce8989 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PathVariableMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PathVariableMethodArgumentResolver.java @@ -23,7 +23,6 @@ import org.springframework.core.convert.ConversionService; import org.springframework.messaging.Message; import org.springframework.messaging.handler.annotation.PathVariable; import org.springframework.messaging.handler.annotation.ValueConstants; -import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler; /** * Resolves method parameters annotated with {@link PathVariable @PathVariable}. @@ -39,6 +38,9 @@ import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler */ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + public static final String PATH_TEMPLATE_VARIABLES_HEADER = + PathVariableMethodArgumentResolver.class.getSimpleName() + ".templateVariables"; + public PathVariableMethodArgumentResolver(ConversionService cs) { super(cs, null); @@ -57,9 +59,8 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod @Override protected Object resolveArgumentInternal(MethodParameter parameter, Message message, String name) throws Exception { - String headerName = AnnotationMethodMessageHandler.PATH_TEMPLATE_VARIABLES_HEADER; @SuppressWarnings("unchecked") - Map vars = (Map) message.getHeaders().get(headerName); + Map vars = (Map) message.getHeaders().get(PATH_TEMPLATE_VARIABLES_HEADER); return (vars != null) ? vars.get(name) : null; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/condition/AbstractMessageCondition.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/condition/AbstractMessageCondition.java new file mode 100644 index 00000000000..baae8389431 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/condition/AbstractMessageCondition.java @@ -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> implements MessageCondition { + + + /** + * @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(); + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/condition/DestinationPatternsMessageCondition.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/condition/DestinationPatternsMessageCondition.java new file mode 100644 index 00000000000..f45f4ae516b --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/condition/DestinationPatternsMessageCondition.java @@ -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 { + + private final Set 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 patterns, PathMatcher pathMatcher) { + this.patterns = Collections.unmodifiableSet(prependLeadingSlash(patterns)); + this.pathMatcher = (pathMatcher != null) ? pathMatcher : new AntPathMatcher(); + } + + private static List asList(String... patterns) { + return patterns != null ? Arrays.asList(patterns) : Collections.emptyList(); + } + + private static Set prependLeadingSlash(Collection patterns) { + if (patterns == null) { + return Collections.emptySet(); + } + Set result = new LinkedHashSet(patterns.size()); + for (String pattern : patterns) { + if (StringUtils.hasLength(pattern) && !pattern.startsWith("/")) { + pattern = "/" + pattern; + } + result.add(pattern); + } + return result; + } + + public Set getPatterns() { + return this.patterns; + } + + @Override + protected Collection 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: + *
    + *
  • 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)}. + *
  • If only one instance has patterns, use them. + *
  • If neither instance has patterns, use an empty String (i.e. ""). + *
+ */ + @Override + public DestinationPatternsMessageCondition combine(DestinationPatternsMessageCondition other) { + Set result = new LinkedHashSet(); + 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 matches = new ArrayList(); + 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. + * + *

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 patternComparator = this.pathMatcher.getPatternComparator(destination); + + Iterator iterator = patterns.iterator(); + Iterator 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; + } + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/condition/MessageCondition.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/condition/MessageCondition.java new file mode 100644 index 00000000000..a723c308a3f --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/condition/MessageCondition.java @@ -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. + * + *

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 The kind of condition that this condition can be combined + * with or compared to + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public interface MessageCondition { + + /** + * 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); + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/AbstractExceptionHandlerMethodResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/AbstractExceptionHandlerMethodResolver.java new file mode 100644 index 00000000000..dbdcc387055 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/AbstractExceptionHandlerMethodResolver.java @@ -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, Method> mappedMethods = new ConcurrentHashMap, Method>(16); + + private final Map, Method> exceptionLookupCache = new ConcurrentHashMap, Method>(16); + + + /** + * Protected constructor accepting exception-to-method mappings. + */ + protected AbstractExceptionHandlerMethodResolver(Map, 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> getExceptionsFromMethodSignature(Method method) { + List> result = new ArrayList>(); + for (Class paramType : method.getParameterTypes()) { + if (Throwable.class.isAssignableFrom(paramType)) { + result.add((Class) paramType); + } + } + Assert.notEmpty(result, "No exception types mapped to {" + method + "}"); + return result; + } + + /** + * 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 exceptionType = exception.getClass(); + Method method = this.exceptionLookupCache.get(exceptionType); + if (method == null) { + method = getMappedMethod(exceptionType); + this.exceptionLookupCache.put(exceptionType, method != null ? method : NO_METHOD_FOUND); + } + return method != NO_METHOD_FOUND ? method : null; + } + + /** + * Return the method mapped to the given exception type or {@code null}. + */ + private Method getMappedMethod(Class exceptionType) { + List> matches = new ArrayList>(); + for(Class mappedException : this.mappedMethods.keySet()) { + if (mappedException.isAssignableFrom(exceptionType)) { + matches.add(mappedException); + } + } + if (!matches.isEmpty()) { + Collections.sort(matches, new ExceptionDepthComparator(exceptionType)); + return this.mappedMethods.get(matches.get(0)); + } + else { + return null; + } + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/AbstractMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/AbstractMethodMessageHandler.java new file mode 100644 index 00000000000..a69b921a0c5 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/AbstractMethodMessageHandler.java @@ -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. + *

+ * Also supports discovering and invoking exception handling methods to process + * exceptions raised during message handling. + * + * @param 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 + implements MessageHandler, ApplicationContextAware, InitializingBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + public static final String LOOKUP_DESTINATION_HEADER = "lookupDestination"; + + + private Collection destinationPrefixes = new ArrayList(); + + private List customArgumentResolvers = new ArrayList(); + private List customReturnValueHandlers = new ArrayList(); + + private HandlerMethodArgumentResolverComposite argumentResolvers = new HandlerMethodArgumentResolverComposite(); + private HandlerMethodReturnValueHandlerComposite returnValueHandlers =new HandlerMethodReturnValueHandlerComposite(); + + private ApplicationContext applicationContext; + + private final Map handlerMethods = new LinkedHashMap(); + + private final MultiValueMap destinationLookup = new LinkedMultiValueMap(); + + private final Map, AbstractExceptionHandlerMethodResolver> exceptionHandlerCache = + new ConcurrentHashMap, 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. + *

+ * By default the list of prefixes is empty in which case all destinations match. + */ + public void setDestinationPrefixes(Collection prefixes) { + this.destinationPrefixes.clear(); + if (prefixes != null) { + for (String prefix : prefixes) { + prefix = prefix.trim(); + if (!prefix.endsWith("/")) { + prefix += "/"; + } + this.destinationPrefixes.add(prefix); + } + } + } + + public Collection 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 customArgumentResolvers) { + Assert.notNull(customArgumentResolvers, "The 'customArgumentResolvers' cannot be null."); + this.customArgumentResolvers = customArgumentResolvers; + } + + public List 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 customReturnValueHandlers) { + Assert.notNull(customReturnValueHandlers, "The 'customReturnValueHandlers' cannot be null."); + this.customReturnValueHandlers = customReturnValueHandlers; + } + + public List 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 argumentResolvers) { + if (argumentResolvers == null) { + this.argumentResolvers.clear(); + return; + } + this.argumentResolvers.addResolvers(argumentResolvers); + } + + public List 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 returnValueHandlers) { + if (returnValueHandlers == null) { + this.returnValueHandlers.clear(); + return; + } + this.returnValueHandlers.addHandlers(returnValueHandlers); + } + + public List getReturnValueHandlers() { + return this.returnValueHandlers.getReturnValueHandlers(); + } + + /** + * Return a map with all handler methods and their mappings. + */ + public Map 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)}. + *

+ * Sub-classes should also take into account custom argument types configured via + * {@link #setCustomArgumentResolvers(java.util.List)}. + */ + protected abstract List 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)}. + *

+ * Sub-classes should also take into account custom return value types configured via + * {@link #setCustomReturnValueHandlers(java.util.List)}. + */ + protected abstract List 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 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 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. + *

+ * 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 matches = new ArrayList(); + + List mappingsByUrl = this.destinationLookup.get(lookupDestination); + if (mappingsByUrl != null) { + addMatchesToCollection(mappingsByUrl, message, matches); + } + + if (matches.isEmpty()) { + // No direct hits, go through all mappings + Set allMappings = this.handlerMethods.keySet(); + addMatchesToCollection(allMappings, message, matches); + } + + if (matches.isEmpty()) { + handleNoMatch(handlerMethods.keySet(), lookupDestination, message); + return; + } + + Comparator 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 mappingsToCheck, Message message, List 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 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 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 { + + private final Comparator comparator; + + public MatchComparator(Comparator comparator) { + this.comparator = comparator; + } + + @Override + public int compare(Match match1, Match match2) { + return this.comparator.compare(match1.mapping, match2.mapping); + } + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodArgumentResolverComposite.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodArgumentResolverComposite.java index ad9a8901ebf..c3d552ca5b0 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodArgumentResolverComposite.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodArgumentResolverComposite.java @@ -40,7 +40,7 @@ public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgu protected final Log logger = LogFactory.getLog(getClass()); - private final List argumentResolvers = new LinkedList(); + private final List argumentResolvers = new LinkedList(); private final Map argumentResolverCache = new ConcurrentHashMap(256); @@ -53,6 +53,13 @@ public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgu return Collections.unmodifiableList(this.argumentResolvers); } + /** + * Clear the list of configured resolvers. + */ + public void clear() { + this.argumentResolvers.clear(); + } + /** * Whether the given {@linkplain MethodParameter method parameter} is supported by any registered * {@link HandlerMethodArgumentResolver}. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodReturnValueHandlerComposite.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodReturnValueHandlerComposite.java index 19ce620bf16..299e22430ed 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodReturnValueHandlerComposite.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/method/HandlerMethodReturnValueHandlerComposite.java @@ -17,6 +17,7 @@ package org.springframework.messaging.handler.method; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.apache.commons.logging.Log; @@ -37,6 +38,20 @@ public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodRe private final List returnValueHandlers = new ArrayList(); + /** + * Return a read-only list with the configured handlers. + */ + public List getReturnValueHandlers() { + return Collections.unmodifiableList(this.returnValueHandlers); + } + + /** + * Clear the list of configured handlers. + */ + public void clear() { + this.returnValueHandlers.clear(); + } + /** * Add the given {@link HandlerMethodReturnValueHandler}. */ diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java index bfb90c472f2..847993a8b59 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java @@ -39,17 +39,17 @@ import org.springframework.util.Assert; */ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor { - public static final String CONNECT_MESSAGE_HEADER = "connectMessage"; + public static final String CONNECT_MESSAGE_HEADER = "simpConnectMessage"; - public static final String DESTINATION_HEADER = "destination"; + public static final String DESTINATION_HEADER = "simpDestination"; - public static final String MESSAGE_TYPE_HEADER = "messageType"; + public static final String MESSAGE_TYPE_HEADER = "simpMessageType"; - public static final String SESSION_ID_HEADER = "sessionId"; + public static final String SESSION_ID_HEADER = "simpSessionId"; - public static final String SUBSCRIPTION_ID_HEADER = "subscriptionId"; + public static final String SUBSCRIPTION_ID_HEADER = "simpSubscriptionId"; - public static final String USER_HEADER = "user"; + public static final String USER_HEADER = "simpUser"; /** diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/WebSocketMessageBrokerConfigurationSupport.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/WebSocketMessageBrokerConfigurationSupport.java index 98d1f677f07..9ebb9d842c8 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/WebSocketMessageBrokerConfigurationSupport.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/WebSocketMessageBrokerConfigurationSupport.java @@ -25,11 +25,8 @@ import org.springframework.messaging.SubscribableChannel; import org.springframework.messaging.handler.websocket.SubProtocolWebSocketHandler; import org.springframework.messaging.simp.SimpMessageSendingOperations; import org.springframework.messaging.simp.SimpMessagingTemplate; -import org.springframework.messaging.simp.handler.AbstractBrokerMessageHandler; -import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler; -import org.springframework.messaging.simp.handler.MutableUserQueueSuffixResolver; -import org.springframework.messaging.simp.handler.SimpleUserQueueSuffixResolver; -import org.springframework.messaging.simp.handler.UserDestinationMessageHandler; +import org.springframework.messaging.simp.handler.*; +import org.springframework.messaging.simp.handler.SimpAnnotationMethodMessageHandler; import org.springframework.messaging.support.channel.ExecutorSubscribableChannel; import org.springframework.messaging.support.converter.ByteArrayMessageConverter; import org.springframework.messaging.support.converter.CompositeMessageConverter; @@ -139,9 +136,9 @@ public abstract class WebSocketMessageBrokerConfigurationSupport { // Handling of messages by the application @Bean - public AnnotationMethodMessageHandler annotationMethodMessageHandler() { - AnnotationMethodMessageHandler handler = - new AnnotationMethodMessageHandler(brokerMessagingTemplate(), webSocketResponseChannel()); + public SimpAnnotationMethodMessageHandler annotationMethodMessageHandler() { + SimpAnnotationMethodMessageHandler handler = + new SimpAnnotationMethodMessageHandler(brokerMessagingTemplate(), webSocketResponseChannel()); handler.setDestinationPrefixes(getMessageBrokerConfigurer().getAnnotationMethodDestinationPrefixes()); handler.setMessageConverter(simpMessageConverter()); webSocketRequestChannel().subscribe(handler); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandler.java deleted file mode 100644 index 48652a0c8db..00000000000 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandler.java +++ /dev/null @@ -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 destinationPrefixes = new ArrayList(); - - private MessageConverter messageConverter; - - private ConversionService conversionService = new DefaultFormattingConversionService(); - - private ApplicationContext applicationContext; - - private Map messageMethods = new HashMap(); - - private Map subscribeMethods = new HashMap(); - - private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = - new ConcurrentHashMap, ExceptionHandlerMethodResolver>(64); - - private List customArgumentResolvers = new ArrayList(); - - private List customReturnValueHandlers = new ArrayList(); - - 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 converters = new ArrayList(); - 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"). - *

- * 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. - *

- * Prefixes that do not have a trailing slash will have one automatically appended. - */ - public void setDestinationPrefixes(Collection destinationPrefixes) { - this.destinationPrefixes.clear(); - if (destinationPrefixes != null) { - for (String prefix : destinationPrefixes) { - prefix = prefix.trim(); - if (!prefix.endsWith("/")) { - prefix += "/"; - } - this.destinationPrefixes.add(prefix); - } - } - } - - public Collection 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. - *

- * 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 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 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 void initHandlerMethods(Object handler, Class handlerType, - final Class annotationType, Map handlerMethods) { - - Set 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 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 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 handlerMethods) { - List matches = new ArrayList(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 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 getMatchComparator(final String destination, final PathMatcher pathMatcher) { - return new Comparator() { - @Override - public int compare(Match one, Match other) { - Comparator 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; - } - } -} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/SimpAnnotationMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/SimpAnnotationMethodMessageHandler.java new file mode 100644 index 00000000000..603e770f914 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/SimpAnnotationMethodMessageHandler.java @@ -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. + *

+ * 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 { + + 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 converters = new ArrayList(); + 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. + *

+ * 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. + *

+ * 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 initArgumentResolvers() { + + ConfigurableBeanFactory beanFactory = + (ClassUtils.isAssignableValue(ConfigurableApplicationContext.class, getApplicationContext())) ? + ((ConfigurableApplicationContext) getApplicationContext()).getBeanFactory() : null; + + List resolvers = new ArrayList(); + + // 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 initReturnValueHandlers() { + + List handlers = new ArrayList(); + + // 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 getDirectLookupDestinations(SimpMessageMappingInfo mapping) { + Set result = new LinkedHashSet(); + 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() { + @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 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 set, String lookupDestination, Message message) { + if (logger.isTraceEnabled()) { + logger.trace("No match for " + lookupDestination); + } + } + + @Override + protected AbstractExceptionHandlerMethodResolver createExceptionHandlerMethodResolverFor(Class beanType) { + return new AnnotationExceptionHandlerMethodResolver(beanType); + } + +} \ No newline at end of file diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/SimpMessageMappingInfo.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/SimpMessageMappingInfo.java new file mode 100644 index 00000000000..f7043e35d55 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/SimpMessageMappingInfo.java @@ -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: + *

    + *
  1. {@link SimpMessageTypeMessageCondition} + *
  2. {@link DestinationPatternsMessageCondition} + *
+ * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class SimpMessageMappingInfo implements MessageCondition { + + 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(); + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/SimpMessageTypeMessageCondition.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/SimpMessageTypeMessageCondition.java new file mode 100644 index 00000000000..ea7b471b7dc --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/SimpMessageTypeMessageCondition.java @@ -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 { + + 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; + } +} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/ExceptionHandlerMethodResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/AnnotationExceptionHandlerMethodResolverTests.java similarity index 77% rename from spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/ExceptionHandlerMethodResolverTests.java rename to spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/AnnotationExceptionHandlerMethodResolverTests.java index 4fb5cad5239..6a15cd8050f 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/ExceptionHandlerMethodResolverTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/AnnotationExceptionHandlerMethodResolverTests.java @@ -30,29 +30,29 @@ import static org.junit.Assert.*; /** - * Test fixture for {@link ExceptionHandlerMethodResolver} tests. + * Test fixture for {@link AnnotationExceptionHandlerMethodResolver} tests. * * @author Rossen Stoyanchev */ -public class ExceptionHandlerMethodResolverTests { +public class AnnotationExceptionHandlerMethodResolverTests { @Test public void resolveMethodFromAnnotation() { - ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class); + AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class); IOException exception = new IOException(); assertEquals("handleIOException", resolver.resolveMethod(exception).getName()); } @Test public void resolveMethodFromArgument() { - ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class); + AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class); IllegalArgumentException exception = new IllegalArgumentException(); assertEquals("handleIllegalArgumentException", resolver.resolveMethod(exception).getName()); } @Test public void resolveMethodExceptionSubType() { - ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class); + AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class); IOException ioException = new FileNotFoundException(); assertEquals("handleIOException", resolver.resolveMethod(ioException).getName()); SocketException bindException = new BindException(); @@ -61,14 +61,14 @@ public class ExceptionHandlerMethodResolverTests { @Test public void resolveMethodBestMatch() { - ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class); + AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class); SocketException exception = new SocketException(); assertEquals("handleSocketException", resolver.resolveMethod(exception).getName()); } @Test public void resolveMethodNoMatch() { - ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class); + AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class); Exception exception = new Exception(); assertNull("1st lookup", resolver.resolveMethod(exception)); assertNull("2nd lookup from cache", resolver.resolveMethod(exception)); @@ -76,19 +76,19 @@ public class ExceptionHandlerMethodResolverTests { @Test public void resolveMethodInherited() { - ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(InheritedController.class); + AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(InheritedController.class); IOException exception = new IOException(); assertEquals("handleIOException", resolver.resolveMethod(exception).getName()); } @Test(expected = IllegalStateException.class) public void ambiguousExceptionMapping() { - new ExceptionHandlerMethodResolver(AmbiguousController.class); + new AnnotationExceptionHandlerMethodResolver(AmbiguousController.class); } @Test(expected = IllegalArgumentException.class) public void noExceptionMapping() { - new ExceptionHandlerMethodResolver(NoExceptionController.class); + new AnnotationExceptionHandlerMethodResolver(NoExceptionController.class); } @Controller diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PathVariableMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PathVariableMethodArgumentResolverTests.java index 4f18b35cd52..a81e1d3b649 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PathVariableMethodArgumentResolverTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PathVariableMethodArgumentResolverTests.java @@ -28,7 +28,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.messaging.Message; import org.springframework.messaging.handler.annotation.PathVariable; -import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler; +import org.springframework.messaging.simp.handler.SimpAnnotationMethodMessageHandler; import org.springframework.messaging.support.MessageBuilder; import static org.junit.Assert.*; @@ -74,7 +74,7 @@ public class PathVariableMethodArgumentResolverTests { pathParams.put("foo","bar"); pathParams.put("name","value"); Message message = MessageBuilder.withPayload(new byte[0]) - .setHeader(AnnotationMethodMessageHandler.PATH_TEMPLATE_VARIABLES_HEADER, pathParams).build(); + .setHeader(PathVariableMethodArgumentResolver.PATH_TEMPLATE_VARIABLES_HEADER, pathParams).build(); Object result = this.resolver.resolveArgument(this.paramAnnotated, message); assertEquals("bar",result); result = this.resolver.resolveArgument(this.paramAnnotatedValue, message); diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/condition/DestinationPatternsMessageConditionTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/condition/DestinationPatternsMessageConditionTests.java new file mode 100644 index 00000000000..4290abe37ac --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/condition/DestinationPatternsMessageConditionTests.java @@ -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(); + } + +} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/config/WebSocketMessageBrokerConfigurationSupportTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/config/WebSocketMessageBrokerConfigurationSupportTests.java index 677bdba4e45..fdf6ad7499b 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/config/WebSocketMessageBrokerConfigurationSupportTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/config/WebSocketMessageBrokerConfigurationSupportTests.java @@ -34,7 +34,7 @@ import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.handler.websocket.SubProtocolWebSocketHandler; import org.springframework.messaging.simp.SimpMessageType; import org.springframework.messaging.simp.annotation.SubscribeEvent; -import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler; +import org.springframework.messaging.simp.handler.SimpAnnotationMethodMessageHandler; import org.springframework.messaging.simp.handler.MutableUserQueueSuffixResolver; import org.springframework.messaging.simp.handler.SimpleBrokerMessageHandler; import org.springframework.messaging.simp.handler.UserDestinationMessageHandler; @@ -103,7 +103,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests { List values = captor.getAllValues(); assertEquals(3, values.size()); - assertTrue(values.contains(cxtSimpleBroker.getBean(AnnotationMethodMessageHandler.class))); + assertTrue(values.contains(cxtSimpleBroker.getBean(SimpAnnotationMethodMessageHandler.class))); assertTrue(values.contains(cxtSimpleBroker.getBean(UserDestinationMessageHandler.class))); assertTrue(values.contains(cxtSimpleBroker.getBean(SimpleBrokerMessageHandler.class))); } @@ -117,7 +117,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests { List values = captor.getAllValues(); assertEquals(3, values.size()); - assertTrue(values.contains(cxtStompBroker.getBean(AnnotationMethodMessageHandler.class))); + assertTrue(values.contains(cxtStompBroker.getBean(SimpAnnotationMethodMessageHandler.class))); assertTrue(values.contains(cxtStompBroker.getBean(UserDestinationMessageHandler.class))); assertTrue(values.contains(cxtStompBroker.getBean(StompBrokerRelayMessageHandler.class))); } @@ -152,7 +152,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests { public void webSocketResponseChannelUsedByAnnotatedMethod() { SubscribableChannel channel = this.cxtSimpleBroker.getBean("webSocketResponseChannel", SubscribableChannel.class); - AnnotationMethodMessageHandler messageHandler = this.cxtSimpleBroker.getBean(AnnotationMethodMessageHandler.class); + SimpAnnotationMethodMessageHandler messageHandler = this.cxtSimpleBroker.getBean(SimpAnnotationMethodMessageHandler.class); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SUBSCRIBE); headers.setSessionId("sess1"); @@ -235,7 +235,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests { @Test public void brokerChannelUsedByAnnotatedMethod() { SubscribableChannel channel = this.cxtSimpleBroker.getBean("brokerChannel", SubscribableChannel.class); - AnnotationMethodMessageHandler messageHandler = this.cxtSimpleBroker.getBean(AnnotationMethodMessageHandler.class); + SimpAnnotationMethodMessageHandler messageHandler = this.cxtSimpleBroker.getBean(SimpAnnotationMethodMessageHandler.class); StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); headers.setDestination("/foo"); diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/handler/AnnotationMethodIntegrationTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/handler/SimpAnnotationMethodIntegrationTests.java similarity index 97% rename from spring-messaging/src/test/java/org/springframework/messaging/simp/handler/AnnotationMethodIntegrationTests.java rename to spring-messaging/src/test/java/org/springframework/messaging/simp/handler/SimpAnnotationMethodIntegrationTests.java index 2ae643f7e3d..85b8a1f08c7 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/handler/AnnotationMethodIntegrationTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/handler/SimpAnnotationMethodIntegrationTests.java @@ -62,7 +62,7 @@ import static org.springframework.messaging.simp.stomp.StompTextMessageBuilder.* * @author Rossen Stoyanchev */ @RunWith(Parameterized.class) -public class AnnotationMethodIntegrationTests extends AbstractWebSocketIntegrationTests { +public class SimpAnnotationMethodIntegrationTests extends AbstractWebSocketIntegrationTests { @Parameters public static Iterable arguments() { @@ -190,7 +190,7 @@ public class AnnotationMethodIntegrationTests extends AbstractWebSocketIntegrati } @Configuration - @ComponentScan(basePackageClasses=AnnotationMethodIntegrationTests.class, + @ComponentScan(basePackageClasses=SimpAnnotationMethodIntegrationTests.class, useDefaultFilters=false, includeFilters=@ComponentScan.Filter(IntegrationTestController.class)) static class TestMessageBrokerConfigurer implements WebSocketMessageBrokerConfigurer { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/handler/SimpAnnotationMethodMessageHandlerTests.java similarity index 89% rename from spring-messaging/src/test/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandlerTests.java rename to spring-messaging/src/test/java/org/springframework/messaging/simp/handler/SimpAnnotationMethodMessageHandlerTests.java index d36c2c18576..bf983973781 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/handler/SimpAnnotationMethodMessageHandlerTests.java @@ -41,13 +41,13 @@ import static org.junit.Assert.*; /** - * Test fixture for {@link AnnotationMethodMessageHandler}. + * Test fixture for {@link SimpAnnotationMethodMessageHandler}. * @author Rossen Stoyanchev * @author Brian Clozel */ -public class AnnotationMethodMessageHandlerTests { +public class SimpAnnotationMethodMessageHandlerTests { - private TestAnnotationMethodMessageHandler messageHandler; + private TestSimpAnnotationMethodMessageHandler messageHandler; private TestController testController; @@ -56,7 +56,7 @@ public class AnnotationMethodMessageHandlerTests { public void setup() { MessageChannel channel = Mockito.mock(MessageChannel.class); SimpMessageSendingOperations brokerTemplate = new SimpMessagingTemplate(channel); - this.messageHandler = new TestAnnotationMethodMessageHandler(brokerTemplate, channel); + this.messageHandler = new TestSimpAnnotationMethodMessageHandler(brokerTemplate, channel); this.messageHandler.setApplicationContext(new StaticApplicationContext()); this.messageHandler.afterPropertiesSet(); @@ -69,7 +69,7 @@ public class AnnotationMethodMessageHandlerTests { @Test public void headerArgumentResolution() { SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(); - headers.setDestination("/headers"); + headers.setDestination("/pre/headers"); headers.setHeader("foo", "bar"); Message message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build(); this.messageHandler.handleMessage(message); @@ -87,7 +87,7 @@ public class AnnotationMethodMessageHandlerTests { @Test public void messageMappingPathVariableResolution() { SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(); - headers.setDestination("/message/bar/value"); + headers.setDestination("/pre/message/bar/value"); Message message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build(); this.messageHandler.handleMessage(message); @@ -99,7 +99,7 @@ public class AnnotationMethodMessageHandlerTests { @Test public void subscribeEventPathVariableResolution() { SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.SUBSCRIBE); - headers.setDestination("/sub/bar/value"); + headers.setDestination("/pre/sub/bar/value"); Message message = MessageBuilder.withPayload(new byte[0]) .copyHeaders(headers.toMap()).build(); this.messageHandler.handleMessage(message); @@ -112,7 +112,7 @@ public class AnnotationMethodMessageHandlerTests { @Test public void antPatchMatchWildcard() { SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(); - headers.setDestination("/pathmatch/wildcard/test"); + headers.setDestination("/pre/pathmatch/wildcard/test"); Message message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build(); this.messageHandler.handleMessage(message); @@ -122,7 +122,7 @@ public class AnnotationMethodMessageHandlerTests { @Test public void bestMatchWildcard() { SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(); - headers.setDestination("/bestmatch/bar/path"); + headers.setDestination("/pre/bestmatch/bar/path"); Message message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build(); this.messageHandler.handleMessage(message); @@ -133,7 +133,7 @@ public class AnnotationMethodMessageHandlerTests { @Test public void simpleBinding() { SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(); - headers.setDestination("/binding/id/12"); + headers.setDestination("/pre/binding/id/12"); Message message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build(); this.messageHandler.handleMessage(message); @@ -142,9 +142,10 @@ public class AnnotationMethodMessageHandlerTests { assertEquals(12L, this.testController.arguments.get("id")); } - private static class TestAnnotationMethodMessageHandler extends AnnotationMethodMessageHandler { - public TestAnnotationMethodMessageHandler(SimpMessageSendingOperations brokerTemplate, + private static class TestSimpAnnotationMethodMessageHandler extends SimpAnnotationMethodMessageHandler { + + public TestSimpAnnotationMethodMessageHandler(SimpMessageSendingOperations brokerTemplate, MessageChannel webSocketResponseChannel) { super(brokerTemplate, webSocketResponseChannel); @@ -157,6 +158,8 @@ public class AnnotationMethodMessageHandlerTests { @Controller + @MessageMapping("/pre") + @SubscribeEvent("/pre") private static class TestController { private String method; diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/handler/SimpMessageTypeMessageConditionTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/handler/SimpMessageTypeMessageConditionTests.java new file mode 100644 index 00000000000..4e3d6d2bbf3 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/handler/SimpMessageTypeMessageConditionTests.java @@ -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 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 message(SimpMessageType messageType) { + MessageBuilder 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); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java index 27958ebbcad..7c98dcfbdba 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java @@ -337,7 +337,9 @@ public abstract class AbstractHandlerMethodMapping 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 { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java index b523bba2541..a9e29e57bbe 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java @@ -98,7 +98,6 @@ public final class PatternsRequestCondition extends AbstractRequestCondition patterns, UrlPathHelper urlPathHelper, PathMatcher pathMatcher, boolean useSuffixPatternMatch, boolean useTrailingSlashMatch,