diff --git a/spring-websocket/src/main/java/org/springframework/websocket/HandlerProvider.java b/spring-websocket/src/main/java/org/springframework/websocket/HandlerProvider.java index f8d2d90bd2..135c674d8b 100644 --- a/spring-websocket/src/main/java/org/springframework/websocket/HandlerProvider.java +++ b/spring-websocket/src/main/java/org/springframework/websocket/HandlerProvider.java @@ -17,6 +17,7 @@ package org.springframework.websocket; import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -32,14 +33,14 @@ import org.springframework.util.ClassUtils; */ public class HandlerProvider implements BeanFactoryAware { + private Log logger = LogFactory.getLog(this.getClass()); + private final T handlerBean; private final Class handlerClass; private AutowireCapableBeanFactory beanFactory; - private Log logger; - public HandlerProvider(T handlerBean) { Assert.notNull(handlerBean, "handlerBean is required"); diff --git a/spring-websocket/src/main/java/org/springframework/websocket/WebSocketHandshakeRequest.java b/spring-websocket/src/main/java/org/springframework/websocket/WebSocketHandshakeRequest.java new file mode 100644 index 0000000000..762cc2fe8b --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/websocket/WebSocketHandshakeRequest.java @@ -0,0 +1,48 @@ +/* + * 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; + +import java.net.URI; + +import org.springframework.http.HttpHeaders; + + +/** + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class WebSocketHandshakeRequest { + + private final URI uri; + + private final HttpHeaders headers; + + + public WebSocketHandshakeRequest(HttpHeaders headers, URI uri) { + this.headers = HttpHeaders.readOnlyHttpHeaders(headers); + this.uri = uri; + } + + public URI getUri() { + return this.uri; + } + + public HttpHeaders getHeaders() { + return this.headers; + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/websocket/client/AbstractEndpointConnectionManager.java b/spring-websocket/src/main/java/org/springframework/websocket/client/AbstractWebSocketConnectionManager.java similarity index 65% rename from spring-websocket/src/main/java/org/springframework/websocket/client/AbstractEndpointConnectionManager.java rename to spring-websocket/src/main/java/org/springframework/websocket/client/AbstractWebSocketConnectionManager.java index 9a24e04af5..7839c7c2a3 100644 --- a/spring-websocket/src/main/java/org/springframework/websocket/client/AbstractEndpointConnectionManager.java +++ b/spring-websocket/src/main/java/org/springframework/websocket/client/AbstractWebSocketConnectionManager.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -13,17 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.websocket.client; -import java.io.IOException; import java.net.URI; -import javax.websocket.ContainerProvider; -import javax.websocket.DeploymentException; -import javax.websocket.Session; -import javax.websocket.WebSocketContainer; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.SmartLifecycle; @@ -37,7 +30,7 @@ import org.springframework.web.util.UriComponentsBuilder; * @author Rossen Stoyanchev * @since 4.0 */ -public abstract class AbstractEndpointConnectionManager implements SmartLifecycle { +public abstract class AbstractWebSocketConnectionManager implements SmartLifecycle { protected final Log logger = LogFactory.getLog(getClass()); @@ -47,35 +40,15 @@ public abstract class AbstractEndpointConnectionManager implements SmartLifecycl private int phase = Integer.MAX_VALUE; - private final WebSocketContainer webSocketContainer = ContainerProvider.getWebSocketContainer(); - - private Session session; - private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("EndpointConnectionManager-"); private final Object lifecycleMonitor = new Object(); - public AbstractEndpointConnectionManager(String uriTemplate, Object... uriVariables) { + public AbstractWebSocketConnectionManager(String uriTemplate, Object... uriVariables) { this.uri = UriComponentsBuilder.fromUriString(uriTemplate).buildAndExpand(uriVariables).encode().toUri(); } - public void setAsyncSendTimeout(long timeoutInMillis) { - this.webSocketContainer.setAsyncSendTimeout(timeoutInMillis); - } - - public void setMaxSessionIdleTimeout(long timeoutInMillis) { - this.webSocketContainer.setDefaultMaxSessionIdleTimeout(timeoutInMillis); - } - - public void setMaxTextMessageBufferSize(int bufferSize) { - this.webSocketContainer.setDefaultMaxTextMessageBufferSize(bufferSize); - } - - public void setMaxBinaryMessageBufferSize(Integer bufferSize) { - this.webSocketContainer.setDefaultMaxBinaryMessageBufferSize(bufferSize); - } - /** * Set whether to auto-connect to the remote endpoint after this connection manager * has been initialized and the Spring context has been refreshed. @@ -117,14 +90,11 @@ public abstract class AbstractEndpointConnectionManager implements SmartLifecycl return this.uri; } - protected WebSocketContainer getWebSocketContainer() { - return this.webSocketContainer; - } - /** - * Auto-connects to the configured {@link #setDefaultUri(URI) default URI}. + * Connect to the configured {@link #setDefaultUri(URI) default URI}. If already + * connected, the method has no impact. */ - public void start() { + public final void start() { synchronized (this.lifecycleMonitor) { if (!isRunning()) { this.taskExecutor.execute(new Runnable() { @@ -132,12 +102,12 @@ public abstract class AbstractEndpointConnectionManager implements SmartLifecycl public void run() { synchronized (lifecycleMonitor) { try { - logger.info("Connecting to endpoint at URI " + uri); - session = connect(); + logger.info("Connecting to WebSocket at " + uri); + openConnection(); logger.info("Successfully connected"); } catch (Throwable ex) { - logger.error("Failed to connect to endpoint at " + uri, ex); + logger.error("Failed to connect", ex); } } } @@ -146,25 +116,26 @@ public abstract class AbstractEndpointConnectionManager implements SmartLifecycl } } - protected abstract Session connect() throws DeploymentException, IOException; + protected abstract void openConnection() throws Exception; /** - * Deactivates the configured message endpoint. + * Closes the configured message WebSocket connection. */ - public void stop() { + public final void stop() { synchronized (this.lifecycleMonitor) { if (isRunning()) { try { - this.session.close(); + closeConnection(); } - catch (IOException e) { - // ignore + catch (Throwable e) { + logger.error("Failed to stop WebSocket connection", e); } } - this.session = null; } } + protected abstract void closeConnection() throws Exception; + public void stop(Runnable callback) { synchronized (this.lifecycleMonitor) { this.stop(); @@ -177,12 +148,10 @@ public abstract class AbstractEndpointConnectionManager implements SmartLifecycl */ public boolean isRunning() { synchronized (this.lifecycleMonitor) { - if ((this.session != null) && this.session.isOpen()) { - return true; - } - this.session = null; - return false; + 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 new file mode 100644 index 0000000000..ae82de0dc0 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/websocket/client/WebSocketClient.java @@ -0,0 +1,48 @@ +/* + * 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; + +import java.net.URI; + +import org.springframework.http.HttpHeaders; +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}. + * + * @author Rossen Stoyanchev + * @since 4.0 + * + * @see WebSocketConnectionManager + */ +public interface WebSocketClient { + + + WebSocketSession doHandshake(WebSocketHandler handler, String uriTemplate, Object... uriVariables) + throws WebSocketConnectFailureException; + + WebSocketSession doHandshake(WebSocketHandler handler, URI uri) + throws WebSocketConnectFailureException; + + WebSocketSession doHandshake(WebSocketHandler handler, HttpHeaders headers, URI uri) + throws WebSocketConnectFailureException; + +} diff --git a/spring-websocket/src/main/java/org/springframework/websocket/client/WebSocketConnectFailureException.java b/spring-websocket/src/main/java/org/springframework/websocket/client/WebSocketConnectFailureException.java new file mode 100644 index 0000000000..553423cb92 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/websocket/client/WebSocketConnectFailureException.java @@ -0,0 +1,38 @@ +/* + * 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; + +import org.springframework.core.NestedRuntimeException; + + +/** + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +@SuppressWarnings("serial") +public class WebSocketConnectFailureException extends NestedRuntimeException { + + + public WebSocketConnectFailureException(String msg, Throwable cause) { + super(msg, cause); + } + + public WebSocketConnectFailureException(String msg) { + super(msg); + } + +} 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 new file mode 100644 index 0000000000..cb16e5be44 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/websocket/client/WebSocketConnectionManager.java @@ -0,0 +1,76 @@ +/* + * 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; + +import java.util.List; + +import org.springframework.http.HttpHeaders; +import org.springframework.websocket.HandlerProvider; +import org.springframework.websocket.WebSocketHandler; +import org.springframework.websocket.WebSocketSession; + + +/** + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class WebSocketConnectionManager extends AbstractWebSocketConnectionManager { + + private final WebSocketClient client; + + private final HandlerProvider webSocketHandlerProvider; + + private WebSocketSession webSocketSession; + + private List subProtocols; + + + public WebSocketConnectionManager(WebSocketClient webSocketClient, + WebSocketHandler webSocketHandler, String uriTemplate, Object... uriVariables) { + + super(uriTemplate, uriVariables); + this.webSocketHandlerProvider = new HandlerProvider(webSocketHandler); + this.client = webSocketClient; + } + + public void setSubProtocols(List subProtocols) { + this.subProtocols = subProtocols; + } + + public List getSubProtocols() { + return this.subProtocols; + } + + @Override + protected void openConnection() throws Exception { + WebSocketHandler webSocketHandler = this.webSocketHandlerProvider.getHandler(); + HttpHeaders headers = new HttpHeaders(); + headers.setSecWebSocketProtocol(this.subProtocols); + this.webSocketSession = this.client.doHandshake(webSocketHandler, headers, getUri()); + } + + @Override + protected void closeConnection() throws Exception { + this.webSocketSession.close(); + } + + @Override + protected boolean isConnected() { + return ((this.webSocketSession != null) && (this.webSocketSession.isOpen())); + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/websocket/client/AnnotatedEndpointConnectionManager.java b/spring-websocket/src/main/java/org/springframework/websocket/client/endpoint/AnnotatedEndpointConnectionManager.java similarity index 73% rename from spring-websocket/src/main/java/org/springframework/websocket/client/AnnotatedEndpointConnectionManager.java rename to spring-websocket/src/main/java/org/springframework/websocket/client/endpoint/AnnotatedEndpointConnectionManager.java index a1005e3507..75433591ca 100644 --- a/spring-websocket/src/main/java/org/springframework/websocket/client/AnnotatedEndpointConnectionManager.java +++ b/spring-websocket/src/main/java/org/springframework/websocket/client/endpoint/AnnotatedEndpointConnectionManager.java @@ -14,15 +14,10 @@ * limitations under the License. */ -package org.springframework.websocket.client; +package org.springframework.websocket.client.endpoint; -import java.io.IOException; - -import javax.websocket.DeploymentException; import javax.websocket.Session; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -34,24 +29,20 @@ import org.springframework.websocket.HandlerProvider; * @author Rossen Stoyanchev * @since 4.0 */ -public class AnnotatedEndpointConnectionManager extends AbstractEndpointConnectionManager +public class AnnotatedEndpointConnectionManager extends EndpointConnectionManagerSupport implements BeanFactoryAware { - private static Log logger = LogFactory.getLog(AnnotatedEndpointConnectionManager.class); - private final HandlerProvider endpointProvider; public AnnotatedEndpointConnectionManager(Class endpointClass, String uriTemplate, Object... uriVariables) { super(uriTemplate, uriVariables); this.endpointProvider = new HandlerProvider(endpointClass); - this.endpointProvider.setLogger(logger); } public AnnotatedEndpointConnectionManager(Object endpointBean, String uriTemplate, Object... uriVariables) { super(uriTemplate, uriVariables); this.endpointProvider = new HandlerProvider(endpointBean); - this.endpointProvider.setLogger(logger); } @Override @@ -59,10 +50,12 @@ public class AnnotatedEndpointConnectionManager extends AbstractEndpointConnecti this.endpointProvider.setBeanFactory(beanFactory); } + @Override - protected Session connect() throws DeploymentException, IOException { + protected void openConnection() throws Exception { Object endpoint = this.endpointProvider.getHandler(); - return getWebSocketContainer().connectToServer(endpoint, getUri()); + Session session = getWebSocketContainer().connectToServer(endpoint, getUri()); + updateSession(session); } } diff --git a/spring-websocket/src/main/java/org/springframework/websocket/client/EndpointConnectionManager.java b/spring-websocket/src/main/java/org/springframework/websocket/client/endpoint/EndpointConnectionManager.java similarity index 77% rename from spring-websocket/src/main/java/org/springframework/websocket/client/EndpointConnectionManager.java rename to spring-websocket/src/main/java/org/springframework/websocket/client/endpoint/EndpointConnectionManager.java index 9ca8653bbf..b7006ab2fc 100644 --- a/spring-websocket/src/main/java/org/springframework/websocket/client/EndpointConnectionManager.java +++ b/spring-websocket/src/main/java/org/springframework/websocket/client/endpoint/EndpointConnectionManager.java @@ -14,23 +14,19 @@ * limitations under the License. */ -package org.springframework.websocket.client; +package org.springframework.websocket.client.endpoint; -import java.io.IOException; import java.util.Arrays; import java.util.List; import javax.websocket.ClientEndpointConfig; import javax.websocket.ClientEndpointConfig.Configurator; import javax.websocket.Decoder; -import javax.websocket.DeploymentException; import javax.websocket.Encoder; import javax.websocket.Endpoint; import javax.websocket.Extension; import javax.websocket.Session; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -43,27 +39,23 @@ import org.springframework.websocket.HandlerProvider; * @author Rossen Stoyanchev * @since 4.0 */ -public class EndpointConnectionManager extends AbstractEndpointConnectionManager implements BeanFactoryAware { - - private static Log logger = LogFactory.getLog(EndpointConnectionManager.class); +public class EndpointConnectionManager extends EndpointConnectionManagerSupport implements BeanFactoryAware { private final ClientEndpointConfig.Builder configBuilder = ClientEndpointConfig.Builder.create(); private final HandlerProvider endpointProvider; - public EndpointConnectionManager(Class endpointClass, String uriTemplate, Object... uriVariables) { - super(uriTemplate, uriVariables); - Assert.notNull(endpointClass, "endpointClass is required"); - this.endpointProvider = new HandlerProvider(endpointClass); - this.endpointProvider.setLogger(logger); - } - public EndpointConnectionManager(Endpoint endpointBean, String uriTemplate, Object... uriVariables) { super(uriTemplate, uriVariables); Assert.notNull(endpointBean, "endpointBean is required"); this.endpointProvider = new HandlerProvider(endpointBean); - this.endpointProvider.setLogger(logger); + } + + public EndpointConnectionManager(Class endpointClass, String uriTemplate, Object... uriVars) { + super(uriTemplate, uriVars); + Assert.notNull(endpointClass, "endpointClass is required"); + this.endpointProvider = new HandlerProvider(endpointClass); } public void setSubProtocols(String... subprotocols) { @@ -92,10 +84,11 @@ public class EndpointConnectionManager extends AbstractEndpointConnectionManager } @Override - protected Session connect() throws DeploymentException, IOException { - Endpoint typedEndpoint = this.endpointProvider.getHandler(); + protected void openConnection() throws Exception { + Endpoint endpoint = this.endpointProvider.getHandler(); ClientEndpointConfig endpointConfig = this.configBuilder.build(); - return getWebSocketContainer().connectToServer(typedEndpoint, endpointConfig, getUri()); + Session session = getWebSocketContainer().connectToServer(endpoint, endpointConfig, getUri()); + updateSession(session); } } diff --git a/spring-websocket/src/main/java/org/springframework/websocket/client/endpoint/EndpointConnectionManagerSupport.java b/spring-websocket/src/main/java/org/springframework/websocket/client/endpoint/EndpointConnectionManagerSupport.java new file mode 100644 index 0000000000..ba4fdbcdfb --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/websocket/client/endpoint/EndpointConnectionManagerSupport.java @@ -0,0 +1,71 @@ +/* + * 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.endpoint; + +import javax.websocket.ContainerProvider; +import javax.websocket.Session; +import javax.websocket.WebSocketContainer; + +import org.springframework.websocket.client.AbstractWebSocketConnectionManager; + + +/** + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public abstract class EndpointConnectionManagerSupport extends AbstractWebSocketConnectionManager { + + private WebSocketContainer webSocketContainer = ContainerProvider.getWebSocketContainer(); + + private Session session; + + + public EndpointConnectionManagerSupport(String uriTemplate, Object... uriVariables) { + super(uriTemplate, uriVariables); + } + + public void setWebSocketContainer(WebSocketContainer webSocketContainer) { + this.webSocketContainer = webSocketContainer; + } + + public WebSocketContainer getWebSocketContainer() { + return this.webSocketContainer; + } + + protected void updateSession(Session session) { + this.session = session; + } + + @Override + protected void closeConnection() throws Exception { + try { + if (isConnected()) { + this.session.close(); + } + } + finally { + this.session = null; + } + } + + @Override + protected boolean isConnected() { + return ((this.session != null) && this.session.isOpen()); + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/websocket/client/endpoint/StandardWebSocketClient.java b/spring-websocket/src/main/java/org/springframework/websocket/client/endpoint/StandardWebSocketClient.java new file mode 100644 index 0000000000..522bfcdf1e --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/websocket/client/endpoint/StandardWebSocketClient.java @@ -0,0 +1,105 @@ +/* + * 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.endpoint; + +import java.net.URI; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.websocket.ClientEndpointConfig; +import javax.websocket.ClientEndpointConfig.Configurator; +import javax.websocket.ContainerProvider; +import javax.websocket.Endpoint; +import javax.websocket.Session; +import javax.websocket.WebSocketContainer; + +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.client.WebSocketClient; +import org.springframework.websocket.client.WebSocketConnectFailureException; +import org.springframework.websocket.endpoint.StandardWebSocketSession; +import org.springframework.websocket.endpoint.WebSocketHandlerEndpoint; + + +/** + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class StandardWebSocketClient implements WebSocketClient { + + private static final Set EXCLUDED_HEADERS = new HashSet( + Arrays.asList("Sec-WebSocket-Accept", "Sec-WebSocket-Extensions", "Sec-WebSocket-Key", + "Sec-WebSocket-Protocol", "Sec-WebSocket-Version")); + + private WebSocketContainer webSocketContainer = ContainerProvider.getWebSocketContainer(); + + + public void setWebSocketContainer(WebSocketContainer container) { + this.webSocketContainer = container; + } + + public WebSocketSession doHandshake(WebSocketHandler handler, String uriTemplate, + Object... uriVariables) throws WebSocketConnectFailureException { + + URI uri = UriComponentsBuilder.fromUriString(uriTemplate).buildAndExpand(uriVariables).encode().toUri(); + return doHandshake(handler, uri); + } + + @Override + public WebSocketSession doHandshake(WebSocketHandler handler, URI uri) throws WebSocketConnectFailureException { + return doHandshake(handler, null, uri); + } + + @Override + public WebSocketSession doHandshake(WebSocketHandler handler, final HttpHeaders httpHeaders, URI uri) + throws WebSocketConnectFailureException { + + Endpoint endpoint = new WebSocketHandlerEndpoint(handler); + + ClientEndpointConfig.Builder configBuidler = ClientEndpointConfig.Builder.create(); + if (httpHeaders != null) { + List protocols = httpHeaders.getSecWebSocketProtocol(); + if (!protocols.isEmpty()) { + configBuidler.preferredSubprotocols(protocols); + } + configBuidler.configurator(new Configurator() { + @Override + public void beforeRequest(Map> headers) { + for (String headerName : httpHeaders.keySet()) { + if (!EXCLUDED_HEADERS.contains(headerName)) { + headers.put(headerName, httpHeaders.get(headerName)); + } + } + } + }); + } + + try { + Session session = this.webSocketContainer.connectToServer(endpoint, configBuidler.build(), uri); + return new StandardWebSocketSession(session); + } + catch (Exception e) { + throw new WebSocketConnectFailureException("Failed to connect to " + uri, e); + } + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/websocket/client/endpoint/WebSocketContainerFactoryBean.java b/spring-websocket/src/main/java/org/springframework/websocket/client/endpoint/WebSocketContainerFactoryBean.java new file mode 100644 index 0000000000..a6d9d53ebf --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/websocket/client/endpoint/WebSocketContainerFactoryBean.java @@ -0,0 +1,84 @@ +/* + * 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.endpoint; + +import javax.websocket.ContainerProvider; +import javax.websocket.WebSocketContainer; + +import org.springframework.beans.factory.FactoryBean; + + +/** + * A FactoryBean for creating and configuring a {@link javax.websocket.WebSocketContainer} + * through Spring XML configuration. In Java configuration, ignore this class and use + * {@code ContainerProvider.getWebSocketContainer()} instead. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class WebSocketContainerFactoryBean implements FactoryBean { + + private final WebSocketContainer webSocketContainer = ContainerProvider.getWebSocketContainer(); + + + public void setAsyncSendTimeout(long timeoutInMillis) { + this.webSocketContainer.setAsyncSendTimeout(timeoutInMillis); + } + + public long getAsyncSendTimeout() { + return this.webSocketContainer.getDefaultAsyncSendTimeout(); + } + + public void setMaxSessionIdleTimeout(long timeoutInMillis) { + this.webSocketContainer.setDefaultMaxSessionIdleTimeout(timeoutInMillis); + } + + public long getMaxSessionIdleTimeout() { + return this.webSocketContainer.getDefaultMaxSessionIdleTimeout(); + } + + public void setMaxTextMessageBufferSize(int bufferSize) { + this.webSocketContainer.setDefaultMaxTextMessageBufferSize(bufferSize); + } + + public int getMaxTextMessageBufferSize() { + return this.webSocketContainer.getDefaultMaxTextMessageBufferSize(); + } + + public void setMaxBinaryMessageBufferSize(int bufferSize) { + this.webSocketContainer.setDefaultMaxBinaryMessageBufferSize(bufferSize); + } + + public int getMaxBinaryMessageBufferSize() { + return this.webSocketContainer.getDefaultMaxBinaryMessageBufferSize(); + } + + @Override + public WebSocketContainer getObject() throws Exception { + return this.webSocketContainer; + } + + @Override + public Class getObjectType() { + return WebSocketContainer.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/websocket/client/endpoint/package-info.java b/spring-websocket/src/main/java/org/springframework/websocket/client/endpoint/package-info.java new file mode 100644 index 0000000000..777e2ae7da --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/websocket/client/endpoint/package-info.java @@ -0,0 +1,8 @@ +/** + * Client-side classes for use with standard Java WebSocket endpoints including + * {@link org.springframework.websocket.client.endpoint.EndpointConnectionManager} and + * {@link org.springframework.websocket.client.endpoint.AnnotatedEndpointConnectionManager} + * for connecting to server endpoints using type-based or annotated endpoints respectively. + */ +package org.springframework.websocket.client.endpoint; + diff --git a/spring-websocket/src/main/java/org/springframework/websocket/client/package-info.java b/spring-websocket/src/main/java/org/springframework/websocket/client/package-info.java index 28e7e9eb86..817c86c58d 100644 --- a/spring-websocket/src/main/java/org/springframework/websocket/client/package-info.java +++ b/spring-websocket/src/main/java/org/springframework/websocket/client/package-info.java @@ -1,6 +1,5 @@ - /** - * Client-side support for WebSocket applications. + * Server-side abstractions for WebSocket applications. * */ package org.springframework.websocket.client; diff --git a/spring-websocket/src/main/java/org/springframework/websocket/endpoint/WebSocketHandlerEndpoint.java b/spring-websocket/src/main/java/org/springframework/websocket/endpoint/WebSocketHandlerEndpoint.java index 73840558ac..7593adbfd0 100644 --- a/spring-websocket/src/main/java/org/springframework/websocket/endpoint/WebSocketHandlerEndpoint.java +++ b/spring-websocket/src/main/java/org/springframework/websocket/endpoint/WebSocketHandlerEndpoint.java @@ -54,8 +54,7 @@ public class WebSocketHandlerEndpoint extends Endpoint { @Override public void onOpen(javax.websocket.Session session, EndpointConfig config) { if (logger.isDebugEnabled()) { - logger.debug("Client connected, WebSocket session id=" + session.getId() - + ", path=" + session.getRequestURI().getPath()); + logger.debug("Client connected, WebSocket session id=" + session.getId() + ", uri=" + session.getRequestURI()); } try { WebSocketSession webSocketSession = new StandardWebSocketSession(session); diff --git a/spring-websocket/src/main/java/org/springframework/websocket/server/endpoint/EndpointExporter.java b/spring-websocket/src/main/java/org/springframework/websocket/server/endpoint/EndpointExporter.java index 76194c4ecd..1d09241846 100644 --- a/spring-websocket/src/main/java/org/springframework/websocket/server/endpoint/EndpointExporter.java +++ b/spring-websocket/src/main/java/org/springframework/websocket/server/endpoint/EndpointExporter.java @@ -15,6 +15,10 @@ */ package org.springframework.websocket.server.endpoint; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Map; import javax.websocket.DeploymentException; @@ -25,13 +29,13 @@ import javax.websocket.server.ServerEndpointConfig; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; /** * BeanPostProcessor that detects beans of type @@ -43,114 +47,79 @@ import org.springframework.util.ObjectUtils; * @author Rossen Stoyanchev * @since 4.0 */ -public abstract class EndpointExporter implements InitializingBean, BeanPostProcessor, BeanFactoryAware { +public class EndpointExporter implements InitializingBean, BeanPostProcessor, ApplicationContextAware { + + private static final boolean isServletApiPresent = + ClassUtils.isPresent("javax.servlet.ServletContext", EndpointExporter.class.getClassLoader()); private static Log logger = LogFactory.getLog(EndpointExporter.class); - private Class[] annotatedEndpointClasses; + private final List> annotatedEndpointClasses = new ArrayList>(); - private Long maxSessionIdleTimeout; + private final List> annotatedEndpointBeanTypes = new ArrayList>(); - private Integer maxTextMessageBufferSize; - - private Integer maxBinaryMessageBufferSize; + private ApplicationContext applicationContext; + private ServerContainer serverContainer; /** * TODO * @param annotatedEndpointClasses */ public void setAnnotatedEndpointClasses(Class... annotatedEndpointClasses) { - this.annotatedEndpointClasses = annotatedEndpointClasses; - } - - /** - * If this property set it is in turn used to configure - * {@link ServerContainer#setDefaultMaxSessionIdleTimeout(long)}. - */ - public void setMaxSessionIdleTimeout(long maxSessionIdleTimeout) { - this.maxSessionIdleTimeout = maxSessionIdleTimeout; - } - - public Long getMaxSessionIdleTimeout() { - return this.maxSessionIdleTimeout; - } - - /** - * If this property set it is in turn used to configure - * {@link ServerContainer#setDefaultMaxTextMessageBufferSize(int)} - */ - public void setMaxTextMessageBufferSize(int maxTextMessageBufferSize) { - this.maxTextMessageBufferSize = maxTextMessageBufferSize; - } - - public Integer getMaxTextMessageBufferSize() { - return this.maxTextMessageBufferSize; - } - - /** - * If this property set it is in turn used to configure - * {@link ServerContainer#setDefaultMaxBinaryMessageBufferSize(int)}. - */ - public void setMaxBinaryMessageBufferSize(int maxBinaryMessageBufferSize) { - this.maxBinaryMessageBufferSize = maxBinaryMessageBufferSize; - } - - public Integer getMaxBinaryMessageBufferSize() { - return this.maxBinaryMessageBufferSize; + this.annotatedEndpointClasses.clear(); + this.annotatedEndpointClasses.addAll(Arrays.asList(annotatedEndpointClasses)); } @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - if (beanFactory instanceof ListableBeanFactory) { - ListableBeanFactory lbf = (ListableBeanFactory) beanFactory; - Map annotatedEndpoints = lbf.getBeansWithAnnotation(ServerEndpoint.class); - for (String beanName : annotatedEndpoints.keySet()) { - Class beanType = lbf.getType(beanName); - try { - if (logger.isInfoEnabled()) { - logger.info("Detected @ServerEndpoint bean '" + beanName + "', registering it as an endpoint by type"); - } - getServerContainer().addEndpoint(beanType); - } - catch (DeploymentException e) { - throw new IllegalStateException("Failed to register @ServerEndpoint bean type " + beanName, e); - } + public void setApplicationContext(ApplicationContext applicationContext) { + + this.applicationContext = applicationContext; + + this.serverContainer = getServerContainer(); + + Map beans = applicationContext.getBeansWithAnnotation(ServerEndpoint.class); + for (String beanName : beans.keySet()) { + Class beanType = applicationContext.getType(beanName); + if (logger.isInfoEnabled()) { + logger.info("Detected @ServerEndpoint bean '" + beanName + "', registering it as an endpoint by type"); } + this.annotatedEndpointBeanTypes.add(beanType); } } - /** - * Return the {@link ServerContainer} instance, a process which is undefined outside - * of standalone containers (section 6.4 of the spec). - */ - protected abstract ServerContainer getServerContainer(); + protected ServerContainer getServerContainer() { + if (isServletApiPresent) { + try { + Method getter = ReflectionUtils.findMethod(this.applicationContext.getClass(), "getServletContext"); + Object servletContext = getter.invoke(this.applicationContext); + + Method attrMethod = ReflectionUtils.findMethod(servletContext.getClass(), "getAttribute", String.class); + return (ServerContainer) attrMethod.invoke(servletContext, "javax.websocket.server.ServerContainer"); + } + catch (Exception ex) { + throw new IllegalStateException( + "Failed to get javax.websocket.server.ServerContainer via ServletContext attribute", ex); + } + } + return null; + } @Override public void afterPropertiesSet() throws Exception { - ServerContainer serverContainer = getServerContainer(); Assert.notNull(serverContainer, "javax.websocket.server.ServerContainer not available"); - if (this.maxSessionIdleTimeout != null) { - serverContainer.setDefaultMaxSessionIdleTimeout(this.maxSessionIdleTimeout); - } - if (this.maxTextMessageBufferSize != null) { - serverContainer.setDefaultMaxTextMessageBufferSize(this.maxTextMessageBufferSize); - } - if (this.maxBinaryMessageBufferSize != null) { - serverContainer.setDefaultMaxBinaryMessageBufferSize(this.maxBinaryMessageBufferSize); - } + List> allClasses = new ArrayList>(this.annotatedEndpointClasses); + allClasses.addAll(this.annotatedEndpointBeanTypes); - if (!ObjectUtils.isEmpty(this.annotatedEndpointClasses)) { - for (Class clazz : this.annotatedEndpointClasses) { - try { - logger.info("Registering @ServerEndpoint type " + clazz); - serverContainer.addEndpoint(clazz); - } - catch (DeploymentException e) { - throw new IllegalStateException("Failed to register @ServerEndpoint type " + clazz, e); - } + for (Class clazz : allClasses) { + try { + logger.info("Registering @ServerEndpoint type " + clazz); + this.serverContainer.addEndpoint(clazz); + } + catch (DeploymentException e) { + throw new IllegalStateException("Failed to register @ServerEndpoint type " + clazz, e); } } } diff --git a/spring-websocket/src/main/java/org/springframework/websocket/server/endpoint/ServletEndpointExporter.java b/spring-websocket/src/main/java/org/springframework/websocket/server/endpoint/ServletEndpointExporter.java deleted file mode 100644 index b5b216fc8b..0000000000 --- a/spring-websocket/src/main/java/org/springframework/websocket/server/endpoint/ServletEndpointExporter.java +++ /dev/null @@ -1,60 +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.websocket.server.endpoint; - -import javax.servlet.ServletContext; -import javax.websocket.server.ServerContainer; -import javax.websocket.server.ServerContainerProvider; - -import org.springframework.util.Assert; -import org.springframework.web.context.ServletContextAware; - - -/** - * A sub-class of {@link EndpointExporter} for use with a Servlet container runtime. - * - * @author Rossen Stoyanchev - * @since 4.0 - */ -public class ServletEndpointExporter extends EndpointExporter implements ServletContextAware { - - private static final String SERVER_CONTAINER_ATTR_NAME = "javax.websocket.server.ServerContainer"; - - private ServletContext servletContext; - - - @Override - public void setServletContext(ServletContext servletContext) { - this.servletContext = servletContext; - } - - public ServletContext getServletContext() { - return this.servletContext; - } - - @Override - protected ServerContainer getServerContainer() { - Assert.notNull(this.servletContext, "A ServletContext is needed to access the WebSocket ServerContainer"); - ServerContainer container = (ServerContainer) this.servletContext.getAttribute(SERVER_CONTAINER_ATTR_NAME); - if (container == null) { - // Remove when Tomcat has caught up to http://java.net/jira/browse/WEBSOCKET_SPEC-165 - return ServerContainerProvider.getServerContainer(); - } - return container; - } - -} diff --git a/spring-websocket/src/main/java/org/springframework/websocket/server/endpoint/ServletServerContainerFactoryBean.java b/spring-websocket/src/main/java/org/springframework/websocket/server/endpoint/ServletServerContainerFactoryBean.java new file mode 100644 index 0000000000..427ca9fcbd --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/websocket/server/endpoint/ServletServerContainerFactoryBean.java @@ -0,0 +1,133 @@ +/* + * 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.server.endpoint; + +import javax.servlet.ServletContext; +import javax.websocket.WebSocketContainer; +import javax.websocket.server.ServerContainer; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.sockjs.server.SockJsService; +import org.springframework.util.Assert; +import org.springframework.web.context.ServletContextAware; +import org.springframework.websocket.server.DefaultHandshakeHandler; + + +/** + * A FactoryBean for {@link javax.websocket.server.ServerContainer}. Since + * there is only one {@code ServerContainer} instance accessible under a well-known + * {@code javax.servlet.ServletContext} attribute, simply declaring this FactoryBean and + * 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 + * FactoryBean in order to customize the properties of the (one and only) + * {@code ServerContainer} instance. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class ServletServerContainerFactoryBean + implements FactoryBean, InitializingBean, ServletContextAware { + + private static final String SERVER_CONTAINER_ATTR_NAME = "javax.websocket.server.ServerContainer"; + + private Long asyncSendTimeout; + + private Long maxSessionIdleTimeout; + + private Integer maxTextMessageBufferSize; + + private Integer maxBinaryMessageBufferSize; + + + private ServerContainer serverContainer; + + + public void setAsyncSendTimeout(long timeoutInMillis) { + this.asyncSendTimeout = timeoutInMillis; + } + + public long getAsyncSendTimeout() { + return this.asyncSendTimeout; + } + + public void setMaxSessionIdleTimeout(long timeoutInMillis) { + this.maxSessionIdleTimeout = timeoutInMillis; + } + + public Long getMaxSessionIdleTimeout() { + return this.maxSessionIdleTimeout; + } + + public void setMaxTextMessageBufferSize(int bufferSize) { + this.maxTextMessageBufferSize = bufferSize; + } + + public Integer getMaxTextMessageBufferSize() { + return this.maxTextMessageBufferSize; + } + + public void setMaxBinaryMessageBufferSize(int bufferSize) { + this.maxBinaryMessageBufferSize = bufferSize; + } + + public Integer getMaxBinaryMessageBufferSize() { + return this.maxBinaryMessageBufferSize; + } + + @Override + public void setServletContext(ServletContext servletContext) { + this.serverContainer = (ServerContainer) servletContext.getAttribute(SERVER_CONTAINER_ATTR_NAME); + } + + @Override + public ServerContainer getObject() { + return this.serverContainer; + } + + @Override + public Class getObjectType() { + return ServerContainer.class; + } + + @Override + public boolean isSingleton() { + return false; + } + + @Override + public void afterPropertiesSet() throws Exception { + + Assert.notNull(this.serverContainer, + "A ServletContext is required to access the javax.websocket.server.ServerContainer instance"); + + if (this.asyncSendTimeout != null) { + this.serverContainer.setAsyncSendTimeout(this.asyncSendTimeout); + } + if (this.maxSessionIdleTimeout != null) { + this.serverContainer.setDefaultMaxSessionIdleTimeout(this.maxSessionIdleTimeout); + } + if (this.maxTextMessageBufferSize != null) { + this.serverContainer.setDefaultMaxTextMessageBufferSize(this.maxTextMessageBufferSize); + } + if (this.maxBinaryMessageBufferSize != null) { + this.serverContainer.setDefaultMaxBinaryMessageBufferSize(this.maxBinaryMessageBufferSize); + } + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/websocket/server/support/AbstractEndpointContainerFactoryBean.java b/spring-websocket/src/main/java/org/springframework/websocket/server/support/AbstractEndpointContainerFactoryBean.java new file mode 100644 index 0000000000..ee6211341b --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/websocket/server/support/AbstractEndpointContainerFactoryBean.java @@ -0,0 +1,88 @@ +/* + * 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.server.support; + +import javax.websocket.WebSocketContainer; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; + + +/** +* +* @author Rossen Stoyanchev +* @since 4.0 +*/ +public abstract class AbstractEndpointContainerFactoryBean implements FactoryBean, InitializingBean { + + private WebSocketContainer container; + + + public void setAsyncSendTimeout(long timeoutInMillis) { + this.container.setAsyncSendTimeout(timeoutInMillis); + } + + public long getAsyncSendTimeout() { + return this.container.getDefaultAsyncSendTimeout(); + } + + public void setMaxSessionIdleTimeout(long timeoutInMillis) { + this.container.setDefaultMaxSessionIdleTimeout(timeoutInMillis); + } + + public long getMaxSessionIdleTimeout() { + return this.container.getDefaultMaxSessionIdleTimeout(); + } + + public void setMaxTextMessageBufferSize(int bufferSize) { + this.container.setDefaultMaxTextMessageBufferSize(bufferSize); + } + + public int getMaxTextMessageBufferSize() { + return this.container.getDefaultMaxTextMessageBufferSize(); + } + + public void setMaxBinaryMessageBufferSize(int bufferSize) { + this.container.setDefaultMaxBinaryMessageBufferSize(bufferSize); + } + + public int getMaxBinaryMessageBufferSize() { + return this.container.getDefaultMaxBinaryMessageBufferSize(); + } + + @Override + public void afterPropertiesSet() throws Exception { + this.container = getContainer(); + } + + protected abstract WebSocketContainer getContainer(); + + @Override + public WebSocketContainer getObject() throws Exception { + return this.container; + } + + @Override + public Class getObjectType() { + return WebSocketContainer.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +}