diff --git a/README-WEBSOCKET.md b/README-WEBSOCKET.md index cd9a7077710..27c1e641b6a 100644 --- a/README-WEBSOCKET.md +++ b/README-WEBSOCKET.md @@ -54,6 +54,17 @@ Run the ant build: A usable Tomcat installation can be found in `output/build` +### Jetty 9 + +Download and use the latest Jetty (currently 9.0.2.v20130417). It does not support JSR-356 yet but that's not an issue, since we're using the Jetty 9 native WebSocket API. + +If using Java-based Servlet configuration instead of web.xml, add the following options to Jetty's start.ini: + + OPTIONS=plus + etc/jetty-plus.xml + OPTIONS=annotations + etc/jetty-annotations.xml + ### Glassfish Glassfish also provides JSR-356 support based on Tyrus (the reference implementation). diff --git a/build.gradle b/build.gradle index 72eebf80591..ea959552c8e 100644 --- a/build.gradle +++ b/build.gradle @@ -532,6 +532,7 @@ project("spring-websocket") { exclude group: "org.eclipse.jetty.orbit", module: "javax.servlet" } optional("org.eclipse.jetty.websocket:websocket-server:9.0.1.v20130408") + optional("org.eclipse.jetty.websocket:websocket-client:9.0.1.v20130408") optional("com.fasterxml.jackson.core:jackson-databind:2.0.1") // required for SockJS support currently diff --git a/spring-websocket/src/main/java/org/springframework/websocket/client/AbstractWebSocketConnectionManager.java b/spring-websocket/src/main/java/org/springframework/websocket/client/AbstractWebSocketConnectionManager.java index ab696d9c8f4..a67a9e7350e 100644 --- a/spring-websocket/src/main/java/org/springframework/websocket/client/AbstractWebSocketConnectionManager.java +++ b/spring-websocket/src/main/java/org/springframework/websocket/client/AbstractWebSocketConnectionManager.java @@ -40,6 +40,8 @@ public abstract class AbstractWebSocketConnectionManager implements SmartLifecyc private boolean autoStartup = false; + private boolean isRunning = false; + private int phase = Integer.MAX_VALUE; private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("EndpointConnectionManager-"); @@ -92,6 +94,15 @@ public abstract class AbstractWebSocketConnectionManager implements SmartLifecyc return this.uri; } + /** + * Return whether this ConnectionManager has been started. + */ + public boolean isRunning() { + synchronized (this.lifecycleMonitor) { + return this.isRunning; + } + } + /** * Connect to the configured {@link #setDefaultUri(URI) default URI}. If already * connected, the method has no impact. @@ -99,61 +110,69 @@ public abstract class AbstractWebSocketConnectionManager implements SmartLifecyc public final void start() { synchronized (this.lifecycleMonitor) { if (!isRunning()) { - this.taskExecutor.execute(new Runnable() { - @Override - public void run() { - synchronized (lifecycleMonitor) { - try { - logger.info("Connecting to WebSocket at " + uri); - openConnection(); - logger.info("Successfully connected"); - } - catch (Throwable ex) { - logger.error("Failed to connect", ex); - } - } - } - }); + startInternal(); } } } + protected void startInternal() { + if (logger.isDebugEnabled()) { + logger.debug("Starting " + this.getClass().getSimpleName()); + } + this.isRunning = true; + this.taskExecutor.execute(new Runnable() { + @Override + public void run() { + synchronized (lifecycleMonitor) { + try { + logger.info("Connecting to WebSocket at " + uri); + openConnection(); + logger.info("Successfully connected"); + } + catch (Throwable ex) { + logger.error("Failed to connect", ex); + } + } + } + }); + } + protected abstract void openConnection() throws Exception; - /** - * Closes the configured message WebSocket connection. - */ public final void stop() { synchronized (this.lifecycleMonitor) { if (isRunning()) { - try { - closeConnection(); - } - catch (Throwable e) { - logger.error("Failed to stop WebSocket connection", e); - } + stopInternal(); } } } + 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 abstract boolean isConnected(); + protected abstract void closeConnection() throws Exception; - public void stop(Runnable callback) { + public final void stop(Runnable callback) { synchronized (this.lifecycleMonitor) { this.stop(); callback.run(); } } - /** - * Return whether the configured message endpoint is currently active. - */ - public boolean isRunning() { - synchronized (this.lifecycleMonitor) { - return isConnected(); - } - } - - protected abstract boolean isConnected(); - } diff --git a/spring-websocket/src/main/java/org/springframework/websocket/client/WebSocketClient.java b/spring-websocket/src/main/java/org/springframework/websocket/client/WebSocketClient.java index 5aaa0380f9a..6348f34dc95 100644 --- a/spring-websocket/src/main/java/org/springframework/websocket/client/WebSocketClient.java +++ b/spring-websocket/src/main/java/org/springframework/websocket/client/WebSocketClient.java @@ -22,10 +22,10 @@ import org.springframework.websocket.WebSocketHandler; import org.springframework.websocket.WebSocketSession; /** - * Contract for starting a WebSocket handshake request. - * - *

To automatically start a WebSocket connection when the application starts, see - * {@link WebSocketConnectionManager}. + * Contract for programmatically starting a WebSocket handshake request. For most cases it + * would be more convenient to use the declarative style + * {@link WebSocketConnectionManager} that starts a WebSocket connection to a + * pre-configured URI when the application starts. * * @author Rossen Stoyanchev * @since 4.0 diff --git a/spring-websocket/src/main/java/org/springframework/websocket/client/WebSocketConnectionManager.java b/spring-websocket/src/main/java/org/springframework/websocket/client/WebSocketConnectionManager.java index ea49fc0c6b5..09c0d06cf2a 100644 --- a/spring-websocket/src/main/java/org/springframework/websocket/client/WebSocketConnectionManager.java +++ b/spring-websocket/src/main/java/org/springframework/websocket/client/WebSocketConnectionManager.java @@ -19,6 +19,7 @@ package org.springframework.websocket.client; import java.util.ArrayList; import java.util.List; +import org.springframework.context.SmartLifecycle; import org.springframework.http.HttpHeaders; import org.springframework.util.CollectionUtils; import org.springframework.websocket.WebSocketHandler; @@ -40,13 +41,16 @@ public class WebSocketConnectionManager extends AbstractWebSocketConnectionManag private final List subProtocols = new ArrayList(); + private final boolean syncClientLifecycle; - public WebSocketConnectionManager(WebSocketClient webSocketClient, + + public WebSocketConnectionManager(WebSocketClient client, WebSocketHandler webSocketHandler, String uriTemplate, Object... uriVariables) { super(uriTemplate, uriVariables); - this.client = webSocketClient; + this.client = client; this.webSocketHandler = decorateWebSocketHandler(webSocketHandler); + this.syncClientLifecycle = ((client instanceof SmartLifecycle) && !((SmartLifecycle) client).isRunning()); } /** @@ -71,6 +75,22 @@ public class WebSocketConnectionManager extends AbstractWebSocketConnectionManag return this.subProtocols; } + @Override + public void startInternal() { + if (this.syncClientLifecycle) { + ((SmartLifecycle) this.client).start(); + } + super.startInternal(); + } + + @Override + public void stopInternal() { + if (this.syncClientLifecycle) { + ((SmartLifecycle) client).stop(); + } + super.stopInternal(); + } + @Override protected void openConnection() throws Exception { HttpHeaders headers = new HttpHeaders(); diff --git a/spring-websocket/src/main/java/org/springframework/websocket/client/jetty/JettyWebSocketClient.java b/spring-websocket/src/main/java/org/springframework/websocket/client/jetty/JettyWebSocketClient.java new file mode 100644 index 00000000000..210e0dfc131 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/websocket/client/jetty/JettyWebSocketClient.java @@ -0,0 +1,150 @@ +/* + * 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.websocket.client.jetty; + +import java.net.URI; +import java.util.concurrent.Future; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.eclipse.jetty.websocket.api.Session; +import org.springframework.context.SmartLifecycle; +import org.springframework.http.HttpHeaders; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.websocket.WebSocketHandler; +import org.springframework.websocket.WebSocketSession; +import org.springframework.websocket.adapter.JettyWebSocketListenerAdapter; +import org.springframework.websocket.adapter.JettyWebSocketSessionAdapter; +import org.springframework.websocket.client.WebSocketClient; +import org.springframework.websocket.client.WebSocketConnectFailureException; + + +/** + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class JettyWebSocketClient implements WebSocketClient, SmartLifecycle { + + private static final Log logger = LogFactory.getLog(JettyWebSocketClient.class); + + private final org.eclipse.jetty.websocket.client.WebSocketClient client; + + private boolean autoStartup = true; + + private int phase = Integer.MAX_VALUE; + + private final Object lifecycleMonitor = new Object(); + + + public JettyWebSocketClient() { + this.client = new org.eclipse.jetty.websocket.client.WebSocketClient(); + } + + + // TODO: configure Jetty WebSocketClient properties + + public void setAutoStartup(boolean autoStartup) { + this.autoStartup = autoStartup; + } + + @Override + public boolean isAutoStartup() { + return this.autoStartup; + } + + public void setPhase(int phase) { + this.phase = phase; + } + + @Override + public int getPhase() { + return this.phase; + } + + @Override + public boolean isRunning() { + synchronized (this.lifecycleMonitor) { + return this.client.isStarted(); + } + } + + @Override + public void start() { + synchronized (this.lifecycleMonitor) { + if (!isRunning()) { + try { + if (logger.isDebugEnabled()) { + logger.debug("Starting Jetty WebSocketClient"); + } + this.client.start(); + } + catch (Exception e) { + throw new IllegalStateException("Failed to start Jetty client", e); + } + } + } + } + + @Override + public void stop() { + synchronized (this.lifecycleMonitor) { + if (isRunning()) { + try { + if (logger.isDebugEnabled()) { + logger.debug("Stopping Jetty WebSocketClient"); + } + this.client.stop(); + } + catch (Exception e) { + logger.error("Error stopping Jetty WebSocketClient", e); + } + } + } + } + + @Override + public void stop(Runnable callback) { + this.stop(); + callback.run(); + } + + @Override + public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, String uriTemplate, Object... uriVariables) + throws WebSocketConnectFailureException { + + URI uri = UriComponentsBuilder.fromUriString(uriTemplate).buildAndExpand(uriVariables).encode().toUri(); + return doHandshake(webSocketHandler, null, uri); + } + + @Override + public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, HttpHeaders headers, URI uri) + throws WebSocketConnectFailureException { + + JettyWebSocketListenerAdapter listener = new JettyWebSocketListenerAdapter(webSocketHandler); + + try { + // block for now + Future future = this.client.connect(listener, uri); + Session session = future.get(); + return new JettyWebSocketSessionAdapter(session); + } + catch (Exception e) { + throw new WebSocketConnectFailureException("Failed to connect to " + uri, e); + } + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/websocket/server/support/JettyRequestUpgradeStrategy.java b/spring-websocket/src/main/java/org/springframework/websocket/server/support/JettyRequestUpgradeStrategy.java index 9819a639919..ef5674c6bd0 100644 --- a/spring-websocket/src/main/java/org/springframework/websocket/server/support/JettyRequestUpgradeStrategy.java +++ b/spring-websocket/src/main/java/org/springframework/websocket/server/support/JettyRequestUpgradeStrategy.java @@ -100,10 +100,14 @@ public class JettyRequestUpgradeStrategy implements RequestUpgradeStrategy { private void upgrade(HttpServletRequest request, HttpServletResponse response, String selectedProtocol, final WebSocketHandler webSocketHandler) throws IOException { - Assert.state(this.factory.isUpgradeRequest(request, response), "Not a suitable WebSocket upgrade request"); - Assert.state(this.factory.acceptWebSocket(request, response), "Unable to accept WebSocket"); + Assert.state(this.factory.isUpgradeRequest(request, response), "Expected websocket upgrade request"); request.setAttribute(HANDLER_PROVIDER_ATTR_NAME, webSocketHandler); + + if (!this.factory.acceptWebSocket(request, response)) { + // should never happen + throw new IllegalStateException("WebSocket request not accepted by Jetty"); + } } } diff --git a/spring-websocket/src/main/java/org/springframework/websocket/support/PerConnectionWebSocketHandlerProxy.java b/spring-websocket/src/main/java/org/springframework/websocket/support/PerConnectionWebSocketHandlerProxy.java index 7d39bd0caf7..350c23bc052 100644 --- a/spring-websocket/src/main/java/org/springframework/websocket/support/PerConnectionWebSocketHandlerProxy.java +++ b/spring-websocket/src/main/java/org/springframework/websocket/support/PerConnectionWebSocketHandlerProxy.java @@ -125,4 +125,9 @@ public class PerConnectionWebSocketHandlerProxy implements WebSocketHandler, Bea } } + @Override + public String toString() { + return "PerConnectionWebSocketHandlerProxy [handlerType=" + this.provider.getHandlerType() + "]"; + } + } diff --git a/spring-websocket/src/main/java/org/springframework/websocket/support/WebSocketHandlerDecorator.java b/spring-websocket/src/main/java/org/springframework/websocket/support/WebSocketHandlerDecorator.java index cff8b7202d1..a94fbb8b518 100644 --- a/spring-websocket/src/main/java/org/springframework/websocket/support/WebSocketHandlerDecorator.java +++ b/spring-websocket/src/main/java/org/springframework/websocket/support/WebSocketHandlerDecorator.java @@ -67,4 +67,9 @@ public class WebSocketHandlerDecorator implements WebSocketHandler { return this.delegate.isStreaming(); } + @Override + public String toString() { + return getClass().getSimpleName() + " [delegate=" + this.delegate + "]"; + } + }