/*
* 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.web.socket.config;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.w3c.dom.Element;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.CustomScopeConfigurer;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean;
import org.springframework.messaging.converter.ByteArrayMessageConverter;
import org.springframework.messaging.converter.CompositeMessageConverter;
import org.springframework.messaging.converter.DefaultContentTypeResolver;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.converter.StringMessageConverter;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.SimpSessionScope;
import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler;
import org.springframework.messaging.simp.user.DefaultUserDestinationResolver;
import org.springframework.messaging.simp.user.MultiServerUserRegistry;
import org.springframework.messaging.simp.user.UserDestinationMessageHandler;
import org.springframework.messaging.simp.user.UserRegistryMessageHandler;
import org.springframework.messaging.support.ExecutorSubscribableChannel;
import org.springframework.messaging.support.ImmutableMessageChannelInterceptor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory;
import org.springframework.web.socket.messaging.DefaultSimpUserRegistry;
import org.springframework.web.socket.messaging.StompSubProtocolHandler;
import org.springframework.web.socket.messaging.SubProtocolWebSocketHandler;
import org.springframework.web.socket.messaging.WebSocketAnnotationMethodMessageHandler;
import org.springframework.web.socket.server.support.OriginHandshakeInterceptor;
import org.springframework.web.socket.server.support.WebSocketHandlerMapping;
import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler;
import org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler;
/**
* A {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that provides
* the configuration for the {@code <websocket:message-broker/>} XML namespace element.
*
* <p>Registers a Spring MVC {@link org.springframework.web.servlet.HandlerMapping}
* with order 1 to map HTTP WebSocket handshake requests from STOMP/WebSocket clients.
*
* <p>Registers the following {@link org.springframework.messaging.MessageChannel}s:
* <ul>
* <li>"clientInboundChannel" for receiving messages from clients (e.g. WebSocket clients)
* <li>"clientOutboundChannel" for sending messages to clients (e.g. WebSocket clients)
* <li>"brokerChannel" for sending messages from within the application to the message broker
* </ul>
*
* <p>Registers one of the following based on the selected message broker options:
* <ul>
* <li>a {@link SimpleBrokerMessageHandler} if the <simple-broker/> is used
* <li>a {@link StompBrokerRelayMessageHandler} if the <stomp-broker-relay/> is used
* </ul>
*
* <p>Registers a {@link UserDestinationMessageHandler} for handling user destinations.
*
* @author Brian Clozel
* @author Rossen Stoyanchev
* @since 4.0
*/
class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
public static final String WEB_SOCKET_HANDLER_BEAN_NAME = "subProtocolWebSocketHandler";
public static final String SCHEDULER_BEAN_NAME = "messageBrokerScheduler";
public static final String SOCKJS_SCHEDULER_BEAN_NAME = "messageBrokerSockJsScheduler";
public static final String MESSAGING_TEMPLATE_BEAN_NAME = "brokerMessagingTemplate";
public static final String MESSAGE_CONVERTER_BEAN_NAME = "brokerMessageConverter";
private static final int DEFAULT_MAPPING_ORDER = 1;
private static final boolean jackson2Present = ClassUtils.isPresent(
"com.fasterxml.jackson.databind.ObjectMapper", MessageBrokerBeanDefinitionParser.class.getClassLoader());
private static final boolean javaxValidationPresent =
ClassUtils.isPresent("javax.validation.Validator", MessageBrokerBeanDefinitionParser.class.getClassLoader());
@Override
public BeanDefinition parse(Element element, ParserContext context) {
Object source = context.extractSource(element);
CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source);
context.pushContainingComponent(compDefinition);
Element channelElem = DomUtils.getChildElementByTagName(element, "client-inbound-channel");
RuntimeBeanReference inChannel = getMessageChannel("clientInboundChannel", channelElem, context, source);
channelElem = DomUtils.getChildElementByTagName(element, "client-outbound-channel");
RuntimeBeanReference outChannel = getMessageChannel("clientOutboundChannel", channelElem, context, source);
channelElem = DomUtils.getChildElementByTagName(element, "broker-channel");
RuntimeBeanReference brokerChannel = getMessageChannel("brokerChannel", channelElem, context, source);
RuntimeBeanReference userRegistry = registerUserRegistry(element, context, source);
Object userDestHandler = registerUserDestHandler(element, userRegistry, inChannel, brokerChannel, context, source);
RuntimeBeanReference converter = registerMessageConverter(element, context, source);
RuntimeBeanReference template = registerMessagingTemplate(element, brokerChannel, converter, context, source);
registerAnnotationMethodMessageHandler(element, inChannel, outChannel,converter, template, context, source);
RootBeanDefinition broker = registerMessageBroker(element, inChannel, outChannel, brokerChannel,
userDestHandler, template, userRegistry, context, source);
// WebSocket and sub-protocol handling
ManagedMap<String, Object> urlMap = registerHandlerMapping(element, context, source);
RuntimeBeanReference stompHandler = registerStompHandler(element, inChannel, outChannel, context, source);
for (Element endpointElem : DomUtils.getChildElementsByTagName(element, "stomp-endpoint")) {
RuntimeBeanReference requestHandler = registerRequestHandler(endpointElem, stompHandler, context, source);
String pathAttribute = endpointElem.getAttribute("path");
Assert.state(StringUtils.hasText(pathAttribute), "Invalid <stomp-endpoint> (no path mapping)");
List<String> paths = Arrays.asList(StringUtils.tokenizeToStringArray(pathAttribute, ","));
for (String path : paths) {
path = path.trim();
Assert.state(StringUtils.hasText(path), "Invalid <stomp-endpoint> path attribute: " + pathAttribute);
if (DomUtils.getChildElementByTagName(endpointElem, "sockjs") != null) {
path = path.endsWith("/") ? path + "**" : path + "/**";
}
urlMap.put(path, requestHandler);
}
}
Map<String, Object> scopeMap = Collections.<String, Object>singletonMap("websocket", new SimpSessionScope());
RootBeanDefinition scopeConfigurer = new RootBeanDefinition(CustomScopeConfigurer.class);
scopeConfigurer.getPropertyValues().add("scopes", scopeMap);
registerBeanDefByName("webSocketScopeConfigurer", scopeConfigurer, context, source);
registerWebSocketMessageBrokerStats(broker, inChannel, outChannel, context, source);
context.popAndRegisterContainingComponent();
return null;
}
private RuntimeBeanReference registerUserRegistry(Element element, ParserContext context, Object source) {
Element relayElement = DomUtils.getChildElementByTagName(element, "stomp-broker-relay");
boolean multiServer = (relayElement != null && relayElement.hasAttribute("user-registry-broadcast"));
if (multiServer) {
RootBeanDefinition localRegistryBeanDef = new RootBeanDefinition(DefaultSimpUserRegistry.class);
RootBeanDefinition beanDef = new RootBeanDefinition(MultiServerUserRegistry.class);
beanDef.getConstructorArgumentValues().addIndexedArgumentValue(0, localRegistryBeanDef);
String beanName = registerBeanDef(beanDef, context, source);
return new RuntimeBeanReference(beanName);
}
else {
RootBeanDefinition beanDef = new RootBeanDefinition(DefaultSimpUserRegistry.class);
String beanName = registerBeanDef(beanDef, context, source);
return new RuntimeBeanReference(beanName);
}
}
private ManagedMap<String, Object> registerHandlerMapping(Element element,
ParserContext context, Object source) {
RootBeanDefinition handlerMappingDef = new RootBeanDefinition(WebSocketHandlerMapping.class);
String orderAttribute = element.getAttribute("order");
int order = orderAttribute.isEmpty() ? DEFAULT_MAPPING_ORDER : Integer.valueOf(orderAttribute);
handlerMappingDef.getPropertyValues().add("order", order);
String pathHelper = element.getAttribute("path-helper");
if (StringUtils.hasText(pathHelper)) {
handlerMappingDef.getPropertyValues().add("urlPathHelper", new RuntimeBeanReference(pathHelper));
}
ManagedMap<String, Object> urlMap = new ManagedMap<>();
urlMap.setSource(source);
handlerMappingDef.getPropertyValues().add("urlMap", urlMap);
registerBeanDef(handlerMappingDef, context, source);
return urlMap;
}
private RuntimeBeanReference getMessageChannel(String name, Element element, ParserContext context, Object source) {
RootBeanDefinition executor;
if (element == null) {
executor = getDefaultExecutorBeanDefinition(name);
}
else {
Element executorElem = DomUtils.getChildElementByTagName(element, "executor");
if (executorElem == null) {
executor = getDefaultExecutorBeanDefinition(name);
}
else {
executor = new RootBeanDefinition(ThreadPoolTaskExecutor.class);
if (executorElem.hasAttribute("core-pool-size")) {
executor.getPropertyValues().add("corePoolSize", executorElem.getAttribute("core-pool-size"));
}
if (executorElem.hasAttribute("max-pool-size")) {
executor.getPropertyValues().add("maxPoolSize", executorElem.getAttribute("max-pool-size"));
}
if (executorElem.hasAttribute("keep-alive-seconds")) {
executor.getPropertyValues().add("keepAliveSeconds", executorElem.getAttribute("keep-alive-seconds"));
}
if (executorElem.hasAttribute("queue-capacity")) {
executor.getPropertyValues().add("queueCapacity", executorElem.getAttribute("queue-capacity"));
}
}
}
ConstructorArgumentValues argValues = new ConstructorArgumentValues();
if (executor != null) {
executor.getPropertyValues().add("threadNamePrefix", name + "-");
String executorName = name + "Executor";
registerBeanDefByName(executorName, executor, context, source);
argValues.addIndexedArgumentValue(0, new RuntimeBeanReference(executorName));
}
RootBeanDefinition channelDef = new RootBeanDefinition(ExecutorSubscribableChannel.class, argValues, null);
ManagedList<? super Object> interceptors = new ManagedList<>();
if (element != null) {
Element interceptorsElement = DomUtils.getChildElementByTagName(element, "interceptors");
interceptors.addAll(WebSocketNamespaceUtils.parseBeanSubElements(interceptorsElement, context));
}
interceptors.add(new ImmutableMessageChannelInterceptor());
channelDef.getPropertyValues().add("interceptors", interceptors);
registerBeanDefByName(name, channelDef, context, source);
return new RuntimeBeanReference(name);
}
private RootBeanDefinition getDefaultExecutorBeanDefinition(String channelName) {
if (channelName.equals("brokerChannel")) {
return null;
}
RootBeanDefinition executorDef = new RootBeanDefinition(ThreadPoolTaskExecutor.class);
executorDef.getPropertyValues().add("corePoolSize", Runtime.getRuntime().availableProcessors() * 2);
executorDef.getPropertyValues().add("maxPoolSize", Integer.MAX_VALUE);
executorDef.getPropertyValues().add("queueCapacity", Integer.MAX_VALUE);
executorDef.getPropertyValues().add("allowCoreThreadTimeOut", true);
return executorDef;
}
private RuntimeBeanReference registerStompHandler(Element element, RuntimeBeanReference inChannel,
RuntimeBeanReference outChannel, ParserContext context, Object source) {
RootBeanDefinition stompHandlerDef = new RootBeanDefinition(StompSubProtocolHandler.class);
registerBeanDef(stompHandlerDef, context, source);
Element errorHandlerElem = DomUtils.getChildElementByTagName(element, "stomp-error-handler");
if (errorHandlerElem != null) {
RuntimeBeanReference errorHandlerRef = new RuntimeBeanReference(errorHandlerElem.getAttribute("ref"));
stompHandlerDef.getPropertyValues().add("errorHandler", errorHandlerRef);
}
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
cavs.addIndexedArgumentValue(0, inChannel);
cavs.addIndexedArgumentValue(1, outChannel);
RootBeanDefinition handlerDef = new RootBeanDefinition(SubProtocolWebSocketHandler.class, cavs, null);
handlerDef.getPropertyValues().addPropertyValue("protocolHandlers", stompHandlerDef);
registerBeanDefByName(WEB_SOCKET_HANDLER_BEAN_NAME, handlerDef, context, source);
RuntimeBeanReference result = new RuntimeBeanReference(WEB_SOCKET_HANDLER_BEAN_NAME);
Element transportElem = DomUtils.getChildElementByTagName(element, "transport");
if (transportElem != null) {
if (transportElem.hasAttribute("message-size")) {
stompHandlerDef.getPropertyValues().add("messageSizeLimit", transportElem.getAttribute("message-size"));
}
if (transportElem.hasAttribute("send-timeout")) {
handlerDef.getPropertyValues().add("sendTimeLimit", transportElem.getAttribute("send-timeout"));
}
if (transportElem.hasAttribute("send-buffer-size")) {
handlerDef.getPropertyValues().add("sendBufferSizeLimit", transportElem.getAttribute("send-buffer-size"));
}
Element factoriesElement = DomUtils.getChildElementByTagName(transportElem, "decorator-factories");
if (factoriesElement != null) {
ManagedList<Object> factories = extractBeanSubElements(factoriesElement, context);
RootBeanDefinition factoryBean = new RootBeanDefinition(DecoratingFactoryBean.class);
factoryBean.getConstructorArgumentValues().addIndexedArgumentValue(0, result);
factoryBean.getConstructorArgumentValues().addIndexedArgumentValue(1, factories);
result = new RuntimeBeanReference(registerBeanDef(factoryBean, context, source));
}
}
return result;
}
private RuntimeBeanReference registerRequestHandler(Element element, RuntimeBeanReference subProtoHandler,
ParserContext context, Object source) {
RootBeanDefinition beanDef;
RuntimeBeanReference sockJsService = WebSocketNamespaceUtils.registerSockJsService(
element, SCHEDULER_BEAN_NAME, context, source);
if (sockJsService != null) {
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
cavs.addIndexedArgumentValue(0, sockJsService);
cavs.addIndexedArgumentValue(1, subProtoHandler);
beanDef = new RootBeanDefinition(SockJsHttpRequestHandler.class, cavs, null);
// Register alias for backwards compatibility with 4.1
context.getRegistry().registerAlias(SCHEDULER_BEAN_NAME, SOCKJS_SCHEDULER_BEAN_NAME);
}
else {
RuntimeBeanReference handshakeHandler = WebSocketNamespaceUtils.registerHandshakeHandler(element, context, source);
Element interceptorsElement = DomUtils.getChildElementByTagName(element, "handshake-interceptors");
ManagedList<? super Object> interceptors = WebSocketNamespaceUtils.parseBeanSubElements(interceptorsElement, context);
String allowedOriginsAttribute = element.getAttribute("allowed-origins");
List<String> allowedOrigins = Arrays.asList(StringUtils.tokenizeToStringArray(allowedOriginsAttribute, ","));
interceptors.add(new OriginHandshakeInterceptor(allowedOrigins));
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
cavs.addIndexedArgumentValue(0, subProtoHandler);
if (handshakeHandler != null) {
cavs.addIndexedArgumentValue(1, handshakeHandler);
}
beanDef = new RootBeanDefinition(WebSocketHttpRequestHandler.class, cavs, null);
beanDef.getPropertyValues().add("handshakeInterceptors", interceptors);
}
return new RuntimeBeanReference(registerBeanDef(beanDef, context, source));
}
private RootBeanDefinition registerMessageBroker(Element brokerElement,
RuntimeBeanReference inChannel, RuntimeBeanReference outChannel, RuntimeBeanReference brokerChannel,
Object userDestHandler, RuntimeBeanReference brokerTemplate,
RuntimeBeanReference userRegistry, ParserContext context, Object source) {
Element simpleBrokerElem = DomUtils.getChildElementByTagName(brokerElement, "simple-broker");
Element brokerRelayElem = DomUtils.getChildElementByTagName(brokerElement, "stomp-broker-relay");
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
cavs.addIndexedArgumentValue(0, inChannel);
cavs.addIndexedArgumentValue(1, outChannel);
cavs.addIndexedArgumentValue(2, brokerChannel);
RootBeanDefinition brokerDef;
if (simpleBrokerElem != null) {
String prefix = simpleBrokerElem.getAttribute("prefix");
cavs.addIndexedArgumentValue(3, Arrays.asList(StringUtils.tokenizeToStringArray(prefix, ",")));
brokerDef = new RootBeanDefinition(SimpleBrokerMessageHandler.class, cavs, null);
if (brokerElement.hasAttribute("path-matcher")) {
String pathMatcherRef = brokerElement.getAttribute("path-matcher");
brokerDef.getPropertyValues().add("pathMatcher", new RuntimeBeanReference(pathMatcherRef));
}
if (simpleBrokerElem.hasAttribute("scheduler")) {
String scheduler = simpleBrokerElem.getAttribute("scheduler");
brokerDef.getPropertyValues().add("taskScheduler", new RuntimeBeanReference(scheduler));
}
if (simpleBrokerElem.hasAttribute("heartbeat")) {
String heartbeatValue = simpleBrokerElem.getAttribute("heartbeat");
brokerDef.getPropertyValues().add("heartbeatValue", heartbeatValue);
}
}
else if (brokerRelayElem != null) {
String prefix = brokerRelayElem.getAttribute("prefix");
cavs.addIndexedArgumentValue(3, Arrays.asList(StringUtils.tokenizeToStringArray(prefix, ",")));
MutablePropertyValues values = new MutablePropertyValues();
if (brokerRelayElem.hasAttribute("relay-host")) {
values.add("relayHost", brokerRelayElem.getAttribute("relay-host"));
}
if (brokerRelayElem.hasAttribute("relay-port")) {
values.add("relayPort", brokerRelayElem.getAttribute("relay-port"));
}
if (brokerRelayElem.hasAttribute("client-login")) {
values.add("clientLogin", brokerRelayElem.getAttribute("client-login"));
}
if (brokerRelayElem.hasAttribute("client-passcode")) {
values.add("clientPasscode", brokerRelayElem.getAttribute("client-passcode"));
}
if (brokerRelayElem.hasAttribute("system-login")) {
values.add("systemLogin", brokerRelayElem.getAttribute("system-login"));
}
if (brokerRelayElem.hasAttribute("system-passcode")) {
values.add("systemPasscode", brokerRelayElem.getAttribute("system-passcode"));
}
if (brokerRelayElem.hasAttribute("heartbeat-send-interval")) {
values.add("systemHeartbeatSendInterval", brokerRelayElem.getAttribute("heartbeat-send-interval"));
}
if (brokerRelayElem.hasAttribute("heartbeat-receive-interval")) {
values.add("systemHeartbeatReceiveInterval", brokerRelayElem.getAttribute("heartbeat-receive-interval"));
}
if (brokerRelayElem.hasAttribute("virtual-host")) {
values.add("virtualHost", brokerRelayElem.getAttribute("virtual-host"));
}
ManagedMap<String, Object> map = new ManagedMap<>();
map.setSource(source);
if (brokerRelayElem.hasAttribute("user-destination-broadcast")) {
String destination = brokerRelayElem.getAttribute("user-destination-broadcast");
map.put(destination, userDestHandler);
}
if (brokerRelayElem.hasAttribute("user-registry-broadcast")) {
String destination = brokerRelayElem.getAttribute("user-registry-broadcast");
map.put(destination, registerUserRegistryMessageHandler(userRegistry,
brokerTemplate, destination, context, source));
}
if (!map.isEmpty()) {
values.add("systemSubscriptions", map);
}
Class<?> handlerType = StompBrokerRelayMessageHandler.class;
brokerDef = new RootBeanDefinition(handlerType, cavs, values);
}
else {
// Should not happen
throw new IllegalStateException("Neither <simple-broker> nor <stomp-broker-relay> elements found.");
}
registerBeanDef(brokerDef, context, source);
return brokerDef;
}
private RuntimeBeanReference registerUserRegistryMessageHandler(
RuntimeBeanReference userRegistry, RuntimeBeanReference brokerTemplate,
String destination, ParserContext context, Object source) {
Object scheduler = WebSocketNamespaceUtils.registerScheduler(SCHEDULER_BEAN_NAME, context, source);
RootBeanDefinition beanDef = new RootBeanDefinition(UserRegistryMessageHandler.class);
beanDef.getConstructorArgumentValues().addIndexedArgumentValue(0, userRegistry);
beanDef.getConstructorArgumentValues().addIndexedArgumentValue(1, brokerTemplate);
beanDef.getConstructorArgumentValues().addIndexedArgumentValue(2, destination);
beanDef.getConstructorArgumentValues().addIndexedArgumentValue(3, scheduler);
String beanName = registerBeanDef(beanDef, context, source);
return new RuntimeBeanReference(beanName);
}
private RuntimeBeanReference registerMessageConverter(Element element, ParserContext context, Object source) {
Element convertersElement = DomUtils.getChildElementByTagName(element, "message-converters");
ManagedList<? super Object> converters = new ManagedList<>();
if (convertersElement != null) {
converters.setSource(source);
for (Element beanElement : DomUtils.getChildElementsByTagName(convertersElement, "bean", "ref")) {
Object object = context.getDelegate().parsePropertySubElement(beanElement, null);
converters.add(object);
}
}
if (convertersElement == null || Boolean.valueOf(convertersElement.getAttribute("register-defaults"))) {
converters.setSource(source);
converters.add(new RootBeanDefinition(StringMessageConverter.class));
converters.add(new RootBeanDefinition(ByteArrayMessageConverter.class));
if (jackson2Present) {
RootBeanDefinition jacksonConverterDef = new RootBeanDefinition(MappingJackson2MessageConverter.class);
RootBeanDefinition resolverDef = new RootBeanDefinition(DefaultContentTypeResolver.class);
resolverDef.getPropertyValues().add("defaultMimeType", MimeTypeUtils.APPLICATION_JSON);
jacksonConverterDef.getPropertyValues().add("contentTypeResolver", resolverDef);
// Use Jackson factory in order to have JSR-310 and Joda-Time modules registered automatically
GenericBeanDefinition jacksonFactoryDef = new GenericBeanDefinition();
jacksonFactoryDef.setBeanClass(Jackson2ObjectMapperFactoryBean.class);
jacksonFactoryDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
jacksonFactoryDef.setSource(source);
jacksonConverterDef.getPropertyValues().add("objectMapper", jacksonFactoryDef);
converters.add(jacksonConverterDef);
}
}
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
cavs.addIndexedArgumentValue(0, converters);
RootBeanDefinition messageConverterDef = new RootBeanDefinition(CompositeMessageConverter.class, cavs, null);
String name = MESSAGE_CONVERTER_BEAN_NAME;
registerBeanDefByName(name, messageConverterDef, context, source);
return new RuntimeBeanReference(name);
}
private RuntimeBeanReference registerMessagingTemplate(Element element, RuntimeBeanReference brokerChannel,
RuntimeBeanReference messageConverter, ParserContext context, Object source) {
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
cavs.addIndexedArgumentValue(0, brokerChannel);
RootBeanDefinition beanDef = new RootBeanDefinition(SimpMessagingTemplate.class,cavs, null);
if (element.hasAttribute("user-destination-prefix")) {
beanDef.getPropertyValues().add("userDestinationPrefix", element.getAttribute("user-destination-prefix"));
}
beanDef.getPropertyValues().add("messageConverter", messageConverter);
String name = MESSAGING_TEMPLATE_BEAN_NAME;
registerBeanDefByName(name, beanDef, context, source);
return new RuntimeBeanReference(name);
}
private void registerAnnotationMethodMessageHandler(Element messageBrokerElement,
RuntimeBeanReference inChannel, RuntimeBeanReference outChannel,
RuntimeBeanReference converter, RuntimeBeanReference messagingTemplate,
ParserContext context, Object source) {
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
cavs.addIndexedArgumentValue(0, inChannel);
cavs.addIndexedArgumentValue(1, outChannel);
cavs.addIndexedArgumentValue(2, messagingTemplate);
MutablePropertyValues values = new MutablePropertyValues();
String prefixAttribute = messageBrokerElement.getAttribute("application-destination-prefix");
values.add("destinationPrefixes", Arrays.asList(StringUtils.tokenizeToStringArray(prefixAttribute, ",")));
values.add("messageConverter", converter);
RootBeanDefinition beanDef = new RootBeanDefinition(WebSocketAnnotationMethodMessageHandler.class, cavs, values);
if (messageBrokerElement.hasAttribute("path-matcher")) {
String pathMatcherRef = messageBrokerElement.getAttribute("path-matcher");
beanDef.getPropertyValues().add("pathMatcher", new RuntimeBeanReference(pathMatcherRef));
}
RuntimeBeanReference validatorRef = getValidator(messageBrokerElement, source, context);
if (validatorRef != null) {
beanDef.getPropertyValues().add("validator", validatorRef);
}
Element resolversElement = DomUtils.getChildElementByTagName(messageBrokerElement, "argument-resolvers");
if (resolversElement != null) {
values.add("customArgumentResolvers", extractBeanSubElements(resolversElement, context));
}
Element handlersElement = DomUtils.getChildElementByTagName(messageBrokerElement, "return-value-handlers");
if (handlersElement != null) {
values.add("customReturnValueHandlers", extractBeanSubElements(handlersElement, context));
}
registerBeanDef(beanDef, context, source);
}
private RuntimeBeanReference getValidator(Element messageBrokerElement, Object source, ParserContext parserContext) {
if (messageBrokerElement.hasAttribute("validator")) {
return new RuntimeBeanReference(messageBrokerElement.getAttribute("validator"));
}
else if (javaxValidationPresent) {
RootBeanDefinition validatorDef = new RootBeanDefinition(
"org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean");
validatorDef.setSource(source);
validatorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
String validatorName = parserContext.getReaderContext().registerWithGeneratedName(validatorDef);
parserContext.registerComponent(new BeanComponentDefinition(validatorDef, validatorName));
return new RuntimeBeanReference(validatorName);
}
else {
return null;
}
}
private ManagedList<Object> extractBeanSubElements(Element parentElement, ParserContext parserContext) {
ManagedList<Object> list = new ManagedList<>();
list.setSource(parserContext.extractSource(parentElement));
for (Element beanElement : DomUtils.getChildElementsByTagName(parentElement, "bean", "ref")) {
Object object = parserContext.getDelegate().parsePropertySubElement(beanElement, null);
list.add(object);
}
return list;
}
private RuntimeBeanReference registerUserDestResolver(Element brokerElem,
RuntimeBeanReference userRegistry, ParserContext context, Object source) {
RootBeanDefinition beanDef = new RootBeanDefinition(DefaultUserDestinationResolver.class);
beanDef.getConstructorArgumentValues().addIndexedArgumentValue(0, userRegistry);
if (brokerElem.hasAttribute("user-destination-prefix")) {
beanDef.getPropertyValues().add("userDestinationPrefix", brokerElem.getAttribute("user-destination-prefix"));
}
if (brokerElem.hasAttribute("path-matcher")) {
String pathMatcherRef = brokerElem.getAttribute("path-matcher");
beanDef.getPropertyValues().add("pathMatcher", new RuntimeBeanReference(pathMatcherRef));
}
return new RuntimeBeanReference(registerBeanDef(beanDef, context, source));
}
private RuntimeBeanReference registerUserDestHandler(Element brokerElem,
RuntimeBeanReference userRegistry, RuntimeBeanReference inChannel,
RuntimeBeanReference brokerChannel, ParserContext context, Object source) {
Object userDestResolver = registerUserDestResolver(brokerElem, userRegistry, context, source);
RootBeanDefinition beanDef = new RootBeanDefinition(UserDestinationMessageHandler.class);
beanDef.getConstructorArgumentValues().addIndexedArgumentValue(0, inChannel);
beanDef.getConstructorArgumentValues().addIndexedArgumentValue(1, brokerChannel);
beanDef.getConstructorArgumentValues().addIndexedArgumentValue(2, userDestResolver);
Element relayElement = DomUtils.getChildElementByTagName(brokerElem, "stomp-broker-relay");
if (relayElement != null && relayElement.hasAttribute("user-destination-broadcast")) {
String destination = relayElement.getAttribute("user-destination-broadcast");
beanDef.getPropertyValues().add("broadcastDestination", destination);
}
String beanName = registerBeanDef(beanDef, context, source);
return new RuntimeBeanReference(beanName);
}
private void registerWebSocketMessageBrokerStats(RootBeanDefinition broker, RuntimeBeanReference inChannel,
RuntimeBeanReference outChannel, ParserContext context, Object source) {
RootBeanDefinition beanDef = new RootBeanDefinition(WebSocketMessageBrokerStats.class);
RuntimeBeanReference webSocketHandler = new RuntimeBeanReference(WEB_SOCKET_HANDLER_BEAN_NAME);
beanDef.getPropertyValues().add("subProtocolWebSocketHandler", webSocketHandler);
if (StompBrokerRelayMessageHandler.class == broker.getBeanClass()) {
beanDef.getPropertyValues().add("stompBrokerRelay", broker);
}
String name = inChannel.getBeanName() + "Executor";
if (context.getRegistry().containsBeanDefinition(name)) {
beanDef.getPropertyValues().add("inboundChannelExecutor", context.getRegistry().getBeanDefinition(name));
}
name = outChannel.getBeanName() + "Executor";
if (context.getRegistry().containsBeanDefinition(name)) {
beanDef.getPropertyValues().add("outboundChannelExecutor", context.getRegistry().getBeanDefinition(name));
}
Object scheduler = WebSocketNamespaceUtils.registerScheduler(SCHEDULER_BEAN_NAME, context, source);
beanDef.getPropertyValues().add("sockJsTaskScheduler", scheduler);
registerBeanDefByName("webSocketMessageBrokerStats", beanDef, context, source);
}
private static String registerBeanDef(RootBeanDefinition beanDef, ParserContext context, Object source) {
String name = context.getReaderContext().generateBeanName(beanDef);
registerBeanDefByName(name, beanDef, context, source);
return name;
}
private static void registerBeanDefByName(String name, RootBeanDefinition beanDef, ParserContext context, Object source) {
beanDef.setSource(source);
beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
context.getRegistry().registerBeanDefinition(name, beanDef);
context.registerComponent(new BeanComponentDefinition(beanDef, name));
}
private static class DecoratingFactoryBean implements FactoryBean<WebSocketHandler> {
private final WebSocketHandler handler;
private final List<WebSocketHandlerDecoratorFactory> factories;
private DecoratingFactoryBean(WebSocketHandler handler, List<WebSocketHandlerDecoratorFactory> factories) {
this.handler = handler;
this.factories = factories;
}
@Override
public WebSocketHandler getObject() throws Exception {
WebSocketHandler result = this.handler;
for (WebSocketHandlerDecoratorFactory factory : this.factories) {
result = factory.decorate(result);
}
return result;
}
@Override
public Class<?> getObjectType() {
return WebSocketHandler.class;
}
@Override
public boolean isSingleton() {
return true;
}
}
}