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
new file mode 100644
index 00000000000..00b21e9a745
--- /dev/null
+++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/PathVariable.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation which indicates that a method parameter should be bound to a path template
+ * variable. Supported for {@link org.springframework.messaging.simp.annotation.SubscribeEvent},
+ * {@link org.springframework.messaging.simp.annotation.UnsubscribeEvent},
+ * {@link org.springframework.messaging.handler.annotation.MessageMapping}
+ * annotated handler methods.
+ *
+ *
A {@code @PathVariable} template variable is always required and does not have
+ * a default value to fall back on.
+ *
+ * @author Brian Clozel
+ * @see org.springframework.messaging.simp.annotation.SubscribeEvent
+ * @see org.springframework.messaging.simp.annotation.UnsubscribeEvent
+ * @see org.springframework.messaging.handler.annotation.MessageMapping
+ * @since 4.0
+ */
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface PathVariable {
+
+ /** The path template variable to bind to. */
+ String value() default "";
+}
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
new file mode 100644
index 00000000000..d0952119132
--- /dev/null
+++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PathVariableMethodArgumentResolver.java
@@ -0,0 +1,77 @@
+/*
+ * 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 org.springframework.beans.factory.config.ConfigurableBeanFactory;
+import org.springframework.core.MethodParameter;
+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;
+
+import java.util.Map;
+
+/**
+ * Resolves method parameters annotated with {@link PathVariable @PathVariable}.
+ *
+ *
A @{@link PathVariable} is a named value that gets resolved from a path
+ * template variable that matches the Message destination header.
+ * It is always required and does not have a default value to fall back on.
+ *
+ * @author Brian Clozel
+ * @see org.springframework.messaging.handler.annotation.PathVariable
+ * @see org.springframework.messaging.MessageHeaders
+ * @since 4.0
+ */
+public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
+
+ public PathVariableMethodArgumentResolver(ConversionService cs, ConfigurableBeanFactory beanFactory) {
+ super(cs, beanFactory);
+ }
+
+ @Override
+ public boolean supportsParameter(MethodParameter parameter) {
+ return parameter.hasParameterAnnotation(PathVariable.class);
+ }
+
+ @Override
+ protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
+ PathVariable annotation = parameter.getParameterAnnotation(PathVariable.class);
+ return new PathVariableNamedValueInfo(annotation);
+ }
+
+ @Override
+ protected Object resolveArgumentInternal(MethodParameter parameter, Message> message, String name) throws Exception {
+ Map pathTemplateVars =
+ (Map) message.getHeaders().get(AnnotationMethodMessageHandler.PATH_TEMPLATE_VARIABLES_HEADER);
+ return (pathTemplateVars != null) ? pathTemplateVars.get(name) : null;
+ }
+
+ @Override
+ protected void handleMissingValue(String name, MethodParameter parameter, Message> message) {
+ throw new MessageHandlingException(message, "Missing path template variable '" + name +
+ "' for method parameter type [" + parameter.getParameterType() + "]");
+ }
+
+ private static class PathVariableNamedValueInfo extends NamedValueInfo {
+
+ private PathVariableNamedValueInfo(PathVariable annotation) {
+ super(annotation.value(), true, ValueConstants.DEFAULT_NONE);
+ }
+ }
+}
\ No newline at end of file
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
index df54a075578..51d846124e2 100644
--- 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
@@ -21,6 +21,8 @@ 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;
@@ -50,6 +52,7 @@ import org.springframework.messaging.handler.annotation.support.HeaderMethodArgu
import org.springframework.messaging.handler.annotation.support.HeadersMethodArgumentResolver;
import org.springframework.messaging.handler.annotation.support.MessageMethodArgumentResolver;
import org.springframework.messaging.handler.annotation.support.PayloadArgumentResolver;
+import org.springframework.messaging.handler.annotation.support.PathVariableMethodArgumentResolver;
import org.springframework.messaging.handler.method.HandlerMethod;
import org.springframework.messaging.handler.method.HandlerMethodArgumentResolver;
import org.springframework.messaging.handler.method.HandlerMethodArgumentResolverComposite;
@@ -67,25 +70,35 @@ import org.springframework.messaging.simp.annotation.support.PrincipalMethodArgu
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.MessageHeaderAccessor;
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;
/**
* @author Rossen Stoyanchev
+ * @author Brian Clozel
* @since 4.0
*/
public class AnnotationMethodMessageHandler implements MessageHandler, ApplicationContextAware, InitializingBean {
+ public static final String PATH_TEMPLATE_VARIABLES_HEADER = "Spring-PathTemplateVariables";
+
+ public static final String BEST_MATCHING_PATTERN_HEADER = "Spring-BestMatchingPattern";
+
private static final Log logger = LogFactory.getLog(AnnotationMethodMessageHandler.class);
+ private final PathMatcher pathMatcher = new AntPathMatcher();
+
private final SimpMessageSendingOperations brokerTemplate;
private final SimpMessageSendingOperations webSocketResponseTemplate;
@@ -242,6 +255,7 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati
// Annotation-based argument resolution
this.argumentResolvers.addResolver(new HeaderMethodArgumentResolver(this.conversionService, beanFactory));
this.argumentResolvers.addResolver(new HeadersMethodArgumentResolver());
+ this.argumentResolvers.addResolver(new PathVariableMethodArgumentResolver(this.conversionService, beanFactory));
// Type-based argument resolution
this.argumentResolvers.addResolver(new PrincipalMethodArgumentResolver());
@@ -363,7 +377,7 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati
return;
}
- HandlerMethod match = getHandlerMethod(lookupPath, handlerMethods);
+ MappingInfoMatch match = matchMappingInfo(lookupPath, handlerMethods);
if (match == null) {
if (logger.isTraceEnabled()) {
logger.trace("No matching method, lookup path " + lookupPath);
@@ -371,13 +385,16 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati
return;
}
- HandlerMethod handlerMethod = match.createWithResolvedBean();
+ HandlerMethod handlerMethod = match.handlerMethod.createWithResolvedBean();
InvocableHandlerMethod invocableHandlerMethod = new InvocableHandlerMethod(handlerMethod);
invocableHandlerMethod.setMessageMethodArgumentResolvers(this.argumentResolvers);
try {
headers.setDestination(lookupPath);
+ headers.setHeader(BEST_MATCHING_PATTERN_HEADER,match.mappingDestination);
+ headers.setHeader(PATH_TEMPLATE_VARIABLES_HEADER,
+ pathMatcher.extractUriTemplateVariables(match.mappingDestination,lookupPath));
message = MessageBuilder.withPayload(message.getPayload()).setHeaders(headers).build();
Object returnValue = invocableHandlerMethod.invoke(message);
@@ -444,17 +461,52 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati
}
}
- protected HandlerMethod getHandlerMethod(String destination, Map handlerMethods) {
+ protected MappingInfoMatch matchMappingInfo(String destination,
+ Map handlerMethods) {
+
+ List matches = new ArrayList(4);
for (MappingInfo key : handlerMethods.keySet()) {
for (String mappingDestination : key.getDestinations()) {
- if (destination.equals(mappingDestination)) {
- return handlerMethods.get(key);
+ if (this.pathMatcher.match(mappingDestination, destination)) {
+ matches.add(new MappingInfoMatch(mappingDestination,
+ handlerMethods.get(key)));
+ }
+ }
+ }
+ if(!matches.isEmpty()) {
+ Comparator comparator = getMappingInfoMatchComparator(destination, this.pathMatcher);
+ Collections.sort(matches, comparator);
+
+ if (logger.isTraceEnabled()) {
+ logger.trace("Found " + matches.size() + " matching mapping(s) for [" + destination + "] : " + matches);
+ }
+
+ MappingInfoMatch bestMatch = matches.get(0);
+ if (matches.size() > 1) {
+ MappingInfoMatch 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;
}
return null;
}
+ private Comparator getMappingInfoMatchComparator(String destination,
+ PathMatcher pathMatcher) {
+ return new Comparator() {
+ @Override
+ public int compare(MappingInfoMatch one, MappingInfoMatch other) {
+ Comparator patternComparator = pathMatcher.getPatternComparator(destination);
+ return patternComparator.compare(one.mappingDestination,other.mappingDestination);
+ }
+ };
+ }
private static class MappingInfo {
@@ -493,4 +545,14 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati
}
}
+ private static class MappingInfoMatch {
+
+ private final String mappingDestination;
+ private final HandlerMethod handlerMethod;
+
+ public MappingInfoMatch(String destination, HandlerMethod handlerMethod) {
+ this.mappingDestination = destination;
+ this.handlerMethod = handlerMethod;
+ }
+ }
}
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
new file mode 100644
index 00000000000..840b6e39ace
--- /dev/null
+++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PathVariableMethodArgumentResolverTests.java
@@ -0,0 +1,101 @@
+/*
+ * 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 org.junit.Before;
+import org.junit.Test;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.GenericTypeResolver;
+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.support.MessageBuilder;
+
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test fixture for {@link PathVariableMethodArgumentResolver} tests.
+ * @author Brian Clozel
+ */
+public class PathVariableMethodArgumentResolverTests {
+
+ private PathVariableMethodArgumentResolver resolver;
+
+ private MethodParameter paramAnnotated;
+ private MethodParameter paramAnnotatedValue;
+ private MethodParameter paramNotAnnotated;
+
+ @Before
+ public void setup() throws Exception {
+ GenericApplicationContext cxt = new GenericApplicationContext();
+ cxt.refresh();
+ this.resolver = new PathVariableMethodArgumentResolver(new DefaultConversionService(), cxt.getBeanFactory());
+
+ Method method = getClass().getDeclaredMethod("handleMessage",
+ String.class, String.class, String.class);
+ this.paramAnnotated = new MethodParameter(method, 0);
+ this.paramAnnotatedValue = new MethodParameter(method, 1);
+ this.paramNotAnnotated = new MethodParameter(method, 2);
+
+ this.paramAnnotated.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
+ GenericTypeResolver.resolveParameterType(this.paramAnnotated, PathVariableMethodArgumentResolver.class);
+ this.paramAnnotatedValue.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
+ GenericTypeResolver.resolveParameterType(this.paramAnnotatedValue, PathVariableMethodArgumentResolver.class);
+ }
+
+ @Test
+ public void supportsParameter() {
+ assertTrue(resolver.supportsParameter(paramAnnotated));
+ assertTrue(resolver.supportsParameter(paramAnnotatedValue));
+ assertFalse(resolver.supportsParameter(paramNotAnnotated));
+ }
+
+ @Test
+ public void resolveArgument() throws Exception {
+ Map pathParams = new HashMap();
+ pathParams.put("foo","bar");
+ pathParams.put("name","value");
+ Message message = MessageBuilder.withPayload(new byte[0])
+ .setHeader(AnnotationMethodMessageHandler.PATH_TEMPLATE_VARIABLES_HEADER, pathParams).build();
+ Object result = this.resolver.resolveArgument(this.paramAnnotated, message);
+ assertEquals("bar",result);
+ result = this.resolver.resolveArgument(this.paramAnnotatedValue, message);
+ assertEquals("value",result);
+ }
+
+ @Test(expected = MessageHandlingException.class)
+ public void resolveArgumentNotFound() throws Exception {
+ Message message = MessageBuilder.withPayload(new byte[0]).build();
+ this.resolver.resolveArgument(this.paramAnnotated, message);
+ }
+
+ @SuppressWarnings("unused")
+ private void handleMessage(
+ @PathVariable String foo,
+ @PathVariable(value = "name") String param1,
+ String param3) {
+ }
+}
\ No newline at end of file
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/AnnotationMethodMessageHandlerTests.java
index ab31216a2bf..d36c2c18576 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/AnnotationMethodMessageHandlerTests.java
@@ -28,9 +28,12 @@ import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.MessageMapping;
+import org.springframework.messaging.handler.annotation.PathVariable;
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.support.MessageBuilder;
import org.springframework.stereotype.Controller;
@@ -40,6 +43,7 @@ import static org.junit.Assert.*;
/**
* Test fixture for {@link AnnotationMethodMessageHandler}.
* @author Rossen Stoyanchev
+ * @author Brian Clozel
*/
public class AnnotationMethodMessageHandlerTests {
@@ -80,6 +84,63 @@ public class AnnotationMethodMessageHandlerTests {
this.messageHandler.registerHandler(new DuplicateMappingController());
}
+ @Test
+ public void messageMappingPathVariableResolution() {
+ SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
+ headers.setDestination("/message/bar/value");
+ Message> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
+ this.messageHandler.handleMessage(message);
+
+ assertEquals("messageMappingPathVariable", this.testController.method);
+ assertEquals("bar", this.testController.arguments.get("foo"));
+ assertEquals("value", this.testController.arguments.get("name"));
+ }
+
+ @Test
+ public void subscribeEventPathVariableResolution() {
+ SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.SUBSCRIBE);
+ headers.setDestination("/sub/bar/value");
+ Message> message = MessageBuilder.withPayload(new byte[0])
+ .copyHeaders(headers.toMap()).build();
+ this.messageHandler.handleMessage(message);
+
+ assertEquals("subscribeEventPathVariable", this.testController.method);
+ assertEquals("bar", this.testController.arguments.get("foo"));
+ assertEquals("value", this.testController.arguments.get("name"));
+ }
+
+ @Test
+ public void antPatchMatchWildcard() {
+ SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
+ headers.setDestination("/pathmatch/wildcard/test");
+ Message> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
+ this.messageHandler.handleMessage(message);
+
+ assertEquals("pathMatchWildcard", this.testController.method);
+ }
+
+ @Test
+ public void bestMatchWildcard() {
+ SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
+ headers.setDestination("/bestmatch/bar/path");
+ Message> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
+ this.messageHandler.handleMessage(message);
+
+ assertEquals("bestMatch", this.testController.method);
+ assertEquals("bar", this.testController.arguments.get("foo"));
+ }
+
+ @Test
+ public void simpleBinding() {
+ SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
+ headers.setDestination("/binding/id/12");
+ Message> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
+ this.messageHandler.handleMessage(message);
+
+ assertEquals("simpleBinding", this.testController.method);
+ assertTrue("should be bound to type long", this.testController.arguments.get("id") instanceof Long);
+ assertEquals(12L, this.testController.arguments.get("id"));
+ }
private static class TestAnnotationMethodMessageHandler extends AnnotationMethodMessageHandler {
@@ -109,6 +170,44 @@ public class AnnotationMethodMessageHandlerTests {
this.arguments.put("foo", foo);
this.arguments.put("headers", headers);
}
+
+ @MessageMapping("/message/{foo}/{name}")
+ public void messageMappingPathVariable(@PathVariable("foo") String param1,
+ @PathVariable("name") String param2) {
+ this.method = "messageMappingPathVariable";
+ this.arguments.put("foo", param1);
+ this.arguments.put("name", param2);
+ }
+
+ @SubscribeEvent("/sub/{foo}/{name}")
+ public void subscribeEventPathVariable(@PathVariable("foo") String param1,
+ @PathVariable("name") String param2) {
+ this.method = "subscribeEventPathVariable";
+ this.arguments.put("foo", param1);
+ this.arguments.put("name", param2);
+ }
+
+ @MessageMapping("/pathmatch/wildcard/**")
+ public void pathMatchWildcard() {
+ this.method = "pathMatchWildcard";
+ }
+
+ @MessageMapping("/bestmatch/{foo}/path")
+ public void bestMatch(@PathVariable("foo") String param1) {
+ this.method = "bestMatch";
+ this.arguments.put("foo", param1);
+ }
+
+ @MessageMapping("/bestmatch/**")
+ public void otherMatch() {
+ this.method = "otherMatch";
+ }
+
+ @MessageMapping("/binding/id/{id}")
+ public void simpleBinding(@PathVariable("id") Long id) {
+ this.method = "simpleBinding";
+ this.arguments.put("id", id);
+ }
}
@Controller