diff --git a/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/ServerWebSocketContainer.java b/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/ServerWebSocketContainer.java index 09beea747d..5f9047c144 100644 --- a/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/ServerWebSocketContainer.java +++ b/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/ServerWebSocketContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2021 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. @@ -37,7 +37,7 @@ import org.springframework.web.socket.sockjs.transport.TransportHandler; /** * The {@link IntegrationWebSocketContainer} implementation for the {@code server} - * {@link org.springframework.web.socket.WebSocketHandler} registration. + * {@link WebSocketHandler} registration. *

* Registers an internal {@code IntegrationWebSocketContainer.IntegrationWebSocketHandler} * for provided {@link #paths} with the {@link WebSocketHandlerRegistry}. @@ -69,6 +69,8 @@ public class ServerWebSocketContainer extends IntegrationWebSocketContainer private int phase = 0; + private TaskScheduler sockJsTaskScheduler; + public ServerWebSocketContainer(String... paths) { Assert.notEmpty(paths, "'paths' must not be empty"); this.paths = Arrays.copyOf(paths, paths.length); @@ -118,11 +120,11 @@ public class ServerWebSocketContainer extends IntegrationWebSocketContainer public ServerWebSocketContainer withSockJs(SockJsServiceOptions... sockJsServiceOptions) { if (ObjectUtils.isEmpty(sockJsServiceOptions)) { - this.sockJsServiceOptions = new SockJsServiceOptions(); + setSockJsServiceOptions(new SockJsServiceOptions()); } else { Assert.state(sockJsServiceOptions.length == 1, "Only one 'sockJsServiceOptions' is applicable."); - this.sockJsServiceOptions = sockJsServiceOptions[0]; + setSockJsServiceOptions(sockJsServiceOptions[0]); } return this; } @@ -131,6 +133,21 @@ public class ServerWebSocketContainer extends IntegrationWebSocketContainer this.sockJsServiceOptions = sockJsServiceOptions; } + /** + * Configure a {@link TaskScheduler} for SockJS fallback service. + * This is an alternative for default SockJS service scheduler + * when Websocket endpoint (this server container) is registered at runtime. + * @param sockJsTaskScheduler the {@link TaskScheduler} for SockJS fallback service. + * @since 5.5.1 + */ + public void setSockJsTaskScheduler(TaskScheduler sockJsTaskScheduler) { + this.sockJsTaskScheduler = sockJsTaskScheduler; + } + + public TaskScheduler getSockJsTaskScheduler() { + return this.sockJsTaskScheduler; + } + @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { WebSocketHandler webSocketHandler = this.webSocketHandler; @@ -153,6 +170,8 @@ public class ServerWebSocketContainer extends IntegrationWebSocketContainer if (this.sockJsServiceOptions != null) { SockJsServiceRegistration sockJsServiceRegistration = registration.withSockJS(); JavaUtils.INSTANCE + .acceptIfCondition(this.sockJsServiceOptions.taskScheduler == null, + this.sockJsTaskScheduler, this.sockJsServiceOptions::setTaskScheduler) .acceptIfNotNull(this.sockJsServiceOptions.webSocketEnabled, sockJsServiceRegistration::setWebSocketEnabled) .acceptIfNotNull(this.sockJsServiceOptions.clientLibraryUrl, @@ -226,7 +245,7 @@ public class ServerWebSocketContainer extends IntegrationWebSocketContainer } /** - * @see org.springframework.web.socket.config.annotation.SockJsServiceRegistration + * @see SockJsServiceRegistration */ public static class SockJsServiceOptions { diff --git a/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/config/IntegrationDynamicWebSocketHandlerMapping.java b/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/config/IntegrationDynamicWebSocketHandlerMapping.java index b7e018bfa0..dc59ac0dcb 100644 --- a/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/config/IntegrationDynamicWebSocketHandlerMapping.java +++ b/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/config/IntegrationDynamicWebSocketHandlerMapping.java @@ -16,14 +16,23 @@ package org.springframework.integration.websocket.config; +import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; +import org.springframework.http.server.PathContainer; +import org.springframework.http.server.RequestPath; import org.springframework.web.HttpRequestHandler; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.handler.AbstractHandlerMapping; +import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; +import org.springframework.web.util.pattern.PathPattern; +import org.springframework.web.util.pattern.PathPatternParser; /** * The {@link AbstractHandlerMapping} implementation for dynamic WebSocket endpoint registrations in Spring Integration. @@ -34,23 +43,60 @@ import org.springframework.web.servlet.handler.AbstractHandlerMapping; * * @since 5.5 */ -class IntegrationDynamicWebSocketHandlerMapping extends AbstractHandlerMapping { +class IntegrationDynamicWebSocketHandlerMapping extends AbstractUrlHandlerMapping { private final Map handlerMap = new HashMap<>(); + private final Map pathPatternHandlerMap = new LinkedHashMap<>(); + @Override protected Object getHandlerInternal(HttpServletRequest request) { String lookupPath = initLookupPath(request); HttpRequestHandler httpRequestHandler = this.handlerMap.get(lookupPath); + if (httpRequestHandler == null && usesPathPatterns()) { + RequestPath path = ServletRequestPathUtils.getParsedRequestPath(request); + return lookupByPattern(path); + } return httpRequestHandler != null ? new HandlerExecutionChain(httpRequestHandler) : null; } + private Object lookupByPattern(RequestPath path) { + List matches = null; + for (PathPattern pattern : this.pathPatternHandlerMap.keySet()) { + if (pattern.matches(path.pathWithinApplication())) { + matches = (matches != null ? matches : new ArrayList<>()); + matches.add(pattern); + } + } + if (matches == null) { + return null; + } + if (matches.size() > 1) { + matches.sort(PathPattern.SPECIFICITY_COMPARATOR); + if (logger.isTraceEnabled()) { + logger.trace("Matching patterns " + matches); + } + } + PathPattern pattern = matches.get(0); + HttpRequestHandler handler = this.pathPatternHandlerMap.get(pattern); + PathContainer pathWithinMapping = pattern.extractPathWithinPattern(path.pathWithinApplication()); + return buildPathExposingHandler(handler, pattern.getPatternString(), pathWithinMapping.value(), null); + } + void registerHandler(String path, HttpRequestHandler httpHandler) { this.handlerMap.put(path, httpHandler); + PathPatternParser patternParser = getPatternParser(); + if (patternParser != null) { + this.pathPatternHandlerMap.put(patternParser.parse(path), httpHandler); + } } void unregisterHandler(String path) { this.handlerMap.remove(path); + PathPatternParser patternParser = getPatternParser(); + if (patternParser != null) { + this.pathPatternHandlerMap.remove(patternParser.parse(path)); + } } } diff --git a/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/config/IntegrationServletWebSocketHandlerRegistry.java b/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/config/IntegrationServletWebSocketHandlerRegistry.java index 898d9b72c2..8c486f5abd 100644 --- a/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/config/IntegrationServletWebSocketHandlerRegistry.java +++ b/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/config/IntegrationServletWebSocketHandlerRegistry.java @@ -46,10 +46,14 @@ import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistra class IntegrationServletWebSocketHandlerRegistry extends ServletWebSocketHandlerRegistry implements ApplicationContextAware, DestructionAwareBeanPostProcessor { + private final ThreadLocal currentRegistration = new ThreadLocal<>(); + private final Map> dynamicRegistrations = new HashMap<>(); private ApplicationContext applicationContext; + private TaskScheduler sockJsTaskScheduler; + private volatile IntegrationDynamicWebSocketHandlerMapping dynamicHandlerMapping; IntegrationServletWebSocketHandlerRegistry() { @@ -60,15 +64,10 @@ class IntegrationServletWebSocketHandlerRegistry extends ServletWebSocketHandler this.applicationContext = applicationContext; } - @Override - protected boolean requiresTaskScheduler() { // NOSONAR visibility - return super.requiresTaskScheduler(); - } - - @Override - protected void setTaskScheduler(TaskScheduler scheduler) { // NOSONAR visibility + protected void setTaskScheduler(TaskScheduler scheduler) { super.setTaskScheduler(scheduler); + this.sockJsTaskScheduler = scheduler; } @Override @@ -85,15 +84,7 @@ class IntegrationServletWebSocketHandlerRegistry extends ServletWebSocketHandler IntegrationDynamicWebSocketHandlerRegistration registration = new IntegrationDynamicWebSocketHandlerRegistration(); registration.addHandler(handler, paths); - MultiValueMap mappings = registration.getMapping(); - for (Map.Entry> entry : mappings.entrySet()) { - HttpRequestHandler httpHandler = entry.getKey(); - List patterns = entry.getValue(); - this.dynamicRegistrations.put(handler, patterns); - for (String pattern : patterns) { - this.dynamicHandlerMapping.registerHandler(pattern, httpHandler); - } - } + this.currentRegistration.set(registration); return registration; } else { @@ -102,9 +93,24 @@ class IntegrationServletWebSocketHandlerRegistry extends ServletWebSocketHandler } @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (this.dynamicHandlerMapping != null && bean instanceof ServerWebSocketContainer) { - ((ServerWebSocketContainer) bean).registerWebSocketHandlers(this); + ServerWebSocketContainer serverWebSocketContainer = (ServerWebSocketContainer) bean; + if (serverWebSocketContainer.getSockJsTaskScheduler() == null) { + serverWebSocketContainer.setSockJsTaskScheduler(this.sockJsTaskScheduler); + } + serverWebSocketContainer.registerWebSocketHandlers(this); + IntegrationDynamicWebSocketHandlerRegistration registration = this.currentRegistration.get(); + this.currentRegistration.remove(); + MultiValueMap mappings = registration.getMapping(); + for (Map.Entry> entry : mappings.entrySet()) { + HttpRequestHandler httpHandler = entry.getKey(); + List patterns = entry.getValue(); + this.dynamicRegistrations.put(registration.handler, patterns); + for (String pattern : patterns) { + this.dynamicHandlerMapping.registerHandler(pattern, httpHandler); + } + } } return bean; } @@ -133,6 +139,15 @@ class IntegrationServletWebSocketHandlerRegistry extends ServletWebSocketHandler private static final class IntegrationDynamicWebSocketHandlerRegistration extends ServletWebSocketHandlerRegistration { + private WebSocketHandler handler; + + @Override + public WebSocketHandlerRegistration addHandler(WebSocketHandler handler, String... paths) { + // The IntegrationWebSocketContainer comes only with a single WebSocketHandler + this.handler = handler; + return super.addHandler(handler, paths); + } + MultiValueMap getMapping() { return getMappings(); } diff --git a/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/config/WebSocketIntegrationConfigurationInitializer.java b/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/config/WebSocketIntegrationConfigurationInitializer.java index c50af9ea47..11864abd15 100644 --- a/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/config/WebSocketIntegrationConfigurationInitializer.java +++ b/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/config/WebSocketIntegrationConfigurationInitializer.java @@ -35,6 +35,7 @@ import org.springframework.util.ClassUtils; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.socket.config.annotation.DelegatingWebSocketConfiguration; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.util.pattern.PathPatternParser; /** * The WebSocket Integration infrastructure {@code beanFactory} initializer. @@ -102,6 +103,7 @@ public class WebSocketIntegrationConfigurationInitializer implements Integration () -> { IntegrationDynamicWebSocketHandlerMapping dynamicWebSocketHandlerMapping = new IntegrationDynamicWebSocketHandlerMapping(); + dynamicWebSocketHandlerMapping.setPatternParser(new PathPatternParser()); dynamicWebSocketHandlerMapping.setOrder(0); return dynamicWebSocketHandlerMapping; }), @@ -143,9 +145,7 @@ public class WebSocketIntegrationConfigurationInitializer implements Integration .values() .forEach(configurer -> configurer.registerWebSocketHandlers(this.registry)); } - if (this.registry.requiresTaskScheduler()) { - this.registry.setTaskScheduler(this.sockJsTaskScheduler); - } + this.registry.setTaskScheduler(this.sockJsTaskScheduler); return this.registry.getHandlerMapping(); } diff --git a/spring-integration-websocket/src/test/java/org/springframework/integration/websocket/dsl/WebSocketDslTests.java b/spring-integration-websocket/src/test/java/org/springframework/integration/websocket/dsl/WebSocketDslTests.java index 307fb13db0..8cdf2c7cf9 100644 --- a/spring-integration-websocket/src/test/java/org/springframework/integration/websocket/dsl/WebSocketDslTests.java +++ b/spring-integration-websocket/src/test/java/org/springframework/integration/websocket/dsl/WebSocketDslTests.java @@ -69,7 +69,8 @@ public class WebSocketDslTests { IntegrationFlowContext serverIntegrationFlowContext = serverContext.getBean(IntegrationFlowContext.class); ServerWebSocketContainer serverWebSocketContainer = new ServerWebSocketContainer("/dynamic") - .setHandshakeHandler(serverContext.getBean(HandshakeHandler.class)); + .setHandshakeHandler(serverContext.getBean(HandshakeHandler.class)) + .withSockJs(); WebSocketInboundChannelAdapter webSocketInboundChannelAdapter = new WebSocketInboundChannelAdapter(serverWebSocketContainer); @@ -88,7 +89,7 @@ public class WebSocketDslTests { // Dynamic client flow ClientWebSocketContainer clientWebSocketContainer = - new ClientWebSocketContainer(this.webSocketClient, this.server.getWsBaseUrl() + "/dynamic"); + new ClientWebSocketContainer(this.webSocketClient, this.server.getWsBaseUrl() + "/dynamic/websocket"); clientWebSocketContainer.setAutoStartup(true); WebSocketOutboundMessageHandler webSocketOutboundMessageHandler = new WebSocketOutboundMessageHandler(clientWebSocketContainer);