Add SockJS path detection

This commit is contained in:
Rossen Stoyanchev 2013-05-05 20:51:37 -04:00
parent 97d225ba75
commit 7845ebc428
25 changed files with 654 additions and 133 deletions

View File

@ -16,6 +16,7 @@
package org.springframework.http.server;
/**
* TODO..
*/

View File

@ -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
// ---------------------------------------------------------------------

View File

@ -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();

View File

@ -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);

View File

@ -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());

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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.
*
* <p>
* 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<String> sockJsPrefixes = new ArrayList<String>();
private final Set<String> sockJsPathCache = new CopyOnWriteArraySet<String>();
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).
* <p>
* 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<String>() {
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
}
};
}

View File

@ -217,7 +217,7 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess
* <p>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
* <p>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);
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -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<String> headerHints;
private static final Map<String, TransportType> transportTypes = new HashMap<String, TransportType>();
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

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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,

View File

@ -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;

View File

@ -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 {

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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("<!DOCTYPE html>\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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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"));
}
}

View File

@ -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<TransportType, TransportHandler> handlers = sockJsService.getTransportHandlers();
Map<TransportType, TransportHandler> 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());
}
}

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN" "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
<!-- Appenders -->
<appender name="console" class="org.apache.log4j.ConsoleAppender">
<param name="Target" value="System.out" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d{HH:mm:ss} [%t] %c{1} - %m%n" />
</layout>
</appender>
<logger name="org.springframework.samples">
<level value="debug" />
</logger>
<logger name="org.springframework.web">
<level value="debug" />
</logger>
<logger name="org.springframework.web.socket">
<level value="trace" />
</logger>
<root>
<priority value="warn" />
<appender-ref ref="console" />
</root>
</log4j:configuration>