diff --git a/spring-web/src/main/java/org/springframework/http/server/AsyncServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/AsyncServerHttpRequest.java index 0253ac66e1..bbf973d5d8 100644 --- a/spring-web/src/main/java/org/springframework/http/server/AsyncServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/AsyncServerHttpRequest.java @@ -16,6 +16,7 @@ package org.springframework.http.server; + /** * TODO.. */ diff --git a/spring-web/src/main/java/org/springframework/http/server/AsyncServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/AsyncServletServerHttpRequest.java index 1e53fc7abf..996a7d3fdc 100644 --- a/spring-web/src/main/java/org/springframework/http/server/AsyncServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/AsyncServletServerHttpRequest.java @@ -100,11 +100,6 @@ public class AsyncServletServerHttpRequest extends ServletServerHttpRequest } } - public void dispatch() { - Assert.notNull(this.asyncContext, "Cannot dispatch without an AsyncContext"); - this.asyncContext.dispatch(); - } - public void completeAsync() { Assert.notNull(this.asyncContext, "Cannot dispatch without an AsyncContext"); if (isAsyncStarted() && !isAsyncCompleted()) { @@ -112,6 +107,7 @@ public class AsyncServletServerHttpRequest extends ServletServerHttpRequest } } + // --------------------------------------------------------------------- // Implementation of AsyncListener methods // --------------------------------------------------------------------- diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/client/endpoint/StandardWebSocketClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/client/endpoint/StandardWebSocketClient.java index 0734f7c085..21678e12ef 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/client/endpoint/StandardWebSocketClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/client/endpoint/StandardWebSocketClient.java @@ -75,17 +75,9 @@ public class StandardWebSocketClient implements WebSocketClient { public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, final HttpHeaders httpHeaders, URI uri) throws WebSocketConnectFailureException { - return doHandshake(webSocketHandler, httpHeaders, UriComponentsBuilder.fromUri(uri).build()); - } - - public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, - final HttpHeaders httpHeaders, UriComponents uriComponents) throws WebSocketConnectFailureException { - - URI uri = uriComponents.toUri(); - StandardWebSocketSessionAdapter session = new StandardWebSocketSessionAdapter(); session.setUri(uri); - session.setRemoteHostName(uriComponents.getHost()); + session.setRemoteHostName(uri.getHost()); Endpoint endpoint = new StandardEndpointAdapter(webSocketHandler, session); ClientEndpointConfig.Builder configBuidler = ClientEndpointConfig.Builder.create(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/client/jetty/JettyWebSocketClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/client/jetty/JettyWebSocketClient.java index 6cc19bf561..d5bb04ddfd 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/client/jetty/JettyWebSocketClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/client/jetty/JettyWebSocketClient.java @@ -133,19 +133,11 @@ public class JettyWebSocketClient implements WebSocketClient, SmartLifecycle { public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, HttpHeaders headers, URI uri) throws WebSocketConnectFailureException { - return doHandshake(webSocketHandler, headers, UriComponentsBuilder.fromUri(uri).build()); - } - - public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, HttpHeaders headers, UriComponents uriComponents) - throws WebSocketConnectFailureException { - // TODO: populate headers - URI uri = uriComponents.toUri(); - JettyWebSocketSessionAdapter session = new JettyWebSocketSessionAdapter(); session.setUri(uri); - session.setRemoteHostName(uriComponents.getHost()); + session.setRemoteHostName(uri.getHost()); JettyWebSocketListenerAdapter listener = new JettyWebSocketListenerAdapter(webSocketHandler, session); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/DefaultHandshakeHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/DefaultHandshakeHandler.java index 8390c3079e..f3948b795e 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/DefaultHandshakeHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/DefaultHandshakeHandler.java @@ -149,7 +149,7 @@ public class DefaultHandshakeHandler implements HandshakeHandler { protected void handleInvalidUpgradeHeader(ServerHttpRequest request, ServerHttpResponse response) throws IOException { logger.debug("Invalid Upgrade header " + request.getHeaders().getUpgrade()); response.setStatusCode(HttpStatus.BAD_REQUEST); - response.getBody().write("Can \"Upgrade\" only to \"websocket\".".getBytes("UTF-8")); + response.getBody().write("Can \"Upgrade\" only to \"WebSocket\".".getBytes("UTF-8")); } protected void handleInvalidConnectHeader(ServerHttpRequest request, ServerHttpResponse response) throws IOException { @@ -227,13 +227,13 @@ public class DefaultHandshakeHandler implements HandshakeHandler { private RequestUpgradeStrategy create() { String className; if (tomcatWebSocketPresent) { - className = "org.springframework.websocket.server.support.TomcatRequestUpgradeStrategy"; + className = "org.springframework.web.socket.server.support.TomcatRequestUpgradeStrategy"; } else if (glassFishWebSocketPresent) { - className = "org.springframework.websocket.server.support.GlassFishRequestUpgradeStrategy"; + className = "org.springframework.web.socket.server.support.GlassFishRequestUpgradeStrategy"; } else if (jettyWebSocketPresent) { - className = "org.springframework.websocket.server.support.JettyRequestUpgradeStrategy"; + className = "org.springframework.web.socket.server.support.JettyRequestUpgradeStrategy"; } else { throw new IllegalStateException("No suitable " + RequestUpgradeStrategy.class.getSimpleName()); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/package-info.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/package-info.java index f73eb69625..5b5a29efbe 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/package-info.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/package-info.java @@ -20,9 +20,7 @@ * {@link org.springframework.web.socket.server.endpoint.EndpointExporter} for * registering type-based endpoints, * {@link org.springframework.web.socket.server.endpoint.SpringConfigurator} for - * instantiating annotated endpoints through Spring, and - * {@link org.springframework.websocket.server.support.EndpointHandshakeHandler} - * for integrating endpoints into HTTP request processing. + * instantiating annotated endpoints through Spring. */ package org.springframework.web.socket.server.endpoint; diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractServerSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractServerSockJsSession.java index 725d3c3ac6..710e5e7d33 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractServerSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractServerSockJsSession.java @@ -83,7 +83,6 @@ public abstract class AbstractServerSockJsSession extends AbstractSockJsSession disconnect(status); } - // TODO: close status/reason protected abstract void disconnect(CloseStatus status) throws IOException; /** @@ -104,12 +103,14 @@ public abstract class AbstractServerSockJsSession extends AbstractSockJsSession else { logger.warn("Terminating connection due to failure to send message: " + ex.getMessage()); } - close(); + disconnect(CloseStatus.SERVER_ERROR); + close(CloseStatus.SERVER_ERROR); throw ex; } catch (Throwable ex) { logger.warn("Terminating connection due to failure to send message: " + ex.getMessage()); - close(); + disconnect(CloseStatus.SERVER_ERROR); + close(CloseStatus.SERVER_ERROR); throw new SockJsRuntimeException("Failed to write " + frame, ex); } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsService.java index 31f15781ed..0abf1cc523 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsService.java @@ -17,11 +17,16 @@ package org.springframework.web.socket.sockjs; import java.io.IOException; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Random; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -39,7 +44,18 @@ import org.springframework.util.StringUtils; import org.springframework.web.socket.WebSocketHandler; /** - * Provides support for SockJS configuration options and serves the static SockJS URLs. + * An abstract class for {@link SockJsService} implementations. Provides configuration + * support, SockJS path resolution, and processing for static SockJS requests (e.g. + * "/info", "/iframe.html", etc). Sub-classes are responsible for handling transport + * requests. + * + *

+ * It is expected that this service is mapped correctly to one or more prefixes such as + * "/echo" including all sub-URLs (e.g. "/echo/**"). A SockJS service itself is generally + * unaware of request mapping details but nevertheless must be able to extract the SockJS + * path, which is the portion of the request path following the prefix. In most cases, + * this class can auto-detect the SockJS path but you can also explicitly configure the + * list of valid prefixes with {@link #setValidSockJsPrefixes(String...)}. * * @author Rossen Stoyanchev * @since 4.0 @@ -51,7 +67,7 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf private static final int ONE_YEAR = 365 * 24 * 60 * 60; - private String name = "SockJS Service " + ObjectUtils.getIdentityHexString(this); + private String name = "SockJSService@" + ObjectUtils.getIdentityHexString(this); private String clientLibraryUrl = "https://d1fxtkz8shb9d2.cloudfront.net/sockjs-0.3.4.min.js"; @@ -67,6 +83,9 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf private final TaskScheduler taskScheduler; + private final List sockJsPrefixes = new ArrayList(); + + private final Set sockJsPathCache = new CopyOnWriteArraySet(); public AbstractSockJsService(TaskScheduler scheduler) { @@ -85,6 +104,38 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf return this.name; } + /** + * Use this property to configure one or more prefixes that this SockJS service is + * allowed to serve. The prefix (e.g. "/echo") is needed to extract the SockJS + * specific portion of the URL (e.g. "${prefix}/info", "${prefix}/iframe.html", etc). + *

+ * This property is not strictly required. In most cases, the SockJS path can be + * auto-detected since the initial request from the SockJS client is of the form + * "{prefix}/info". Assuming the SockJS service is mapped correctly (e.g. using + * Ant-style pattern "/echo/**") this should work fine. This property can be used + * to configure explicitly the prefixes this service is allowed to service. + * + * @param prefixes the prefixes to use; prefixes do not need to include the portions + * of the path that represent Servlet container context or Servlet path. + */ + public void setValidSockJsPrefixes(String... prefixes) { + + this.sockJsPrefixes.clear(); + for (String prefix : prefixes) { + if (prefix.endsWith("/") && (prefix.length() > 1)) { + prefix = prefix.substring(0, prefix.length() - 1); + } + this.sockJsPrefixes.add(prefix); + } + + // sort with longest prefix at the top + Collections.sort(this.sockJsPrefixes, Collections.reverseOrder(new Comparator() { + public int compare(String o1, String o2) { + return new Integer(o1.length()).compareTo(new Integer(o2.length())); + } + })); + } + /** * Transports which don't support cross-domain communication natively (e.g. * "eventsource", "htmlfile") rely on serving a simple page (using the @@ -198,10 +249,18 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf * * @throws Exception */ - public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response, - String sockJsPath, WebSocketHandler webSocketHandler) throws IOException, TransportErrorException { + public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler) + throws IOException, TransportErrorException { - logger.debug(request.getMethod() + " [" + sockJsPath + "]"); + String sockJsPath = getSockJsPath(request); + if (sockJsPath == null) { + logger.warn("Could not determine SockJS path for URL \"" + request.getURI().getPath() + + ". Consider setting validSockJsPrefixes."); + response.setStatusCode(HttpStatus.NOT_FOUND); + return; + } + + logger.debug(request.getMethod() + " with SockJS path [" + sockJsPath + "]"); try { request.getHeaders(); @@ -225,13 +284,13 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf return; } else if (sockJsPath.equals("/websocket")) { - handleRawWebSocketRequest(request, response, webSocketHandler); + handleRawWebSocketRequest(request, response, handler); return; } String[] pathSegments = StringUtils.tokenizeToStringArray(sockJsPath.substring(1), "/"); if (pathSegments.length != 3) { - logger.debug("Expected /{server}/{session}/{transport} but got " + sockJsPath); + logger.warn("Expected \"/{server}/{session}/{transport}\" but got \"" + sockJsPath + "\""); response.setStatusCode(HttpStatus.NOT_FOUND); return; } @@ -245,13 +304,62 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf return; } - handleTransportRequest(request, response, sessionId, TransportType.fromValue(transport), webSocketHandler); + handleTransportRequest(request, response, sessionId, TransportType.fromValue(transport), handler); } finally { response.flush(); } } + /** + * Return the SockJS path or null if the path could not be determined. + */ + private String getSockJsPath(ServerHttpRequest request) { + + String path = request.getURI().getPath(); + + // SockJS prefix hints? + if (!this.sockJsPrefixes.isEmpty()) { + for (String prefix : this.sockJsPrefixes) { + int index = path.indexOf(prefix); + if (index != -1) { + this.sockJsPathCache.add(path.substring(0, index + prefix.length())); + return path.substring(index + prefix.length()); + } + } + } + + // SockJS info request? + if (path.endsWith("/info")) { + this.sockJsPathCache.add(path.substring(0, path.length() - 6)); + return "/info"; + } + + // Have we seen this prefix before (following the initial /info request)? + String match = null; + for (String sockJsPath : this.sockJsPathCache) { + if (path.startsWith(sockJsPath)) { + if ((match == null) || (match.length() < sockJsPath.length())) { + match = sockJsPath; + } + } + } + if (match != null) { + return path.substring(match.length()); + } + + // SockJS greeting? + String pathNoSlash = path.endsWith("/") ? path.substring(0, path.length() - 1) : path; + String lastSegment = pathNoSlash.substring(pathNoSlash.lastIndexOf('/') + 1); + + if ((TransportType.fromValue(lastSegment) == null) && !lastSegment.startsWith("iframe")) { + this.sockJsPathCache.add(path); + return ""; + } + + return null; + } + protected abstract void handleRawWebSocketRequest(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler webSocketHandler) throws IOException; @@ -263,18 +371,18 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf protected boolean validateRequest(String serverId, String sessionId, String transport) { if (!StringUtils.hasText(serverId) || !StringUtils.hasText(sessionId) || !StringUtils.hasText(transport)) { - logger.debug("Empty server, session, or transport value"); + logger.warn("Empty server, session, or transport value"); return false; } // Server and session id's must not contain "." if (serverId.contains(".") || sessionId.contains(".")) { - logger.debug("Server or session contain a \".\""); + logger.warn("Server or session contain a \".\""); return false; } if (!isWebSocketEnabled() && transport.equals(TransportType.WEBSOCKET.value())) { - logger.debug("Websocket transport is disabled"); + logger.warn("Websocket transport is disabled"); return false; } @@ -346,7 +454,7 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf response.setStatusCode(HttpStatus.NO_CONTENT); - addCorsHeaders(request, response, HttpMethod.GET, HttpMethod.OPTIONS); + addCorsHeaders(request, response, HttpMethod.OPTIONS, HttpMethod.GET); addCacheHeaders(response); } else { @@ -404,4 +512,5 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf } }; + } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsSession.java index c7009b9c96..eb461db072 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsSession.java @@ -217,7 +217,7 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess *

Performs cleanup and notifies the {@link SockJsHandler}. */ public final void close() throws IOException { - close(CloseStatus.NORMAL); + close(new CloseStatus(3000, "Go away!")); } /** @@ -225,7 +225,7 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess *

Performs cleanup and notifies the {@link SockJsHandler}. */ public final void close(CloseStatus status) throws IOException { - if (!isClosed()) { + if (isOpen()) { if (logger.isDebugEnabled()) { logger.debug("Closing " + this + ", " + status); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsFrame.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsFrame.java index 436e31f79b..17233f7e0e 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsFrame.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsFrame.java @@ -78,6 +78,30 @@ public class SockJsFrame { return this.content.getBytes(Charset.forName("UTF-8")); } + public static String escapeCharacters(char[] chars) { + StringBuilder result = new StringBuilder(); + for (char ch : chars) { + if (isSockJsEscapeCharacter(ch)) { + result.append('\\').append('u'); + String hex = Integer.toHexString(ch).toLowerCase(); + for (int i = 0; i < (4 - hex.length()); i++) { + result.append('0'); + } + result.append(hex); + } + else { + result.append(ch); + } + } + return result.toString(); + } + + private static boolean isSockJsEscapeCharacter(char ch) { + return (ch >= '\u0000' && ch <= '\u001F') || (ch >= '\u200C' && ch <= '\u200F') + || (ch >= '\u2028' && ch <= '\u202F') || (ch >= '\u2060' && ch <= '\u206F') + || (ch >= '\uFFF0' && ch <= '\uFFFF') || (ch >= '\uD800' && ch <= '\uDFFF'); + } + public String toString() { String result = this.content; if (result.length() > 80) { @@ -101,7 +125,7 @@ public class SockJsFrame { sb.append('"'); // TODO: dependency on Jackson char[] quotedChars = JsonStringEncoder.getInstance().quoteAsString(messages[i]); - sb.append(escapeSockJsCharacters(quotedChars)); + sb.append(escapeCharacters(quotedChars)); sb.append('"'); if (i < messages.length - 1) { sb.append(','); @@ -110,30 +134,6 @@ public class SockJsFrame { sb.append(']'); return sb.toString(); } - - private static String escapeSockJsCharacters(char[] chars) { - StringBuilder result = new StringBuilder(); - for (char ch : chars) { - if (isSockJsEscapeCharacter(ch)) { - result.append('\\').append('u'); - String hex = Integer.toHexString(ch).toLowerCase(); - for (int i = 0; i < (4 - hex.length()); i++) { - result.append('0'); - } - result.append(hex); - } - else { - result.append(ch); - } - } - return result.toString(); - } - - private static boolean isSockJsEscapeCharacter(char ch) { - return (ch >= '\u0000' && ch <= '\u001F') || (ch >= '\u200C' && ch <= '\u200F') - || (ch >= '\u2028' && ch <= '\u202F') || (ch >= '\u2060' && ch <= '\u206F') - || (ch >= '\uFFF0' && ch <= '\uFFFF') || (ch >= '\uD800' && ch <= '\uDFFF'); - } } public interface FrameFormat { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsService.java index 915ab840fb..45e34d4c69 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsService.java @@ -23,13 +23,14 @@ import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.socket.WebSocketHandler; /** + * * @author Rossen Stoyanchev * @since 4.0 */ public interface SockJsService { - void handleRequest(ServerHttpRequest request, ServerHttpResponse response, - String sockJsPath, WebSocketHandler webSocketHandler) throws IOException, TransportErrorException; + void handleRequest(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler) + throws IOException, TransportErrorException; } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/TransportType.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/TransportType.java index 05f89bac38..1c2c5814d7 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/TransportType.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/TransportType.java @@ -17,7 +17,9 @@ package org.springframework.web.socket.sockjs; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.springframework.http.HttpMethod; @@ -50,6 +52,14 @@ public enum TransportType { private final List headerHints; + private static final Map transportTypes = new HashMap(); + + static { + for (TransportType type : values()) { + transportTypes.put(type.value, type); + } + } + private TransportType(String value, HttpMethod httpMethod, String... headerHints) { this.value = value; @@ -57,6 +67,7 @@ public enum TransportType { this.headerHints = Arrays.asList(headerHints); } + public String value() { return this.value; } @@ -80,13 +91,8 @@ public enum TransportType { return this.headerHints.contains("jsessionid"); } - public static TransportType fromValue(String transportValue) { - for (TransportType type : values()) { - if (type.value().equals(transportValue)) { - return type; - } - } - throw new IllegalArgumentException("No matching constant for [" + transportValue + "]"); + public static TransportType fromValue(String value) { + return transportTypes.get(value); } @Override diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/DefaultSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/DefaultSockJsService.java index c0fec3e6bb..6f8e497678 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/DefaultSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/DefaultSockJsService.java @@ -177,7 +177,7 @@ public class DefaultSockJsService extends AbstractSockJsService { if (!supportedMethod.equals(request.getMethod())) { if (HttpMethod.OPTIONS.equals(request.getMethod()) && transportType.supportsCors()) { response.setStatusCode(HttpStatus.NO_CONTENT); - addCorsHeaders(request, response, supportedMethod, HttpMethod.OPTIONS); + addCorsHeaders(request, response, HttpMethod.OPTIONS, supportedMethod); addCacheHeaders(response); } else { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/SockJsHttpRequestHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/SockJsHttpRequestHandler.java index 336df01ca0..8a335a76d2 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/SockJsHttpRequestHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/SockJsHttpRequestHandler.java @@ -32,8 +32,6 @@ import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.sockjs.SockJsService; import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator; import org.springframework.web.socket.support.LoggingWebSocketHandlerDecorator; -import org.springframework.web.util.NestedServletException; -import org.springframework.web.util.UrlPathHelper; /** * @author Rossen Stoyanchev @@ -41,29 +39,17 @@ import org.springframework.web.util.UrlPathHelper; */ public class SockJsHttpRequestHandler implements HttpRequestHandler { - private final String prefix; - private final SockJsService sockJsService; private final WebSocketHandler webSocketHandler; - private final UrlPathHelper urlPathHelper = new UrlPathHelper(); - /** * Class constructor with {@link SockJsHandler} instance ... - * - * @param prefix the path prefix for the SockJS service. All requests with a path - * that begins with the specified prefix will be handled by this service. In a - * Servlet container this is the path within the current servlet mapping. */ - public SockJsHttpRequestHandler(String prefix, SockJsService sockJsService, WebSocketHandler webSocketHandler) { - - Assert.hasText(prefix, "prefix is required"); + public SockJsHttpRequestHandler(SockJsService sockJsService, WebSocketHandler webSocketHandler) { Assert.notNull(sockJsService, "sockJsService is required"); Assert.notNull(webSocketHandler, "webSocketHandler is required"); - - this.prefix = prefix; this.sockJsService = sockJsService; this.webSocketHandler = decorateWebSocketHandler(webSocketHandler); } @@ -79,35 +65,14 @@ public class SockJsHttpRequestHandler implements HttpRequestHandler { return new LoggingWebSocketHandlerDecorator(handler); } - public String getPrefix() { - return this.prefix; - } - - public String getPattern() { - return this.prefix + "/**"; - } - @Override public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String lookupPath = this.urlPathHelper.getLookupPathForRequest(request); - - Assert.isTrue(lookupPath.startsWith(this.prefix), - "Request path does not match the prefix of the SockJsService " + this.prefix); - - String sockJsPath = lookupPath.substring(prefix.length()); - ServerHttpRequest httpRequest = new AsyncServletServerHttpRequest(request, response); ServerHttpResponse httpResponse = new ServletServerHttpResponse(response); - try { - this.sockJsService.handleRequest(httpRequest, httpResponse, sockJsPath, this.webSocketHandler); - } - catch (Exception ex) { - // TODO - throw new NestedServletException("SockJS service failure", ex); - } + this.sockJsService.handleRequest(httpRequest, httpResponse, this.webSocketHandler); } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpReceivingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpReceivingTransportHandler.java index 313794ea1c..237343c9ac 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpReceivingTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpReceivingTransportHandler.java @@ -28,6 +28,7 @@ import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.sockjs.AbstractSockJsSession; +import org.springframework.web.socket.sockjs.SockJsFrame; import org.springframework.web.socket.sockjs.SockJsRuntimeException; import org.springframework.web.socket.sockjs.TransportErrorException; import org.springframework.web.socket.sockjs.TransportHandler; @@ -61,6 +62,7 @@ public abstract class AbstractHttpReceivingTransportHandler implements Transport if (session == null) { response.setStatusCode(HttpStatus.NOT_FOUND); + logger.warn("Session not found"); return; } @@ -75,22 +77,28 @@ public abstract class AbstractHttpReceivingTransportHandler implements Transport messages = readMessages(request); } catch (JsonMappingException ex) { + logger.error("Failed to read message: ", ex); sendInternalServerError(response, "Payload expected.", session.getId()); return; } catch (IOException ex) { + logger.error("Failed to read message: ", ex); sendInternalServerError(response, "Broken JSON encoding.", session.getId()); return; } catch (Throwable t) { + logger.error("Failed to read message: ", t); sendInternalServerError(response, "Failed to process messages", session.getId()); return; } if (logger.isTraceEnabled()) { - logger.trace("Received messages: " + Arrays.asList(messages)); + logger.trace("Received message(s): " + Arrays.asList(messages)); } + response.setStatusCode(getResponseStatus()); + response.getHeaders().setContentType(new MediaType("text", "plain", Charset.forName("UTF-8"))); + try { session.delegateMessages(messages); } @@ -98,9 +106,6 @@ public abstract class AbstractHttpReceivingTransportHandler implements Transport ExceptionWebSocketHandlerDecorator.tryCloseWithError(session, t, logger); throw new SockJsRuntimeException("Unhandled WebSocketHandler error in " + this, t); } - - response.setStatusCode(getResponseStatus()); - response.getHeaders().setContentType(new MediaType("text", "plain", Charset.forName("UTF-8"))); } protected void sendInternalServerError(ServerHttpResponse response, String error, diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpServerSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpServerSockJsSession.java index c04e4c54ee..be38656c67 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpServerSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpServerSockJsSession.java @@ -157,12 +157,13 @@ public abstract class AbstractHttpServerSockJsSession extends AbstractServerSock protected synchronized void resetRequest() { updateLastActiveTime(); - if (isActive()) { + if (isActive() && this.asyncRequest.isAsyncStarted()) { try { + logger.debug("Completing async request"); this.asyncRequest.completeAsync(); } catch (Throwable ex) { - logger.warn("Failed to complete async request: " + ex.getMessage()); + logger.error("Failed to complete async request: " + ex.getMessage()); } } this.asyncRequest = null; diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/StreamingServerSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/StreamingServerSockJsSession.java index f01bb94c99..fe37568a61 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/StreamingServerSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/StreamingServerSockJsSession.java @@ -41,7 +41,11 @@ public class StreamingServerSockJsSession extends AbstractHttpServerSockJsSessio FrameFormat frameFormat) throws TransportErrorException { super.setInitialRequest(request, response, frameFormat); - super.setLongPollingRequest(request, response, frameFormat); + + // the WebSocketHandler delegate may have closed the session + if (!isClosed()) { + super.setLongPollingRequest(request, response, frameFormat); + } } protected void flushCache() throws IOException { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/WebSocketServerSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/WebSocketServerSockJsSession.java index fb91266a2b..48bd272d7e 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/WebSocketServerSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/WebSocketServerSockJsSession.java @@ -62,7 +62,7 @@ public class WebSocketServerSockJsSession extends AbstractServerSockJsSession { @Override public boolean isActive() { - return this.webSocketSession.isOpen(); + return ((this.webSocketSession != null) && this.webSocketSession.isOpen()); } public void handleMessage(TextMessage message, WebSocketSession wsSession) throws Exception { diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/AbstractHttpRequestTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/AbstractHttpRequestTests.java new file mode 100644 index 0000000000..7ef451307e --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/AbstractHttpRequestTests.java @@ -0,0 +1,61 @@ +/* + * 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.web.socket; + +import org.junit.Before; +import org.springframework.http.server.AsyncServletServerHttpRequest; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.mock.web.test.MockHttpServletRequest; +import org.springframework.mock.web.test.MockHttpServletResponse; + + +/** + * @author Rossen Stoyanchev + */ +public class AbstractHttpRequestTests { + + protected ServerHttpRequest request; + + protected ServerHttpResponse response; + + protected MockHttpServletRequest servletRequest; + + protected MockHttpServletResponse servletResponse; + + + @Before + public void setUp() { + this.servletRequest = new MockHttpServletRequest(); + this.servletResponse = new MockHttpServletResponse(); + this.request = new AsyncServletServerHttpRequest(this.servletRequest, this.servletResponse); + this.response = new ServletServerHttpResponse(this.servletResponse); + } + + + protected void setRequest(String method, String requestUri) { + this.servletRequest.setMethod(method); + this.servletRequest.setRequestURI(requestUri); + } + + protected void resetResponse() { + this.servletResponse = new MockHttpServletResponse(); + this.response = new ServletServerHttpResponse(this.servletResponse); + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/AbstractSockJsServiceTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/AbstractSockJsServiceTests.java new file mode 100644 index 0000000000..fcc5325127 --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/AbstractSockJsServiceTests.java @@ -0,0 +1,145 @@ +/* + * 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.web.socket.sockjs; + +import java.io.IOException; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.web.socket.AbstractHttpRequestTests; +import org.springframework.web.socket.WebSocketHandler; + +import static org.junit.Assert.*; + +/** + * @author Rossen Stoyanchev + */ +public class AbstractSockJsServiceTests extends AbstractHttpRequestTests { + + private TestSockJsService service; + + private WebSocketHandler handler; + + + @Before + public void setUp() { + super.setUp(); + this.service = new TestSockJsService(new ThreadPoolTaskScheduler()); + } + + @Test + public void getSockJsPath() throws Exception { + + handleRequest("/echo", HttpStatus.OK); + assertEquals("Welcome to SockJS!\n", this.servletResponse.getContentAsString()); + + handleRequest("/echo/info", HttpStatus.OK); + assertTrue(this.servletResponse.getContentAsString().startsWith("{\"entropy\":")); + + handleRequest("/echo/", HttpStatus.OK); + assertEquals("Welcome to SockJS!\n", this.servletResponse.getContentAsString()); + + handleRequest("/echo/iframe.html", HttpStatus.OK); + assertTrue(this.servletResponse.getContentAsString().startsWith("\n")); + + handleRequest("/echo/websocket", HttpStatus.OK); + assertNull(this.service.sessionId); + assertSame(this.handler, this.service.handler); + + handleRequest("/echo/server1/session2/xhr", HttpStatus.OK); + assertEquals("session2", this.service.sessionId); + assertEquals(TransportType.XHR, this.service.transportType); + assertSame(this.handler, this.service.handler); + + handleRequest("/echo/other", HttpStatus.NOT_FOUND); + handleRequest("/echo//", HttpStatus.NOT_FOUND); + handleRequest("/echo///", HttpStatus.NOT_FOUND); + } + + + @Test + public void getSockJsPathGreetingRequest() throws Exception { + handleRequest("/echo", HttpStatus.OK); + assertEquals("Welcome to SockJS!\n", this.servletResponse.getContentAsString()); + } + + @Test + public void getSockJsPathInfoRequest() throws Exception { + handleRequest("/echo/info", HttpStatus.OK); + assertTrue(this.servletResponse.getContentAsString().startsWith("{\"entropy\":")); + } + + @Test + public void getSockJsPathWithConfiguredPrefix() throws Exception { + this.service.setValidSockJsPrefixes("/echo"); + handleRequest("/echo/s1/s2/xhr", HttpStatus.OK); + } + + @Test + public void getInfoOptions() throws Exception { + setRequest("OPTIONS", "/echo/info"); + this.service.handleRequest(this.request, this.response, this.handler); + + assertEquals(204, servletResponse.getStatus()); + } + + + private void handleRequest(String uri, HttpStatus httpStatus) throws IOException { + resetResponse(); + setRequest("GET", uri); + this.service.handleRequest(this.request, this.response, this.handler); + + assertEquals(httpStatus.value(), this.servletResponse.getStatus()); + } + + private static class TestSockJsService extends AbstractSockJsService { + + private String sessionId; + + private TransportType transportType; + + private WebSocketHandler handler; + + public TestSockJsService(TaskScheduler scheduler) { + super(scheduler); + } + + @Override + protected void handleRawWebSocketRequest(ServerHttpRequest request, + ServerHttpResponse response, WebSocketHandler handler) throws IOException { + + this.handler = handler; + } + + @Override + protected void handleTransportRequest(ServerHttpRequest request, + ServerHttpResponse response, String sessionId, + TransportType transportType, WebSocketHandler handler) + throws IOException, TransportErrorException { + + this.sessionId = sessionId; + this.transportType = transportType; + this.handler = handler; + } + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/StubSockJsConfig.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/StubSockJsConfig.java new file mode 100644 index 0000000000..0f2a4c4f05 --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/StubSockJsConfig.java @@ -0,0 +1,59 @@ +/* + * 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.web.socket.sockjs; + +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + + +/** + * @author Rossen Stoyanchev + */ +public class StubSockJsConfig implements SockJsConfiguration { + + private int streamBytesLimit = 128 * 1024; + + private long heartbeatTime = 25 * 1000; + + private TaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); + + + public int getStreamBytesLimit() { + return streamBytesLimit; + } + + public void setStreamBytesLimit(int streamBytesLimit) { + this.streamBytesLimit = streamBytesLimit; + } + + public long getHeartbeatTime() { + return heartbeatTime; + } + + public void setHeartbeatTime(long heartbeatTime) { + this.heartbeatTime = heartbeatTime; + } + + public TaskScheduler getTaskScheduler() { + return taskScheduler; + } + + public void setTaskScheduler(TaskScheduler taskScheduler) { + this.taskScheduler = taskScheduler; + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/StubTaskScheduler.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/StubTaskScheduler.java new file mode 100644 index 0000000000..57849d1ba1 --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/StubTaskScheduler.java @@ -0,0 +1,87 @@ +/* + * 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.web.socket.sockjs; + +import java.util.Date; +import java.util.concurrent.Callable; +import java.util.concurrent.Delayed; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.Trigger; + + +/** + * @author Rossen Stoyanchev + */ +public class StubTaskScheduler implements TaskScheduler { + + @Override + public ScheduledFuture schedule(Runnable task, Trigger trigger) { + return new StubScheduledFuture(); + } + + @Override + public ScheduledFuture schedule(Runnable task, Date startTime) { + return new StubScheduledFuture(); + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period) { + return new StubScheduledFuture(); + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable task, long period) { + return new StubScheduledFuture(); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay) { + return new StubScheduledFuture(); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay) { + return new StubScheduledFuture(); + } + + + private static class StubScheduledFuture extends FutureTask implements ScheduledFuture { + + @SuppressWarnings("unchecked") + public StubScheduledFuture() { + super(new Callable() { + public Object call() throws Exception { + return null; + } + }); + } + + @Override + public long getDelay(TimeUnit unit) { + return 0; + } + + @Override + public int compareTo(Delayed o) { + return 0; + } + } +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/TransportTypeTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/TransportTypeTests.java new file mode 100644 index 0000000000..9f4b34c17f --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/TransportTypeTests.java @@ -0,0 +1,42 @@ +/* + * 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.web.socket.sockjs; + +import org.junit.Test; + +import static org.junit.Assert.*; + + +/** + * @author Rossen Stoyanchev + */ +public class TransportTypeTests { + + + @Test + public void testFromValue() { + assertEquals(TransportType.WEBSOCKET, TransportType.fromValue("websocket")); + assertEquals(TransportType.XHR, TransportType.fromValue("xhr")); + assertEquals(TransportType.XHR_SEND, TransportType.fromValue("xhr_send")); + assertEquals(TransportType.JSONP, TransportType.fromValue("jsonp")); + assertEquals(TransportType.JSONP_SEND, TransportType.fromValue("jsonp_send")); + assertEquals(TransportType.XHR_STREAMING, TransportType.fromValue("xhr_streaming")); + assertEquals(TransportType.EVENT_SOURCE, TransportType.fromValue("eventsource")); + assertEquals(TransportType.HTML_FILE, TransportType.fromValue("htmlfile")); + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/DefaultSockJsServiceTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/DefaultSockJsServiceTests.java index a135e51a40..a8630d5319 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/DefaultSockJsServiceTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/DefaultSockJsServiceTests.java @@ -18,11 +18,13 @@ package org.springframework.web.socket.sockjs.support; import java.util.Map; +import org.junit.Before; import org.junit.Test; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.web.socket.AbstractHttpRequestTests; +import org.springframework.web.socket.adapter.TextWebSocketHandlerAdapter; +import org.springframework.web.socket.sockjs.StubTaskScheduler; import org.springframework.web.socket.sockjs.TransportHandler; import org.springframework.web.socket.sockjs.TransportType; -import org.springframework.web.socket.sockjs.support.DefaultSockJsService; import static org.junit.Assert.*; @@ -32,14 +34,22 @@ import static org.junit.Assert.*; * * @author Rossen Stoyanchev */ -public class DefaultSockJsServiceTests { +public class DefaultSockJsServiceTests extends AbstractHttpRequestTests { + private DefaultSockJsService service; + + + @Before + public void setUp() { + super.setUp(); + this.service = new DefaultSockJsService(new StubTaskScheduler()); + this.service.setValidSockJsPrefixes("/echo"); + } @Test - public void testDefaultTransportHandlers() { + public void defaultTransportHandlers() { - DefaultSockJsService sockJsService = new DefaultSockJsService(new ThreadPoolTaskScheduler()); - Map handlers = sockJsService.getTransportHandlers(); + Map handlers = service.getTransportHandlers(); assertEquals(8, handlers.size()); assertNotNull(handlers.get(TransportType.WEBSOCKET)); @@ -52,5 +62,21 @@ public class DefaultSockJsServiceTests { assertNotNull(handlers.get(TransportType.EVENT_SOURCE)); } + @Test + public void xhrSend() throws Exception { + + setRequest("POST", "/echo/000/c5839f69/xhr"); + this.service.handleRequest(this.request, this.response, new TextWebSocketHandlerAdapter()); + + resetResponse(); + setRequest("POST", "/echo/000/c5839f69/xhr_send"); + this.servletRequest.setContent("[\"x\"]".getBytes("UTF-8")); + + this.service.handleRequest(this.request, this.response, new TextWebSocketHandlerAdapter()); + + assertEquals(204, this.servletResponse.getStatus()); + assertEquals("text/plain;charset=UTF-8", this.servletResponse.getContentType()); + } + } diff --git a/spring-websocket/src/test/resources/log4j.xml b/spring-websocket/src/test/resources/log4j.xml new file mode 100644 index 0000000000..8fa59bf2f3 --- /dev/null +++ b/spring-websocket/src/test/resources/log4j.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +