diff --git a/README-WEBSOCKET.md b/README-WEBSOCKET.md
new file mode 100644
index 0000000000..27c1e641b6
--- /dev/null
+++ b/README-WEBSOCKET.md
@@ -0,0 +1,88 @@
+
+## Maven Snapshots
+
+Maven snapshots of this branch are available through the Spring snapshot repository:
+
+
+ spring-snapshots
+ http://repo.springsource.org/snapshot
+ true
+ false
+
+
+Use version `4.0.0.WEBSOCKET-SNAPSHOT`, for example:
+
+
+ org.springframework
+ spring-context
+ 4.0.0.WEBSOCKET-SNAPSHOT
+
+
+ org.springframework
+ spring-web
+ 4.0.0.WEBSOCKET-SNAPSHOT
+
+
+ org.springframework
+ spring-websocket
+ 4.0.0.WEBSOCKET-SNAPSHOT
+
+
+
+
+### Tomcat
+
+Tomcat provides early JSR-356 support. You'll need to build the latest source, which is relatively easy to do.
+
+Check out Tomcat trunk:
+
+ mkdir tomcat
+ cd tomcat
+ svn co http://svn.apache.org/repos/asf/tomcat/trunk/
+ cd trunk
+
+Create `build.properties` in the trunk directory with similar content:
+
+ # ----- Default Base Path for Dependent Packages -----
+ # Replace this path with the path where dependencies binaries should be downloaded
+ base.path=~/dev/sources/apache/tomcat/download
+
+Run the ant build:
+
+ ant clean
+ ant
+
+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).
+
+Download a [Glassfish 4 build](http://dlc.sun.com.edgesuite.net/glassfish/4.0/) (e.g. glassfish-4.0-b84.zip from the promoted builds)
+
+Unzip the downloaded file.
+
+Start the server:
+
+ cd /glassfish4
+ bin/asadmin start-domain
+
+Deploy a WAR file. Here is [a sample script](https://github.com/rstoyanchev/spring-websocket-test/blob/master/redeploy-glassfish.sh).
+
+Watch the logs:
+
+ cd /glassfish4
+ less `glassfish/domains/domain1/logs/server.log`
+
+
diff --git a/build.gradle b/build.gradle
index 4dd89a12fa..ea0c656522 100644
--- a/build.gradle
+++ b/build.gradle
@@ -499,6 +499,42 @@ project("spring-orm-hibernate4") {
}
}
+project("spring-websocket") {
+ description = "Spring WebSocket support"
+ dependencies {
+ compile(project(":spring-core"))
+ compile(project(":spring-context"))
+ compile(project(":spring-web"))
+
+ optional("org.apache.tomcat:tomcat-servlet-api:8.0-SNAPSHOT") // TODO: replace with "javax.servlet:javax.servlet-api"
+ optional("org.apache.tomcat:tomcat-websocket-api:8.0-SNAPSHOT") // TODO: replace with "javax.websocket:javax.websocket-api"
+
+ optional("org.apache.tomcat:tomcat-websocket:8.0-SNAPSHOT") {
+ exclude group: "org.apache.tomcat", module: "tomcat-websocket-api"
+ exclude group: "org.apache.tomcat", module: "tomcat-servlet-api"
+ }
+
+ optional("org.glassfish.tyrus:tyrus-websocket-core:1.0-SNAPSHOT")
+ optional("org.glassfish.tyrus:tyrus-container-servlet:1.0-SNAPSHOT")
+
+ optional("org.eclipse.jetty:jetty-webapp:9.0.1.v20130408") {
+ 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
+
+ }
+
+ repositories {
+ maven { url "http://repo.springsource.org/libs-release" }
+ maven { url "https://maven.java.net/content/groups/public/" } // javax.websocket-*
+ maven { url "https://repository.apache.org/content/repositories/snapshots" } // tomcat-websocket snapshots
+ maven { url "https://maven.java.net/content/repositories/snapshots" } // tyrus/glassfish snapshots
+ }
+}
+
project("spring-webmvc") {
description = "Spring Web MVC"
diff --git a/gradle.properties b/gradle.properties
index 2db7ae75f2..712344802e 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1 +1 @@
-version=4.0.0.BUILD-SNAPSHOT
+version=4.0.0.WEBSOCKET-SNAPSHOT
diff --git a/settings.gradle b/settings.gradle
index 71b5e8408d..3511d2a86f 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -21,6 +21,7 @@ include "spring-web"
include "spring-webmvc"
include "spring-webmvc-portlet"
include "spring-webmvc-tiles3"
+include "spring-websocket"
// Exposes gradle buildSrc for IDE support
include "buildSrc"
diff --git a/spring-test/src/main/java/org/springframework/mock/http/MockHttpInputMessage.java b/spring-test/src/main/java/org/springframework/mock/http/MockHttpInputMessage.java
index 5de14e9954..d73a1ea0ef 100644
--- a/spring-test/src/main/java/org/springframework/mock/http/MockHttpInputMessage.java
+++ b/spring-test/src/main/java/org/springframework/mock/http/MockHttpInputMessage.java
@@ -19,6 +19,7 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
+import org.springframework.http.Cookies;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.util.Assert;
@@ -35,6 +36,8 @@ public class MockHttpInputMessage implements HttpInputMessage {
private final InputStream body;
+ private final Cookies cookies = new Cookies();
+
public MockHttpInputMessage(byte[] contents) {
this.body = (contents != null) ? new ByteArrayInputStream(contents) : null;
@@ -53,4 +56,8 @@ public class MockHttpInputMessage implements HttpInputMessage {
return this.body;
}
+ @Override
+ public Cookies getCookies() {
+ return this.cookies ;
+ }
}
diff --git a/spring-test/src/main/java/org/springframework/mock/http/MockHttpOutputMessage.java b/spring-test/src/main/java/org/springframework/mock/http/MockHttpOutputMessage.java
index 43fa1b3e7e..8cda7862a5 100644
--- a/spring-test/src/main/java/org/springframework/mock/http/MockHttpOutputMessage.java
+++ b/spring-test/src/main/java/org/springframework/mock/http/MockHttpOutputMessage.java
@@ -21,6 +21,7 @@ import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
+import org.springframework.http.Cookies;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
@@ -38,6 +39,7 @@ public class MockHttpOutputMessage implements HttpOutputMessage {
private final ByteArrayOutputStream body = new ByteArrayOutputStream();
+ private final Cookies cookies = new Cookies();
/**
* Return the headers.
@@ -83,4 +85,9 @@ public class MockHttpOutputMessage implements HttpOutputMessage {
}
}
+ @Override
+ public Cookies getCookies() {
+ return this.cookies;
+ }
+
}
diff --git a/spring-web/src/main/java/org/springframework/http/Cookie.java b/spring-web/src/main/java/org/springframework/http/Cookie.java
new file mode 100644
index 0000000000..a60c73c87f
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/Cookie.java
@@ -0,0 +1,25 @@
+/*
+ * 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.http;
+
+
+public interface Cookie {
+
+ String getName();
+
+ String getValue();
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/Cookies.java b/spring-web/src/main/java/org/springframework/http/Cookies.java
new file mode 100644
index 0000000000..7dc13536c9
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/Cookies.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2002-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.http;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+
+public class Cookies {
+
+ private final List cookies;
+
+
+ public Cookies() {
+ this.cookies = new ArrayList();
+ }
+
+ private Cookies(Cookies cookies) {
+ this.cookies = Collections.unmodifiableList(cookies.getCookies());
+ }
+
+ public static Cookies readOnlyCookies(Cookies cookies) {
+ return new Cookies(cookies);
+ }
+
+ public List getCookies() {
+ return this.cookies;
+ }
+
+ public Cookie getCookie(String name) {
+ for (Cookie c : this.cookies) {
+ if (c.getName().equals(name)) {
+ return c;
+ }
+ }
+ return null;
+ }
+
+ public Cookie addCookie(String name, String value) {
+ DefaultCookie cookie = new DefaultCookie(name, value);
+ this.cookies.add(cookie);
+ return cookie;
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/DefaultCookie.java b/spring-web/src/main/java/org/springframework/http/DefaultCookie.java
new file mode 100644
index 0000000000..82a09ba26e
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/DefaultCookie.java
@@ -0,0 +1,40 @@
+/*
+ * 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.http;
+
+import org.springframework.util.Assert;
+
+public class DefaultCookie implements Cookie {
+
+ private final String name;
+
+ private final String value;
+
+ DefaultCookie(String name, String value) {
+ Assert.hasText(name, "cookie name must not be empty");
+ this.name = name;
+ this.value = value;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java
index 5d480d5e4b..e972709572 100644
--- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java
+++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java
@@ -17,14 +17,10 @@
package org.springframework.http;
import java.io.Serializable;
-
import java.net.URI;
-
import java.nio.charset.Charset;
-
import java.text.ParseException;
import java.text.SimpleDateFormat;
-
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -40,6 +36,7 @@ import java.util.Set;
import java.util.TimeZone;
import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
@@ -71,6 +68,8 @@ public class HttpHeaders implements MultiValueMap, Serializable
private static final String CACHE_CONTROL = "Cache-Control";
+ private static final String CONNECTION = "Connection";
+
private static final String CONTENT_DISPOSITION = "Content-Disposition";
private static final String CONTENT_LENGTH = "Content-Length";
@@ -91,8 +90,22 @@ public class HttpHeaders implements MultiValueMap, Serializable
private static final String LOCATION = "Location";
+ private static final String ORIGIN = "Origin";
+
+ private static final String SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept";
+
+ private static final String SEC_WEBSOCKET_EXTENSIONS = "Sec-WebSocket-Extensions";
+
+ private static final String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key";
+
+ private static final String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol";
+
+ private static final String SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version";
+
private static final String PRAGMA = "Pragma";
+ private static final String UPGARDE = "Upgrade";
+
private static final String[] DATE_FORMATS = new String[] {
"EEE, dd MMM yyyy HH:mm:ss zzz",
@@ -251,6 +264,30 @@ public class HttpHeaders implements MultiValueMap, Serializable
return getFirst(CACHE_CONTROL);
}
+ /**
+ * Sets the (new) value of the {@code Connection} header.
+ * @param connection the value of the header
+ */
+ public void setConnection(String connection) {
+ set(CONNECTION, connection);
+ }
+
+ /**
+ * Sets the (new) value of the {@code Connection} header.
+ * @param connection the value of the header
+ */
+ public void setConnection(List connection) {
+ set(CONNECTION, toCommaDelimitedString(connection));
+ }
+
+ /**
+ * Returns the value of the {@code Connection} header.
+ * @return the value of the header
+ */
+ public List getConnection() {
+ return getFirstValueAsList(CONNECTION);
+ }
+
/**
* Sets the (new) value of the {@code Content-Disposition} header for {@code form-data}.
* @param name the control name
@@ -393,15 +430,19 @@ public class HttpHeaders implements MultiValueMap, Serializable
* @param ifNoneMatchList the new value of the header
*/
public void setIfNoneMatch(List ifNoneMatchList) {
+ set(IF_NONE_MATCH, toCommaDelimitedString(ifNoneMatchList));
+ }
+
+ private String toCommaDelimitedString(List list) {
StringBuilder builder = new StringBuilder();
- for (Iterator iterator = ifNoneMatchList.iterator(); iterator.hasNext();) {
+ for (Iterator iterator = list.iterator(); iterator.hasNext();) {
String ifNoneMatch = iterator.next();
builder.append(ifNoneMatch);
if (iterator.hasNext()) {
builder.append(", ");
}
}
- set(IF_NONE_MATCH, builder.toString());
+ return builder.toString();
}
/**
@@ -409,9 +450,13 @@ public class HttpHeaders implements MultiValueMap, Serializable
* @return the header value
*/
public List getIfNoneMatch() {
+ return getFirstValueAsList(IF_NONE_MATCH);
+ }
+
+ private List getFirstValueAsList(String header) {
List result = new ArrayList();
- String value = getFirst(IF_NONE_MATCH);
+ String value = getFirst(header);
if (value != null) {
String[] tokens = value.split(",\\s*");
for (String token : tokens) {
@@ -457,6 +502,130 @@ public class HttpHeaders implements MultiValueMap, Serializable
return (value != null ? URI.create(value) : null);
}
+ /**
+ * Sets the (new) value of the {@code Origin} header.
+ * @param origin the value of the header
+ */
+ public void setOrigin(String origin) {
+ set(ORIGIN, origin);
+ }
+
+ /**
+ * Returns the value of the {@code Origin} header.
+ * @return the value of the header
+ */
+ public String getOrigin() {
+ return getFirst(ORIGIN);
+ }
+
+ /**
+ * Sets the (new) value of the {@code Sec-WebSocket-Accept} header.
+ * @param secWebSocketAccept the value of the header
+ */
+ public void setSecWebSocketAccept(String secWebSocketAccept) {
+ set(SEC_WEBSOCKET_ACCEPT, secWebSocketAccept);
+ }
+
+ /**
+ * Returns the value of the {@code Sec-WebSocket-Accept} header.
+ * @return the value of the header
+ */
+ public String getSecWebSocketAccept() {
+ return getFirst(SEC_WEBSOCKET_ACCEPT);
+ }
+
+ /**
+ * Returns the value of the {@code Sec-WebSocket-Extensions} header.
+ * @return the value of the header
+ */
+ public List getSecWebSocketExtensions() {
+ List values = get(SEC_WEBSOCKET_EXTENSIONS);
+ if (CollectionUtils.isEmpty(values)) {
+ return Collections.emptyList();
+ }
+ else if (values.size() == 1) {
+ return getFirstValueAsList(SEC_WEBSOCKET_EXTENSIONS);
+ }
+ else {
+ return values;
+ }
+ }
+
+ /**
+ * Sets the (new) value of the {@code Sec-WebSocket-Extensions} header.
+ * @param secWebSocketExtensions the value of the header
+ */
+ public void setSecWebSocketExtensions(List secWebSocketExtensions) {
+ set(SEC_WEBSOCKET_EXTENSIONS, toCommaDelimitedString(secWebSocketExtensions));
+ }
+
+ /**
+ * Sets the (new) value of the {@code Sec-WebSocket-Key} header.
+ * @param secWebSocketKey the value of the header
+ */
+ public void setSecWebSocketKey(String secWebSocketKey) {
+ set(SEC_WEBSOCKET_KEY, secWebSocketKey);
+ }
+
+ /**
+ * Returns the value of the {@code Sec-WebSocket-Key} header.
+ * @return the value of the header
+ */
+ public String getSecWebSocketKey() {
+ return getFirst(SEC_WEBSOCKET_KEY);
+ }
+
+ /**
+ * Sets the (new) value of the {@code Sec-WebSocket-Protocol} header.
+ * @param secWebSocketProtocol the value of the header
+ */
+ public void setSecWebSocketProtocol(String secWebSocketProtocol) {
+ if (secWebSocketProtocol != null) {
+ set(SEC_WEBSOCKET_PROTOCOL, secWebSocketProtocol);
+ }
+ }
+
+ /**
+ * Sets the (new) value of the {@code Sec-WebSocket-Protocol} header.
+ * @param secWebSocketProtocols the value of the header
+ */
+ public void setSecWebSocketProtocol(List secWebSocketProtocols) {
+ set(SEC_WEBSOCKET_PROTOCOL, toCommaDelimitedString(secWebSocketProtocols));
+ }
+
+ /**
+ * Returns the value of the {@code Sec-WebSocket-Key} header.
+ * @return the value of the header
+ */
+ public List getSecWebSocketProtocol() {
+ List values = get(SEC_WEBSOCKET_PROTOCOL);
+ if (CollectionUtils.isEmpty(values)) {
+ return Collections.emptyList();
+ }
+ else if (values.size() == 1) {
+ return getFirstValueAsList(SEC_WEBSOCKET_PROTOCOL);
+ }
+ else {
+ return values;
+ }
+ }
+
+ /**
+ * Sets the (new) value of the {@code Sec-WebSocket-Version} header.
+ * @param secWebSocketKey the value of the header
+ */
+ public void setSecWebSocketVersion(String secWebSocketVersion) {
+ set(SEC_WEBSOCKET_VERSION, secWebSocketVersion);
+ }
+
+ /**
+ * Returns the value of the {@code Sec-WebSocket-Version} header.
+ * @return the value of the header
+ */
+ public String getSecWebSocketVersion() {
+ return getFirst(SEC_WEBSOCKET_VERSION);
+ }
+
/**
* Sets the (new) value of the {@code Pragma} header.
* @param pragma the value of the header
@@ -473,6 +642,22 @@ public class HttpHeaders implements MultiValueMap, Serializable
return getFirst(PRAGMA);
}
+ /**
+ * Sets the (new) value of the {@code Upgrade} header.
+ * @param upgrade the value of the header
+ */
+ public void setUpgrade(String upgrade) {
+ set(UPGARDE, upgrade);
+ }
+
+ /**
+ * Returns the value of the {@code Upgrade} header.
+ * @return the value of the header
+ */
+ public String getUpgrade() {
+ return getFirst(UPGARDE);
+ }
+
// Utility methods
private long getFirstDate(String headerName) {
diff --git a/spring-web/src/main/java/org/springframework/http/HttpMessage.java b/spring-web/src/main/java/org/springframework/http/HttpMessage.java
index 80f7ca292d..05824e67ab 100644
--- a/spring-web/src/main/java/org/springframework/http/HttpMessage.java
+++ b/spring-web/src/main/java/org/springframework/http/HttpMessage.java
@@ -31,4 +31,9 @@ public interface HttpMessage {
*/
HttpHeaders getHeaders();
+ /**
+ * TODO ..
+ */
+ Cookies getCookies();
+
}
diff --git a/spring-web/src/main/java/org/springframework/http/client/AbstractClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/AbstractClientHttpRequest.java
index 47422a0065..9084967078 100644
--- a/spring-web/src/main/java/org/springframework/http/client/AbstractClientHttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/http/client/AbstractClientHttpRequest.java
@@ -19,6 +19,7 @@ package org.springframework.http.client;
import java.io.IOException;
import java.io.OutputStream;
+import org.springframework.http.Cookies;
import org.springframework.http.HttpHeaders;
import org.springframework.util.Assert;
@@ -44,6 +45,11 @@ public abstract class AbstractClientHttpRequest implements ClientHttpRequest {
return getBodyInternal(this.headers);
}
+ public Cookies getCookies() {
+ // TODO
+ throw new UnsupportedOperationException();
+ }
+
public final ClientHttpResponse execute() throws IOException {
checkExecuted();
ClientHttpResponse result = executeInternal(this.headers);
diff --git a/spring-web/src/main/java/org/springframework/http/client/AbstractClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/AbstractClientHttpResponse.java
index cd6166575b..33c123cff9 100644
--- a/spring-web/src/main/java/org/springframework/http/client/AbstractClientHttpResponse.java
+++ b/spring-web/src/main/java/org/springframework/http/client/AbstractClientHttpResponse.java
@@ -18,6 +18,7 @@ package org.springframework.http.client;
import java.io.IOException;
+import org.springframework.http.Cookies;
import org.springframework.http.HttpStatus;
/**
@@ -32,4 +33,9 @@ public abstract class AbstractClientHttpResponse implements ClientHttpResponse {
return HttpStatus.valueOf(getRawStatusCode());
}
+ public Cookies getCookies() {
+ // TODO
+ throw new UnsupportedOperationException();
+ }
+
}
diff --git a/spring-web/src/main/java/org/springframework/http/client/BufferingClientHttpRequestWrapper.java b/spring-web/src/main/java/org/springframework/http/client/BufferingClientHttpRequestWrapper.java
index bb87844420..794ca6ac2a 100644
--- a/spring-web/src/main/java/org/springframework/http/client/BufferingClientHttpRequestWrapper.java
+++ b/spring-web/src/main/java/org/springframework/http/client/BufferingClientHttpRequestWrapper.java
@@ -17,9 +17,9 @@
package org.springframework.http.client;
import java.io.IOException;
-import java.io.OutputStream;
import java.net.URI;
+import org.springframework.http.Cookies;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
@@ -58,4 +58,9 @@ final class BufferingClientHttpRequestWrapper extends AbstractBufferingClientHtt
return new BufferingClientHttpResponseWrapper(response);
}
+ @Override
+ public Cookies getCookies() {
+ return this.request.getCookies();
+ }
+
}
diff --git a/spring-web/src/main/java/org/springframework/http/client/BufferingClientHttpResponseWrapper.java b/spring-web/src/main/java/org/springframework/http/client/BufferingClientHttpResponseWrapper.java
index f075b202bd..382b3fa20a 100644
--- a/spring-web/src/main/java/org/springframework/http/client/BufferingClientHttpResponseWrapper.java
+++ b/spring-web/src/main/java/org/springframework/http/client/BufferingClientHttpResponseWrapper.java
@@ -20,9 +20,9 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
+import org.springframework.http.Cookies;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
-import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;
/**
@@ -67,6 +67,10 @@ final class BufferingClientHttpResponseWrapper implements ClientHttpResponse {
return new ByteArrayInputStream(this.body);
}
+ public Cookies getCookies() {
+ return this.response.getCookies();
+ }
+
public void close() {
this.response.close();
}
diff --git a/spring-web/src/main/java/org/springframework/http/client/support/HttpRequestWrapper.java b/spring-web/src/main/java/org/springframework/http/client/support/HttpRequestWrapper.java
index 4aecd01dcd..c9c8ef9955 100644
--- a/spring-web/src/main/java/org/springframework/http/client/support/HttpRequestWrapper.java
+++ b/spring-web/src/main/java/org/springframework/http/client/support/HttpRequestWrapper.java
@@ -18,6 +18,7 @@ package org.springframework.http.client.support;
import java.net.URI;
+import org.springframework.http.Cookies;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
@@ -73,4 +74,11 @@ public class HttpRequestWrapper implements HttpRequest {
return this.request.getHeaders();
}
+ /**
+ * Returns the cookies of the wrapped request.
+ */
+ public Cookies getCookies() {
+ return this.request.getCookies();
+ }
+
}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java
index 516eb92ab2..5ddcfead7b 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java
@@ -30,6 +30,7 @@ import java.util.Map;
import java.util.Random;
import org.springframework.core.io.Resource;
+import org.springframework.http.Cookies;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
@@ -383,6 +384,11 @@ public class FormHttpMessageConverter implements HttpMessageConverter> entry : this.headers.entrySet()) {
diff --git a/spring-web/src/main/java/org/springframework/http/server/AsyncServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/AsyncServerHttpRequest.java
new file mode 100644
index 0000000000..bbf973d5d8
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/server/AsyncServerHttpRequest.java
@@ -0,0 +1,35 @@
+/*
+ * 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.http.server;
+
+
+
+/**
+ * TODO..
+ */
+public interface AsyncServerHttpRequest extends ServerHttpRequest {
+
+ void setTimeout(long timeout);
+
+ void startAsync();
+
+ boolean isAsyncStarted();
+
+ void completeAsync();
+
+ boolean isAsyncCompleted();
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/server/AsyncServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/AsyncServletServerHttpRequest.java
new file mode 100644
index 0000000000..996a7d3fdc
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/server/AsyncServletServerHttpRequest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.http.server;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.AsyncEvent;
+import javax.servlet.AsyncListener;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.util.Assert;
+
+
+public class AsyncServletServerHttpRequest extends ServletServerHttpRequest
+ implements AsyncServerHttpRequest, AsyncListener {
+
+ private Long timeout;
+
+ private AsyncContext asyncContext;
+
+ private AtomicBoolean asyncCompleted = new AtomicBoolean(false);
+
+ private final List timeoutHandlers = new ArrayList();
+
+ private final List completionHandlers = new ArrayList();
+
+ private final HttpServletResponse servletResponse;
+
+
+ /**
+ * Create a new instance for the given request/response pair.
+ */
+ public AsyncServletServerHttpRequest(HttpServletRequest request, HttpServletResponse response) {
+ super(request);
+ this.servletResponse = response;
+ }
+
+ /**
+ * Timeout period begins after the container thread has exited.
+ */
+ public void setTimeout(long timeout) {
+ Assert.state(!isAsyncStarted(), "Cannot change the timeout with concurrent handling in progress");
+ this.timeout = timeout;
+ }
+
+ public void addTimeoutHandler(Runnable timeoutHandler) {
+ this.timeoutHandlers.add(timeoutHandler);
+ }
+
+ public void addCompletionHandler(Runnable runnable) {
+ this.completionHandlers.add(runnable);
+ }
+
+ public boolean isAsyncStarted() {
+ return ((this.asyncContext != null) && getServletRequest().isAsyncStarted());
+ }
+
+ /**
+ * Whether async request processing has completed.
+ *
It is important to avoid use of request and response objects after async
+ * processing has completed. Servlet containers often re-use them.
+ */
+ public boolean isAsyncCompleted() {
+ return this.asyncCompleted.get();
+ }
+
+ public void startAsync() {
+ Assert.state(getServletRequest().isAsyncSupported(),
+ "Async support must be enabled on a servlet and for all filters involved " +
+ "in async request processing. This is done in Java code using the Servlet API " +
+ "or by adding \"true\" to servlet and " +
+ "filter declarations in web.xml.");
+ Assert.state(!isAsyncCompleted(), "Async processing has already completed");
+ if (isAsyncStarted()) {
+ return;
+ }
+ this.asyncContext = getServletRequest().startAsync(getServletRequest(), this.servletResponse);
+ this.asyncContext.addListener(this);
+ if (this.timeout != null) {
+ this.asyncContext.setTimeout(this.timeout);
+ }
+ }
+
+ public void completeAsync() {
+ Assert.notNull(this.asyncContext, "Cannot dispatch without an AsyncContext");
+ if (isAsyncStarted() && !isAsyncCompleted()) {
+ this.asyncContext.complete();
+ }
+ }
+
+
+ // ---------------------------------------------------------------------
+ // Implementation of AsyncListener methods
+ // ---------------------------------------------------------------------
+
+ @Override
+ public void onStartAsync(AsyncEvent event) throws IOException {
+ }
+
+ @Override
+ public void onError(AsyncEvent event) throws IOException {
+ }
+
+ @Override
+ public void onTimeout(AsyncEvent event) throws IOException {
+ try {
+ for (Runnable handler : this.timeoutHandlers) {
+ handler.run();
+ }
+ }
+ catch (Throwable t) {
+ // ignore
+ }
+ }
+
+ @Override
+ public void onComplete(AsyncEvent event) throws IOException {
+ try {
+ for (Runnable handler : this.completionHandlers) {
+ handler.run();
+ }
+ }
+ catch (Throwable t) {
+ // ignore
+ }
+ this.asyncContext = null;
+ this.asyncCompleted.set(true);
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/server/ServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/ServerHttpRequest.java
index ce8fcd3ad2..c765fd3492 100644
--- a/spring-web/src/main/java/org/springframework/http/server/ServerHttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/http/server/ServerHttpRequest.java
@@ -16,8 +16,11 @@
package org.springframework.http.server;
+import java.security.Principal;
+
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpRequest;
+import org.springframework.util.MultiValueMap;
/**
* Represents a server-side HTTP request.
@@ -27,4 +30,26 @@ import org.springframework.http.HttpRequest;
*/
public interface ServerHttpRequest extends HttpRequest, HttpInputMessage {
+ /**
+ * Returns the map of query parameters. Empty if no query has been set.
+ */
+ MultiValueMap getQueryParams();
+
+ /**
+ * Return a {@link java.security.Principal} instance containing the name of the
+ * authenticated user. If the user has not been authenticated, the method returns
+ * null.
+ */
+ Principal getPrincipal();
+
+ /**
+ * Return the host name of the endpoint on the other end.
+ */
+ String getRemoteHostName();
+
+ /**
+ * Return the IP address of the endpoint on the other end.
+ */
+ String getRemoteAddress();
+
}
diff --git a/spring-web/src/main/java/org/springframework/http/server/ServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/ServerHttpResponse.java
index 8ce306271f..02541fab92 100644
--- a/spring-web/src/main/java/org/springframework/http/server/ServerHttpResponse.java
+++ b/spring-web/src/main/java/org/springframework/http/server/ServerHttpResponse.java
@@ -17,6 +17,7 @@
package org.springframework.http.server;
import java.io.Closeable;
+import java.io.IOException;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.HttpStatus;
@@ -35,6 +36,11 @@ public interface ServerHttpResponse extends HttpOutputMessage, Closeable {
*/
void setStatusCode(HttpStatus status);
+ /**
+ * TODO
+ */
+ void flush() throws IOException;
+
/**
* Close this response, freeing any resources created.
*/
diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java
index 5dd0dbd424..2236df64cd 100644
--- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java
@@ -26,6 +26,7 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
+import java.security.Principal;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
@@ -33,12 +34,16 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
+import org.springframework.http.Cookies;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
/**
* {@link ServerHttpRequest} implementation that is based on a {@link HttpServletRequest}.
@@ -58,6 +63,10 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
private HttpHeaders headers;
+ private Cookies cookies;
+
+ private MultiValueMap queryParams;
+
/**
* Construct a new instance of the ServletServerHttpRequest based on the given {@link HttpServletRequest}.
@@ -123,6 +132,45 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
return this.headers;
}
+ @Override
+ public Principal getPrincipal() {
+ return this.servletRequest.getUserPrincipal();
+ }
+
+ @Override
+ public String getRemoteHostName() {
+ return this.servletRequest.getRemoteHost();
+ }
+
+ @Override
+ public String getRemoteAddress() {
+ return this.servletRequest.getRemoteAddr();
+ }
+
+ public Cookies getCookies() {
+ if (this.cookies == null) {
+ this.cookies = new Cookies();
+ if (this.servletRequest.getCookies() != null) {
+ for (Cookie cookie : this.servletRequest.getCookies()) {
+ this.cookies.addCookie(cookie.getName(), cookie.getValue());
+ }
+ }
+ }
+ return this.cookies;
+ }
+
+ public MultiValueMap getQueryParams() {
+ if (this.queryParams == null) {
+ this.queryParams = new LinkedMultiValueMap(this.servletRequest.getParameterMap().size());
+ for (String name : this.servletRequest.getParameterMap().keySet()) {
+ for (String value : this.servletRequest.getParameterValues(name)) {
+ this.queryParams.add(name, value);
+ }
+ }
+ }
+ return this.queryParams;
+ }
+
public InputStream getBody() throws IOException {
if (isFormPost(this.servletRequest)) {
return getBodyFromServletRequestParameters(this.servletRequest);
diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java
index 985085e51e..3a2ba0445d 100644
--- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java
+++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java
@@ -22,6 +22,8 @@ import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
+import org.springframework.http.Cookie;
+import org.springframework.http.Cookies;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
@@ -40,6 +42,8 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
private boolean headersWritten = false;
+ private final Cookies cookies = new Cookies();
+
/**
* Construct a new instance of the ServletServerHttpResponse based on the given {@link HttpServletResponse}.
@@ -66,12 +70,25 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers);
}
+ public Cookies getCookies() {
+ return (this.headersWritten ? Cookies.readOnlyCookies(this.cookies) : this.cookies);
+ }
+
public OutputStream getBody() throws IOException {
+ writeCookies();
writeHeaders();
return this.servletResponse.getOutputStream();
}
+ @Override
+ public void flush() throws IOException {
+ writeCookies();
+ writeHeaders();
+ this.servletResponse.flushBuffer();
+ }
+
public void close() {
+ writeCookies();
writeHeaders();
}
@@ -95,4 +112,13 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
}
}
+ private void writeCookies() {
+ if (!this.headersWritten) {
+ for (Cookie source : this.cookies.getCookies()) {
+ javax.servlet.http.Cookie target = new javax.servlet.http.Cookie(source.getName(), source.getValue());
+ target.setPath("/");
+ this.servletResponse.addCookie(target);
+ }
+ }
+ }
}
diff --git a/spring-web/src/test/java/org/springframework/http/MockHttpInputMessage.java b/spring-web/src/test/java/org/springframework/http/MockHttpInputMessage.java
index 18412ce33c..0ca8a29aca 100644
--- a/spring-web/src/test/java/org/springframework/http/MockHttpInputMessage.java
+++ b/spring-web/src/test/java/org/springframework/http/MockHttpInputMessage.java
@@ -31,6 +31,9 @@ public class MockHttpInputMessage implements HttpInputMessage {
private final InputStream body;
+ private final Cookies cookies = new Cookies();
+
+
public MockHttpInputMessage(byte[] contents) {
Assert.notNull(contents, "'contents' must not be null");
this.body = new ByteArrayInputStream(contents);
@@ -50,4 +53,9 @@ public class MockHttpInputMessage implements HttpInputMessage {
public InputStream getBody() throws IOException {
return body;
}
+
+ @Override
+ public Cookies getCookies() {
+ return this.cookies ;
+ }
}
diff --git a/spring-web/src/test/java/org/springframework/http/MockHttpOutputMessage.java b/spring-web/src/test/java/org/springframework/http/MockHttpOutputMessage.java
index cb08fa91a1..3287a7d93f 100644
--- a/spring-web/src/test/java/org/springframework/http/MockHttpOutputMessage.java
+++ b/spring-web/src/test/java/org/springframework/http/MockHttpOutputMessage.java
@@ -32,6 +32,9 @@ public class MockHttpOutputMessage implements HttpOutputMessage {
private final ByteArrayOutputStream body = spy(new ByteArrayOutputStream());
+ private final Cookies cookies = new Cookies();
+
+
@Override
public HttpHeaders getHeaders() {
return headers;
@@ -50,4 +53,9 @@ public class MockHttpOutputMessage implements HttpOutputMessage {
byte[] bytes = getBodyAsBytes();
return new String(bytes, charset);
}
+
+ @Override
+ public Cookies getCookies() {
+ return this.cookies;
+ }
}
diff --git a/spring-web/src/test/java/org/springframework/http/client/InterceptingClientHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/InterceptingClientHttpRequestFactoryTests.java
index 2df6331b8e..0358bf42bf 100644
--- a/spring-web/src/test/java/org/springframework/http/client/InterceptingClientHttpRequestFactoryTests.java
+++ b/spring-web/src/test/java/org/springframework/http/client/InterceptingClientHttpRequestFactoryTests.java
@@ -29,6 +29,7 @@ import java.util.List;
import org.junit.Before;
import org.junit.Test;
+import org.springframework.http.Cookies;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
@@ -253,6 +254,8 @@ public class InterceptingClientHttpRequestFactoryTests {
private boolean executed = false;
+ private Cookies cookies = new Cookies();
+
private RequestMock() {
}
@@ -289,6 +292,11 @@ public class InterceptingClientHttpRequestFactoryTests {
executed = true;
return responseMock;
}
+
+ @Override
+ public Cookies getCookies() {
+ return this.cookies ;
+ }
}
private static class ResponseMock implements ClientHttpResponse {
@@ -299,6 +307,8 @@ public class InterceptingClientHttpRequestFactoryTests {
private HttpHeaders headers = new HttpHeaders();
+ private Cookies cookies = new Cookies();
+
@Override
public HttpStatus getStatusCode() throws IOException {
return statusCode;
@@ -327,5 +337,10 @@ public class InterceptingClientHttpRequestFactoryTests {
@Override
public void close() {
}
+
+ @Override
+ public Cookies getCookies() {
+ return this.cookies ;
+ }
}
}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/BinaryMessage.java b/spring-websocket/src/main/java/org/springframework/web/socket/BinaryMessage.java
new file mode 100644
index 0000000000..cf6b35594c
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/BinaryMessage.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2002-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.web.socket;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+
+/**
+ * A {@link WebSocketMessage} that contains a binary {@link ByteBuffer} payload.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.0
+ * @see WebSocketMessage
+ */
+public final class BinaryMessage extends WebSocketMessage {
+
+ private byte[] bytes;
+
+ private final boolean last;
+
+
+ /**
+ * Create a new {@link BinaryMessage} instance.
+ * @param payload a non-null payload
+ */
+ public BinaryMessage(ByteBuffer payload) {
+ this(payload, true);
+ }
+
+ /**
+ * Create a new {@link BinaryMessage} instance.
+ * @param payload a non-null payload
+ * @param isLast if the message is the last of a series of partial messages
+ */
+ public BinaryMessage(ByteBuffer payload, boolean isLast) {
+ super(payload);
+ this.bytes = null;
+ this.last = isLast;
+ }
+
+ /**
+ * Create a new {@link BinaryMessage} instance.
+ * @param payload a non-null payload
+ */
+ public BinaryMessage(byte[] payload) {
+ this(payload, true);
+ }
+
+ /**
+ * Create a new {@link BinaryMessage} instance.
+ * @param payload a non-null payload
+ * @param isLast if the message is the last of a series of partial messages
+ */
+ public BinaryMessage(byte[] payload, boolean isLast) {
+ this(payload, 0, (payload == null ? 0 : payload.length), isLast);
+ }
+
+ /**
+ * Create a new {@link BinaryMessage} instance by wrapping an existing byte array.
+ * @param payload a non-null payload, NOTE: this value is not copied so care must be
+ * taken not to modify the array.
+ * @param isLast if the message is the last of a series of partial messages
+ */
+ public BinaryMessage(byte[] payload, int offset, int len) {
+ this(payload, offset, len, true);
+ }
+
+ /**
+ * Create a new {@link BinaryMessage} instance by wrapping an existing byte array.
+ * @param payload a non-null payload, NOTE: this value is not copied so care must be
+ * taken not to modify the array.
+ * @param offset the offet into the array where the payload starts
+ * @param len the length of the array considered for the payload
+ * @param isLast if the message is the last of a series of partial messages
+ */
+ public BinaryMessage(byte[] payload, int offset, int len, boolean isLast) {
+ super(payload != null ? ByteBuffer.wrap(payload, offset, len) : null);
+ if(offset == 0 && len == payload.length) {
+ this.bytes = payload;
+ }
+ this.last = isLast;
+ }
+
+ /**
+ * Returns if this is the last part in a series of partial messages. If this is
+ * not a partial message this method will return {@code true}.
+ */
+ public boolean isLast() {
+ return this.last;
+ }
+
+ /**
+ * Returns access to the message payload as a byte array. NOTE: the returned array
+ * should be considered read-only and should not be modified.
+ */
+ public byte[] getByteArray() {
+ if(this.bytes == null && getPayload() != null) {
+ this.bytes = getRemainingBytes(getPayload());
+ }
+ return this.bytes;
+ }
+
+ private byte[] getRemainingBytes(ByteBuffer payload) {
+ byte[] result = new byte[getPayload().remaining()];
+ getPayload().get(result);
+ return result;
+ }
+
+ /**
+ * Returns access to the message payload as an {@link InputStream}.
+ */
+ public InputStream getInputStream() {
+ byte[] array = getByteArray();
+ return (array != null) ? new ByteArrayInputStream(array) : null;
+ }
+
+ @Override
+ public String toString() {
+ int size = (getPayload() != null) ? getPayload().remaining() : 0;
+ return "WebSocket binary message size=" + size;
+ }
+
+}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/CloseStatus.java b/spring-websocket/src/main/java/org/springframework/web/socket/CloseStatus.java
new file mode 100644
index 0000000000..9e63bdf8e6
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/CloseStatus.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2002-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.web.socket;
+
+import org.eclipse.jetty.websocket.api.StatusCode;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * Represents a WebSocket close status code and reason. Status codes in the 1xxx range are
+ * pre-defined by the protocol. Optionally, a status code may be sent with a reason.
+ *
+ * See RFC 6455, Section 7.4.1
+ * "Defined Status Codes".
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.0
+ */
+public final class CloseStatus {
+
+ /**
+ * "1000 indicates a normal closure, meaning that the purpose for which the connection
+ * was established has been fulfilled."
+ */
+ public static final CloseStatus NORMAL = new CloseStatus(1000);
+
+ /**
+ * "1001 indicates that an endpoint is "going away", such as a server going down or a
+ * browser having navigated away from a page."
+ */
+ public static final CloseStatus GOING_AWAY = new CloseStatus(1001);
+
+ /**
+ * "1002 indicates that an endpoint is terminating the connection due to a protocol
+ * error."
+ */
+ public static final CloseStatus PROTOCOL_ERROR = new CloseStatus(1002);
+
+ /**
+ * "1003 indicates that an endpoint is terminating the connection because it has
+ * received a type of data it cannot accept (e.g., an endpoint that understands only
+ * text data MAY send this if it receives a binary message)."
+ */
+ public static final CloseStatus NOT_ACCEPTABLE = new CloseStatus(1003);
+
+ // 10004: Reserved.
+ // The specific meaning might be defined in the future.
+
+ /**
+ * "1005 is a reserved value and MUST NOT be set as a status code in a Close control
+ * frame by an endpoint. It is designated for use in applications expecting a status
+ * code to indicate that no status code was actually present."
+ */
+ public static final CloseStatus NO_STATUS_CODE = new CloseStatus(1005);
+
+ /**
+ * "1006 is a reserved value and MUST NOT be set as a status code in a Close control
+ * frame by an endpoint. It is designated for use in applications expecting a status
+ * code to indicate that the connection was closed abnormally, e.g., without sending
+ * or receiving a Close control frame."
+ */
+ public static final CloseStatus NO_CLOSE_FRAME = new CloseStatus(1006);
+
+ /**
+ * "1007 indicates that an endpoint is terminating the connection because it has
+ * received data within a message that was not consistent with the type of the message
+ * (e.g., non-UTF-8 [RFC3629] data within a text message)."
+ */
+ public static final CloseStatus BAD_DATA = new CloseStatus(1007);
+
+ /**
+ * "1008 indicates that an endpoint is terminating the connection because it has
+ * received a message that violates its policy. This is a generic status code that can
+ * be returned when there is no other more suitable status code (e.g., 1003 or 1009)
+ * or if there is a need to hide specific details about the policy."
+ */
+ public static final CloseStatus POLICY_VIOLATION = new CloseStatus(1008);
+
+ /**
+ * "1009 indicates that an endpoint is terminating the connection because it has
+ * received a message that is too big for it to process."
+ */
+ public static final CloseStatus TOO_BIG_TO_PROCESS = new CloseStatus(1009);
+
+ /**
+ * "1010 indicates that an endpoint (client) is terminating the connection because it
+ * has expected the server to negotiate one or more extension, but the server didn't
+ * return them in the response message of the WebSocket handshake. The list of
+ * extensions that are needed SHOULD appear in the /reason/ part of the Close frame.
+ * Note that this status code is not used by the server, because it can fail the
+ * WebSocket handshake instead."
+ */
+ public static final CloseStatus REQUIRED_EXTENSION = new CloseStatus(1010);
+
+ /**
+ * "1011 indicates that a server is terminating the connection because it encountered
+ * an unexpected condition that prevented it from fulfilling the request."
+ */
+ public static final CloseStatus SERVER_ERROR = new CloseStatus(1011);
+
+ /**
+ * "1012 indicates that the service is restarted. A client may reconnect, and if it
+ * chooses to do, should reconnect using a randomized delay of 5 - 30s."
+ */
+ public static final CloseStatus SERVICE_RESTARTED = new CloseStatus(1012);
+
+ /**
+ * "1013 indicates that the service is experiencing overload. A client should only
+ * connect to a different IP (when there are multiple for the target) or reconnect to
+ * the same IP upon user action."
+ */
+ public static final CloseStatus SERVICE_OVERLOAD = new CloseStatus(1013);
+
+ /**
+ * "1015 is a reserved value and MUST NOT be set as a status code in a Close control
+ * frame by an endpoint. It is designated for use in applications expecting a status
+ * code to indicate that the connection was closed due to a failure to perform a TLS
+ * handshake (e.g., the server certificate can't be verified)."
+ */
+ public static final CloseStatus TLS_HANDSHAKE_FAILURE = new CloseStatus(1015);
+
+
+ private final int code;
+
+ private final String reason;
+
+
+ /**
+ * Create a new {@link CloseStatus} instance.
+ * @param code the status code
+ */
+ public CloseStatus(int code) {
+ this(code, null);
+ }
+
+ /**
+ * Create a new {@link CloseStatus} instance.
+ * @param code
+ * @param reason
+ */
+ public CloseStatus(int code, String reason) {
+ Assert.isTrue((code >= 1000 && code < 5000), "Invalid code");
+ this.code = code;
+ this.reason = reason;
+ }
+
+ /**
+ * Returns the status code.
+ */
+ public int getCode() {
+ return this.code;
+ }
+
+ /**
+ * Returns the reason or {@code null}.
+ */
+ public String getReason() {
+ return this.reason;
+ }
+
+ /**
+ * Crate a new {@link CloseStatus} from this one with the specified reason.
+ * @param reason the reason
+ * @return a new {@link StatusCode} instance
+ */
+ public CloseStatus withReason(String reason) {
+ Assert.hasText(reason, "Reason must not be empty");
+ return new CloseStatus(this.code, reason);
+ }
+
+ @Override
+ public int hashCode() {
+ return this.code * 29 + ObjectUtils.nullSafeHashCode(this.reason);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof CloseStatus)) {
+ return false;
+ }
+ CloseStatus otherStatus = (CloseStatus) other;
+ return (this.code == otherStatus.code && ObjectUtils.nullSafeEquals(this.reason, otherStatus.reason));
+ }
+
+ public boolean equalsCode(CloseStatus other) {
+ return this.code == other.code;
+ }
+
+ @Override
+ public String toString() {
+ return "CloseStatus [code=" + this.code + ", reason=" + this.reason + "]";
+ }
+
+}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/TextMessage.java b/spring-websocket/src/main/java/org/springframework/web/socket/TextMessage.java
new file mode 100644
index 0000000000..0bcc9f9b7c
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/TextMessage.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2002-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.web.socket;
+
+import java.io.Reader;
+import java.io.StringReader;
+
+/**
+ * A {@link WebSocketMessage} that contains a textual {@link String} payload.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.0
+ */
+public final class TextMessage extends WebSocketMessage {
+
+ /**
+ * Create a new {@link TextMessage} instance.
+ * @param payload the payload
+ */
+ public TextMessage(CharSequence payload) {
+ super(payload.toString());
+ }
+
+ /**
+ * Returns access to the message payload as a {@link Reader}.
+ */
+ public Reader getReader() {
+ return new StringReader(getPayload());
+ }
+
+}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketHandler.java
new file mode 100644
index 0000000000..ffdcdb2cf3
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketHandler.java
@@ -0,0 +1,80 @@
+/*
+ * 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;
+
+/**
+ * A handler for WebSocket messages and lifecycle events.
+ *
+ *
Implementations of this interface are encouraged to handle exceptions locally where
+ * it makes sense or alternatively let the exception bubble up in which case the exception
+ * is logged and the session closed with {@link CloseStatus#SERVER_ERROR SERVER_ERROR(1011)} by default.
+ * The exception handling strategy is provided by
+ * {@link org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator ExceptionWebSocketHandlerDecorator},
+ * which can be customized or replaced by decorating the {@link WebSocketHandler} with a
+ * different decorator.
+ *
+ * @param The type of message being handled {@link TextMessage}, {@link BinaryMessage}
+ * (or {@link WebSocketMessage} for both).
+ *
+ * @author Rossen Stoyanchev
+ * @author Phillip Webb
+ * @since 4.0
+ */
+public interface WebSocketHandler {
+
+ /**
+ * Invoked after WebSocket negotiation has succeeded and the WebSocket connection is
+ * opened and ready for use.
+ *
+ * @throws Exception this method can handle or propagate exceptions; see class-level
+ * Javadoc for details.
+ */
+ void afterConnectionEstablished(WebSocketSession session) throws Exception;
+
+ /**
+ * Invoked when a new WebSocket message arrives.
+ *
+ * @throws Exception this method can handle or propagate exceptions; see class-level
+ * Javadoc for details.
+ */
+ void handleMessage(WebSocketSession session, WebSocketMessage> message) throws Exception;
+
+ /**
+ * Handle an error from the underlying WebSocket message transport.
+ *
+ * @throws Exception this method can handle or propagate exceptions; see class-level
+ * Javadoc for details.
+ */
+ void handleTransportError(WebSocketSession session, Throwable exception) throws Exception;
+
+ /**
+ * Invoked after the WebSocket connection has been closed by either side, or after a
+ * transport error has occurred. Although the session may technically still be open,
+ * depending on the underlying implementation, sending messages at this point is
+ * discouraged and most likely will not succeed.
+ *
+ * @throws Exception this method can handle or propagate exceptions; see class-level
+ * Javadoc for details.
+ */
+ void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception;
+
+ /**
+ * Whether this WebSocketHandler wishes to receive messages broken up in parts.
+ */
+ boolean isStreaming();
+
+}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketMessage.java b/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketMessage.java
new file mode 100644
index 0000000000..329c9d2092
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketMessage.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.web.socket;
+
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * A message that can be handled or sent during a WebSocket interaction. There are only
+ * two sub-classes {@link BinaryMessage} or a {@link TextMessage} with no further
+ * sub-classing expected.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.0
+ * @see BinaryMessage
+ * @see TextMessage
+ */
+public abstract class WebSocketMessage {
+
+ private final T payload;
+
+
+ /**
+ * Create a new {@link WebSocketMessage} instance with the given payload.
+ * @param payload a non-null payload
+ */
+ WebSocketMessage(T payload) {
+ Assert.notNull(payload, "Payload must not be null");
+ this.payload = payload;
+ }
+
+
+ /**
+ * Returns the message payload. This will never be {@code null}.
+ */
+ public T getPayload() {
+ return this.payload;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + " [payload=" + this.payload + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return WebSocketMessage.class.hashCode() * 13 + ObjectUtils.nullSafeHashCode(this.payload);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof WebSocketMessage)) {
+ return false;
+ }
+ WebSocketMessage otherMessage = (WebSocketMessage) other;
+ return ObjectUtils.nullSafeEquals(this.payload, otherMessage.payload);
+ }
+
+}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketSession.java
new file mode 100644
index 0000000000..c3e119b1f4
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketSession.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.web.socket;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.security.Principal;
+
+/**
+ * Allows sending messages over a WebSocket connection as well as closing it.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.0
+ */
+public interface WebSocketSession {
+
+ /**
+ * Return a unique session identifier.
+ */
+ String getId();
+
+ /**
+ * Return the URI used to open the WebSocket connection.
+ */
+ URI getUri();
+
+ /**
+ * Return whether the underlying socket is using a secure transport.
+ */
+ boolean isSecure();
+
+ /**
+ * Return a {@link java.security.Principal} instance containing the name of the
+ * authenticated user. If the user has not been authenticated, the method returns
+ * null.
+ */
+ Principal getPrincipal();
+
+ /**
+ * Return the host name of the endpoint on the other end.
+ */
+ String getRemoteHostName();
+
+ /**
+ * Return the IP address of the endpoint on the other end.
+ */
+ String getRemoteAddress();
+
+ /**
+ * Return whether the connection is still open.
+ */
+ boolean isOpen();
+
+ /**
+ * Send a WebSocket message either {@link TextMessage} or
+ * {@link BinaryMessage}.
+ */
+ void sendMessage(WebSocketMessage> message) throws IOException;
+
+ /**
+ * Close the WebSocket connection with status 1000, i.e. equivalent to:
+ *
+ * session.close(CloseStatus.NORMAL);
+ *
+ */
+ void close() throws IOException;
+
+ /**
+ * Close the WebSocket connection with the given close status.
+ */
+ void close(CloseStatus status) throws IOException;
+
+}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/AbstractWebSocketSesssionAdapter.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/AbstractWebSocketSesssionAdapter.java
new file mode 100644
index 0000000000..606c37ac7a
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/AbstractWebSocketSesssionAdapter.java
@@ -0,0 +1,84 @@
+/*
+ *
+ * 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.adapter;
+
+import java.io.IOException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.util.Assert;
+import org.springframework.web.socket.BinaryMessage;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+
+/**
+ * An base class for implementations adapting {@link WebSocketSession}.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.0
+ */
+public abstract class AbstractWebSocketSesssionAdapter implements ConfigurableWebSocketSession {
+
+ protected final Log logger = LogFactory.getLog(getClass());
+
+
+ public abstract void initSession(T session);
+
+ @Override
+ public final void sendMessage(WebSocketMessage message) throws IOException {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Sending " + message + ", " + this);
+ }
+ Assert.isTrue(isOpen(), "Cannot send message after connection closed.");
+ if (message instanceof TextMessage) {
+ sendTextMessage((TextMessage) message);
+ }
+ else if (message instanceof BinaryMessage) {
+ sendBinaryMessage((BinaryMessage) message);
+ }
+ else {
+ throw new IllegalStateException("Unexpected WebSocketMessage type: " + message);
+ }
+ }
+
+ protected abstract void sendTextMessage(TextMessage message) throws IOException ;
+
+ protected abstract void sendBinaryMessage(BinaryMessage message) throws IOException ;
+
+ @Override
+ public void close() throws IOException {
+ close(CloseStatus.NORMAL);
+ }
+
+ @Override
+ public final void close(CloseStatus status) throws IOException {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Closing " + this);
+ }
+ closeInternal(status);
+ }
+
+ protected abstract void closeInternal(CloseStatus status) throws IOException;
+
+ @Override
+ public String toString() {
+ return "WebSocket session id=" + getId();
+ }
+
+}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/BinaryWebSocketHandlerAdapter.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/BinaryWebSocketHandlerAdapter.java
new file mode 100644
index 0000000000..3f0bef0012
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/BinaryWebSocketHandlerAdapter.java
@@ -0,0 +1,47 @@
+/*
+ * 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.adapter;
+
+import java.io.IOException;
+
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.WebSocketSession;
+
+
+/**
+ * A {@link WebSocketHandler} for binary messages with empty methods.
+ *
+ * @author Rossen Stoyanchev
+ * @author Phillip Webb
+ * @since 4.0
+ */
+public class BinaryWebSocketHandlerAdapter extends WebSocketHandlerAdapter {
+
+
+ @Override
+ protected void handleTextMessage(WebSocketSession session, TextMessage message) {
+ try {
+ session.close(CloseStatus.NOT_ACCEPTABLE.withReason("Text messages not supported"));
+ }
+ catch (IOException e) {
+ // ignore
+ }
+ }
+
+}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/ConfigurableWebSocketSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/ConfigurableWebSocketSession.java
new file mode 100644
index 0000000000..f894640a60
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/ConfigurableWebSocketSession.java
@@ -0,0 +1,39 @@
+/*
+ * 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.adapter;
+
+import java.net.URI;
+import java.security.Principal;
+
+import org.springframework.web.socket.WebSocketSession;
+
+
+/**
+ * @author Rossen Stoyanchev
+ * @since 4.0
+ */
+public interface ConfigurableWebSocketSession extends WebSocketSession {
+
+ void setUri(URI uri);
+
+ void setRemoteHostName(String name);
+
+ void setRemoteAddress(String address);
+
+ void setPrincipal(Principal principal);
+
+}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/JettyWebSocketListenerAdapter.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/JettyWebSocketListenerAdapter.java
new file mode 100644
index 0000000000..e33359ff04
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/JettyWebSocketListenerAdapter.java
@@ -0,0 +1,107 @@
+/*
+ * 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.adapter;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.WebSocketListener;
+import org.springframework.util.Assert;
+import org.springframework.web.socket.BinaryMessage;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator;
+
+/**
+ * Adapts Spring's {@link WebSocketHandler} to Jetty's {@link WebSocketListener}.
+ *
+ * @author Phillip Webb
+ * @since 4.0
+ */
+public class JettyWebSocketListenerAdapter implements WebSocketListener {
+
+ private static final Log logger = LogFactory.getLog(JettyWebSocketListenerAdapter.class);
+
+ private final WebSocketHandler webSocketHandler;
+
+ private JettyWebSocketSessionAdapter wsSession;
+
+
+ public JettyWebSocketListenerAdapter(WebSocketHandler webSocketHandler, JettyWebSocketSessionAdapter wsSession) {
+ Assert.notNull(webSocketHandler, "webSocketHandler is required");
+ Assert.notNull(wsSession, "wsSession is required");
+ this.webSocketHandler = webSocketHandler;
+ this.wsSession = wsSession;
+ }
+
+
+ @Override
+ public void onWebSocketConnect(Session session) {
+ this.wsSession.initSession(session);
+ try {
+ this.webSocketHandler.afterConnectionEstablished(this.wsSession);
+ }
+ catch (Throwable t) {
+ ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, t, logger);
+ }
+ }
+
+ @Override
+ public void onWebSocketText(String payload) {
+ TextMessage message = new TextMessage(payload);
+ try {
+ this.webSocketHandler.handleMessage(this.wsSession, message);
+ }
+ catch (Throwable t) {
+ ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, t, logger);
+ }
+ }
+
+ @Override
+ public void onWebSocketBinary(byte[] payload, int offset, int len) {
+ BinaryMessage message = new BinaryMessage(payload, offset, len);
+ try {
+ this.webSocketHandler.handleMessage(this.wsSession, message);
+ }
+ catch (Throwable t) {
+ ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, t, logger);
+ }
+ }
+
+ @Override
+ public void onWebSocketClose(int statusCode, String reason) {
+ CloseStatus closeStatus = new CloseStatus(statusCode, reason);
+ try {
+ this.webSocketHandler.afterConnectionClosed(this.wsSession, closeStatus);
+ }
+ catch (Throwable t) {
+ logger.error("Unhandled error for " + this.wsSession, t);
+ }
+ }
+
+ @Override
+ public void onWebSocketError(Throwable cause) {
+ try {
+ this.webSocketHandler.handleTransportError(this.wsSession, cause);
+ }
+ catch (Throwable t) {
+ ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, t, logger);
+ }
+ }
+
+}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/JettyWebSocketSessionAdapter.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/JettyWebSocketSessionAdapter.java
new file mode 100644
index 0000000000..8a3a4e3403
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/JettyWebSocketSessionAdapter.java
@@ -0,0 +1,124 @@
+/*
+ * 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.adapter;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.security.Principal;
+
+import org.eclipse.jetty.websocket.api.Session;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+import org.springframework.web.socket.BinaryMessage;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+
+/**
+ * Adapts Jetty's {@link Session} to Spring's {@link WebSocketSession}.
+ *
+ * @author Phillip Webb
+ * @author Rossen Stoyanchev
+ * @since 4.0
+ */
+public class JettyWebSocketSessionAdapter
+ extends AbstractWebSocketSesssionAdapter {
+
+ private Session session;
+
+ private Principal principal;
+
+
+ @Override
+ public void initSession(Session session) {
+ Assert.notNull(session, "session is required");
+ this.session = session;
+ }
+
+ @Override
+ public String getId() {
+ return ObjectUtils.getIdentityHexString(this.session);
+ }
+
+ @Override
+ public boolean isSecure() {
+ return this.session.isSecure();
+ }
+
+ @Override
+ public URI getUri() {
+ return this.session.getUpgradeRequest().getRequestURI();
+ }
+
+ @Override
+ public void setUri(URI uri) {
+ }
+
+ @Override
+ public Principal getPrincipal() {
+ return this.principal;
+ }
+
+ @Override
+ public void setPrincipal(Principal principal) {
+ this.principal = principal;
+ }
+
+ @Override
+ public String getRemoteHostName() {
+ return this.session.getRemoteAddress().getHostName();
+ }
+
+ @Override
+ public void setRemoteHostName(String address) {
+ // ignore
+ }
+
+ @Override
+ public String getRemoteAddress() {
+ InetSocketAddress address = this.session.getRemoteAddress();
+ return address.isUnresolved() ? null : address.getAddress().getHostAddress();
+ }
+
+ @Override
+ public void setRemoteAddress(String address) {
+ // ignore
+ }
+
+ @Override
+ public boolean isOpen() {
+ return this.session.isOpen();
+ }
+
+ @Override
+ protected void sendTextMessage(TextMessage message) throws IOException {
+ this.session.getRemote().sendString(message.getPayload());
+ }
+
+ @Override
+ protected void sendBinaryMessage(BinaryMessage message) throws IOException {
+ this.session.getRemote().sendBytes(message.getPayload());
+ }
+
+ @Override
+ protected void closeInternal(CloseStatus status) throws IOException {
+ this.session.close(status.getCode(), status.getReason());
+ }
+
+}
\ No newline at end of file
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/StandardEndpointAdapter.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/StandardEndpointAdapter.java
new file mode 100644
index 0000000000..7d27878015
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/StandardEndpointAdapter.java
@@ -0,0 +1,139 @@
+/*
+ * 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.adapter;
+
+import java.nio.ByteBuffer;
+
+import javax.websocket.CloseReason;
+import javax.websocket.Endpoint;
+import javax.websocket.EndpointConfig;
+import javax.websocket.MessageHandler;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.util.Assert;
+import org.springframework.web.socket.BinaryMessage;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator;
+
+
+/**
+ * A wrapper around a {@link WebSocketHandler} that adapts it to {@link Endpoint}.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.0
+ */
+public class StandardEndpointAdapter extends Endpoint {
+
+ private static final Log logger = LogFactory.getLog(StandardEndpointAdapter.class);
+
+ private final WebSocketHandler handler;
+
+ private final StandardWebSocketSessionAdapter wsSession;
+
+
+ public StandardEndpointAdapter(WebSocketHandler handler, StandardWebSocketSessionAdapter wsSession) {
+ Assert.notNull(handler, "handler is required");
+ Assert.notNull(wsSession, "wsSession is required");
+ this.handler = handler;
+ this.wsSession = wsSession;
+ }
+
+
+ @Override
+ public void onOpen(final javax.websocket.Session session, EndpointConfig config) {
+
+ this.wsSession.initSession(session);
+
+ try {
+ this.handler.afterConnectionEstablished(this.wsSession);
+ }
+ catch (Throwable t) {
+ ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, t, logger);
+ return;
+ }
+
+ session.addMessageHandler(new MessageHandler.Whole() {
+ @Override
+ public void onMessage(String message) {
+ handleTextMessage(session, message);
+ }
+ });
+
+ if (!this.handler.isStreaming()) {
+ session.addMessageHandler(new MessageHandler.Whole() {
+ @Override
+ public void onMessage(ByteBuffer message) {
+ handleBinaryMessage(session, message, true);
+ }
+ });
+ }
+ else {
+ session.addMessageHandler(new MessageHandler.Partial() {
+ @Override
+ public void onMessage(ByteBuffer messagePart, boolean isLast) {
+ handleBinaryMessage(session, messagePart, isLast);
+ }
+ });
+ }
+
+ }
+
+ private void handleTextMessage(javax.websocket.Session session, String payload) {
+ TextMessage textMessage = new TextMessage(payload);
+ try {
+ this.handler.handleMessage(this.wsSession, textMessage);
+ }
+ catch (Throwable t) {
+ ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, t, logger);
+ }
+ }
+
+ private void handleBinaryMessage(javax.websocket.Session session, ByteBuffer payload, boolean isLast) {
+ BinaryMessage binaryMessage = new BinaryMessage(payload, isLast);
+ try {
+ this.handler.handleMessage(this.wsSession, binaryMessage);
+ }
+ catch (Throwable t) {
+ ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, t, logger);
+ }
+ }
+
+ @Override
+ public void onClose(javax.websocket.Session session, CloseReason reason) {
+ CloseStatus closeStatus = new CloseStatus(reason.getCloseCode().getCode(), reason.getReasonPhrase());
+ try {
+ this.handler.afterConnectionClosed(this.wsSession, closeStatus);
+ }
+ catch (Throwable t) {
+ logger.error("Unhandled error for " + this.wsSession, t);
+ }
+ }
+
+ @Override
+ public void onError(javax.websocket.Session session, Throwable exception) {
+ try {
+ this.handler.handleTransportError(this.wsSession, exception);
+ }
+ catch (Throwable t) {
+ ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, t, logger);
+ }
+ }
+
+}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/StandardWebSocketSessionAdapter.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/StandardWebSocketSessionAdapter.java
new file mode 100644
index 0000000000..8c5ccaff90
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/StandardWebSocketSessionAdapter.java
@@ -0,0 +1,126 @@
+/*
+ * 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.adapter;
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.Principal;
+
+import javax.websocket.CloseReason;
+import javax.websocket.CloseReason.CloseCodes;
+
+import org.springframework.util.Assert;
+import org.springframework.web.socket.BinaryMessage;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+/**
+ * A standard Java implementation of {@link WebSocketSession} that delegates to
+ * {@link javax.websocket.Session}.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.0
+ */
+public class StandardWebSocketSessionAdapter extends AbstractWebSocketSesssionAdapter {
+
+ private javax.websocket.Session session;
+
+ private URI uri;
+
+ private String remoteHostName;
+
+ private String remoteAddress;
+
+
+ public void initSession(javax.websocket.Session session) {
+ Assert.notNull(session, "session is required");
+ this.session = session;
+ }
+
+ @Override
+ public String getId() {
+ return this.session.getId();
+ }
+
+ @Override
+ public URI getUri() {
+ return this.uri;
+ }
+
+ @Override
+ public void setUri(URI uri) {
+ this.uri = uri;
+ }
+
+
+ @Override
+ public boolean isSecure() {
+ return this.session.isSecure();
+ }
+
+ @Override
+ public Principal getPrincipal() {
+ return this.session.getUserPrincipal();
+ }
+
+ @Override
+ public void setPrincipal(Principal principal) {
+ // ignore
+ }
+
+ @Override
+ public String getRemoteHostName() {
+ return this.remoteHostName;
+ }
+
+ @Override
+ public void setRemoteHostName(String name) {
+ this.remoteHostName = name;
+ }
+
+ @Override
+ public String getRemoteAddress() {
+ return this.remoteAddress;
+ }
+
+ @Override
+ public void setRemoteAddress(String address) {
+ this.remoteAddress = address;
+ }
+
+ @Override
+ public boolean isOpen() {
+ return this.session.isOpen();
+ }
+
+ @Override
+ protected void sendTextMessage(TextMessage message) throws IOException {
+ this.session.getBasicRemote().sendText(message.getPayload());
+ }
+
+ @Override
+ protected void sendBinaryMessage(BinaryMessage message) throws IOException {
+ this.session.getBasicRemote().sendBinary(message.getPayload());
+ }
+
+ @Override
+ protected void closeInternal(CloseStatus status) throws IOException {
+ this.session.close(new CloseReason(CloseCodes.getCloseCode(status.getCode()), status.getReason()));
+ }
+
+}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/TextWebSocketHandlerAdapter.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/TextWebSocketHandlerAdapter.java
new file mode 100644
index 0000000000..f207885f38
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/TextWebSocketHandlerAdapter.java
@@ -0,0 +1,47 @@
+/*
+ * 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.adapter;
+
+import java.io.IOException;
+
+import org.springframework.web.socket.BinaryMessage;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.WebSocketSession;
+
+
+/**
+ * A {@link WebSocketHandler} for text messages with empty methods.
+ *
+ * @author Rossen Stoyanchev
+ * @author Phillip Webb
+ * @since 4.0
+ */
+public class TextWebSocketHandlerAdapter extends WebSocketHandlerAdapter {
+
+
+ @Override
+ protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
+ try {
+ session.close(CloseStatus.NOT_ACCEPTABLE.withReason("Binary messages not supported"));
+ }
+ catch (IOException e) {
+ // ignore
+ }
+ }
+
+}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/WebSocketHandlerAdapter.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/WebSocketHandlerAdapter.java
new file mode 100644
index 0000000000..7b97ca5876
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/WebSocketHandlerAdapter.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.web.socket.adapter;
+
+import org.springframework.web.socket.BinaryMessage;
+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.WebSocketSession;
+
+
+/**
+ * A {@link WebSocketHandler} for both text and binary messages with empty methods.
+ *
+ * @author Rossen Stoyanchev
+ * @author Phillip Webb
+ * @since 4.0
+ *
+ * @see TextWebSocketHandlerAdapter
+ * @see BinaryWebSocketHandlerAdapter
+ */
+public class WebSocketHandlerAdapter implements WebSocketHandler {
+
+ @Override
+ public void afterConnectionEstablished(WebSocketSession session) throws Exception {
+ }
+
+ @Override
+ public final void handleMessage(WebSocketSession session, WebSocketMessage> message) throws Exception {
+ if (message instanceof TextMessage) {
+ handleTextMessage(session, (TextMessage) message);
+ }
+ else if (message instanceof BinaryMessage) {
+ handleBinaryMessage(session, (BinaryMessage) message);
+ }
+ else {
+ // should not happen
+ throw new IllegalStateException("Unexpected WebSocket message type: " + message);
+ }
+ }
+
+ protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
+ }
+
+ protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
+ }
+
+ @Override
+ public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
+ }
+
+ @Override
+ public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
+ }
+
+ @Override
+ public boolean isStreaming() {
+ return false;
+ }
+
+}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/package-info.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/package-info.java
new file mode 100644
index 0000000000..c3e015ceed
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ * Classes adapting Spring's WebSocket API classes to and from various WebSocket
+ * implementations. Also contains convenient base classes for
+ * {@link org.springframework.web.socket.WebSocketHandler} implementations.
+ */
+package org.springframework.web.socket.adapter;
+
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/client/ConnectionManagerSupport.java b/spring-websocket/src/main/java/org/springframework/web/socket/client/ConnectionManagerSupport.java
new file mode 100644
index 0000000000..6cfe503d61
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/client/ConnectionManagerSupport.java
@@ -0,0 +1,178 @@
+/*
+ * 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.client;
+
+import java.net.URI;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.context.SmartLifecycle;
+import org.springframework.core.task.SimpleAsyncTaskExecutor;
+import org.springframework.core.task.TaskExecutor;
+import org.springframework.web.util.UriComponentsBuilder;
+
+/**
+ * Abstract base class for WebSocketConnection managers.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.0
+ */
+public abstract class ConnectionManagerSupport implements SmartLifecycle {
+
+ protected final Log logger = LogFactory.getLog(getClass());
+
+
+ private final URI uri;
+
+ private boolean autoStartup = false;
+
+ private boolean isRunning = false;
+
+ private int phase = Integer.MAX_VALUE;
+
+ private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("EndpointConnectionManager-");
+
+ private final Object lifecycleMonitor = new Object();
+
+
+ public ConnectionManagerSupport(String uriTemplate, Object... uriVariables) {
+ this.uri = UriComponentsBuilder.fromUriString(uriTemplate).buildAndExpand(uriVariables).encode().toUri();
+ }
+
+ /**
+ * Set whether to auto-connect to the remote endpoint after this connection manager
+ * has been initialized and the Spring context has been refreshed.
+ *
Default is "false".
+ */
+ public void setAutoStartup(boolean autoStartup) {
+ this.autoStartup = autoStartup;
+ }
+
+ /**
+ * Return the value for the 'autoStartup' property. If "true", this endpoint
+ * connection manager will connect to the remote endpoint upon a
+ * ContextRefreshedEvent.
+ */
+ public boolean isAutoStartup() {
+ return this.autoStartup;
+ }
+
+ /**
+ * Specify the phase in which a connection should be established to the remote
+ * endpoint and subsequently closed. The startup order proceeds from lowest to
+ * highest, and the shutdown order is the reverse of that. By default this value is
+ * Integer.MAX_VALUE meaning that this endpoint connection factory connects as late as
+ * possible and is closed as soon as possible.
+ */
+ public void setPhase(int phase) {
+ this.phase = phase;
+ }
+
+ /**
+ * Return the phase in which this endpoint connection factory will be auto-connected
+ * and stopped.
+ */
+ public int getPhase() {
+ return this.phase;
+ }
+
+ protected URI getUri() {
+ 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.
+ */
+ public final void start() {
+ synchronized (this.lifecycleMonitor) {
+ if (!isRunning()) {
+ 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;
+
+ public final void stop() {
+ synchronized (this.lifecycleMonitor) {
+ if (isRunning()) {
+ 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 final void stop(Runnable callback) {
+ synchronized (this.lifecycleMonitor) {
+ this.stop();
+ callback.run();
+ }
+ }
+
+}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketClient.java
new file mode 100644
index 0000000000..9bef4eefb2
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketClient.java
@@ -0,0 +1,44 @@
+/*
+ * 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.client;
+
+import java.net.URI;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.WebSocketSession;
+
+/**
+ * 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
+ *
+ * @see WebSocketConnectionManager
+ */
+public interface WebSocketClient {
+
+
+ WebSocketSession doHandshake(WebSocketHandler webSocketHandler,
+ String uriTemplate, Object... uriVariables) throws WebSocketConnectFailureException;
+
+ WebSocketSession doHandshake(WebSocketHandler webSocketHandler, HttpHeaders headers, URI uri)
+ throws WebSocketConnectFailureException;
+
+}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketConnectFailureException.java b/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketConnectFailureException.java
new file mode 100644
index 0000000000..184a3e195a
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketConnectFailureException.java
@@ -0,0 +1,37 @@
+/*
+ * 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.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/web/socket/client/WebSocketConnectionManager.java b/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketConnectionManager.java
new file mode 100644
index 0000000000..1ee44acf41
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketConnectionManager.java
@@ -0,0 +1,111 @@
+/*
+ * 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.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.web.socket.WebSocketHandler;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator;
+import org.springframework.web.socket.support.LoggingWebSocketHandlerDecorator;
+
+/**
+ * @author Rossen Stoyanchev
+ * @since 4.0
+ */
+public class WebSocketConnectionManager extends ConnectionManagerSupport {
+
+ private final WebSocketClient client;
+
+ private final WebSocketHandler webSocketHandler;
+
+ private WebSocketSession webSocketSession;
+
+ private final List subProtocols = new ArrayList();
+
+ private final boolean syncClientLifecycle;
+
+
+ public WebSocketConnectionManager(WebSocketClient client,
+ WebSocketHandler webSocketHandler, String uriTemplate, Object... uriVariables) {
+
+ super(uriTemplate, uriVariables);
+ this.client = client;
+ this.webSocketHandler = decorateWebSocketHandler(webSocketHandler);
+ this.syncClientLifecycle = ((client instanceof SmartLifecycle) && !((SmartLifecycle) client).isRunning());
+ }
+
+ /**
+ * Decorate the WebSocketHandler provided to the class constructor.
+ *
+ * By default {@link ExceptionWebSocketHandlerDecorator} and
+ * {@link LoggingWebSocketHandlerDecorator} are applied are added.
+ */
+ protected WebSocketHandler decorateWebSocketHandler(WebSocketHandler handler) {
+ handler = new ExceptionWebSocketHandlerDecorator(handler);
+ return new LoggingWebSocketHandlerDecorator(handler);
+ }
+
+ public void setSubProtocols(List subProtocols) {
+ this.subProtocols.clear();
+ if (!CollectionUtils.isEmpty(subProtocols)) {
+ this.subProtocols.addAll(subProtocols);
+ }
+ }
+
+ public List getSubProtocols() {
+ 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();
+ headers.setSecWebSocketProtocol(this.subProtocols);
+ this.webSocketSession = this.client.doHandshake(this.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/web/socket/client/endpoint/AnnotatedEndpointConnectionManager.java b/spring-websocket/src/main/java/org/springframework/web/socket/client/endpoint/AnnotatedEndpointConnectionManager.java
new file mode 100644
index 0000000000..36e713edca
--- /dev/null
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/client/endpoint/AnnotatedEndpointConnectionManager.java
@@ -0,0 +1,95 @@
+/*
+ * 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.client.endpoint;
+
+import javax.websocket.ContainerProvider;
+import javax.websocket.Session;
+import javax.websocket.WebSocketContainer;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.web.socket.client.ConnectionManagerSupport;
+import org.springframework.web.socket.support.BeanCreatingHandlerProvider;
+
+/**
+ * @author Rossen Stoyanchev
+ * @since 4.0
+ */
+public class AnnotatedEndpointConnectionManager extends ConnectionManagerSupport implements BeanFactoryAware {
+
+ private final Object endpoint;
+
+ private final BeanCreatingHandlerProvider