/*
* Copyright 2002-2016 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.config;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.converter.ByteArrayMessageConverter;
import org.springframework.messaging.converter.CompositeMessageConverter;
import org.springframework.messaging.converter.ContentTypeResolver;
import org.springframework.messaging.converter.DefaultContentTypeResolver;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.converter.StringMessageConverter;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
import org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry;
import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.simp.user.DefaultUserDestinationResolver;
import org.springframework.messaging.simp.user.MultiServerUserRegistry;
import org.springframework.messaging.simp.user.SimpUserRegistry;
import org.springframework.messaging.simp.user.UserDestinationMessageHandler;
import org.springframework.messaging.simp.user.UserRegistryMessageHandler;
import org.springframework.messaging.support.AbstractSubscribableChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.messaging.support.ExecutorSubscribableChannel;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Controller;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.MimeTypeUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* Test fixture for {@link AbstractMessageBrokerConfiguration}.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @author Sebastien Deleuze
*/
public class MessageBrokerConfigurationTests {
private ApplicationContext defaultContext = new AnnotationConfigApplicationContext(DefaultConfig.class);
private ApplicationContext simpleBrokerContext = new AnnotationConfigApplicationContext(SimpleBrokerConfig.class);
private ApplicationContext brokerRelayContext = new AnnotationConfigApplicationContext(BrokerRelayConfig.class);
private ApplicationContext customContext = new AnnotationConfigApplicationContext(CustomConfig.class);
@Test
public void clientInboundChannel() {
TestChannel channel = this.simpleBrokerContext.getBean("clientInboundChannel", TestChannel.class);
Set<MessageHandler> handlers = channel.getSubscribers();
assertEquals(3, handlers.size());
assertTrue(handlers.contains(simpleBrokerContext.getBean(SimpAnnotationMethodMessageHandler.class)));
assertTrue(handlers.contains(simpleBrokerContext.getBean(UserDestinationMessageHandler.class)));
assertTrue(handlers.contains(simpleBrokerContext.getBean(SimpleBrokerMessageHandler.class)));
}
@Test
public void clientInboundChannelWithBrokerRelay() {
TestChannel channel = this.brokerRelayContext.getBean("clientInboundChannel", TestChannel.class);
Set<MessageHandler> handlers = channel.getSubscribers();
assertEquals(3, handlers.size());
assertTrue(handlers.contains(brokerRelayContext.getBean(SimpAnnotationMethodMessageHandler.class)));
assertTrue(handlers.contains(brokerRelayContext.getBean(UserDestinationMessageHandler.class)));
assertTrue(handlers.contains(brokerRelayContext.getBean(StompBrokerRelayMessageHandler.class)));
}
@Test
public void clientInboundChannelCustomized() {
AbstractSubscribableChannel channel = this.customContext.getBean(
"clientInboundChannel", AbstractSubscribableChannel.class);
assertEquals(3, channel.getInterceptors().size());
CustomThreadPoolTaskExecutor taskExecutor = this.customContext.getBean(
"clientInboundChannelExecutor", CustomThreadPoolTaskExecutor.class);
assertEquals(11, taskExecutor.getCorePoolSize());
assertEquals(12, taskExecutor.getMaxPoolSize());
assertEquals(13, taskExecutor.getKeepAliveSeconds());
}
@Test
public void clientOutboundChannelUsedByAnnotatedMethod() {
TestChannel channel = this.simpleBrokerContext.getBean("clientOutboundChannel", TestChannel.class);
SimpAnnotationMethodMessageHandler messageHandler = this.simpleBrokerContext.getBean(SimpAnnotationMethodMessageHandler.class);
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SUBSCRIBE);
headers.setSessionId("sess1");
headers.setSessionAttributes(new ConcurrentHashMap<>());
headers.setSubscriptionId("subs1");
headers.setDestination("/foo");
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
messageHandler.handleMessage(message);
message = channel.messages.get(0);
headers = StompHeaderAccessor.wrap(message);
assertEquals(SimpMessageType.MESSAGE, headers.getMessageType());
assertEquals("/foo", headers.getDestination());
assertEquals("bar", new String((byte[]) message.getPayload()));
}
@Test
public void clientOutboundChannelUsedBySimpleBroker() {
TestChannel channel = this.simpleBrokerContext.getBean("clientOutboundChannel", TestChannel.class);
SimpleBrokerMessageHandler broker = this.simpleBrokerContext.getBean(SimpleBrokerMessageHandler.class);
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SUBSCRIBE);
headers.setSessionId("sess1");
headers.setSubscriptionId("subs1");
headers.setDestination("/foo");
Message<?> message = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders());
// subscribe
broker.handleMessage(message);
headers = StompHeaderAccessor.create(StompCommand.SEND);
headers.setSessionId("sess1");
headers.setDestination("/foo");
message = MessageBuilder.createMessage("bar".getBytes(), headers.getMessageHeaders());
// message
broker.handleMessage(message);
message = channel.messages.get(0);
headers = StompHeaderAccessor.wrap(message);
assertEquals(SimpMessageType.MESSAGE, headers.getMessageType());
assertEquals("/foo", headers.getDestination());
assertEquals("bar", new String((byte[]) message.getPayload()));
}
@Test
public void clientOutboundChannelCustomized() {
AbstractSubscribableChannel channel = this.customContext.getBean(
"clientOutboundChannel", AbstractSubscribableChannel.class);
assertEquals(3, channel.getInterceptors().size());
ThreadPoolTaskExecutor taskExecutor = this.customContext.getBean(
"clientOutboundChannelExecutor", ThreadPoolTaskExecutor.class);
assertEquals(21, taskExecutor.getCorePoolSize());
assertEquals(22, taskExecutor.getMaxPoolSize());
assertEquals(23, taskExecutor.getKeepAliveSeconds());
}
@Test
public void brokerChannel() {
TestChannel channel = this.simpleBrokerContext.getBean("brokerChannel", TestChannel.class);
Set<MessageHandler> handlers = channel.getSubscribers();
assertEquals(2, handlers.size());
assertTrue(handlers.contains(simpleBrokerContext.getBean(UserDestinationMessageHandler.class)));
assertTrue(handlers.contains(simpleBrokerContext.getBean(SimpleBrokerMessageHandler.class)));
assertNull(channel.getExecutor());
}
@Test
public void brokerChannelWithBrokerRelay() {
TestChannel channel = this.brokerRelayContext.getBean("brokerChannel", TestChannel.class);
Set<MessageHandler> handlers = channel.getSubscribers();
assertEquals(2, handlers.size());
assertTrue(handlers.contains(brokerRelayContext.getBean(UserDestinationMessageHandler.class)));
assertTrue(handlers.contains(brokerRelayContext.getBean(StompBrokerRelayMessageHandler.class)));
}
@Test
public void brokerChannelUsedByAnnotatedMethod() {
TestChannel channel = this.simpleBrokerContext.getBean("brokerChannel", TestChannel.class);
SimpAnnotationMethodMessageHandler messageHandler =
this.simpleBrokerContext.getBean(SimpAnnotationMethodMessageHandler.class);
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND);
headers.setSessionId("sess1");
headers.setSessionAttributes(new ConcurrentHashMap<>());
headers.setDestination("/foo");
Message<?> message = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders());
messageHandler.handleMessage(message);
message = channel.messages.get(0);
headers = StompHeaderAccessor.wrap(message);
assertEquals(SimpMessageType.MESSAGE, headers.getMessageType());
assertEquals("/bar", headers.getDestination());
assertEquals("bar", new String((byte[]) message.getPayload()));
}
@Test
public void brokerChannelCustomized() {
AbstractSubscribableChannel channel = this.customContext.getBean(
"brokerChannel", AbstractSubscribableChannel.class);
assertEquals(4, channel.getInterceptors().size());
ThreadPoolTaskExecutor taskExecutor = this.customContext.getBean(
"brokerChannelExecutor", ThreadPoolTaskExecutor.class);
assertEquals(31, taskExecutor.getCorePoolSize());
assertEquals(32, taskExecutor.getMaxPoolSize());
assertEquals(33, taskExecutor.getKeepAliveSeconds());
}
@Test
public void configureMessageConvertersDefault() {
AbstractMessageBrokerConfiguration config = new BaseTestMessageBrokerConfig();
CompositeMessageConverter compositeConverter = config.brokerMessageConverter();
List<MessageConverter> converters = compositeConverter.getConverters();
assertThat(converters.size(), Matchers.is(3));
assertThat(converters.get(0), Matchers.instanceOf(StringMessageConverter.class));
assertThat(converters.get(1), Matchers.instanceOf(ByteArrayMessageConverter.class));
assertThat(converters.get(2), Matchers.instanceOf(MappingJackson2MessageConverter.class));
ContentTypeResolver resolver = ((MappingJackson2MessageConverter) converters.get(2)).getContentTypeResolver();
assertEquals(MimeTypeUtils.APPLICATION_JSON, ((DefaultContentTypeResolver) resolver).getDefaultMimeType());
}
@Test
public void threadPoolSizeDefault() {
String name = "clientInboundChannelExecutor";
ThreadPoolTaskExecutor executor = this.defaultContext.getBean(name, ThreadPoolTaskExecutor.class);
assertEquals(Runtime.getRuntime().availableProcessors() * 2, executor.getCorePoolSize());
// No way to verify queue capacity
name = "clientOutboundChannelExecutor";
executor = this.defaultContext.getBean(name, ThreadPoolTaskExecutor.class);
assertEquals(Runtime.getRuntime().availableProcessors() * 2, executor.getCorePoolSize());
name = "brokerChannelExecutor";
executor = this.defaultContext.getBean(name, ThreadPoolTaskExecutor.class);
assertEquals(0, executor.getCorePoolSize());
assertEquals(1, executor.getMaxPoolSize());
}
@Test
public void configureMessageConvertersCustom() {
final MessageConverter testConverter = mock(MessageConverter.class);
AbstractMessageBrokerConfiguration config = new BaseTestMessageBrokerConfig() {
@Override
protected boolean configureMessageConverters(List<MessageConverter> messageConverters) {
messageConverters.add(testConverter);
return false;
}
};
CompositeMessageConverter compositeConverter = config.brokerMessageConverter();
assertThat(compositeConverter.getConverters().size(), Matchers.is(1));
Iterator<MessageConverter> iterator = compositeConverter.getConverters().iterator();
assertThat(iterator.next(), Matchers.is(testConverter));
}
@Test
public void configureMessageConvertersCustomAndDefault() {
final MessageConverter testConverter = mock(MessageConverter.class);
AbstractMessageBrokerConfiguration config = new BaseTestMessageBrokerConfig() {
@Override
protected boolean configureMessageConverters(List<MessageConverter> messageConverters) {
messageConverters.add(testConverter);
return true;
}
};
CompositeMessageConverter compositeConverter = config.brokerMessageConverter();
assertThat(compositeConverter.getConverters().size(), Matchers.is(4));
Iterator<MessageConverter> iterator = compositeConverter.getConverters().iterator();
assertThat(iterator.next(), Matchers.is(testConverter));
assertThat(iterator.next(), Matchers.instanceOf(StringMessageConverter.class));
assertThat(iterator.next(), Matchers.instanceOf(ByteArrayMessageConverter.class));
assertThat(iterator.next(), Matchers.instanceOf(MappingJackson2MessageConverter.class));
}
@Test
public void customArgumentAndReturnValueTypes() throws Exception {
SimpAnnotationMethodMessageHandler handler = this.customContext.getBean(SimpAnnotationMethodMessageHandler.class);
List<HandlerMethodArgumentResolver> customResolvers = handler.getCustomArgumentResolvers();
assertEquals(1, customResolvers.size());
assertTrue(handler.getArgumentResolvers().contains(customResolvers.get(0)));
List<HandlerMethodReturnValueHandler> customHandlers = handler.getCustomReturnValueHandlers();
assertEquals(1, customHandlers.size());
assertTrue(handler.getReturnValueHandlers().contains(customHandlers.get(0)));
}
@Test
public void simpValidatorDefault() {
AbstractMessageBrokerConfiguration config = new BaseTestMessageBrokerConfig() {};
config.setApplicationContext(new StaticApplicationContext());
assertThat(config.simpValidator(), Matchers.notNullValue());
assertThat(config.simpValidator(), Matchers.instanceOf(OptionalValidatorFactoryBean.class));
}
@Test
public void simpValidatorCustom() {
final Validator validator = mock(Validator.class);
AbstractMessageBrokerConfiguration config = new BaseTestMessageBrokerConfig() {
@Override
public Validator getValidator() {
return validator;
}
};
assertSame(validator, config.simpValidator());
}
@Test
public void simpValidatorMvc() {
StaticApplicationContext appCxt = new StaticApplicationContext();
appCxt.registerSingleton("mvcValidator", TestValidator.class);
AbstractMessageBrokerConfiguration config = new BaseTestMessageBrokerConfig() {};
config.setApplicationContext(appCxt);
assertThat(config.simpValidator(), Matchers.notNullValue());
assertThat(config.simpValidator(), Matchers.instanceOf(TestValidator.class));
}
@Test
public void simpValidatorInjected() {
SimpAnnotationMethodMessageHandler messageHandler =
this.simpleBrokerContext.getBean(SimpAnnotationMethodMessageHandler.class);
assertThat(messageHandler.getValidator(), Matchers.notNullValue(Validator.class));
}
@Test
public void customPathMatcher() {
SimpleBrokerMessageHandler broker = this.customContext.getBean(SimpleBrokerMessageHandler.class);
DefaultSubscriptionRegistry registry = (DefaultSubscriptionRegistry) broker.getSubscriptionRegistry();
assertEquals("a.a", registry.getPathMatcher().combine("a", "a"));
SimpAnnotationMethodMessageHandler handler = this.customContext.getBean(SimpAnnotationMethodMessageHandler.class);
assertEquals("a.a", handler.getPathMatcher().combine("a", "a"));
DefaultUserDestinationResolver resolver = this.customContext.getBean(DefaultUserDestinationResolver.class);
assertNotNull(resolver);
assertEquals(false, new DirectFieldAccessor(resolver).getPropertyValue("keepLeadingSlash"));
}
@Test
public void customCacheLimit() {
SimpleBrokerMessageHandler broker = this.customContext.getBean(SimpleBrokerMessageHandler.class);
DefaultSubscriptionRegistry registry = (DefaultSubscriptionRegistry) broker.getSubscriptionRegistry();
assertEquals(8192, registry.getCacheLimit());
}
@Test
public void userBroadcasts() throws Exception {
SimpUserRegistry userRegistry = this.brokerRelayContext.getBean(SimpUserRegistry.class);
assertEquals(MultiServerUserRegistry.class, userRegistry.getClass());
UserDestinationMessageHandler handler1 = this.brokerRelayContext.getBean(UserDestinationMessageHandler.class);
assertEquals("/topic/unresolved-user-destination", handler1.getBroadcastDestination());
UserRegistryMessageHandler handler2 = this.brokerRelayContext.getBean(UserRegistryMessageHandler.class);
assertEquals("/topic/simp-user-registry", handler2.getBroadcastDestination());
StompBrokerRelayMessageHandler relay = this.brokerRelayContext.getBean(StompBrokerRelayMessageHandler.class);
assertNotNull(relay.getSystemSubscriptions());
assertEquals(2, relay.getSystemSubscriptions().size());
assertSame(handler1, relay.getSystemSubscriptions().get("/topic/unresolved-user-destination"));
assertSame(handler2, relay.getSystemSubscriptions().get("/topic/simp-user-registry"));
}
@Test
public void userBroadcastsDisabledWithSimpleBroker() throws Exception {
SimpUserRegistry registry = this.simpleBrokerContext.getBean(SimpUserRegistry.class);
assertNotNull(registry);
assertNotEquals(MultiServerUserRegistry.class, registry.getClass());
UserDestinationMessageHandler handler = this.simpleBrokerContext.getBean(UserDestinationMessageHandler.class);
assertNull(handler.getBroadcastDestination());
String name = "userRegistryMessageHandler";
MessageHandler messageHandler = this.simpleBrokerContext.getBean(name, MessageHandler.class);
assertNotEquals(UserRegistryMessageHandler.class, messageHandler.getClass());
}
@SuppressWarnings("unused")
@Controller
static class TestController {
@SubscribeMapping("/foo")
public String handleSubscribe() {
return "bar";
}
@MessageMapping("/foo")
@SendTo("/bar")
public String handleMessage() {
return "bar";
}
}
static class BaseTestMessageBrokerConfig extends AbstractMessageBrokerConfiguration {
@Override
protected SimpUserRegistry createLocalUserRegistry() {
return mock(SimpUserRegistry.class);
}
}
@SuppressWarnings("unused")
@Configuration
static class SimpleBrokerConfig extends BaseTestMessageBrokerConfig {
@Bean
public TestController subscriptionController() {
return new TestController();
}
@Override
@Bean
public AbstractSubscribableChannel clientInboundChannel() {
return new TestChannel();
}
@Override
@Bean
public AbstractSubscribableChannel clientOutboundChannel() {
return new TestChannel();
}
@Override
@Bean
public AbstractSubscribableChannel brokerChannel() {
return new TestChannel();
}
}
@Configuration
static class BrokerRelayConfig extends SimpleBrokerConfig {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/topic", "/queue").setAutoStartup(true)
.setUserDestinationBroadcast("/topic/unresolved-user-destination")
.setUserRegistryBroadcast("/topic/simp-user-registry");
}
}
@Configuration
static class DefaultConfig extends BaseTestMessageBrokerConfig {
}
@Configuration
static class CustomConfig extends BaseTestMessageBrokerConfig {
private ChannelInterceptor interceptor = new ChannelInterceptorAdapter() {};
@Override
protected void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(this.interceptor);
registration.taskExecutor(new CustomThreadPoolTaskExecutor())
.corePoolSize(11).maxPoolSize(12).keepAliveSeconds(13).queueCapacity(14);
}
@Override
protected void configureClientOutboundChannel(ChannelRegistration registration) {
registration.setInterceptors(this.interceptor, this.interceptor);
registration.taskExecutor().corePoolSize(21).maxPoolSize(22).keepAliveSeconds(23).queueCapacity(24);
}
@Override
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(mock(HandlerMethodArgumentResolver.class));
}
@Override
protected void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
returnValueHandlers.add(mock(HandlerMethodReturnValueHandler.class));
}
@Override
protected void configureMessageBroker(MessageBrokerRegistry registry) {
registry.configureBrokerChannel().setInterceptors(this.interceptor, this.interceptor, this.interceptor);
registry.configureBrokerChannel().taskExecutor().corePoolSize(31).maxPoolSize(32).keepAliveSeconds(33).queueCapacity(34);
registry.setPathMatcher(new AntPathMatcher(".")).enableSimpleBroker("/topic", "/queue");
registry.setCacheLimit(8192);
}
}
private static class TestChannel extends ExecutorSubscribableChannel {
private final List<Message<?>> messages = new ArrayList<>();
@Override
public boolean sendInternal(Message<?> message, long timeout) {
this.messages.add(message);
return true;
}
}
private static class TestValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return false;
}
@Override
public void validate(Object target, Errors errors) {
}
}
@SuppressWarnings("serial")
private static class CustomThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
}
}