/*
* Copyright 2002-2017 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.annotation.support;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.handler.HandlerMethod;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException;
import org.springframework.messaging.simp.SimpAttributes;
import org.springframework.messaging.simp.SimpAttributesContextHolder;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Controller;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.concurrent.ListenableFutureTask;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.BDDMockito.any;
import static org.mockito.BDDMockito.*;
/**
* Test fixture for
* {@link org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler}.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @author Sebastien Deleuze
*/
@SuppressWarnings("unused")
public class SimpAnnotationMethodMessageHandlerTests {
private static final String TEST_INVALID_VALUE = "invalidValue";
private TestSimpAnnotationMethodMessageHandler messageHandler;
private TestController testController;
@Mock
private SubscribableChannel channel;
@Mock
private MessageConverter converter;
@Captor
private ArgumentCaptor<Object> payloadCaptor;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
SimpMessagingTemplate brokerTemplate = new SimpMessagingTemplate(this.channel);
brokerTemplate.setMessageConverter(this.converter);
this.messageHandler = new TestSimpAnnotationMethodMessageHandler(brokerTemplate, this.channel, this.channel);
this.messageHandler.setApplicationContext(new StaticApplicationContext());
this.messageHandler.setValidator(new StringTestValidator(TEST_INVALID_VALUE));
this.messageHandler.afterPropertiesSet();
this.testController = new TestController();
}
@Test
@SuppressWarnings("unchecked")
public void headerArgumentResolution() {
Map<String, Object> headers = Collections.singletonMap("foo", "bar");
Message<?> message = createMessage("/pre/headers", headers);
this.messageHandler.registerHandler(this.testController);
this.messageHandler.handleMessage(message);
assertEquals("headers", this.testController.method);
assertEquals("bar", this.testController.arguments.get("foo"));
assertEquals("bar", ((Map<String, Object>) this.testController.arguments.get("headers")).get("foo"));
}
@Test
public void optionalHeaderArgumentResolutionWhenPresent() {
Map<String, Object> headers = Collections.singletonMap("foo", "bar");
Message<?> message = createMessage("/pre/optionalHeaders", headers);
this.messageHandler.registerHandler(this.testController);
this.messageHandler.handleMessage(message);
assertEquals("optionalHeaders", this.testController.method);
assertEquals("bar", this.testController.arguments.get("foo1"));
assertEquals("bar", this.testController.arguments.get("foo2"));
}
@Test
public void optionalHeaderArgumentResolutionWhenNotPresent() {
Message<?> message = createMessage("/pre/optionalHeaders");
this.messageHandler.registerHandler(this.testController);
this.messageHandler.handleMessage(message);
assertEquals("optionalHeaders", this.testController.method);
assertNull(this.testController.arguments.get("foo1"));
assertNull(this.testController.arguments.get("foo2"));
}
@Test
public void messageMappingDestinationVariableResolution() {
Message<?> message = createMessage("/pre/message/bar/value");
this.messageHandler.registerHandler(this.testController);
this.messageHandler.handleMessage(message);
assertEquals("messageMappingDestinationVariable", this.testController.method);
assertEquals("bar", this.testController.arguments.get("foo"));
assertEquals("value", this.testController.arguments.get("name"));
}
@Test
public void subscribeEventDestinationVariableResolution() {
Message<?> message = createMessage("/pre/sub/bar/value");
this.messageHandler.registerHandler(this.testController);
this.messageHandler.handleMessage(message);
assertEquals("subscribeEventDestinationVariable", this.testController.method);
assertEquals("bar", this.testController.arguments.get("foo"));
assertEquals("value", this.testController.arguments.get("name"));
}
@Test
public void simpleBinding() {
Message<?> message = createMessage("/pre/binding/id/12");
this.messageHandler.registerHandler(this.testController);
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"));
}
@Test
public void validationError() {
Message<?> message = createMessage("/pre/validation/payload");
this.messageHandler.registerHandler(this.testController);
this.messageHandler.handleMessage(message);
assertEquals("handleValidationException", this.testController.method);
}
@Test
public void exceptionWithHandlerMethodArg() {
Message<?> message = createMessage("/pre/illegalState");
this.messageHandler.registerHandler(this.testController);
this.messageHandler.handleMessage(message);
assertEquals("handleExceptionWithHandlerMethodArg", this.testController.method);
HandlerMethod handlerMethod = (HandlerMethod) this.testController.arguments.get("handlerMethod");
assertNotNull(handlerMethod);
assertEquals("illegalState", handlerMethod.getMethod().getName());
}
@Test
public void simpScope() {
Map<String, Object> sessionAttributes = new ConcurrentHashMap<>();
sessionAttributes.put("name", "value");
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
headers.setSessionId("session1");
headers.setSessionAttributes(sessionAttributes);
headers.setDestination("/pre/scope");
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
this.messageHandler.registerHandler(this.testController);
this.messageHandler.handleMessage(message);
assertEquals("scope", this.testController.method);
}
@Test
public void dotPathSeparator() {
DotPathSeparatorController controller = new DotPathSeparatorController();
this.messageHandler.setPathMatcher(new AntPathMatcher("."));
this.messageHandler.registerHandler(controller);
this.messageHandler.setDestinationPrefixes(Arrays.asList("/app1", "/app2/"));
Message<?> message = createMessage("/app1/pre.foo");
this.messageHandler.registerHandler(this.testController);
this.messageHandler.handleMessage(message);
assertEquals("handleFoo", controller.method);
message = createMessage("/app2/pre.foo");
this.messageHandler.handleMessage(message);
assertEquals("handleFoo", controller.method);
}
@Test
@SuppressWarnings("unchecked")
public void listenableFutureSuccess() {
Message emptyMessage = (Message) MessageBuilder.withPayload(new byte[0]).build();
given(this.channel.send(any(Message.class))).willReturn(true);
given(this.converter.toMessage(any(), any(MessageHeaders.class))).willReturn(emptyMessage);
ListenableFutureController controller = new ListenableFutureController();
this.messageHandler.registerHandler(controller);
this.messageHandler.setDestinationPrefixes(Arrays.asList("/app1", "/app2/"));
Message<?> message = createMessage("/app1/listenable-future/success");
this.messageHandler.handleMessage(message);
assertNotNull(controller.future);
controller.future.run();
verify(this.converter).toMessage(this.payloadCaptor.capture(), any(MessageHeaders.class));
assertEquals("foo", this.payloadCaptor.getValue());
}
@Test
@SuppressWarnings("unchecked")
public void listenableFutureFailure() {
Message emptyMessage = (Message) MessageBuilder.withPayload(new byte[0]).build();
given(this.channel.send(any(Message.class))).willReturn(true);
given(this.converter.toMessage(any(), any(MessageHeaders.class))).willReturn(emptyMessage);
ListenableFutureController controller = new ListenableFutureController();
this.messageHandler.registerHandler(controller);
this.messageHandler.setDestinationPrefixes(Arrays.asList("/app1", "/app2/"));
Message<?> message = createMessage("/app1/listenable-future/failure");
this.messageHandler.handleMessage(message);
controller.future.run();
assertTrue(controller.exceptionCaught);
}
@Test
@SuppressWarnings("unchecked")
public void completableFutureSuccess() {
Message emptyMessage = (Message) MessageBuilder.withPayload(new byte[0]).build();
given(this.channel.send(any(Message.class))).willReturn(true);
given(this.converter.toMessage(any(), any(MessageHeaders.class))).willReturn(emptyMessage);
CompletableFutureController controller = new CompletableFutureController();
this.messageHandler.registerHandler(controller);
this.messageHandler.setDestinationPrefixes(Arrays.asList("/app1", "/app2/"));
Message<?> message = createMessage("/app1/completable-future");
this.messageHandler.handleMessage(message);
assertNotNull(controller.future);
controller.future.complete("foo");
verify(this.converter).toMessage(this.payloadCaptor.capture(), any(MessageHeaders.class));
assertEquals("foo", this.payloadCaptor.getValue());
}
@Test
@SuppressWarnings("unchecked")
public void completableFutureFailure() {
Message emptyMessage = (Message) MessageBuilder.withPayload(new byte[0]).build();
given(this.channel.send(any(Message.class))).willReturn(true);
given(this.converter.toMessage(any(), any(MessageHeaders.class))).willReturn(emptyMessage);
CompletableFutureController controller = new CompletableFutureController();
this.messageHandler.registerHandler(controller);
this.messageHandler.setDestinationPrefixes(Arrays.asList("/app1", "/app2/"));
Message<?> message = createMessage("/app1/completable-future");
this.messageHandler.handleMessage(message);
controller.future.completeExceptionally(new IllegalStateException());
assertTrue(controller.exceptionCaught);
}
@Test
public void placeholder() throws Exception {
Message<?> message = createMessage("/pre/myValue");
this.messageHandler.setEmbeddedValueResolver(value -> ("/${myProperty}".equals(value) ? "/myValue" : value));
this.messageHandler.registerHandler(this.testController);
this.messageHandler.handleMessage(message);
assertEquals("placeholder", this.testController.method);
}
private Message<?> createMessage(String destination) {
return createMessage(destination, null);
}
private Message<?> createMessage(String destination, Map<String, Object> headers) {
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create();
accessor.setSessionId("session1");
accessor.setSessionAttributes(new HashMap<>());
accessor.setDestination(destination);
if (headers != null) {
for (Map.Entry<String, Object> entry : headers.entrySet()) {
accessor.setHeader(entry.getKey(), entry.getValue());
}
}
return MessageBuilder.withPayload(new byte[0]).setHeaders(accessor).build();
}
private static class TestSimpAnnotationMethodMessageHandler extends SimpAnnotationMethodMessageHandler {
public TestSimpAnnotationMethodMessageHandler(SimpMessageSendingOperations brokerTemplate,
SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel) {
super(clientInboundChannel, clientOutboundChannel, brokerTemplate);
}
public void registerHandler(Object handler) {
super.detectHandlerMethods(handler);
}
}
@Controller
@MessageMapping("/pre")
private static class TestController {
private String method;
private Map<String, Object> arguments = new LinkedHashMap<>();
@MessageMapping("/headers")
public void headers(@Header String foo, @Headers Map<String, Object> headers) {
this.method = "headers";
this.arguments.put("foo", foo);
this.arguments.put("headers", headers);
}
@MessageMapping("/optionalHeaders")
public void optionalHeaders(@Header(name="foo", required=false) String foo1, @Header("foo") Optional<String> foo2) {
this.method = "optionalHeaders";
this.arguments.put("foo1", foo1);
this.arguments.put("foo2", (foo2.isPresent() ? foo2.get() : null));
}
@MessageMapping("/message/{foo}/{name}")
public void messageMappingDestinationVariable(@DestinationVariable("foo") String param1,
@DestinationVariable("name") String param2) {
this.method = "messageMappingDestinationVariable";
this.arguments.put("foo", param1);
this.arguments.put("name", param2);
}
@SubscribeMapping("/sub/{foo}/{name}")
public void subscribeEventDestinationVariable(@DestinationVariable("foo") String param1,
@DestinationVariable("name") String param2) {
this.method = "subscribeEventDestinationVariable";
this.arguments.put("foo", param1);
this.arguments.put("name", param2);
}
@MessageMapping("/binding/id/{id}")
public void simpleBinding(@DestinationVariable("id") Long id) {
this.method = "simpleBinding";
this.arguments.put("id", id);
}
@MessageMapping("/validation/payload")
public void payloadValidation(@Validated @Payload String payload) {
this.method = "payloadValidation";
this.arguments.put("message", payload);
}
@MessageMapping("/illegalState")
public void illegalState() {
throw new IllegalStateException();
}
@MessageExceptionHandler(MethodArgumentNotValidException.class)
public void handleValidationException() {
this.method = "handleValidationException";
}
@MessageExceptionHandler(IllegalStateException.class)
public void handleExceptionWithHandlerMethodArg(HandlerMethod handlerMethod) {
this.method = "handleExceptionWithHandlerMethodArg";
this.arguments.put("handlerMethod", handlerMethod);
}
@MessageMapping("/scope")
public void scope() {
SimpAttributes simpAttributes = SimpAttributesContextHolder.currentAttributes();
assertThat(simpAttributes.getAttribute("name"), is("value"));
this.method = "scope";
}
@MessageMapping("/${myProperty}")
public void placeholder() {
this.method = "placeholder";
}
}
@Controller
@MessageMapping("pre")
private static class DotPathSeparatorController {
private String method;
@MessageMapping("foo")
public void handleFoo() {
this.method = "handleFoo";
}
}
@Controller
@MessageMapping("listenable-future")
private static class ListenableFutureController {
private ListenableFutureTask<String> future;
private boolean exceptionCaught = false;
@MessageMapping("success")
public ListenableFutureTask<String> handleListenableFuture() {
this.future = new ListenableFutureTask<>(() -> "foo");
return this.future;
}
@MessageMapping("failure")
public ListenableFutureTask<String> handleListenableFutureException() {
this.future = new ListenableFutureTask<>(() -> {
throw new IllegalStateException();
});
return this.future;
}
@MessageExceptionHandler(IllegalStateException.class)
public void handleValidationException() {
this.exceptionCaught = true;
}
}
@Controller
private static class CompletableFutureController {
private CompletableFuture<String> future;
private boolean exceptionCaught = false;
@MessageMapping("completable-future")
public CompletableFuture<String> handleCompletableFuture() {
this.future = new CompletableFuture<>();
return this.future;
}
@MessageExceptionHandler(IllegalStateException.class)
public void handleValidationException() {
this.exceptionCaught = true;
}
}
private static class StringTestValidator implements Validator {
private final String invalidValue;
public StringTestValidator(String invalidValue) {
this.invalidValue = invalidValue;
}
@Override
public boolean supports(Class<?> clazz) {
return String.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
String value = (String) target;
if (invalidValue.equals(value)) {
errors.reject("invalid value '"+invalidValue+"'");
}
}
}
}