Add spring-websocket module tests

This commit is contained in:
Rossen Stoyanchev
2013-05-14 12:00:51 -04:00
parent 6a5acb9372
commit 05084d504b
52 changed files with 2932 additions and 392 deletions

View File

@@ -26,7 +26,7 @@ import org.springframework.core.task.TaskExecutor;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Abstract base class for WebSocketConnection managers.
* Abstract base class for WebSocket connection managers.
*
* @author Rossen Stoyanchev
* @since 4.0
@@ -147,25 +147,25 @@ public abstract class ConnectionManagerSupport implements SmartLifecycle {
public final void stop() {
synchronized (this.lifecycleMonitor) {
if (isRunning()) {
stopInternal();
if (logger.isDebugEnabled()) {
logger.debug("Stopping " + this.getClass().getSimpleName());
}
try {
stopInternal();
}
catch (Throwable e) {
logger.error("Failed to stop WebSocket connection", e);
}
finally {
this.isRunning = false;
}
}
}
}
protected void stopInternal() {
if (logger.isDebugEnabled()) {
logger.debug("Stopping " + this.getClass().getSimpleName());
}
try {
if (isConnected()) {
closeConnection();
}
}
catch (Throwable e) {
logger.error("Failed to stop WebSocket connection", e);
}
finally {
this.isRunning = false;
protected void stopInternal() throws Exception {
if (isConnected()) {
closeConnection();
}
}

View File

@@ -84,7 +84,7 @@ public class WebSocketConnectionManager extends ConnectionManagerSupport {
}
@Override
public void stopInternal() {
public void stopInternal() throws Exception {
if (this.syncClientLifecycle) {
((SmartLifecycle) client).stop();
}

View File

@@ -52,12 +52,15 @@ public class StandardWebSocketClient implements WebSocketClient {
private static final Log logger = LogFactory.getLog(StandardWebSocketClient.class);
private static final Set<String> EXCLUDED_HEADERS = new HashSet<String>(
Arrays.asList("Sec-WebSocket-Accept", "Sec-WebSocket-Extensions", "Sec-WebSocket-Key",
"Sec-WebSocket-Protocol", "Sec-WebSocket-Version"));
private WebSocketContainer webSocketContainer;
private WebSocketContainer webSocketContainer = ContainerProvider.getWebSocketContainer();
public WebSocketContainer getWebSocketContainer() {
if (this.webSocketContainer == null) {
this.webSocketContainer = ContainerProvider.getWebSocketContainer();
}
return this.webSocketContainer;
}
public void setWebSocketContainer(WebSocketContainer container) {
this.webSocketContainer = container;
@@ -72,8 +75,8 @@ public class StandardWebSocketClient implements WebSocketClient {
}
@Override
public WebSocketSession doHandshake(WebSocketHandler webSocketHandler,
final HttpHeaders httpHeaders, URI uri) throws WebSocketConnectFailureException {
public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, HttpHeaders httpHeaders, URI uri)
throws WebSocketConnectFailureException {
StandardWebSocketSessionAdapter session = new StandardWebSocketSessionAdapter();
session.setUri(uri);
@@ -86,29 +89,7 @@ public class StandardWebSocketClient implements WebSocketClient {
if (!protocols.isEmpty()) {
configBuidler.preferredSubprotocols(protocols);
}
configBuidler.configurator(new Configurator() {
@Override
public void beforeRequest(Map<String, List<String>> headers) {
for (String headerName : httpHeaders.keySet()) {
if (!EXCLUDED_HEADERS.contains(headerName)) {
List<String> value = httpHeaders.get(headerName);
if (logger.isTraceEnabled()) {
logger.trace("Adding header [" + headerName + "=" + value + "]");
}
headers.put(headerName, value);
}
}
if (logger.isTraceEnabled()) {
logger.trace("Handshake request headers: " + headers);
}
}
@Override
public void afterResponse(HandshakeResponse handshakeResponse) {
if (logger.isTraceEnabled()) {
logger.trace("Handshake response headers: " + handshakeResponse.getHeaders());
}
}
});
configBuidler.configurator(new StandardWebSocketClientConfigurator(httpHeaders));
}
try {
@@ -121,4 +102,41 @@ public class StandardWebSocketClient implements WebSocketClient {
}
}
private static class StandardWebSocketClientConfigurator extends Configurator {
private static final Set<String> EXCLUDED_HEADERS = new HashSet<String>(
Arrays.asList("Sec-WebSocket-Accept", "Sec-WebSocket-Extensions", "Sec-WebSocket-Key",
"Sec-WebSocket-Protocol", "Sec-WebSocket-Version"));
private final HttpHeaders httpHeaders;
public StandardWebSocketClientConfigurator(HttpHeaders httpHeaders) {
this.httpHeaders = httpHeaders;
}
@Override
public void beforeRequest(Map<String, List<String>> headers) {
for (String headerName : this.httpHeaders.keySet()) {
if (!EXCLUDED_HEADERS.contains(headerName)) {
List<String> value = this.httpHeaders.get(headerName);
if (logger.isTraceEnabled()) {
logger.trace("Adding header [" + headerName + "=" + value + "]");
}
headers.put(headerName, value);
}
}
if (logger.isTraceEnabled()) {
logger.trace("Handshake request headers: " + headers);
}
}
@Override
public void afterResponse(HandshakeResponse handshakeResponse) {
if (logger.isTraceEnabled()) {
logger.trace("Handshake response headers: " + handshakeResponse.getHeaders());
}
}
}
}

View File

@@ -48,12 +48,13 @@ import org.springframework.util.ReflectionUtils;
* @author Rossen Stoyanchev
* @since 4.0
*/
public class EndpointExporter implements InitializingBean, BeanPostProcessor, ApplicationContextAware {
public class ServerEndpointExporter implements InitializingBean, BeanPostProcessor, ApplicationContextAware {
private static final boolean isServletApiPresent =
ClassUtils.isPresent("javax.servlet.ServletContext", EndpointExporter.class.getClassLoader());
ClassUtils.isPresent("javax.servlet.ServletContext", ServerEndpointExporter.class.getClassLoader());
private static Log logger = LogFactory.getLog(ServerEndpointExporter.class);
private static Log logger = LogFactory.getLog(EndpointExporter.class);
private final List<Class<?>> annotatedEndpointClasses = new ArrayList<Class<?>>();
@@ -63,6 +64,7 @@ public class EndpointExporter implements InitializingBean, BeanPostProcessor, Ap
private ServerContainer serverContainer;
/**
* TODO
* @param annotatedEndpointClasses

View File

@@ -38,16 +38,14 @@ import org.springframework.web.socket.support.BeanCreatingHandlerProvider;
/**
* An implementation of {@link javax.websocket.server.ServerEndpointConfig} that also
* holds the target {@link javax.websocket.Endpoint} as a reference or a bean name.
*
* <p>
* Beans of this type are detected by {@link EndpointExporter} and
* registered with a Java WebSocket runtime at startup.
* holds the target {@link javax.websocket.Endpoint} provided as a reference or as a bean
* name. Beans of this type are detected by {@link ServerEndpointExporter} and registered
* with a Java WebSocket runtime at startup.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class EndpointRegistration implements ServerEndpointConfig, BeanFactoryAware {
public class ServerEndpointRegistration implements ServerEndpointConfig, BeanFactoryAware {
private final String path;
@@ -65,7 +63,7 @@ public class EndpointRegistration implements ServerEndpointConfig, BeanFactoryAw
private final Map<String, Object> userProperties = new HashMap<String, Object>();
private Configurator configurator = new Configurator() {};
private Configurator configurator = new EndpointRegistrationConfigurator();
/**
@@ -74,7 +72,7 @@ public class EndpointRegistration implements ServerEndpointConfig, BeanFactoryAw
* @param path
* @param endpointClass
*/
public EndpointRegistration(String path, Class<? extends Endpoint> endpointClass) {
public ServerEndpointRegistration(String path, Class<? extends Endpoint> endpointClass) {
Assert.hasText(path, "path must not be empty");
Assert.notNull(endpointClass, "endpointClass is required");
this.path = path;
@@ -82,7 +80,7 @@ public class EndpointRegistration implements ServerEndpointConfig, BeanFactoryAw
this.endpoint = null;
}
public EndpointRegistration(String path, Endpoint endpoint) {
public ServerEndpointRegistration(String path, Endpoint endpoint) {
Assert.hasText(path, "path must not be empty");
Assert.notNull(endpoint, "endpoint is required");
this.path = path;
@@ -152,38 +150,9 @@ public class EndpointRegistration implements ServerEndpointConfig, BeanFactoryAw
return this.decoders;
}
/**
* The {@link Configurator#getEndpointInstance(Class)} method is always ignored.
*/
public void setConfigurator(Configurator configurator) {
this.configurator = configurator;
}
@Override
public Configurator getConfigurator() {
return new Configurator() {
@SuppressWarnings("unchecked")
@Override
public <T> T getEndpointInstance(Class<T> clazz) throws InstantiationException {
return (T) EndpointRegistration.this.getEndpoint();
}
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
EndpointRegistration.this.configurator.modifyHandshake(sec, request, response);
}
@Override
public boolean checkOrigin(String originHeaderValue) {
return EndpointRegistration.this.configurator.checkOrigin(originHeaderValue);
}
@Override
public String getNegotiatedSubprotocol(List<String> supported, List<String> requested) {
return EndpointRegistration.this.configurator.getNegotiatedSubprotocol(supported, requested);
}
@Override
public List<Extension> getNegotiatedExtensions(List<Extension> installed, List<Extension> requested) {
return EndpointRegistration.this.configurator.getNegotiatedExtensions(installed, requested);
}
};
return this.configurator;
}
@Override
@@ -193,4 +162,50 @@ public class EndpointRegistration implements ServerEndpointConfig, BeanFactoryAw
}
}
protected void modifyHandshake(HandshakeRequest request, HandshakeResponse response) {
this.configurator.modifyHandshake(this, request, response);
}
protected boolean checkOrigin(String originHeaderValue) {
return this.configurator.checkOrigin(originHeaderValue);
}
protected String getNegotiatedSubprotocol(List<String> supported, List<String> requested) {
return this.configurator.getNegotiatedSubprotocol(supported, requested);
}
protected List<Extension> getNegotiatedExtensions(List<Extension> installed, List<Extension> requested) {
return this.configurator.getNegotiatedExtensions(installed, requested);
}
private class EndpointRegistrationConfigurator extends Configurator {
@SuppressWarnings("unchecked")
@Override
public <T> T getEndpointInstance(Class<T> clazz) throws InstantiationException {
return (T) ServerEndpointRegistration.this.getEndpoint();
}
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
super.modifyHandshake(sec, request, response);
}
@Override
public boolean checkOrigin(String originHeaderValue) {
return super.checkOrigin(originHeaderValue);
}
@Override
public String getNegotiatedSubprotocol(List<String> supported, List<String> requested) {
return super.getNegotiatedSubprotocol(supported, requested);
}
@Override
public List<Extension> getNegotiatedExtensions(List<Extension> installed, List<Extension> requested) {
return super.getNegotiatedExtensions(installed, requested);
}
}
}

View File

@@ -34,7 +34,7 @@ import org.springframework.web.socket.sockjs.SockJsService;
* using its setters allows configuring the {@code ServerContainer} through Spring
* configuration. This is useful even if the ServerContainer is not injected into any
* other bean. For example, an application can configure a {@link DefaultHandshakeHandler}
* , a {@link SockJsService}, or {@link EndpointExporter}, and separately declare this
* , a {@link SockJsService}, or {@link ServerEndpointExporter}, and separately declare this
* FactoryBean in order to customize the properties of the (one and only)
* {@code ServerContainer} instance.
*
@@ -44,9 +44,6 @@ import org.springframework.web.socket.sockjs.SockJsService;
public class ServletServerContainerFactoryBean
implements FactoryBean<WebSocketContainer>, InitializingBean, ServletContextAware {
private static final String SERVER_CONTAINER_ATTR_NAME = "javax.websocket.server.ServerContainer";
private Long asyncSendTimeout;
private Long maxSessionIdleTimeout;
@@ -92,7 +89,7 @@ public class ServletServerContainerFactoryBean
@Override
public void setServletContext(ServletContext servletContext) {
this.serverContainer = (ServerContainer) servletContext.getAttribute(SERVER_CONTAINER_ATTR_NAME);
this.serverContainer = (ServerContainer) servletContext.getAttribute("javax.websocket.server.ServerContainer");
}
@Override

View File

@@ -27,9 +27,9 @@ import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.WebApplicationContext;
/**
* This should be used in conjuction with {@link ServerEndpoint @ServerEndpoint} classes.
* This should be used in conjunction with {@link ServerEndpoint @ServerEndpoint} classes.
*
* <p>For {@link javax.websocket.Endpoint}, see {@link EndpointExporter}.
* <p>For {@link javax.websocket.Endpoint}, see {@link ServerEndpointExporter}.
*
* @author Rossen Stoyanchev
* @since 4.0
@@ -56,7 +56,7 @@ public class SpringConfigurator extends Configurator {
}
return wac.getAutowireCapableBeanFactory().createBean(endpointClass);
}
if (beans.size() == 1) {
else if (beans.size() == 1) {
if (logger.isTraceEnabled()) {
logger.trace("Using @ServerEndpoint singleton " + beans.keySet().iterator().next());
}

View File

@@ -0,0 +1,6 @@
package org.springframework.web.socket.server.endpoint;
public class Test {
}

View File

@@ -16,8 +16,8 @@
/**
* Server classes for use with standard Java WebSocket endpoints including
* {@link org.springframework.web.socket.server.endpoint.EndpointRegistration} and
* {@link org.springframework.web.socket.server.endpoint.EndpointExporter} for
* {@link org.springframework.web.socket.server.endpoint.ServerEndpointRegistration} and
* {@link org.springframework.web.socket.server.endpoint.ServerEndpointExporter} for
* registering type-based endpoints,
* {@link org.springframework.web.socket.server.endpoint.SpringConfigurator} for
* instantiating annotated endpoints through Spring.

View File

@@ -50,7 +50,7 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.socket.server.HandshakeFailureException;
import org.springframework.web.socket.server.endpoint.EndpointRegistration;
import org.springframework.web.socket.server.endpoint.ServerEndpointRegistration;
/**
* GlassFish support for upgrading an {@link HttpServletRequest} during a WebSocket
@@ -136,7 +136,7 @@ public class GlassFishRequestUpgradeStrategy extends AbstractEndpointUpgradeStra
String randomValue = String.valueOf(random.nextLong());
String endpointPath = requestUri.endsWith("/") ? requestUri + randomValue : requestUri + "/" + randomValue;
EndpointRegistration endpointConfig = new EndpointRegistration(endpointPath, endpoint);
ServerEndpointRegistration endpointConfig = new ServerEndpointRegistration(endpointPath, endpoint);
endpointConfig.setSubprotocols(Arrays.asList(selectedProtocol));
return new TyrusEndpoint(new EndpointWrapper(endpoint, endpointConfig,

View File

@@ -34,7 +34,7 @@ import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.socket.server.HandshakeFailureException;
import org.springframework.web.socket.server.endpoint.EndpointRegistration;
import org.springframework.web.socket.server.endpoint.ServerEndpointRegistration;
/**
* Tomcat support for upgrading an {@link HttpServletRequest} during a WebSocket handshake.
@@ -77,7 +77,7 @@ public class TomcatRequestUpgradeStrategy extends AbstractEndpointUpgradeStrateg
// TODO: use ServletContext attribute when Tomcat is updated
WsServerContainer serverContainer = WsServerContainer.getServerContainer();
ServerEndpointConfig endpointConfig = new EndpointRegistration("/shouldntmatter", endpoint);
ServerEndpointConfig endpointConfig = new ServerEndpointRegistration("/shouldntmatter", endpoint);
upgradeHandler.preInit(endpoint, endpointConfig, serverContainer, webSocketRequest,
selectedProtocol, Collections.<String, String> emptyMap(), servletRequest.isSecure());

View File

@@ -1,162 +0,0 @@
/*
* 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.EOFException;
import java.io.IOException;
import java.net.SocketException;
import java.util.Date;
import java.util.concurrent.ScheduledFuture;
import org.springframework.util.Assert;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
/**
* Provides partial implementations of {@link SockJsSession} methods to send messages,
* including heartbeat messages and to manage session state.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public abstract class AbstractServerSockJsSession extends AbstractSockJsSession {
private final SockJsConfiguration sockJsConfig;
private ScheduledFuture<?> heartbeatTask;
public AbstractServerSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) {
super(sessionId, handler);
this.sockJsConfig = config;
}
protected SockJsConfiguration getSockJsConfig() {
return this.sockJsConfig;
}
@Override
public final synchronized void sendMessage(WebSocketMessage message) throws IOException {
Assert.isTrue(!isClosed(), "Cannot send a message when session is closed");
Assert.isInstanceOf(TextMessage.class, message, "Expected text message: " + message);
sendMessageInternal(((TextMessage) message).getPayload());
}
protected abstract void sendMessageInternal(String message) throws IOException;
@Override
public void connectionClosedInternal(CloseStatus status) {
updateLastActiveTime();
cancelHeartbeat();
}
@Override
public final synchronized void closeInternal(CloseStatus status) throws IOException {
if (isActive()) {
// TODO: deliver messages "in flight" before sending close frame
try {
// bypass writeFrame
writeFrameInternal(SockJsFrame.closeFrame(status.getCode(), status.getReason()));
}
catch (Throwable ex) {
logger.warn("Failed to send SockJS close frame: " + ex.getMessage());
}
}
updateLastActiveTime();
cancelHeartbeat();
disconnect(status);
}
protected abstract void disconnect(CloseStatus status) throws IOException;
/**
* For internal use within a TransportHandler and the (TransportHandler-specific)
* session sub-class.
*/
protected void writeFrame(SockJsFrame frame) throws IOException {
if (logger.isTraceEnabled()) {
logger.trace("Preparing to write " + frame);
}
try {
writeFrameInternal(frame);
}
catch (IOException ex) {
if (ex instanceof EOFException || ex instanceof SocketException) {
logger.warn("Client went away. Terminating connection");
}
else {
logger.warn("Terminating connection due to failure to send message: " + ex.getMessage());
}
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());
disconnect(CloseStatus.SERVER_ERROR);
close(CloseStatus.SERVER_ERROR);
throw new SockJsRuntimeException("Failed to write " + frame, ex);
}
}
protected abstract void writeFrameInternal(SockJsFrame frame) throws Exception;
public synchronized void sendHeartbeat() throws Exception {
if (isActive()) {
writeFrame(SockJsFrame.heartbeatFrame());
scheduleHeartbeat();
}
}
protected void scheduleHeartbeat() {
Assert.notNull(getSockJsConfig().getTaskScheduler(), "heartbeatScheduler not configured");
cancelHeartbeat();
if (!isActive()) {
return;
}
Date time = new Date(System.currentTimeMillis() + getSockJsConfig().getHeartbeatTime());
this.heartbeatTask = getSockJsConfig().getTaskScheduler().schedule(new Runnable() {
@Override
public void run() {
try {
sendHeartbeat();
}
catch (Throwable t) {
// ignore
}
}
}, time);
if (logger.isTraceEnabled()) {
logger.trace("Scheduled heartbeat after " + getSockJsConfig().getHeartbeatTime()/1000 + " seconds");
}
}
protected void cancelHeartbeat() {
if ((this.heartbeatTask != null) && !this.heartbeatTask.isDone()) {
if (logger.isTraceEnabled()) {
logger.trace("Cancelling heartbeat");
}
this.heartbeatTask.cancel(false);
}
this.heartbeatTask = null;
}
}

View File

@@ -332,6 +332,7 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf
return path.substring(index + prefix.length());
}
}
return null;
}
// SockJS info request?
@@ -519,5 +520,4 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf
}
};
}

View File

@@ -16,9 +16,13 @@
package org.springframework.web.socket.sockjs;
import java.io.EOFException;
import java.io.IOException;
import java.net.SocketException;
import java.net.URI;
import java.security.Principal;
import java.util.Date;
import java.util.concurrent.ScheduledFuture;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -26,6 +30,7 @@ import org.springframework.util.Assert;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.adapter.ConfigurableWebSocketSession;
@@ -50,24 +55,29 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess
private Principal principal;
private final SockJsConfiguration sockJsConfig;
private WebSocketHandler handler;
private State state = State.NEW;
private long timeCreated = System.currentTimeMillis();
private long timeLastActive = System.currentTimeMillis();
private long timeLastActive = timeCreated;
private ScheduledFuture<?> heartbeatTask;
/**
* @param sessionId
* @param webSocketHandler the recipient of SockJS messages
*/
public AbstractSockJsSession(String sessionId, WebSocketHandler webSocketHandler) {
public AbstractSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler webSocketHandler) {
Assert.notNull(sessionId, "sessionId is required");
Assert.notNull(webSocketHandler, "webSocketHandler is required");
this.id = sessionId;
this.handler = webSocketHandler;
this.sockJsConfig = config;
}
@Override
@@ -120,6 +130,10 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess
this.principal = principal;
}
public SockJsConfiguration getSockJsConfig() {
return this.sockJsConfig;
}
public boolean isNew() {
return State.NEW.equals(this.state);
}
@@ -167,35 +181,12 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess
this.handler.afterConnectionEstablished(this);
}
/**
* Close due to error arising from SockJS transport handling.
*/
protected void tryCloseWithSockJsTransportError(Throwable ex, CloseStatus closeStatus) {
logger.error("Closing due to transport error for " + this, ex);
try {
delegateError(ex);
}
catch (Throwable delegateEx) {
logger.error("Unhandled error for " + this, delegateEx);
try {
close(closeStatus);
}
catch (Throwable closeEx) {
logger.error("Unhandled error for " + this, closeEx);
}
}
}
public void delegateMessages(String[] messages) throws Exception {
for (String message : messages) {
this.handler.handleMessage(this, new TextMessage(message));
}
}
public void delegateError(Throwable ex) throws Exception {
this.handler.handleTransportError(this, ex);
}
/**
* Invoked in reaction to the underlying connection being closed by the remote side
* (or the WebSocket container) in order to perform cleanup and notify the
@@ -208,7 +199,8 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess
logger.debug(this + " was closed, " + status);
}
try {
connectionClosedInternal(status);
updateLastActiveTime();
cancelHeartbeat();
}
finally {
this.state = State.CLOSED;
@@ -217,9 +209,18 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess
}
}
protected void connectionClosedInternal(CloseStatus status) {
public void delegateError(Throwable ex) throws Exception {
this.handler.handleTransportError(this, ex);
}
public final synchronized void sendMessage(WebSocketMessage message) throws IOException {
Assert.isTrue(!isClosed(), "Cannot send a message when session is closed");
Assert.isInstanceOf(TextMessage.class, message, "Expected text message: " + message);
sendMessageInternal(((TextMessage) message).getPayload());
}
protected abstract void sendMessageInternal(String message) throws IOException;
/**
* {@inheritDoc}
* <p>Performs cleanup and notifies the {@link SockJsHandler}.
@@ -240,7 +241,19 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess
logger.debug("Closing " + this + ", " + status);
}
try {
closeInternal(status);
if (isActive()) {
// TODO: deliver messages "in flight" before sending close frame
try {
// bypass writeFrame
writeFrameInternal(SockJsFrame.closeFrame(status.getCode(), status.getReason()));
}
catch (Throwable ex) {
logger.warn("Failed to send SockJS close frame: " + ex.getMessage());
}
}
updateLastActiveTime();
cancelHeartbeat();
disconnect(status);
}
finally {
this.state = State.CLOSED;
@@ -254,7 +267,97 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess
}
}
protected abstract void closeInternal(CloseStatus status) throws IOException;
protected abstract void disconnect(CloseStatus status) throws IOException;
/**
* Close due to error arising from SockJS transport handling.
*/
protected void tryCloseWithSockJsTransportError(Throwable ex, CloseStatus closeStatus) {
logger.error("Closing due to transport error for " + this, ex);
try {
delegateError(ex);
}
catch (Throwable delegateEx) {
logger.error("Unhandled error for " + this, delegateEx);
try {
close(closeStatus);
}
catch (Throwable closeEx) {
logger.error("Unhandled error for " + this, closeEx);
}
}
}
/**
* For internal use within a TransportHandler and the (TransportHandler-specific)
* session sub-class.
*/
protected void writeFrame(SockJsFrame frame) throws IOException {
if (logger.isTraceEnabled()) {
logger.trace("Preparing to write " + frame);
}
try {
writeFrameInternal(frame);
}
catch (IOException ex) {
if (ex instanceof EOFException || ex instanceof SocketException) {
logger.warn("Client went away. Terminating connection");
}
else {
logger.warn("Terminating connection due to failure to send message: " + ex.getMessage());
}
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());
disconnect(CloseStatus.SERVER_ERROR);
close(CloseStatus.SERVER_ERROR);
throw new SockJsRuntimeException("Failed to write " + frame, ex);
}
}
protected abstract void writeFrameInternal(SockJsFrame frame) throws Exception;
public synchronized void sendHeartbeat() throws Exception {
if (isActive()) {
writeFrame(SockJsFrame.heartbeatFrame());
scheduleHeartbeat();
}
}
protected void scheduleHeartbeat() {
Assert.notNull(this.sockJsConfig.getTaskScheduler(), "heartbeatScheduler not configured");
cancelHeartbeat();
if (!isActive()) {
return;
}
Date time = new Date(System.currentTimeMillis() + this.sockJsConfig.getHeartbeatTime());
this.heartbeatTask = this.sockJsConfig.getTaskScheduler().schedule(new Runnable() {
public void run() {
try {
sendHeartbeat();
}
catch (Throwable t) {
// ignore
}
}
}, time);
if (logger.isTraceEnabled()) {
logger.trace("Scheduled heartbeat after " + this.sockJsConfig.getHeartbeatTime()/1000 + " seconds");
}
}
protected void cancelHeartbeat() {
if ((this.heartbeatTask != null) && !this.heartbeatTask.isDone()) {
if (logger.isTraceEnabled()) {
logger.trace("Cancelling heartbeat");
}
this.heartbeatTask.cancel(false);
}
this.heartbeatTask = null;
}
@Override

View File

@@ -41,6 +41,7 @@ public class SockJsFrame {
private SockJsFrame(String content) {
Assert.notNull("content is required");
this.content = content;
}
@@ -116,6 +117,22 @@ public class SockJsFrame {
return "SockJsFrame content='" + result.replace("\n", "\\n").replace("\r", "\\r") + "'";
}
@Override
public int hashCode() {
return this.content.hashCode();
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof SockJsFrame)) {
return false;
}
return this.content.equals(((SockJsFrame) other).content);
}
private static class MessageFrame extends SockJsFrame {

View File

@@ -79,7 +79,7 @@ public enum TransportType {
return this.httpMethod;
}
public boolean setsNoCache() {
public boolean sendsNoCacheInstruction() {
return this.headerHints.contains("no_cache");
}

View File

@@ -35,6 +35,7 @@ import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.DefaultHandshakeHandler;
import org.springframework.web.socket.server.HandshakeHandler;
@@ -84,7 +85,8 @@ public class DefaultSockJsService extends AbstractSockJsService {
* application stops.
*/
public DefaultSockJsService(TaskScheduler taskScheduler) {
this(taskScheduler, null);
super(taskScheduler);
addTransportHandlers(getDefaultTransportHandlers());
}
/**
@@ -105,9 +107,16 @@ public class DefaultSockJsService extends AbstractSockJsService {
super(taskScheduler);
transportHandlers = CollectionUtils.isEmpty(transportHandlers) ? getDefaultTransportHandlers() : transportHandlers;
addTransportHandlers(transportHandlers);
addTransportHandlers(Arrays.asList(transportHandlerOverrides));
if (!CollectionUtils.isEmpty(transportHandlers)) {
addTransportHandlers(transportHandlers);
}
if (!ObjectUtils.isEmpty(transportHandlerOverrides)) {
addTransportHandlers(Arrays.asList(transportHandlerOverrides));
}
if (this.transportHandlers.isEmpty()) {
logger.warn("No transport handlers");
}
}
protected final Set<TransportHandler> getDefaultTransportHandlers() {
@@ -194,7 +203,7 @@ public class DefaultSockJsService extends AbstractSockJsService {
transportHandler, request, response);
if (session != null) {
if (transportType.setsNoCache()) {
if (transportType.sendsNoCacheInstruction()) {
addNoCacheHeaders(response);
}

View File

@@ -28,8 +28,6 @@ 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;
import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator;
@@ -38,7 +36,7 @@ import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* TODO
* Base class for HTTP-based transports that read input messages.
*
* @author Rossen Stoyanchev
* @since 4.0
@@ -57,8 +55,7 @@ public abstract class AbstractHttpReceivingTransportHandler implements Transport
@Override
public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler webSocketHandler, AbstractSockJsSession session)
throws TransportErrorException {
WebSocketHandler webSocketHandler, AbstractSockJsSession session) throws TransportErrorException {
if (session == null) {
response.setStatusCode(HttpStatus.NOT_FOUND);
@@ -77,21 +74,26 @@ public abstract class AbstractHttpReceivingTransportHandler implements Transport
messages = readMessages(request);
}
catch (JsonMappingException ex) {
logger.error("Failed to read message: ", ex);
logger.error("Failed to read message: " + ex.getMessage());
sendInternalServerError(response, "Payload expected.", session.getId());
return;
}
catch (IOException ex) {
logger.error("Failed to read message: ", ex);
logger.error("Failed to read message: " + ex.getMessage());
sendInternalServerError(response, "Broken JSON encoding.", session.getId());
return;
}
catch (Throwable t) {
logger.error("Failed to read message: ", t);
logger.error("Failed to read message: " + t.getMessage());
sendInternalServerError(response, "Failed to process messages", session.getId());
return;
}
if (messages == null) {
sendInternalServerError(response, "Payload expected.", session.getId());
return;
}
if (logger.isTraceEnabled()) {
logger.trace("Received message(s): " + Arrays.asList(messages));
}
@@ -104,7 +106,7 @@ public abstract class AbstractHttpReceivingTransportHandler implements Transport
}
catch (Throwable t) {
ExceptionWebSocketHandlerDecorator.tryCloseWithError(session, t, logger);
throw new SockJsRuntimeException("Unhandled WebSocketHandler error in " + this, t);
throw new TransportErrorException("Unhandled WebSocketHandler error in " + this, t, session.getId());
}
}

View File

@@ -28,9 +28,9 @@ import org.springframework.web.socket.sockjs.AbstractSockJsSession;
import org.springframework.web.socket.sockjs.ConfigurableTransportHandler;
import org.springframework.web.socket.sockjs.SockJsConfiguration;
import org.springframework.web.socket.sockjs.SockJsFrame;
import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat;
import org.springframework.web.socket.sockjs.SockJsSessionFactory;
import org.springframework.web.socket.sockjs.TransportErrorException;
import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat;
/**
* TODO
@@ -57,18 +57,17 @@ public abstract class AbstractHttpSendingTransportHandler
@Override
public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler webSocketHandler, AbstractSockJsSession session)
throws TransportErrorException {
WebSocketHandler webSocketHandler, AbstractSockJsSession session) throws TransportErrorException {
// Set content type before writing
response.getHeaders().setContentType(getContentType());
AbstractHttpServerSockJsSession httpServerSession = (AbstractHttpServerSockJsSession) session;
AbstractHttpSockJsSession httpServerSession = (AbstractHttpSockJsSession) session;
handleRequestInternal(request, response, httpServerSession);
}
protected void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response,
AbstractHttpServerSockJsSession httpServerSession) throws TransportErrorException {
AbstractHttpSockJsSession httpServerSession) throws TransportErrorException {
if (httpServerSession.isNew()) {
logger.debug("Opening " + getTransportType() + " connection");

View File

@@ -26,11 +26,11 @@ import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.sockjs.AbstractServerSockJsSession;
import org.springframework.web.socket.sockjs.AbstractSockJsSession;
import org.springframework.web.socket.sockjs.SockJsConfiguration;
import org.springframework.web.socket.sockjs.SockJsFrame;
import org.springframework.web.socket.sockjs.TransportErrorException;
import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat;
import org.springframework.web.socket.sockjs.TransportErrorException;
import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator;
/**
@@ -39,7 +39,7 @@ import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator
* @author Rossen Stoyanchev
* @since 4.0
*/
public abstract class AbstractHttpServerSockJsSession extends AbstractServerSockJsSession {
public abstract class AbstractHttpSockJsSession extends AbstractSockJsSession {
private FrameFormat frameFormat;
@@ -50,7 +50,7 @@ public abstract class AbstractHttpServerSockJsSession extends AbstractServerSock
private ServerHttpResponse response;
public AbstractHttpServerSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) {
public AbstractHttpSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) {
super(sessionId, config, handler);
}

View File

@@ -46,9 +46,9 @@ public class EventSourceTransportHandler extends AbstractHttpSendingTransportHan
}
@Override
public StreamingServerSockJsSession createSession(String sessionId, WebSocketHandler handler) {
public StreamingSockJsSession createSession(String sessionId, WebSocketHandler handler) {
Assert.notNull(getSockJsConfig(), "This transport requires SockJsConfiguration");
return new StreamingServerSockJsSession(sessionId, getSockJsConfig(), handler) {
return new StreamingSockJsSession(sessionId, getSockJsConfig(), handler) {
@Override
protected void writePrelude() throws IOException {
getResponse().getBody().write('\r');

View File

@@ -80,10 +80,10 @@ public class HtmlFileTransportHandler extends AbstractHttpSendingTransportHandle
}
@Override
public StreamingServerSockJsSession createSession(String sessionId, WebSocketHandler handler) {
public StreamingSockJsSession createSession(String sessionId, WebSocketHandler handler) {
Assert.notNull(getSockJsConfig(), "This transport requires SockJsConfiguration");
return new StreamingServerSockJsSession(sessionId, getSockJsConfig(), handler) {
return new StreamingSockJsSession(sessionId, getSockJsConfig(), handler) {
@Override
protected void writePrelude() throws IOException {
@@ -99,7 +99,7 @@ public class HtmlFileTransportHandler extends AbstractHttpSendingTransportHandle
@Override
public void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response,
AbstractHttpServerSockJsSession session) throws TransportErrorException {
AbstractHttpSockJsSession session) throws TransportErrorException {
try {
String callback = request.getQueryParams().getFirst("c");

View File

@@ -50,14 +50,14 @@ public class JsonpPollingTransportHandler extends AbstractHttpSendingTransportHa
}
@Override
public PollingServerSockJsSession createSession(String sessionId, WebSocketHandler handler) {
public PollingSockJsSession createSession(String sessionId, WebSocketHandler handler) {
Assert.notNull(getSockJsConfig(), "This transport requires SockJsConfiguration");
return new PollingServerSockJsSession(sessionId, getSockJsConfig(), handler);
return new PollingSockJsSession(sessionId, getSockJsConfig(), handler);
}
@Override
public void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response,
AbstractHttpServerSockJsSession session) throws TransportErrorException {
AbstractHttpSockJsSession session) throws TransportErrorException {
try {
String callback = request.getQueryParams().getFirst("c");

View File

@@ -20,14 +20,20 @@ import java.io.IOException;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.socket.sockjs.AbstractSockJsSession;
import org.springframework.web.socket.sockjs.TransportErrorException;
import org.springframework.web.socket.sockjs.TransportType;
public class JsonpTransportHandler extends AbstractHttpReceivingTransportHandler {
private final FormHttpMessageConverter formConverter = new FormHttpMessageConverter();
@Override
public TransportType getTransportType() {
return TransportType.JSONP_SEND;
@@ -37,13 +43,6 @@ public class JsonpTransportHandler extends AbstractHttpReceivingTransportHandler
public void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response,
AbstractSockJsSession sockJsSession) throws TransportErrorException {
if (MediaType.APPLICATION_FORM_URLENCODED.equals(request.getHeaders().getContentType())) {
if (request.getQueryParams().getFirst("d") == null) {
sendInternalServerError(response, "Payload expected.", sockJsSession.getId());
return;
}
}
super.handleRequestInternal(request, response, sockJsSession);
try {
@@ -57,8 +56,9 @@ public class JsonpTransportHandler extends AbstractHttpReceivingTransportHandler
@Override
protected String[] readMessages(ServerHttpRequest request) throws IOException {
if (MediaType.APPLICATION_FORM_URLENCODED.equals(request.getHeaders().getContentType())) {
String d = request.getQueryParams().getFirst("d");
return getObjectMapper().readValue(d, String[].class);
MultiValueMap<String, String> map = this.formConverter.read(null, request);
String d = map.getFirst("d");
return (StringUtils.hasText(d)) ? getObjectMapper().readValue(d, String[].class) : null;
}
else {
return getObjectMapper().readValue(request.getBody(), String[].class);

View File

@@ -22,9 +22,9 @@ import org.springframework.web.socket.sockjs.SockJsConfiguration;
import org.springframework.web.socket.sockjs.SockJsFrame;
public class PollingServerSockJsSession extends AbstractHttpServerSockJsSession {
public class PollingSockJsSession extends AbstractHttpSockJsSession {
public PollingServerSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) {
public PollingSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) {
super(sessionId, config, handler);
}

View File

@@ -26,12 +26,12 @@ import org.springframework.web.socket.sockjs.SockJsFrame;
import org.springframework.web.socket.sockjs.TransportErrorException;
import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat;
public class StreamingServerSockJsSession extends AbstractHttpServerSockJsSession {
public class StreamingSockJsSession extends AbstractHttpSockJsSession {
private int byteCount;
public StreamingServerSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) {
public StreamingSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) {
super(sessionId, config, handler);
}

View File

@@ -23,7 +23,7 @@ import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.sockjs.AbstractServerSockJsSession;
import org.springframework.web.socket.sockjs.AbstractSockJsSession;
import org.springframework.web.socket.sockjs.SockJsConfiguration;
import org.springframework.web.socket.sockjs.SockJsFrame;
@@ -31,10 +31,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
/**
* A WebSocket implementation of {@link AbstractSockJsSession}. Delegates to a
* {@link WebSocketSession}.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class WebSocketServerSockJsSession extends AbstractServerSockJsSession {
public class WebSocketServerSockJsSession extends AbstractSockJsSession {
private WebSocketSession webSocketSession;
@@ -103,5 +106,5 @@ public class WebSocketServerSockJsSession extends AbstractServerSockJsSession {
protected void disconnect(CloseStatus status) throws IOException {
this.webSocketSession.close(status);
}
}
}

View File

@@ -51,9 +51,9 @@ public class XhrPollingTransportHandler extends AbstractHttpSendingTransportHand
}
@Override
public PollingServerSockJsSession createSession(String sessionId, WebSocketHandler handler) {
public PollingSockJsSession createSession(String sessionId, WebSocketHandler handler) {
Assert.notNull(getSockJsConfig(), "This transport requires SockJsConfiguration");
return new PollingServerSockJsSession(sessionId, getSockJsConfig(), handler);
return new PollingSockJsSession(sessionId, getSockJsConfig(), handler);
}
}

View File

@@ -47,10 +47,10 @@ public class XhrStreamingTransportHandler extends AbstractHttpSendingTransportHa
}
@Override
public StreamingServerSockJsSession createSession(String sessionId, WebSocketHandler handler) {
public StreamingSockJsSession createSession(String sessionId, WebSocketHandler handler) {
Assert.notNull(getSockJsConfig(), "This transport requires SockJsConfiguration");
return new StreamingServerSockJsSession(sessionId, getSockJsConfig(), handler) {
return new StreamingSockJsSession(sessionId, getSockJsConfig(), handler) {
@Override
protected void writePrelude() throws IOException {

View File

@@ -38,7 +38,7 @@ public class WebSocketHandlerDecorator implements WebSocketHandler {
}
protected WebSocketHandler getDelegate() {
public WebSocketHandler getDelegate() {
return this.delegate;
}