Merge branch 'websocket'
This commit is contained in:
commit
e7f38e5b17
|
|
@ -0,0 +1,88 @@
|
|||
|
||||
## Maven Snapshots
|
||||
|
||||
Maven snapshots of this branch are available through the Spring snapshot repository:
|
||||
|
||||
<repository>
|
||||
<id>spring-snapshots</id>
|
||||
<url>http://repo.springsource.org/snapshot</url>
|
||||
<snapshots><enabled>true</enabled></snapshots>
|
||||
<releases><enabled>false</enabled></releases>
|
||||
</repository>
|
||||
|
||||
Use version `4.0.0.WEBSOCKET-SNAPSHOT`, for example:
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
<version>4.0.0.WEBSOCKET-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
<version>4.0.0.WEBSOCKET-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-websocket</artifactId>
|
||||
<version>4.0.0.WEBSOCKET-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
### 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 <unzip_dir>/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 <unzip_dir>/glassfish4
|
||||
less `glassfish/domains/domain1/logs/server.log`
|
||||
|
||||
|
||||
36
build.gradle
36
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"
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
version=4.0.0.BUILD-SNAPSHOT
|
||||
version=4.0.0.WEBSOCKET-SNAPSHOT
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 ;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
@ -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<Cookie> cookies;
|
||||
|
||||
|
||||
public Cookies() {
|
||||
this.cookies = new ArrayList<Cookie>();
|
||||
}
|
||||
|
||||
private Cookies(Cookies cookies) {
|
||||
this.cookies = Collections.unmodifiableList(cookies.getCookies());
|
||||
}
|
||||
|
||||
public static Cookies readOnlyCookies(Cookies cookies) {
|
||||
return new Cookies(cookies);
|
||||
}
|
||||
|
||||
public List<Cookie> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<String, String>, 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<String, String>, 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<String, String>, 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<String> connection) {
|
||||
set(CONNECTION, toCommaDelimitedString(connection));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the {@code Connection} header.
|
||||
* @return the value of the header
|
||||
*/
|
||||
public List<String> 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<String, String>, Serializable
|
|||
* @param ifNoneMatchList the new value of the header
|
||||
*/
|
||||
public void setIfNoneMatch(List<String> ifNoneMatchList) {
|
||||
set(IF_NONE_MATCH, toCommaDelimitedString(ifNoneMatchList));
|
||||
}
|
||||
|
||||
private String toCommaDelimitedString(List<String> list) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (Iterator<String> iterator = ifNoneMatchList.iterator(); iterator.hasNext();) {
|
||||
for (Iterator<String> 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<String, String>, Serializable
|
|||
* @return the header value
|
||||
*/
|
||||
public List<String> getIfNoneMatch() {
|
||||
return getFirstValueAsList(IF_NONE_MATCH);
|
||||
}
|
||||
|
||||
private List<String> getFirstValueAsList(String header) {
|
||||
List<String> result = new ArrayList<String>();
|
||||
|
||||
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<String, String>, 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<String> getSecWebSocketExtensions() {
|
||||
List<String> 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<String> 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<String> 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<String> getSecWebSocketProtocol() {
|
||||
List<String> 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<String, String>, 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) {
|
||||
|
|
|
|||
|
|
@ -31,4 +31,9 @@ public interface HttpMessage {
|
|||
*/
|
||||
HttpHeaders getHeaders();
|
||||
|
||||
/**
|
||||
* TODO ..
|
||||
*/
|
||||
Cookies getCookies();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<MultiValue
|
|||
return this.os;
|
||||
}
|
||||
|
||||
public Cookies getCookies() {
|
||||
// TODO
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
private void writeHeaders() throws IOException {
|
||||
if (!this.headersWritten) {
|
||||
for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
@ -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<Runnable> timeoutHandlers = new ArrayList<Runnable>();
|
||||
|
||||
private final List<Runnable> completionHandlers = new ArrayList<Runnable>();
|
||||
|
||||
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.
|
||||
* <p>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 \"<async-supported>true</async-supported>\" 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<String, String> 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
|
||||
* <code>null</code>.
|
||||
*/
|
||||
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();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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<String, String> 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<String, String> getQueryParams() {
|
||||
if (this.queryParams == null) {
|
||||
this.queryParams = new LinkedMultiValueMap<String, String>(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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ByteBuffer> {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
* <p>
|
||||
* See <a href="https://tools.ietf.org/html/rfc6455#section-7.4.1">RFC 6455, Section 7.4.1
|
||||
* "Defined Status Codes"</a>.
|
||||
*
|
||||
* @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 + "]";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<String> {
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
*
|
||||
* <p> 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 <T> 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();
|
||||
|
||||
}
|
||||
|
|
@ -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<T> {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
* <code>null</code>.
|
||||
*/
|
||||
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:
|
||||
* <pre>
|
||||
* session.close(CloseStatus.NORMAL);
|
||||
* </pre>
|
||||
*/
|
||||
void close() throws IOException;
|
||||
|
||||
/**
|
||||
* Close the WebSocket connection with the given close status.
|
||||
*/
|
||||
void close(CloseStatus status) throws IOException;
|
||||
|
||||
}
|
||||
|
|
@ -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<T> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<org.eclipse.jetty.websocket.api.Session> {
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<String>() {
|
||||
@Override
|
||||
public void onMessage(String message) {
|
||||
handleTextMessage(session, message);
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.handler.isStreaming()) {
|
||||
session.addMessageHandler(new MessageHandler.Whole<ByteBuffer>() {
|
||||
@Override
|
||||
public void onMessage(ByteBuffer message) {
|
||||
handleBinaryMessage(session, message, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
session.addMessageHandler(new MessageHandler.Partial<ByteBuffer>() {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<javax.websocket.Session> {
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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.
|
||||
* <p>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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<String> subProtocols = new ArrayList<String>();
|
||||
|
||||
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.
|
||||
* <p>
|
||||
* 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<String> subProtocols) {
|
||||
this.subProtocols.clear();
|
||||
if (!CollectionUtils.isEmpty(subProtocols)) {
|
||||
this.subProtocols.addAll(subProtocols);
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> 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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<Object> endpointProvider;
|
||||
|
||||
private WebSocketContainer webSocketContainer = ContainerProvider.getWebSocketContainer();
|
||||
|
||||
private Session session;
|
||||
|
||||
|
||||
public AnnotatedEndpointConnectionManager(Object endpoint, String uriTemplate, Object... uriVariables) {
|
||||
super(uriTemplate, uriVariables);
|
||||
this.endpointProvider = null;
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
public AnnotatedEndpointConnectionManager(Class<?> endpointClass, String uriTemplate, Object... uriVariables) {
|
||||
super(uriTemplate, uriVariables);
|
||||
this.endpointProvider = new BeanCreatingHandlerProvider<Object>(endpointClass);
|
||||
this.endpoint = null;
|
||||
}
|
||||
|
||||
|
||||
public void setWebSocketContainer(WebSocketContainer webSocketContainer) {
|
||||
this.webSocketContainer = webSocketContainer;
|
||||
}
|
||||
|
||||
public WebSocketContainer getWebSocketContainer() {
|
||||
return this.webSocketContainer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
if (this.endpointProvider != null) {
|
||||
this.endpointProvider.setBeanFactory(beanFactory);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void openConnection() throws Exception {
|
||||
Object endpoint = (this.endpoint != null) ? this.endpoint : this.endpointProvider.getHandler();
|
||||
this.session = this.webSocketContainer.connectToServer(endpoint, getUri());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void closeConnection() throws Exception {
|
||||
try {
|
||||
if (isConnected()) {
|
||||
this.session.close();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.session = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isConnected() {
|
||||
return ((this.session != null) && this.session.isOpen());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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 java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.websocket.ClientEndpointConfig;
|
||||
import javax.websocket.ClientEndpointConfig.Configurator;
|
||||
import javax.websocket.ContainerProvider;
|
||||
import javax.websocket.Decoder;
|
||||
import javax.websocket.Encoder;
|
||||
import javax.websocket.Endpoint;
|
||||
import javax.websocket.Extension;
|
||||
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.util.Assert;
|
||||
import org.springframework.web.socket.client.ConnectionManagerSupport;
|
||||
import org.springframework.web.socket.support.BeanCreatingHandlerProvider;
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class EndpointConnectionManager extends ConnectionManagerSupport implements BeanFactoryAware {
|
||||
|
||||
private final Endpoint endpoint;
|
||||
|
||||
private final BeanCreatingHandlerProvider<Endpoint> endpointProvider;
|
||||
|
||||
private final ClientEndpointConfig.Builder configBuilder = ClientEndpointConfig.Builder.create();
|
||||
|
||||
private WebSocketContainer webSocketContainer = ContainerProvider.getWebSocketContainer();
|
||||
|
||||
private Session session;
|
||||
|
||||
|
||||
public EndpointConnectionManager(Endpoint endpoint, String uriTemplate, Object... uriVariables) {
|
||||
super(uriTemplate, uriVariables);
|
||||
Assert.notNull(endpoint, "endpoint is required");
|
||||
this.endpointProvider = null;
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
public EndpointConnectionManager(Class<? extends Endpoint> endpointClass, String uriTemplate, Object... uriVars) {
|
||||
super(uriTemplate, uriVars);
|
||||
Assert.notNull(endpointClass, "endpointClass is required");
|
||||
this.endpointProvider = new BeanCreatingHandlerProvider<Endpoint>(endpointClass);
|
||||
this.endpoint = null;
|
||||
}
|
||||
|
||||
|
||||
public void setSubProtocols(String... subprotocols) {
|
||||
this.configBuilder.preferredSubprotocols(Arrays.asList(subprotocols));
|
||||
}
|
||||
|
||||
public void setExtensions(Extension... extensions) {
|
||||
this.configBuilder.extensions(Arrays.asList(extensions));
|
||||
}
|
||||
|
||||
public void setEncoders(List<Class<? extends Encoder>> encoders) {
|
||||
this.configBuilder.encoders(encoders);
|
||||
}
|
||||
|
||||
public void setDecoders(List<Class<? extends Decoder>> decoders) {
|
||||
this.configBuilder.decoders(decoders);
|
||||
}
|
||||
|
||||
public void setConfigurator(Configurator configurator) {
|
||||
this.configBuilder.configurator(configurator);
|
||||
}
|
||||
|
||||
public void setWebSocketContainer(WebSocketContainer webSocketContainer) {
|
||||
this.webSocketContainer = webSocketContainer;
|
||||
}
|
||||
|
||||
public WebSocketContainer getWebSocketContainer() {
|
||||
return this.webSocketContainer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
if (this.endpointProvider != null) {
|
||||
this.endpointProvider.setBeanFactory(beanFactory);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void openConnection() throws Exception {
|
||||
Endpoint endpoint = (this.endpoint != null) ? this.endpoint : this.endpointProvider.getHandler();
|
||||
ClientEndpointConfig endpointConfig = this.configBuilder.build();
|
||||
this.session = getWebSocketContainer().connectToServer(endpoint, endpointConfig, getUri());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void closeConnection() throws Exception {
|
||||
try {
|
||||
if (isConnected()) {
|
||||
this.session.close();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.session = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isConnected() {
|
||||
return ((this.session != null) && this.session.isOpen());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.client.endpoint;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.websocket.ClientEndpointConfig;
|
||||
import javax.websocket.ClientEndpointConfig.Configurator;
|
||||
import javax.websocket.ContainerProvider;
|
||||
import javax.websocket.Endpoint;
|
||||
import javax.websocket.HandshakeResponse;
|
||||
import javax.websocket.WebSocketContainer;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.adapter.StandardEndpointAdapter;
|
||||
import org.springframework.web.socket.adapter.StandardWebSocketSessionAdapter;
|
||||
import org.springframework.web.socket.client.WebSocketClient;
|
||||
import org.springframework.web.socket.client.WebSocketConnectFailureException;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* A standard Java {@link WebSocketClient}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class StandardWebSocketClient implements WebSocketClient {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(StandardWebSocketClient.class);
|
||||
|
||||
private static final Set<String> EXCLUDED_HEADERS = new HashSet<String>(
|
||||
Arrays.asList("Sec-WebSocket-Accept", "Sec-WebSocket-Extensions", "Sec-WebSocket-Key",
|
||||
"Sec-WebSocket-Protocol", "Sec-WebSocket-Version"));
|
||||
|
||||
private WebSocketContainer webSocketContainer = ContainerProvider.getWebSocketContainer();
|
||||
|
||||
|
||||
public void setWebSocketContainer(WebSocketContainer container) {
|
||||
this.webSocketContainer = container;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, String uriTemplate, Object... uriVariables)
|
||||
throws WebSocketConnectFailureException {
|
||||
|
||||
UriComponents uriComponents = UriComponentsBuilder.fromUriString(uriTemplate).buildAndExpand(uriVariables).encode();
|
||||
return doHandshake(webSocketHandler, null, uriComponents);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebSocketSession doHandshake(WebSocketHandler webSocketHandler,
|
||||
final HttpHeaders httpHeaders, URI uri) throws WebSocketConnectFailureException {
|
||||
|
||||
StandardWebSocketSessionAdapter session = new StandardWebSocketSessionAdapter();
|
||||
session.setUri(uri);
|
||||
session.setRemoteHostName(uri.getHost());
|
||||
Endpoint endpoint = new StandardEndpointAdapter(webSocketHandler, session);
|
||||
|
||||
ClientEndpointConfig.Builder configBuidler = ClientEndpointConfig.Builder.create();
|
||||
if (httpHeaders != null) {
|
||||
List<String> protocols = httpHeaders.getSecWebSocketProtocol();
|
||||
if (!protocols.isEmpty()) {
|
||||
configBuidler.preferredSubprotocols(protocols);
|
||||
}
|
||||
configBuidler.configurator(new Configurator() {
|
||||
@Override
|
||||
public void beforeRequest(Map<String, List<String>> headers) {
|
||||
for (String headerName : httpHeaders.keySet()) {
|
||||
if (!EXCLUDED_HEADERS.contains(headerName)) {
|
||||
List<String> value = httpHeaders.get(headerName);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Adding header [" + headerName + "=" + value + "]");
|
||||
}
|
||||
headers.put(headerName, value);
|
||||
}
|
||||
}
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Handshake request headers: " + headers);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void afterResponse(HandshakeResponse handshakeResponse) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Handshake response headers: " + handshakeResponse.getHeaders());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: do not block
|
||||
this.webSocketContainer.connectToServer(endpoint, configBuidler.build(), uri);
|
||||
return session;
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new WebSocketConnectFailureException("Failed to connect to " + uri, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.client.endpoint;
|
||||
|
||||
import javax.websocket.ContainerProvider;
|
||||
import javax.websocket.WebSocketContainer;
|
||||
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
|
||||
/**
|
||||
* A FactoryBean for creating and configuring a {@link javax.websocket.WebSocketContainer}
|
||||
* through Spring XML configuration. In Java configuration, ignore this class and use
|
||||
* {@code ContainerProvider.getWebSocketContainer()} instead.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class WebSocketContainerFactoryBean implements FactoryBean<WebSocketContainer> {
|
||||
|
||||
private final WebSocketContainer webSocketContainer = ContainerProvider.getWebSocketContainer();
|
||||
|
||||
|
||||
public void setAsyncSendTimeout(long timeoutInMillis) {
|
||||
this.webSocketContainer.setAsyncSendTimeout(timeoutInMillis);
|
||||
}
|
||||
|
||||
public long getAsyncSendTimeout() {
|
||||
return this.webSocketContainer.getDefaultAsyncSendTimeout();
|
||||
}
|
||||
|
||||
public void setMaxSessionIdleTimeout(long timeoutInMillis) {
|
||||
this.webSocketContainer.setDefaultMaxSessionIdleTimeout(timeoutInMillis);
|
||||
}
|
||||
|
||||
public long getMaxSessionIdleTimeout() {
|
||||
return this.webSocketContainer.getDefaultMaxSessionIdleTimeout();
|
||||
}
|
||||
|
||||
public void setMaxTextMessageBufferSize(int bufferSize) {
|
||||
this.webSocketContainer.setDefaultMaxTextMessageBufferSize(bufferSize);
|
||||
}
|
||||
|
||||
public int getMaxTextMessageBufferSize() {
|
||||
return this.webSocketContainer.getDefaultMaxTextMessageBufferSize();
|
||||
}
|
||||
|
||||
public void setMaxBinaryMessageBufferSize(int bufferSize) {
|
||||
this.webSocketContainer.setDefaultMaxBinaryMessageBufferSize(bufferSize);
|
||||
}
|
||||
|
||||
public int getMaxBinaryMessageBufferSize() {
|
||||
return this.webSocketContainer.getDefaultMaxBinaryMessageBufferSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebSocketContainer getObject() throws Exception {
|
||||
return this.webSocketContainer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return WebSocketContainer.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSingleton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Client-side classes for use with standard Java WebSocket endpoints including
|
||||
* {@link org.springframework.web.socket.client.endpoint.EndpointConnectionManager} and
|
||||
* {@link org.springframework.web.socket.client.endpoint.AnnotatedEndpointConnectionManager}
|
||||
* for connecting to server endpoints using type-based or annotated endpoints respectively.
|
||||
*/
|
||||
package org.springframework.web.socket.client.endpoint;
|
||||
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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.jetty;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.adapter.JettyWebSocketListenerAdapter;
|
||||
import org.springframework.web.socket.adapter.JettyWebSocketSessionAdapter;
|
||||
import org.springframework.web.socket.client.WebSocketClient;
|
||||
import org.springframework.web.socket.client.WebSocketConnectFailureException;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class JettyWebSocketClient implements WebSocketClient, SmartLifecycle {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(JettyWebSocketClient.class);
|
||||
|
||||
private final org.eclipse.jetty.websocket.client.WebSocketClient client;
|
||||
|
||||
private boolean autoStartup = true;
|
||||
|
||||
private int phase = Integer.MAX_VALUE;
|
||||
|
||||
private final Object lifecycleMonitor = new Object();
|
||||
|
||||
|
||||
public JettyWebSocketClient() {
|
||||
this.client = new org.eclipse.jetty.websocket.client.WebSocketClient();
|
||||
}
|
||||
|
||||
|
||||
// TODO: configure Jetty WebSocketClient properties
|
||||
|
||||
public void setAutoStartup(boolean autoStartup) {
|
||||
this.autoStartup = autoStartup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutoStartup() {
|
||||
return this.autoStartup;
|
||||
}
|
||||
|
||||
public void setPhase(int phase) {
|
||||
this.phase = phase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPhase() {
|
||||
return this.phase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
return this.client.isStarted();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
if (!isRunning()) {
|
||||
try {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Starting Jetty WebSocketClient");
|
||||
}
|
||||
this.client.start();
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to start Jetty client", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
if (isRunning()) {
|
||||
try {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Stopping Jetty WebSocketClient");
|
||||
}
|
||||
this.client.stop();
|
||||
}
|
||||
catch (Exception e) {
|
||||
logger.error("Error stopping Jetty WebSocketClient", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(Runnable callback) {
|
||||
this.stop();
|
||||
callback.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, String uriTemplate, Object... uriVariables)
|
||||
throws WebSocketConnectFailureException {
|
||||
|
||||
UriComponents uriComponents = UriComponentsBuilder.fromUriString(uriTemplate).buildAndExpand(uriVariables).encode();
|
||||
return doHandshake(webSocketHandler, null, uriComponents);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, HttpHeaders headers, URI uri)
|
||||
throws WebSocketConnectFailureException {
|
||||
|
||||
// TODO: populate headers
|
||||
|
||||
JettyWebSocketSessionAdapter session = new JettyWebSocketSessionAdapter();
|
||||
session.setUri(uri);
|
||||
session.setRemoteHostName(uri.getHost());
|
||||
|
||||
JettyWebSocketListenerAdapter listener = new JettyWebSocketListenerAdapter(webSocketHandler, session);
|
||||
|
||||
try {
|
||||
// TODO: do not block
|
||||
this.client.connect(listener, uri).get();
|
||||
return session;
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new WebSocketConnectFailureException("Failed to connect to " + uri, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Server-side abstractions for WebSocket applications.
|
||||
*/
|
||||
package org.springframework.web.socket.client;
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Common abstractions and Spring configuration support for WebSocket applications.
|
||||
*/
|
||||
package org.springframework.web.socket;
|
||||
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
/*
|
||||
* 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.server;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.xml.bind.DatatypeConverter;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* <p>
|
||||
* A container-specific {@link RequestUpgradeStrategy} is required since standard Java
|
||||
* WebSocket currently does not provide a way to initiate a WebSocket handshake.
|
||||
* Currently available are implementations for Tomcat and GlassFish.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class DefaultHandshakeHandler implements HandshakeHandler {
|
||||
|
||||
private static final String GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
|
||||
protected Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private List<String> supportedProtocols = new ArrayList<String>();
|
||||
|
||||
private RequestUpgradeStrategy requestUpgradeStrategy;
|
||||
|
||||
|
||||
/**
|
||||
* Default constructor that auto-detects and instantiates a
|
||||
* {@link RequestUpgradeStrategy} suitable for the runtime container.
|
||||
*
|
||||
* @throws IllegalStateException if no {@link RequestUpgradeStrategy} can be found.
|
||||
*/
|
||||
public DefaultHandshakeHandler() {
|
||||
this.requestUpgradeStrategy = new RequestUpgradeStrategyFactory().create();
|
||||
}
|
||||
|
||||
/**
|
||||
* A constructor that accepts a runtime specific {@link RequestUpgradeStrategy}.
|
||||
* @param upgradeStrategy the upgrade strategy
|
||||
*/
|
||||
public DefaultHandshakeHandler(RequestUpgradeStrategy upgradeStrategy) {
|
||||
this.requestUpgradeStrategy = upgradeStrategy;
|
||||
}
|
||||
|
||||
|
||||
public void setSupportedProtocols(String... protocols) {
|
||||
this.supportedProtocols = Arrays.asList(protocols);
|
||||
}
|
||||
|
||||
public String[] getSupportedProtocols() {
|
||||
return this.supportedProtocols.toArray(new String[this.supportedProtocols.size()]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean doHandshake(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSocketHandler webSocketHandler) throws IOException, HandshakeFailureException {
|
||||
|
||||
logger.debug("Starting handshake for " + request.getURI());
|
||||
|
||||
if (!HttpMethod.GET.equals(request.getMethod())) {
|
||||
response.setStatusCode(HttpStatus.METHOD_NOT_ALLOWED);
|
||||
response.getHeaders().setAllow(Collections.singleton(HttpMethod.GET));
|
||||
logger.debug("Only HTTP GET is allowed, current method is " + request.getMethod());
|
||||
return false;
|
||||
}
|
||||
if (!"WebSocket".equalsIgnoreCase(request.getHeaders().getUpgrade())) {
|
||||
handleInvalidUpgradeHeader(request, response);
|
||||
return false;
|
||||
}
|
||||
if (!request.getHeaders().getConnection().contains("Upgrade") &&
|
||||
!request.getHeaders().getConnection().contains("upgrade")) {
|
||||
handleInvalidConnectHeader(request, response);
|
||||
return false;
|
||||
}
|
||||
if (!isWebSocketVersionSupported(request)) {
|
||||
handleWebSocketVersionNotSupported(request, response);
|
||||
return false;
|
||||
}
|
||||
if (!isValidOrigin(request)) {
|
||||
response.setStatusCode(HttpStatus.FORBIDDEN);
|
||||
return false;
|
||||
}
|
||||
String wsKey = request.getHeaders().getSecWebSocketKey();
|
||||
if (wsKey == null) {
|
||||
logger.debug("Missing \"Sec-WebSocket-Key\" header");
|
||||
response.setStatusCode(HttpStatus.BAD_REQUEST);
|
||||
return false;
|
||||
}
|
||||
|
||||
String selectedProtocol = selectProtocol(request.getHeaders().getSecWebSocketProtocol());
|
||||
// TODO: select extensions
|
||||
|
||||
logger.debug("Upgrading HTTP request");
|
||||
|
||||
response.setStatusCode(HttpStatus.SWITCHING_PROTOCOLS);
|
||||
response.getHeaders().setUpgrade("WebSocket");
|
||||
response.getHeaders().setConnection("Upgrade");
|
||||
response.getHeaders().setSecWebSocketProtocol(selectedProtocol);
|
||||
response.getHeaders().setSecWebSocketAccept(getWebSocketKeyHash(wsKey));
|
||||
// TODO: response.getHeaders().setSecWebSocketExtensions(extensions);
|
||||
|
||||
response.flush();
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Upgrading with " + webSocketHandler);
|
||||
}
|
||||
|
||||
this.requestUpgradeStrategy.upgrade(request, response, selectedProtocol, webSocketHandler);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void handleInvalidUpgradeHeader(ServerHttpRequest request, ServerHttpResponse response) throws IOException {
|
||||
logger.debug("Invalid Upgrade header " + request.getHeaders().getUpgrade());
|
||||
response.setStatusCode(HttpStatus.BAD_REQUEST);
|
||||
response.getBody().write("Can \"Upgrade\" only to \"WebSocket\".".getBytes("UTF-8"));
|
||||
}
|
||||
|
||||
protected void handleInvalidConnectHeader(ServerHttpRequest request, ServerHttpResponse response) throws IOException {
|
||||
logger.debug("Invalid Connection header " + request.getHeaders().getConnection());
|
||||
response.setStatusCode(HttpStatus.BAD_REQUEST);
|
||||
response.getBody().write("\"Connection\" must be \"upgrade\".".getBytes("UTF-8"));
|
||||
}
|
||||
|
||||
protected boolean isWebSocketVersionSupported(ServerHttpRequest request) {
|
||||
String requestedVersion = request.getHeaders().getSecWebSocketVersion();
|
||||
for (String supportedVersion : getSupportedVerions()) {
|
||||
if (supportedVersion.equals(requestedVersion)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected String[] getSupportedVerions() {
|
||||
return this.requestUpgradeStrategy.getSupportedVersions();
|
||||
}
|
||||
|
||||
protected void handleWebSocketVersionNotSupported(ServerHttpRequest request, ServerHttpResponse response) {
|
||||
logger.debug("WebSocket version not supported " + request.getHeaders().get("Sec-WebSocket-Version"));
|
||||
response.setStatusCode(HttpStatus.UPGRADE_REQUIRED);
|
||||
response.getHeaders().setSecWebSocketVersion(StringUtils.arrayToCommaDelimitedString(getSupportedVerions()));
|
||||
}
|
||||
|
||||
protected boolean isValidOrigin(ServerHttpRequest request) {
|
||||
String origin = request.getHeaders().getOrigin();
|
||||
if (origin != null) {
|
||||
// UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(origin);
|
||||
// TODO
|
||||
// A simple strategy checks against the current request's scheme/port/host
|
||||
// Or match scheme, port, and host against configured allowed origins (wild cards for hosts?)
|
||||
// return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected String selectProtocol(List<String> requestedProtocols) {
|
||||
if (CollectionUtils.isEmpty(requestedProtocols)) {
|
||||
for (String protocol : requestedProtocols) {
|
||||
if (this.supportedProtocols.contains(protocol)) {
|
||||
return protocol;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getWebSocketKeyHash(String key) throws HandshakeFailureException {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA1");
|
||||
byte[] bytes = digest.digest((key + GUID).getBytes(Charset.forName("ISO-8859-1")));
|
||||
return DatatypeConverter.printBase64Binary(bytes);
|
||||
}
|
||||
catch (NoSuchAlgorithmException ex) {
|
||||
throw new HandshakeFailureException("Failed to generate value for Sec-WebSocket-Key header", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class RequestUpgradeStrategyFactory {
|
||||
|
||||
private static final boolean tomcatWebSocketPresent = ClassUtils.isPresent(
|
||||
"org.apache.tomcat.websocket.server.WsHttpUpgradeHandler", DefaultHandshakeHandler.class.getClassLoader());
|
||||
|
||||
private static final boolean glassFishWebSocketPresent = ClassUtils.isPresent(
|
||||
"org.glassfish.tyrus.servlet.TyrusHttpUpgradeHandler", DefaultHandshakeHandler.class.getClassLoader());
|
||||
|
||||
private static final boolean jettyWebSocketPresent = ClassUtils.isPresent(
|
||||
"org.eclipse.jetty.websocket.server.UpgradeContext", DefaultHandshakeHandler.class.getClassLoader());
|
||||
|
||||
private RequestUpgradeStrategy create() {
|
||||
String className;
|
||||
if (tomcatWebSocketPresent) {
|
||||
className = "org.springframework.web.socket.server.support.TomcatRequestUpgradeStrategy";
|
||||
}
|
||||
else if (glassFishWebSocketPresent) {
|
||||
className = "org.springframework.web.socket.server.support.GlassFishRequestUpgradeStrategy";
|
||||
}
|
||||
else if (jettyWebSocketPresent) {
|
||||
className = "org.springframework.web.socket.server.support.JettyRequestUpgradeStrategy";
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException("No suitable " + RequestUpgradeStrategy.class.getSimpleName());
|
||||
}
|
||||
try {
|
||||
Class<?> clazz = ClassUtils.forName(className, DefaultHandshakeHandler.class.getClassLoader());
|
||||
return (RequestUpgradeStrategy) BeanUtils.instantiateClass(clazz.getConstructor());
|
||||
}
|
||||
catch (Throwable t) {
|
||||
throw new IllegalStateException("Failed to instantiate " + className, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.server;
|
||||
|
||||
import org.springframework.core.NestedRuntimeException;
|
||||
|
||||
|
||||
/**
|
||||
* Thrown when handshake processing failed to complete due to an internal, unrecoverable
|
||||
* error. This implies a server error (HTTP status code 500) as opposed to a failure in
|
||||
* the handshake negotiation.
|
||||
*
|
||||
* <p>
|
||||
* By contrast, when handshake negotiation fails, the response status code will be 200 and
|
||||
* the response headers and body will have been updated to reflect the cause for the
|
||||
* failure. A {@link HandshakeHandler} implementation will have protected methods to
|
||||
* customize updates to the response in those cases.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class HandshakeFailureException extends NestedRuntimeException {
|
||||
|
||||
|
||||
public HandshakeFailureException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
|
||||
public HandshakeFailureException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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.server;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
|
||||
/**
|
||||
* Contract for processing a WebSocket handshake request.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface HandshakeHandler {
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @param webSocketHandler
|
||||
* @return
|
||||
*
|
||||
* @throws IOException thrown when accessing or setting the response
|
||||
*
|
||||
* @throws HandshakeFailureException thrown when handshake processing failed to
|
||||
* complete due to an internal, unrecoverable error, i.e. a server error as
|
||||
* opposed to a failure to successfully negotiate the requirements of the
|
||||
* handshake request.
|
||||
*/
|
||||
boolean doHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler webSocketHandler)
|
||||
throws IOException, HandshakeFailureException;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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.server;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
|
||||
/**
|
||||
* A strategy for performing container-specific steps to upgrade an HTTP request during a
|
||||
* WebSocket handshake. Intended for use within {@link HandshakeHandler} implementations.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface RequestUpgradeStrategy {
|
||||
|
||||
/**
|
||||
* Return the supported WebSocket protocol versions.
|
||||
*/
|
||||
String[] getSupportedVersions();
|
||||
|
||||
/**
|
||||
* Perform runtime specific steps to complete the upgrade. Invoked after successful
|
||||
* negotiation of the handshake request.
|
||||
*
|
||||
* @param webSocketHandler the handler for WebSocket messages
|
||||
*
|
||||
* @throws HandshakeFailureException thrown when handshake processing failed to
|
||||
* complete due to an internal, unrecoverable error, i.e. a server error as
|
||||
* opposed to a failure to successfully negotiate the requirements of the
|
||||
* handshake request.
|
||||
*/
|
||||
void upgrade(ServerHttpRequest request, ServerHttpResponse response, String selectedProtocol,
|
||||
WebSocketHandler webSocketHandler) throws IOException, HandshakeFailureException;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* 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.server.endpoint;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.websocket.DeploymentException;
|
||||
import javax.websocket.server.ServerContainer;
|
||||
import javax.websocket.server.ServerEndpoint;
|
||||
import javax.websocket.server.ServerEndpointConfig;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* BeanPostProcessor that detects beans of type
|
||||
* {@link javax.websocket.server.ServerEndpointConfig} and registers the provided
|
||||
* {@link javax.websocket.Endpoint} with a standard Java WebSocket runtime.
|
||||
*
|
||||
* <p>If the runtime is a Servlet container, use {@link ServletEndpointExporter}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class EndpointExporter implements InitializingBean, BeanPostProcessor, ApplicationContextAware {
|
||||
|
||||
private static final boolean isServletApiPresent =
|
||||
ClassUtils.isPresent("javax.servlet.ServletContext", EndpointExporter.class.getClassLoader());
|
||||
|
||||
private static Log logger = LogFactory.getLog(EndpointExporter.class);
|
||||
|
||||
private final List<Class<?>> annotatedEndpointClasses = new ArrayList<Class<?>>();
|
||||
|
||||
private final List<Class<?>> annotatedEndpointBeanTypes = new ArrayList<Class<?>>();
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private ServerContainer serverContainer;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* @param annotatedEndpointClasses
|
||||
*/
|
||||
public void setAnnotatedEndpointClasses(Class<?>... annotatedEndpointClasses) {
|
||||
this.annotatedEndpointClasses.clear();
|
||||
this.annotatedEndpointClasses.addAll(Arrays.asList(annotatedEndpointClasses));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
|
||||
this.applicationContext = applicationContext;
|
||||
|
||||
this.serverContainer = getServerContainer();
|
||||
|
||||
Map<String, Object> beans = applicationContext.getBeansWithAnnotation(ServerEndpoint.class);
|
||||
for (String beanName : beans.keySet()) {
|
||||
Class<?> beanType = applicationContext.getType(beanName);
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Detected @ServerEndpoint bean '" + beanName + "', registering it as an endpoint by type");
|
||||
}
|
||||
this.annotatedEndpointBeanTypes.add(beanType);
|
||||
}
|
||||
}
|
||||
|
||||
protected ServerContainer getServerContainer() {
|
||||
if (isServletApiPresent) {
|
||||
try {
|
||||
Method getter = ReflectionUtils.findMethod(this.applicationContext.getClass(), "getServletContext");
|
||||
Object servletContext = getter.invoke(this.applicationContext);
|
||||
|
||||
Method attrMethod = ReflectionUtils.findMethod(servletContext.getClass(), "getAttribute", String.class);
|
||||
return (ServerContainer) attrMethod.invoke(servletContext, "javax.websocket.server.ServerContainer");
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(
|
||||
"Failed to get javax.websocket.server.ServerContainer via ServletContext attribute", ex);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
|
||||
Assert.notNull(serverContainer, "javax.websocket.server.ServerContainer not available");
|
||||
|
||||
List<Class<?>> allClasses = new ArrayList<Class<?>>(this.annotatedEndpointClasses);
|
||||
allClasses.addAll(this.annotatedEndpointBeanTypes);
|
||||
|
||||
for (Class<?> clazz : allClasses) {
|
||||
try {
|
||||
logger.info("Registering @ServerEndpoint type " + clazz);
|
||||
this.serverContainer.addEndpoint(clazz);
|
||||
}
|
||||
catch (DeploymentException e) {
|
||||
throw new IllegalStateException("Failed to register @ServerEndpoint type " + clazz, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof ServerEndpointConfig) {
|
||||
ServerEndpointConfig sec = (ServerEndpointConfig) bean;
|
||||
try {
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Registering bean '" + beanName
|
||||
+ "' as javax.websocket.Endpoint under path " + sec.getPath());
|
||||
}
|
||||
getServerContainer().addEndpoint(sec);
|
||||
}
|
||||
catch (DeploymentException e) {
|
||||
throw new IllegalStateException("Failed to deploy Endpoint bean " + bean, e);
|
||||
}
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
|
||||
return bean;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
* 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.server.endpoint;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.websocket.Decoder;
|
||||
import javax.websocket.Encoder;
|
||||
import javax.websocket.Endpoint;
|
||||
import javax.websocket.Extension;
|
||||
import javax.websocket.HandshakeResponse;
|
||||
import javax.websocket.server.HandshakeRequest;
|
||||
import javax.websocket.server.ServerEndpointConfig;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.socket.support.BeanCreatingHandlerProvider;
|
||||
|
||||
|
||||
/**
|
||||
* An implementation of {@link javax.websocket.server.ServerEndpointConfig} that also
|
||||
* holds the target {@link javax.websocket.Endpoint} as a reference or a bean name.
|
||||
*
|
||||
* <p>
|
||||
* Beans of this type are detected by {@link EndpointExporter} and
|
||||
* registered with a Java WebSocket runtime at startup.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class EndpointRegistration implements ServerEndpointConfig, BeanFactoryAware {
|
||||
|
||||
private final String path;
|
||||
|
||||
private final BeanCreatingHandlerProvider<Endpoint> endpointProvider;
|
||||
|
||||
private final Endpoint endpoint;
|
||||
|
||||
private List<Class<? extends Encoder>> encoders = new ArrayList<Class<? extends Encoder>>();
|
||||
|
||||
private List<Class<? extends Decoder>> decoders = new ArrayList<Class<? extends Decoder>>();
|
||||
|
||||
private List<String> subprotocols = new ArrayList<String>();
|
||||
|
||||
private List<Extension> extensions = new ArrayList<Extension>();
|
||||
|
||||
private final Map<String, Object> userProperties = new HashMap<String, Object>();
|
||||
|
||||
private Configurator configurator = new Configurator() {};
|
||||
|
||||
|
||||
/**
|
||||
* Class constructor with the {@code javax.webscoket.Endpoint} class.
|
||||
*
|
||||
* @param path
|
||||
* @param endpointClass
|
||||
*/
|
||||
public EndpointRegistration(String path, Class<? extends Endpoint> endpointClass) {
|
||||
Assert.hasText(path, "path must not be empty");
|
||||
Assert.notNull(endpointClass, "endpointClass is required");
|
||||
this.path = path;
|
||||
this.endpointProvider = new BeanCreatingHandlerProvider<Endpoint>(endpointClass);
|
||||
this.endpoint = null;
|
||||
}
|
||||
|
||||
public EndpointRegistration(String path, Endpoint endpoint) {
|
||||
Assert.hasText(path, "path must not be empty");
|
||||
Assert.notNull(endpoint, "endpoint is required");
|
||||
this.path = path;
|
||||
this.endpointProvider = null;
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getPath() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Endpoint> getEndpointClass() {
|
||||
return (this.endpoint != null) ?
|
||||
this.endpoint.getClass() : ((Class<? extends Endpoint>) this.endpointProvider.getHandlerType());
|
||||
}
|
||||
|
||||
public Endpoint getEndpoint() {
|
||||
return (this.endpoint != null) ? this.endpoint : this.endpointProvider.getHandler();
|
||||
}
|
||||
|
||||
public void setSubprotocols(List<String> subprotocols) {
|
||||
this.subprotocols = subprotocols;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getSubprotocols() {
|
||||
return this.subprotocols;
|
||||
}
|
||||
|
||||
public void setExtensions(List<Extension> extensions) {
|
||||
this.extensions = extensions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Extension> getExtensions() {
|
||||
return this.extensions;
|
||||
}
|
||||
|
||||
public void setUserProperties(Map<String, Object> userProperties) {
|
||||
this.userProperties.clear();
|
||||
this.userProperties.putAll(userProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getUserProperties() {
|
||||
return this.userProperties;
|
||||
}
|
||||
|
||||
public void setEncoders(List<Class<? extends Encoder>> encoders) {
|
||||
this.encoders = encoders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<? extends Encoder>> getEncoders() {
|
||||
return this.encoders;
|
||||
}
|
||||
|
||||
public void setDecoders(List<Class<? extends Decoder>> decoders) {
|
||||
this.decoders = decoders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<? extends Decoder>> getDecoders() {
|
||||
return this.decoders;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link Configurator#getEndpointInstance(Class)} method is always ignored.
|
||||
*/
|
||||
public void setConfigurator(Configurator configurator) {
|
||||
this.configurator = configurator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Configurator getConfigurator() {
|
||||
return new Configurator() {
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T getEndpointInstance(Class<T> clazz) throws InstantiationException {
|
||||
return (T) EndpointRegistration.this.getEndpoint();
|
||||
}
|
||||
@Override
|
||||
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
|
||||
EndpointRegistration.this.configurator.modifyHandshake(sec, request, response);
|
||||
}
|
||||
@Override
|
||||
public boolean checkOrigin(String originHeaderValue) {
|
||||
return EndpointRegistration.this.configurator.checkOrigin(originHeaderValue);
|
||||
}
|
||||
@Override
|
||||
public String getNegotiatedSubprotocol(List<String> supported, List<String> requested) {
|
||||
return EndpointRegistration.this.configurator.getNegotiatedSubprotocol(supported, requested);
|
||||
}
|
||||
@Override
|
||||
public List<Extension> getNegotiatedExtensions(List<Extension> installed, List<Extension> requested) {
|
||||
return EndpointRegistration.this.configurator.getNegotiatedExtensions(installed, requested);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
if (this.endpointProvider != null) {
|
||||
this.endpointProvider.setBeanFactory(beanFactory);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.server.endpoint;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.websocket.WebSocketContainer;
|
||||
import javax.websocket.server.ServerContainer;
|
||||
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.context.ServletContextAware;
|
||||
import org.springframework.web.socket.server.DefaultHandshakeHandler;
|
||||
import org.springframework.web.socket.sockjs.SockJsService;
|
||||
|
||||
/**
|
||||
* A FactoryBean for {@link javax.websocket.server.ServerContainer}. Since
|
||||
* there is only one {@code ServerContainer} instance accessible under a well-known
|
||||
* {@code javax.servlet.ServletContext} attribute, simply declaring this FactoryBean and
|
||||
* using its setters allows configuring the {@code ServerContainer} through Spring
|
||||
* configuration. This is useful even if the ServerContainer is not injected into any
|
||||
* other bean. For example, an application can configure a {@link DefaultHandshakeHandler}
|
||||
* , a {@link SockJsService}, or {@link EndpointExporter}, and separately declare this
|
||||
* FactoryBean in order to customize the properties of the (one and only)
|
||||
* {@code ServerContainer} instance.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class ServletServerContainerFactoryBean
|
||||
implements FactoryBean<WebSocketContainer>, InitializingBean, ServletContextAware {
|
||||
|
||||
private static final String SERVER_CONTAINER_ATTR_NAME = "javax.websocket.server.ServerContainer";
|
||||
|
||||
|
||||
private Long asyncSendTimeout;
|
||||
|
||||
private Long maxSessionIdleTimeout;
|
||||
|
||||
private Integer maxTextMessageBufferSize;
|
||||
|
||||
private Integer maxBinaryMessageBufferSize;
|
||||
|
||||
private ServerContainer serverContainer;
|
||||
|
||||
|
||||
public void setAsyncSendTimeout(long timeoutInMillis) {
|
||||
this.asyncSendTimeout = timeoutInMillis;
|
||||
}
|
||||
|
||||
public long getAsyncSendTimeout() {
|
||||
return this.asyncSendTimeout;
|
||||
}
|
||||
|
||||
public void setMaxSessionIdleTimeout(long timeoutInMillis) {
|
||||
this.maxSessionIdleTimeout = timeoutInMillis;
|
||||
}
|
||||
|
||||
public Long getMaxSessionIdleTimeout() {
|
||||
return this.maxSessionIdleTimeout;
|
||||
}
|
||||
|
||||
public void setMaxTextMessageBufferSize(int bufferSize) {
|
||||
this.maxTextMessageBufferSize = bufferSize;
|
||||
}
|
||||
|
||||
public Integer getMaxTextMessageBufferSize() {
|
||||
return this.maxTextMessageBufferSize;
|
||||
}
|
||||
|
||||
public void setMaxBinaryMessageBufferSize(int bufferSize) {
|
||||
this.maxBinaryMessageBufferSize = bufferSize;
|
||||
}
|
||||
|
||||
public Integer getMaxBinaryMessageBufferSize() {
|
||||
return this.maxBinaryMessageBufferSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setServletContext(ServletContext servletContext) {
|
||||
this.serverContainer = (ServerContainer) servletContext.getAttribute(SERVER_CONTAINER_ATTR_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerContainer getObject() {
|
||||
return this.serverContainer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return ServerContainer.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSingleton() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
|
||||
Assert.notNull(this.serverContainer,
|
||||
"A ServletContext is required to access the javax.websocket.server.ServerContainer instance");
|
||||
|
||||
if (this.asyncSendTimeout != null) {
|
||||
this.serverContainer.setAsyncSendTimeout(this.asyncSendTimeout);
|
||||
}
|
||||
if (this.maxSessionIdleTimeout != null) {
|
||||
this.serverContainer.setDefaultMaxSessionIdleTimeout(this.maxSessionIdleTimeout);
|
||||
}
|
||||
if (this.maxTextMessageBufferSize != null) {
|
||||
this.serverContainer.setDefaultMaxTextMessageBufferSize(this.maxTextMessageBufferSize);
|
||||
}
|
||||
if (this.maxBinaryMessageBufferSize != null) {
|
||||
this.serverContainer.setDefaultMaxBinaryMessageBufferSize(this.maxBinaryMessageBufferSize);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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.server.endpoint;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.websocket.server.ServerEndpoint;
|
||||
import javax.websocket.server.ServerEndpointConfig.Configurator;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.web.context.ContextLoader;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
/**
|
||||
* This should be used in conjuction with {@link ServerEndpoint @ServerEndpoint} classes.
|
||||
*
|
||||
* <p>For {@link javax.websocket.Endpoint}, see {@link EndpointExporter}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class SpringConfigurator extends Configurator {
|
||||
|
||||
private static Log logger = LogFactory.getLog(SpringConfigurator.class);
|
||||
|
||||
|
||||
@Override
|
||||
public <T> T getEndpointInstance(Class<T> endpointClass) throws InstantiationException {
|
||||
|
||||
WebApplicationContext wac = ContextLoader.getCurrentWebApplicationContext();
|
||||
if (wac == null) {
|
||||
String message = "Failed to find the root WebApplicationContext. Was ContextLoaderListener not used?";
|
||||
logger.error(message);
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
|
||||
Map<String, T> beans = wac.getBeansOfType(endpointClass);
|
||||
if (beans.isEmpty()) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Creating new @ServerEndpoint instance of type " + endpointClass);
|
||||
}
|
||||
return wac.getAutowireCapableBeanFactory().createBean(endpointClass);
|
||||
}
|
||||
if (beans.size() == 1) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Using @ServerEndpoint singleton " + beans.keySet().iterator().next());
|
||||
}
|
||||
return beans.values().iterator().next();
|
||||
}
|
||||
else {
|
||||
// Should not happen ..
|
||||
String message = "Found more than one matching @ServerEndpoint beans of type " + endpointClass;
|
||||
logger.error(message);
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Server classes for use with standard Java WebSocket endpoints including
|
||||
* {@link org.springframework.web.socket.server.endpoint.EndpointRegistration} and
|
||||
* {@link org.springframework.web.socket.server.endpoint.EndpointExporter} for
|
||||
* registering type-based endpoints,
|
||||
* {@link org.springframework.web.socket.server.endpoint.SpringConfigurator} for
|
||||
* instantiating annotated endpoints through Spring.
|
||||
*/
|
||||
package org.springframework.web.socket.server.endpoint;
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Server-side abstractions for WebSocket applications.
|
||||
*/
|
||||
package org.springframework.web.socket.server;
|
||||
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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.server.support;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.websocket.Endpoint;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.adapter.StandardEndpointAdapter;
|
||||
import org.springframework.web.socket.adapter.StandardWebSocketSessionAdapter;
|
||||
import org.springframework.web.socket.server.HandshakeFailureException;
|
||||
import org.springframework.web.socket.server.RequestUpgradeStrategy;
|
||||
|
||||
/**
|
||||
* A {@link RequestUpgradeStrategy} that supports WebSocket handlers of type
|
||||
* {@link WebSocketHandler} as well as {@link javax.websocket.Endpoint}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public abstract class AbstractEndpointUpgradeStrategy implements RequestUpgradeStrategy {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final ServerWebSocketSessionInitializer wsSessionInitializer = new ServerWebSocketSessionInitializer();
|
||||
|
||||
|
||||
@Override
|
||||
public void upgrade(ServerHttpRequest request, ServerHttpResponse response,
|
||||
String protocol, WebSocketHandler handler) throws IOException, HandshakeFailureException {
|
||||
|
||||
StandardWebSocketSessionAdapter session = new StandardWebSocketSessionAdapter();
|
||||
this.wsSessionInitializer.initialize(request, response, session);
|
||||
StandardEndpointAdapter endpoint = new StandardEndpointAdapter(handler, session);
|
||||
upgradeInternal(request, response, protocol, endpoint);
|
||||
}
|
||||
|
||||
protected abstract void upgradeInternal(ServerHttpRequest request, ServerHttpResponse response,
|
||||
String selectedProtocol, Endpoint endpoint) throws IOException, HandshakeFailureException;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* 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.server.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpServletResponseWrapper;
|
||||
import javax.websocket.DeploymentException;
|
||||
import javax.websocket.Endpoint;
|
||||
|
||||
import org.glassfish.tyrus.core.ComponentProviderService;
|
||||
import org.glassfish.tyrus.core.EndpointWrapper;
|
||||
import org.glassfish.tyrus.core.ErrorCollector;
|
||||
import org.glassfish.tyrus.core.RequestContext;
|
||||
import org.glassfish.tyrus.server.TyrusEndpoint;
|
||||
import org.glassfish.tyrus.servlet.TyrusHttpUpgradeHandler;
|
||||
import org.glassfish.tyrus.websockets.Connection;
|
||||
import org.glassfish.tyrus.websockets.Version;
|
||||
import org.glassfish.tyrus.websockets.WebSocketEngine;
|
||||
import org.glassfish.tyrus.websockets.WebSocketEngine.WebSocketHolderListener;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.socket.server.HandshakeFailureException;
|
||||
import org.springframework.web.socket.server.endpoint.EndpointRegistration;
|
||||
|
||||
/**
|
||||
* GlassFish support for upgrading an {@link HttpServletRequest} during a WebSocket
|
||||
* handshake.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class GlassFishRequestUpgradeStrategy extends AbstractEndpointUpgradeStrategy {
|
||||
|
||||
private final static Random random = new Random();
|
||||
|
||||
|
||||
@Override
|
||||
public String[] getSupportedVersions() {
|
||||
return StringUtils.commaDelimitedListToStringArray(Version.getSupportedWireProtocolVersions());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upgradeInternal(ServerHttpRequest request, ServerHttpResponse response,
|
||||
String selectedProtocol, Endpoint endpoint) throws IOException, HandshakeFailureException {
|
||||
|
||||
Assert.isTrue(request instanceof ServletServerHttpRequest);
|
||||
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
|
||||
|
||||
Assert.isTrue(response instanceof ServletServerHttpResponse);
|
||||
HttpServletResponse servletResponse = ((ServletServerHttpResponse) response).getServletResponse();
|
||||
servletResponse = new AlreadyUpgradedResponseWrapper(servletResponse);
|
||||
|
||||
TyrusEndpoint tyrusEndpoint = createTyrusEndpoint(servletRequest, endpoint, selectedProtocol);
|
||||
WebSocketEngine engine = WebSocketEngine.getEngine();
|
||||
|
||||
try {
|
||||
engine.register(tyrusEndpoint);
|
||||
}
|
||||
catch (DeploymentException ex) {
|
||||
throw new HandshakeFailureException("Failed to deploy endpoint in GlassFish", ex);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!performUpgrade(servletRequest, servletResponse, request.getHeaders(), tyrusEndpoint)) {
|
||||
throw new HandshakeFailureException("Failed to upgrade HttpServletRequest");
|
||||
}
|
||||
}
|
||||
finally {
|
||||
engine.unregister(tyrusEndpoint);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean performUpgrade(HttpServletRequest request, HttpServletResponse response,
|
||||
HttpHeaders headers, TyrusEndpoint tyrusEndpoint) throws IOException {
|
||||
|
||||
final TyrusHttpUpgradeHandler upgradeHandler = request.upgrade(TyrusHttpUpgradeHandler.class);
|
||||
|
||||
Connection connection = createConnection(upgradeHandler, response);
|
||||
|
||||
RequestContext wsRequest = RequestContext.Builder.create()
|
||||
.requestURI(URI.create(tyrusEndpoint.getPath())).requestPath(tyrusEndpoint.getPath())
|
||||
.connection(connection).secure(request.isSecure()).build();
|
||||
|
||||
for (String header : headers.keySet()) {
|
||||
wsRequest.getHeaders().put(header, headers.get(header));
|
||||
}
|
||||
|
||||
return WebSocketEngine.getEngine().upgrade(connection, wsRequest, new WebSocketHolderListener() {
|
||||
@Override
|
||||
public void onWebSocketHolder(WebSocketEngine.WebSocketHolder webSocketHolder) {
|
||||
upgradeHandler.setWebSocketHolder(webSocketHolder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private TyrusEndpoint createTyrusEndpoint(HttpServletRequest request, Endpoint endpoint, String selectedProtocol) {
|
||||
|
||||
// Use randomized path
|
||||
String requestUri = request.getRequestURI();
|
||||
String randomValue = String.valueOf(random.nextLong());
|
||||
String endpointPath = requestUri.endsWith("/") ? requestUri + randomValue : requestUri + "/" + randomValue;
|
||||
|
||||
EndpointRegistration endpointConfig = new EndpointRegistration(endpointPath, endpoint);
|
||||
endpointConfig.setSubprotocols(Arrays.asList(selectedProtocol));
|
||||
|
||||
return new TyrusEndpoint(new EndpointWrapper(endpoint, endpointConfig,
|
||||
ComponentProviderService.create(), null, "/", new ErrorCollector(),
|
||||
endpointConfig.getConfigurator()));
|
||||
}
|
||||
|
||||
private Connection createConnection(TyrusHttpUpgradeHandler handler, HttpServletResponse response) {
|
||||
try {
|
||||
String name = "org.glassfish.tyrus.servlet.ConnectionImpl";
|
||||
Class<?> clazz = ClassUtils.forName(name, GlassFishRequestUpgradeStrategy.class.getClassLoader());
|
||||
Constructor<?> constructor = clazz.getDeclaredConstructor(TyrusHttpUpgradeHandler.class, HttpServletResponse.class);
|
||||
ReflectionUtils.makeAccessible(constructor);
|
||||
return (Connection) constructor.newInstance(handler, response);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Failed to instantiate GlassFish connection", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class AlreadyUpgradedResponseWrapper extends HttpServletResponseWrapper {
|
||||
|
||||
public AlreadyUpgradedResponseWrapper(HttpServletResponse response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStatus(int sc) {
|
||||
Assert.isTrue(sc == HttpStatus.SWITCHING_PROTOCOLS.value(), "Unexpected status code " + sc);
|
||||
}
|
||||
@Override
|
||||
public void addHeader(String name, String value) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* 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.server.support;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.api.UpgradeResponse;
|
||||
import org.eclipse.jetty.websocket.server.HandshakeRFC6455;
|
||||
import org.eclipse.jetty.websocket.server.ServletWebSocketRequest;
|
||||
import org.eclipse.jetty.websocket.server.WebSocketServerFactory;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.adapter.JettyWebSocketListenerAdapter;
|
||||
import org.springframework.web.socket.adapter.JettyWebSocketSessionAdapter;
|
||||
import org.springframework.web.socket.server.HandshakeFailureException;
|
||||
import org.springframework.web.socket.server.RequestUpgradeStrategy;
|
||||
|
||||
/**
|
||||
* {@link RequestUpgradeStrategy} for use with Jetty. Based on Jetty's internal
|
||||
* {@code org.eclipse.jetty.websocket.server.WebSocketHandler} class.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.0
|
||||
*/
|
||||
public class JettyRequestUpgradeStrategy implements RequestUpgradeStrategy {
|
||||
|
||||
// FIXME jetty has options, timeouts etc. Do we need a common abstraction
|
||||
|
||||
// FIXME need a way for someone to plug their own RequestUpgradeStrategy or override
|
||||
// Jetty settings
|
||||
|
||||
// FIXME when to call factory.cleanup();
|
||||
|
||||
private static final String WEBSOCKET_LISTENER_ATTR_NAME = JettyRequestUpgradeStrategy.class.getName()
|
||||
+ ".HANDLER_PROVIDER";
|
||||
|
||||
private WebSocketServerFactory factory;
|
||||
|
||||
private final ServerWebSocketSessionInitializer wsSessionInitializer = new ServerWebSocketSessionInitializer();
|
||||
|
||||
|
||||
public JettyRequestUpgradeStrategy() {
|
||||
this.factory = new WebSocketServerFactory();
|
||||
this.factory.setCreator(new WebSocketCreator() {
|
||||
@Override
|
||||
public Object createWebSocket(UpgradeRequest request, UpgradeResponse response) {
|
||||
Assert.isInstanceOf(ServletWebSocketRequest.class, request);
|
||||
return ((ServletWebSocketRequest) request).getServletAttributes().get(WEBSOCKET_LISTENER_ATTR_NAME);
|
||||
}
|
||||
});
|
||||
try {
|
||||
this.factory.init();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Unable to initialize Jetty WebSocketServerFactory", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String[] getSupportedVersions() {
|
||||
return new String[] { String.valueOf(HandshakeRFC6455.VERSION) };
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upgrade(ServerHttpRequest request, ServerHttpResponse response,
|
||||
String selectedProtocol, WebSocketHandler webSocketHandler) throws IOException {
|
||||
|
||||
Assert.isInstanceOf(ServletServerHttpRequest.class, request);
|
||||
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
|
||||
|
||||
Assert.isInstanceOf(ServletServerHttpResponse.class, response);
|
||||
HttpServletResponse servletResponse = ((ServletServerHttpResponse) response).getServletResponse();
|
||||
|
||||
if (!this.factory.isUpgradeRequest(servletRequest, servletResponse)) {
|
||||
// should never happen
|
||||
throw new HandshakeFailureException("Not a WebSocket request");
|
||||
}
|
||||
|
||||
JettyWebSocketSessionAdapter session = new JettyWebSocketSessionAdapter();
|
||||
this.wsSessionInitializer.initialize(request, response, session);
|
||||
JettyWebSocketListenerAdapter listener = new JettyWebSocketListenerAdapter(webSocketHandler, session);
|
||||
|
||||
servletRequest.setAttribute(WEBSOCKET_LISTENER_ATTR_NAME, listener);
|
||||
|
||||
if (!this.factory.acceptWebSocket(servletRequest, servletResponse)) {
|
||||
// should never happen
|
||||
throw new HandshakeFailureException("WebSocket request not accepted by Jetty");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.server.support;
|
||||
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.socket.adapter.ConfigurableWebSocketSession;
|
||||
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class ServerWebSocketSessionInitializer {
|
||||
|
||||
|
||||
public void initialize(ServerHttpRequest request, ServerHttpResponse response, ConfigurableWebSocketSession session) {
|
||||
session.setUri(request.getURI());
|
||||
session.setRemoteHostName(request.getRemoteHostName());
|
||||
session.setRemoteAddress(request.getRemoteAddress());
|
||||
session.setPrincipal(request.getPrincipal());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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.server.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.websocket.Endpoint;
|
||||
import javax.websocket.server.ServerEndpointConfig;
|
||||
|
||||
import org.apache.tomcat.websocket.server.WsHandshakeRequest;
|
||||
import org.apache.tomcat.websocket.server.WsHttpUpgradeHandler;
|
||||
import org.apache.tomcat.websocket.server.WsServerContainer;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.web.socket.server.HandshakeFailureException;
|
||||
import org.springframework.web.socket.server.endpoint.EndpointRegistration;
|
||||
|
||||
/**
|
||||
* Tomcat support for upgrading an {@link HttpServletRequest} during a WebSocket handshake.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class TomcatRequestUpgradeStrategy extends AbstractEndpointUpgradeStrategy {
|
||||
|
||||
@Override
|
||||
public String[] getSupportedVersions() {
|
||||
return new String[] { "13" };
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upgradeInternal(ServerHttpRequest request, ServerHttpResponse response,
|
||||
String selectedProtocol, Endpoint endpoint) throws IOException {
|
||||
|
||||
Assert.isTrue(request instanceof ServletServerHttpRequest);
|
||||
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
|
||||
|
||||
WsHttpUpgradeHandler upgradeHandler = servletRequest.upgrade(WsHttpUpgradeHandler.class);
|
||||
|
||||
WsHandshakeRequest webSocketRequest = new WsHandshakeRequest(servletRequest);
|
||||
try {
|
||||
Method method = ReflectionUtils.findMethod(WsHandshakeRequest.class, "finished");
|
||||
ReflectionUtils.makeAccessible(method);
|
||||
method.invoke(webSocketRequest);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new HandshakeFailureException("Failed to upgrade HttpServletRequest", ex);
|
||||
}
|
||||
|
||||
// TODO: use ServletContext attribute when Tomcat is updated
|
||||
WsServerContainer serverContainer = WsServerContainer.getServerContainer();
|
||||
|
||||
ServerEndpointConfig endpointConfig = new EndpointRegistration("/shouldntmatter", endpoint);
|
||||
|
||||
upgradeHandler.preInit(endpoint, endpointConfig, serverContainer, webSocketRequest,
|
||||
selectedProtocol, Collections.<String, String> emptyMap(), servletRequest.isSecure());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.server.support;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.HttpRequestHandler;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.DefaultHandshakeHandler;
|
||||
import org.springframework.web.socket.server.HandshakeHandler;
|
||||
import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator;
|
||||
import org.springframework.web.socket.support.LoggingWebSocketHandlerDecorator;
|
||||
|
||||
/**
|
||||
* An {@link HttpRequestHandler} that wraps the invocation of a {@link HandshakeHandler}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class WebSocketHttpRequestHandler implements HttpRequestHandler {
|
||||
|
||||
private final HandshakeHandler handshakeHandler;
|
||||
|
||||
private final WebSocketHandler webSocketHandler;
|
||||
|
||||
|
||||
public WebSocketHttpRequestHandler(WebSocketHandler webSocketHandler) {
|
||||
this(webSocketHandler, new DefaultHandshakeHandler());
|
||||
}
|
||||
|
||||
public WebSocketHttpRequestHandler( WebSocketHandler webSocketHandler, HandshakeHandler handshakeHandler) {
|
||||
Assert.notNull(webSocketHandler, "webSocketHandler is required");
|
||||
Assert.notNull(handshakeHandler, "handshakeHandler is required");
|
||||
this.webSocketHandler = decorateWebSocketHandler(webSocketHandler);
|
||||
this.handshakeHandler = new DefaultHandshakeHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorate the WebSocketHandler provided to the class constructor.
|
||||
* <p>
|
||||
* By default {@link ExceptionWebSocketHandlerDecorator} and
|
||||
* {@link LoggingWebSocketHandlerDecorator} are applied are added.
|
||||
*/
|
||||
protected WebSocketHandler decorateWebSocketHandler(WebSocketHandler handler) {
|
||||
handler = new ExceptionWebSocketHandlerDecorator(handler);
|
||||
return new LoggingWebSocketHandlerDecorator(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
|
||||
throws ServletException, IOException {
|
||||
|
||||
ServerHttpRequest httpRequest = new ServletServerHttpRequest(request);
|
||||
ServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
|
||||
try {
|
||||
this.handshakeHandler.doHandshake(httpRequest, httpResponse, this.webSocketHandler);
|
||||
httpResponse.flush();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Server-side support classes including container-specific strategies for upgrading a
|
||||
* request.
|
||||
*/
|
||||
package org.springframework.web.socket.server.support;
|
||||
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketException;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.WebSocketMessage;
|
||||
|
||||
/**
|
||||
* Provides partial implementations of {@link SockJsSession} methods to send messages,
|
||||
* including heartbeat messages and to manage session state.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public abstract class AbstractServerSockJsSession extends AbstractSockJsSession {
|
||||
|
||||
private final SockJsConfiguration sockJsConfig;
|
||||
|
||||
private ScheduledFuture<?> heartbeatTask;
|
||||
|
||||
|
||||
public AbstractServerSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) {
|
||||
super(sessionId, handler);
|
||||
this.sockJsConfig = config;
|
||||
}
|
||||
|
||||
protected SockJsConfiguration getSockJsConfig() {
|
||||
return this.sockJsConfig;
|
||||
}
|
||||
|
||||
public final synchronized void sendMessage(WebSocketMessage message) throws IOException {
|
||||
Assert.isTrue(!isClosed(), "Cannot send a message when session is closed");
|
||||
Assert.isInstanceOf(TextMessage.class, message, "Expected text message: " + message);
|
||||
sendMessageInternal(((TextMessage) message).getPayload());
|
||||
}
|
||||
|
||||
protected abstract void sendMessageInternal(String message) throws IOException;
|
||||
|
||||
|
||||
@Override
|
||||
public void connectionClosedInternal(CloseStatus status) {
|
||||
updateLastActiveTime();
|
||||
cancelHeartbeat();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final synchronized void closeInternal(CloseStatus status) throws IOException {
|
||||
if (isActive()) {
|
||||
// TODO: deliver messages "in flight" before sending close frame
|
||||
try {
|
||||
// bypass writeFrame
|
||||
writeFrameInternal(SockJsFrame.closeFrame(status.getCode(), status.getReason()));
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
logger.warn("Failed to send SockJS close frame: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
updateLastActiveTime();
|
||||
cancelHeartbeat();
|
||||
disconnect(status);
|
||||
}
|
||||
|
||||
protected abstract void disconnect(CloseStatus status) throws IOException;
|
||||
|
||||
/**
|
||||
* For internal use within a TransportHandler and the (TransportHandler-specific)
|
||||
* session sub-class.
|
||||
*/
|
||||
protected void writeFrame(SockJsFrame frame) throws IOException {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Preparing to write " + frame);
|
||||
}
|
||||
try {
|
||||
writeFrameInternal(frame);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
if (ex instanceof EOFException || ex instanceof SocketException) {
|
||||
logger.warn("Client went away. Terminating connection");
|
||||
}
|
||||
else {
|
||||
logger.warn("Terminating connection due to failure to send message: " + ex.getMessage());
|
||||
}
|
||||
disconnect(CloseStatus.SERVER_ERROR);
|
||||
close(CloseStatus.SERVER_ERROR);
|
||||
throw ex;
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
logger.warn("Terminating connection due to failure to send message: " + ex.getMessage());
|
||||
disconnect(CloseStatus.SERVER_ERROR);
|
||||
close(CloseStatus.SERVER_ERROR);
|
||||
throw new SockJsRuntimeException("Failed to write " + frame, ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void writeFrameInternal(SockJsFrame frame) throws Exception;
|
||||
|
||||
public synchronized void sendHeartbeat() throws Exception {
|
||||
if (isActive()) {
|
||||
writeFrame(SockJsFrame.heartbeatFrame());
|
||||
scheduleHeartbeat();
|
||||
}
|
||||
}
|
||||
|
||||
protected void scheduleHeartbeat() {
|
||||
Assert.notNull(getSockJsConfig().getTaskScheduler(), "heartbeatScheduler not configured");
|
||||
cancelHeartbeat();
|
||||
if (!isActive()) {
|
||||
return;
|
||||
}
|
||||
Date time = new Date(System.currentTimeMillis() + getSockJsConfig().getHeartbeatTime());
|
||||
this.heartbeatTask = getSockJsConfig().getTaskScheduler().schedule(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
sendHeartbeat();
|
||||
}
|
||||
catch (Throwable t) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}, time);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Scheduled heartbeat after " + getSockJsConfig().getHeartbeatTime()/1000 + " seconds");
|
||||
}
|
||||
}
|
||||
|
||||
protected void cancelHeartbeat() {
|
||||
if ((this.heartbeatTask != null) && !this.heartbeatTask.isDone()) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Cancelling heartbeat");
|
||||
}
|
||||
this.heartbeatTask.cancel(false);
|
||||
}
|
||||
this.heartbeatTask = null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,516 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.socket.sockjs;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
|
||||
/**
|
||||
* An abstract class for {@link SockJsService} implementations. Provides configuration
|
||||
* support, SockJS path resolution, and processing for static SockJS requests (e.g.
|
||||
* "/info", "/iframe.html", etc). Sub-classes are responsible for handling transport
|
||||
* requests.
|
||||
*
|
||||
* <p>
|
||||
* It is expected that this service is mapped correctly to one or more prefixes such as
|
||||
* "/echo" including all sub-URLs (e.g. "/echo/**"). A SockJS service itself is generally
|
||||
* unaware of request mapping details but nevertheless must be able to extract the SockJS
|
||||
* path, which is the portion of the request path following the prefix. In most cases,
|
||||
* this class can auto-detect the SockJS path but you can also explicitly configure the
|
||||
* list of valid prefixes with {@link #setValidSockJsPrefixes(String...)}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public abstract class AbstractSockJsService implements SockJsService, SockJsConfiguration {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private static final int ONE_YEAR = 365 * 24 * 60 * 60;
|
||||
|
||||
|
||||
private String name = "SockJSService@" + ObjectUtils.getIdentityHexString(this);
|
||||
|
||||
private String clientLibraryUrl = "https://d1fxtkz8shb9d2.cloudfront.net/sockjs-0.3.4.min.js";
|
||||
|
||||
private int streamBytesLimit = 128 * 1024;
|
||||
|
||||
private boolean jsessionIdCookieRequired = true;
|
||||
|
||||
private long heartbeatTime = 25 * 1000;
|
||||
|
||||
private long disconnectDelay = 5 * 1000;
|
||||
|
||||
private boolean webSocketsEnabled = true;
|
||||
|
||||
private final TaskScheduler taskScheduler;
|
||||
|
||||
private final List<String> sockJsPrefixes = new ArrayList<String>();
|
||||
|
||||
private final Set<String> sockJsPathCache = new CopyOnWriteArraySet<String>();
|
||||
|
||||
|
||||
public AbstractSockJsService(TaskScheduler scheduler) {
|
||||
Assert.notNull(scheduler, "scheduler is required");
|
||||
this.taskScheduler = scheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* A unique name for the service mainly for logging purposes.
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this property to configure one or more prefixes that this SockJS service is
|
||||
* allowed to serve. The prefix (e.g. "/echo") is needed to extract the SockJS
|
||||
* specific portion of the URL (e.g. "${prefix}/info", "${prefix}/iframe.html", etc).
|
||||
* <p>
|
||||
* This property is not strictly required. In most cases, the SockJS path can be
|
||||
* auto-detected since the initial request from the SockJS client is of the form
|
||||
* "{prefix}/info". Assuming the SockJS service is mapped correctly (e.g. using
|
||||
* Ant-style pattern "/echo/**") this should work fine. This property can be used
|
||||
* to configure explicitly the prefixes this service is allowed to service.
|
||||
*
|
||||
* @param prefixes the prefixes to use; prefixes do not need to include the portions
|
||||
* of the path that represent Servlet container context or Servlet path.
|
||||
*/
|
||||
public void setValidSockJsPrefixes(String... prefixes) {
|
||||
|
||||
this.sockJsPrefixes.clear();
|
||||
for (String prefix : prefixes) {
|
||||
if (prefix.endsWith("/") && (prefix.length() > 1)) {
|
||||
prefix = prefix.substring(0, prefix.length() - 1);
|
||||
}
|
||||
this.sockJsPrefixes.add(prefix);
|
||||
}
|
||||
|
||||
// sort with longest prefix at the top
|
||||
Collections.sort(this.sockJsPrefixes, Collections.reverseOrder(new Comparator<String>() {
|
||||
public int compare(String o1, String o2) {
|
||||
return new Integer(o1.length()).compareTo(new Integer(o2.length()));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Transports which don't support cross-domain communication natively (e.g.
|
||||
* "eventsource", "htmlfile") rely on serving a simple page (using the
|
||||
* "foreign" domain) from an invisible iframe. Code run from this iframe
|
||||
* doesn't need to worry about cross-domain issues since it is running from
|
||||
* a domain local to the SockJS server. The iframe does need to load the
|
||||
* SockJS javascript client library and this option allows configuring its
|
||||
* url.
|
||||
* <p>
|
||||
* By default this is set to point to
|
||||
* "https://d1fxtkz8shb9d2.cloudfront.net/sockjs-0.3.4.min.js".
|
||||
*/
|
||||
public AbstractSockJsService setSockJsClientLibraryUrl(String clientLibraryUrl) {
|
||||
this.clientLibraryUrl = clientLibraryUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL to the SockJS JavaScript client library.
|
||||
* @see #setSockJsClientLibraryUrl(String)
|
||||
*/
|
||||
public String getSockJsClientLibraryUrl() {
|
||||
return this.clientLibraryUrl;
|
||||
}
|
||||
|
||||
public AbstractSockJsService setStreamBytesLimit(int streamBytesLimit) {
|
||||
this.streamBytesLimit = streamBytesLimit;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getStreamBytesLimit() {
|
||||
return streamBytesLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Some load balancers do sticky sessions, but only if there is a JSESSIONID
|
||||
* cookie. Even if it is set to a dummy value, it doesn't matter since
|
||||
* session information is added by the load balancer.
|
||||
* <p>
|
||||
* Set this option to indicate if a JSESSIONID cookie should be created. The
|
||||
* default value is "true".
|
||||
*/
|
||||
public AbstractSockJsService setJsessionIdCookieRequired(boolean jsessionIdCookieRequired) {
|
||||
this.jsessionIdCookieRequired = jsessionIdCookieRequired;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether setting JSESSIONID cookie is necessary.
|
||||
* @see #setJsessionIdCookieRequired(boolean)
|
||||
*/
|
||||
public boolean isJsessionIdCookieRequired() {
|
||||
return this.jsessionIdCookieRequired;
|
||||
}
|
||||
|
||||
public AbstractSockJsService setHeartbeatTime(long heartbeatTime) {
|
||||
this.heartbeatTime = heartbeatTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public long getHeartbeatTime() {
|
||||
return this.heartbeatTime;
|
||||
}
|
||||
|
||||
public TaskScheduler getTaskScheduler() {
|
||||
return this.taskScheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* The amount of time in milliseconds before a client is considered
|
||||
* disconnected after not having a receiving connection, i.e. an active
|
||||
* connection over which the server can send data to the client.
|
||||
* <p>
|
||||
* The default value is 5000.
|
||||
*/
|
||||
public void setDisconnectDelay(long disconnectDelay) {
|
||||
this.disconnectDelay = disconnectDelay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the amount of time in milliseconds before a client is considered disconnected.
|
||||
*/
|
||||
public long getDisconnectDelay() {
|
||||
return this.disconnectDelay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Some load balancers don't support websockets. This option can be used to
|
||||
* disable the WebSocket transport on the server side.
|
||||
* <p>
|
||||
* The default value is "true".
|
||||
*/
|
||||
public void setWebSocketsEnabled(boolean webSocketsEnabled) {
|
||||
this.webSocketsEnabled = webSocketsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether WebSocket transport is enabled.
|
||||
* @see #setWebSocketsEnabled(boolean)
|
||||
*/
|
||||
public boolean isWebSocketEnabled() {
|
||||
return this.webSocketsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @param sockJsPath
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler)
|
||||
throws IOException, TransportErrorException {
|
||||
|
||||
String sockJsPath = getSockJsPath(request);
|
||||
if (sockJsPath == null) {
|
||||
logger.warn("Could not determine SockJS path for URL \"" + request.getURI().getPath() +
|
||||
". Consider setting validSockJsPrefixes.");
|
||||
response.setStatusCode(HttpStatus.NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(request.getMethod() + " with SockJS path [" + sockJsPath + "]");
|
||||
|
||||
try {
|
||||
request.getHeaders();
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// Ignore invalid Content-Type (TODO)
|
||||
}
|
||||
|
||||
try {
|
||||
if (sockJsPath.equals("") || sockJsPath.equals("/")) {
|
||||
response.getHeaders().setContentType(new MediaType("text", "plain", Charset.forName("UTF-8")));
|
||||
response.getBody().write("Welcome to SockJS!\n".getBytes("UTF-8"));
|
||||
return;
|
||||
}
|
||||
else if (sockJsPath.equals("/info")) {
|
||||
this.infoHandler.handle(request, response);
|
||||
return;
|
||||
}
|
||||
else if (sockJsPath.matches("/iframe[0-9-.a-z_]*.html")) {
|
||||
this.iframeHandler.handle(request, response);
|
||||
return;
|
||||
}
|
||||
else if (sockJsPath.equals("/websocket")) {
|
||||
handleRawWebSocketRequest(request, response, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
String[] pathSegments = StringUtils.tokenizeToStringArray(sockJsPath.substring(1), "/");
|
||||
if (pathSegments.length != 3) {
|
||||
logger.warn("Expected \"/{server}/{session}/{transport}\" but got \"" + sockJsPath + "\"");
|
||||
response.setStatusCode(HttpStatus.NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
String serverId = pathSegments[0];
|
||||
String sessionId = pathSegments[1];
|
||||
String transport = pathSegments[2];
|
||||
|
||||
if (!validateRequest(serverId, sessionId, transport)) {
|
||||
response.setStatusCode(HttpStatus.NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
handleTransportRequest(request, response, sessionId, TransportType.fromValue(transport), handler);
|
||||
}
|
||||
finally {
|
||||
response.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the SockJS path or null if the path could not be determined.
|
||||
*/
|
||||
private String getSockJsPath(ServerHttpRequest request) {
|
||||
|
||||
String path = request.getURI().getPath();
|
||||
|
||||
// SockJS prefix hints?
|
||||
if (!this.sockJsPrefixes.isEmpty()) {
|
||||
for (String prefix : this.sockJsPrefixes) {
|
||||
int index = path.indexOf(prefix);
|
||||
if (index != -1) {
|
||||
this.sockJsPathCache.add(path.substring(0, index + prefix.length()));
|
||||
return path.substring(index + prefix.length());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SockJS info request?
|
||||
if (path.endsWith("/info")) {
|
||||
this.sockJsPathCache.add(path.substring(0, path.length() - 6));
|
||||
return "/info";
|
||||
}
|
||||
|
||||
// Have we seen this prefix before (following the initial /info request)?
|
||||
String match = null;
|
||||
for (String sockJsPath : this.sockJsPathCache) {
|
||||
if (path.startsWith(sockJsPath)) {
|
||||
if ((match == null) || (match.length() < sockJsPath.length())) {
|
||||
match = sockJsPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (match != null) {
|
||||
return path.substring(match.length());
|
||||
}
|
||||
|
||||
// SockJS greeting?
|
||||
String pathNoSlash = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
|
||||
String lastSegment = pathNoSlash.substring(pathNoSlash.lastIndexOf('/') + 1);
|
||||
|
||||
if ((TransportType.fromValue(lastSegment) == null) && !lastSegment.startsWith("iframe")) {
|
||||
this.sockJsPathCache.add(path);
|
||||
return "";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract void handleRawWebSocketRequest(ServerHttpRequest request,
|
||||
ServerHttpResponse response, WebSocketHandler webSocketHandler) throws IOException;
|
||||
|
||||
protected abstract void handleTransportRequest(ServerHttpRequest request, ServerHttpResponse response,
|
||||
String sessionId, TransportType transportType, WebSocketHandler webSocketHandler)
|
||||
throws IOException, TransportErrorException;
|
||||
|
||||
|
||||
protected boolean validateRequest(String serverId, String sessionId, String transport) {
|
||||
|
||||
if (!StringUtils.hasText(serverId) || !StringUtils.hasText(sessionId) || !StringUtils.hasText(transport)) {
|
||||
logger.warn("Empty server, session, or transport value");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Server and session id's must not contain "."
|
||||
if (serverId.contains(".") || sessionId.contains(".")) {
|
||||
logger.warn("Server or session contain a \".\"");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isWebSocketEnabled() && transport.equals(TransportType.WEBSOCKET.value())) {
|
||||
logger.warn("Websocket transport is disabled");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void addCorsHeaders(ServerHttpRequest request, ServerHttpResponse response, HttpMethod... httpMethods) {
|
||||
|
||||
String origin = request.getHeaders().getFirst("origin");
|
||||
origin = ((origin == null) || origin.equals("null")) ? "*" : origin;
|
||||
|
||||
response.getHeaders().add("Access-Control-Allow-Origin", origin);
|
||||
response.getHeaders().add("Access-Control-Allow-Credentials", "true");
|
||||
|
||||
List<String> accessControllerHeaders = request.getHeaders().get("Access-Control-Request-Headers");
|
||||
if (accessControllerHeaders != null) {
|
||||
for (String header : accessControllerHeaders) {
|
||||
response.getHeaders().add("Access-Control-Allow-Headers", header);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ObjectUtils.isEmpty(httpMethods)) {
|
||||
response.getHeaders().add("Access-Control-Allow-Methods", StringUtils.arrayToDelimitedString(httpMethods, ", "));
|
||||
response.getHeaders().add("Access-Control-Max-Age", String.valueOf(ONE_YEAR));
|
||||
}
|
||||
}
|
||||
|
||||
protected void addCacheHeaders(ServerHttpResponse response) {
|
||||
response.getHeaders().setCacheControl("public, max-age=" + ONE_YEAR);
|
||||
response.getHeaders().setExpires(new Date().getTime() + ONE_YEAR * 1000);
|
||||
}
|
||||
|
||||
protected void addNoCacheHeaders(ServerHttpResponse response) {
|
||||
response.getHeaders().setCacheControl("no-store, no-cache, must-revalidate, max-age=0");
|
||||
}
|
||||
|
||||
protected void sendMethodNotAllowed(ServerHttpResponse response, List<HttpMethod> httpMethods) throws IOException {
|
||||
logger.debug("Sending Method Not Allowed (405)");
|
||||
response.setStatusCode(HttpStatus.METHOD_NOT_ALLOWED);
|
||||
response.getHeaders().setAllow(new HashSet<HttpMethod>(httpMethods));
|
||||
}
|
||||
|
||||
|
||||
private interface SockJsRequestHandler {
|
||||
|
||||
void handle(ServerHttpRequest request, ServerHttpResponse response) throws IOException;
|
||||
}
|
||||
|
||||
private static final Random random = new Random();
|
||||
|
||||
private final SockJsRequestHandler infoHandler = new SockJsRequestHandler() {
|
||||
|
||||
private static final String INFO_CONTENT =
|
||||
"{\"entropy\":%s,\"origins\":[\"*:*\"],\"cookie_needed\":%s,\"websocket\":%s}";
|
||||
|
||||
public void handle(ServerHttpRequest request, ServerHttpResponse response) throws IOException {
|
||||
|
||||
if (HttpMethod.GET.equals(request.getMethod())) {
|
||||
|
||||
response.getHeaders().setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));
|
||||
|
||||
addCorsHeaders(request, response);
|
||||
addNoCacheHeaders(response);
|
||||
|
||||
String content = String.format(INFO_CONTENT, random.nextInt(), isJsessionIdCookieRequired(), isWebSocketEnabled());
|
||||
response.getBody().write(content.getBytes());
|
||||
}
|
||||
else if (HttpMethod.OPTIONS.equals(request.getMethod())) {
|
||||
|
||||
response.setStatusCode(HttpStatus.NO_CONTENT);
|
||||
|
||||
addCorsHeaders(request, response, HttpMethod.OPTIONS, HttpMethod.GET);
|
||||
addCacheHeaders(response);
|
||||
}
|
||||
else {
|
||||
sendMethodNotAllowed(response, Arrays.asList(HttpMethod.OPTIONS, HttpMethod.GET));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final SockJsRequestHandler iframeHandler = new SockJsRequestHandler() {
|
||||
|
||||
private static final String IFRAME_CONTENT =
|
||||
"<!DOCTYPE html>\n" +
|
||||
"<html>\n" +
|
||||
"<head>\n" +
|
||||
" <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n" +
|
||||
" <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" +
|
||||
" <script>\n" +
|
||||
" document.domain = document.domain;\n" +
|
||||
" _sockjs_onload = function(){SockJS.bootstrap_iframe();};\n" +
|
||||
" </script>\n" +
|
||||
" <script src=\"%s\"></script>\n" +
|
||||
"</head>\n" +
|
||||
"<body>\n" +
|
||||
" <h2>Don't panic!</h2>\n" +
|
||||
" <p>This is a SockJS hidden iframe. It's used for cross domain magic.</p>\n" +
|
||||
"</body>\n" +
|
||||
"</html>";
|
||||
|
||||
public void handle(ServerHttpRequest request, ServerHttpResponse response) throws IOException {
|
||||
|
||||
if (!HttpMethod.GET.equals(request.getMethod())) {
|
||||
sendMethodNotAllowed(response, Arrays.asList(HttpMethod.GET));
|
||||
return;
|
||||
}
|
||||
|
||||
String content = String.format(IFRAME_CONTENT, getSockJsClientLibraryUrl());
|
||||
byte[] contentBytes = content.getBytes(Charset.forName("UTF-8"));
|
||||
StringBuilder builder = new StringBuilder("\"0");
|
||||
DigestUtils.appendMd5DigestAsHex(contentBytes, builder);
|
||||
builder.append('"');
|
||||
String etagValue = builder.toString();
|
||||
|
||||
List<String> ifNoneMatch = request.getHeaders().getIfNoneMatch();
|
||||
if (!CollectionUtils.isEmpty(ifNoneMatch) && ifNoneMatch.get(0).equals(etagValue)) {
|
||||
response.setStatusCode(HttpStatus.NOT_MODIFIED);
|
||||
return;
|
||||
}
|
||||
|
||||
response.getHeaders().setContentType(new MediaType("text", "html", Charset.forName("UTF-8")));
|
||||
response.getHeaders().setContentLength(contentBytes.length);
|
||||
|
||||
addCacheHeaders(response);
|
||||
response.getHeaders().setETag(etagValue);
|
||||
response.getBody().write(contentBytes);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.security.Principal;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.adapter.ConfigurableWebSocketSession;
|
||||
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public abstract class AbstractSockJsSession implements ConfigurableWebSocketSession {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
|
||||
private final String id;
|
||||
|
||||
private URI uri;
|
||||
|
||||
private String remoteHostName;
|
||||
|
||||
private String remoteAddress;
|
||||
|
||||
private Principal principal;
|
||||
|
||||
private WebSocketHandler handler;
|
||||
|
||||
private State state = State.NEW;
|
||||
|
||||
private long timeCreated = System.currentTimeMillis();
|
||||
|
||||
private long timeLastActive = System.currentTimeMillis();
|
||||
|
||||
|
||||
/**
|
||||
* @param sessionId
|
||||
* @param webSocketHandler the recipient of SockJS messages
|
||||
*/
|
||||
public AbstractSockJsSession(String sessionId, WebSocketHandler webSocketHandler) {
|
||||
Assert.notNull(sessionId, "sessionId is required");
|
||||
Assert.notNull(webSocketHandler, "webSocketHandler is required");
|
||||
this.id = sessionId;
|
||||
this.handler = webSocketHandler;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getUri() {
|
||||
return this.uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUri(URI uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecure() {
|
||||
return "wss".equals(this.uri.getSchemeSpecificPart());
|
||||
}
|
||||
|
||||
public String getRemoteHostName() {
|
||||
return this.remoteHostName;
|
||||
}
|
||||
|
||||
public void setRemoteHostName(String remoteHostName) {
|
||||
this.remoteHostName = remoteHostName;
|
||||
}
|
||||
|
||||
public String getRemoteAddress() {
|
||||
return this.remoteAddress;
|
||||
}
|
||||
|
||||
public void setRemoteAddress(String remoteAddress) {
|
||||
this.remoteAddress = remoteAddress;
|
||||
}
|
||||
|
||||
public Principal getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
|
||||
public void setPrincipal(Principal principal) {
|
||||
this.principal = principal;
|
||||
}
|
||||
|
||||
public boolean isNew() {
|
||||
return State.NEW.equals(this.state);
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return State.OPEN.equals(this.state);
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
return State.CLOSED.equals(this.state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Polling and Streaming sessions periodically close the current HTTP request and
|
||||
* wait for the next request to come through. During this "downtime" the session is
|
||||
* still open but inactive and unable to send messages and therefore has to buffer
|
||||
* them temporarily. A WebSocket session by contrast is stateful and remain active
|
||||
* until closed.
|
||||
*/
|
||||
public abstract boolean isActive();
|
||||
|
||||
/**
|
||||
* Return the time since the session was last active, or otherwise if the
|
||||
* session is new, the time since the session was created.
|
||||
*/
|
||||
public long getTimeSinceLastActive() {
|
||||
if (isNew()) {
|
||||
return (System.currentTimeMillis() - this.timeCreated);
|
||||
}
|
||||
else {
|
||||
return isActive() ? 0 : System.currentTimeMillis() - this.timeLastActive;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be invoked whenever the session becomes inactive.
|
||||
*/
|
||||
protected void updateLastActiveTime() {
|
||||
this.timeLastActive = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public void delegateConnectionEstablished() throws Exception {
|
||||
this.state = State.OPEN;
|
||||
this.handler.afterConnectionEstablished(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close due to error arising from SockJS transport handling.
|
||||
*/
|
||||
protected void tryCloseWithSockJsTransportError(Throwable ex, CloseStatus closeStatus) {
|
||||
logger.error("Closing due to transport error for " + this, ex);
|
||||
try {
|
||||
delegateError(ex);
|
||||
}
|
||||
catch (Throwable delegateEx) {
|
||||
logger.error("Unhandled error for " + this, delegateEx);
|
||||
try {
|
||||
close(closeStatus);
|
||||
}
|
||||
catch (Throwable closeEx) {
|
||||
logger.error("Unhandled error for " + this, closeEx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void delegateMessages(String[] messages) throws Exception {
|
||||
for (String message : messages) {
|
||||
this.handler.handleMessage(this, new TextMessage(message));
|
||||
}
|
||||
}
|
||||
|
||||
public void delegateError(Throwable ex) throws Exception {
|
||||
this.handler.handleTransportError(this, ex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked in reaction to the underlying connection being closed by the remote side
|
||||
* (or the WebSocket container) in order to perform cleanup and notify the
|
||||
* {@link TextMessageHandler}. This is in contrast to {@link #close()} that pro-actively
|
||||
* closes the connection.
|
||||
*/
|
||||
public final void delegateConnectionClosed(CloseStatus status) throws Exception {
|
||||
if (!isClosed()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(this + " was closed, " + status);
|
||||
}
|
||||
try {
|
||||
connectionClosedInternal(status);
|
||||
}
|
||||
finally {
|
||||
this.state = State.CLOSED;
|
||||
this.handler.afterConnectionClosed(this, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void connectionClosedInternal(CloseStatus status) {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* <p>Performs cleanup and notifies the {@link SockJsHandler}.
|
||||
*/
|
||||
public final void close() throws IOException {
|
||||
close(new CloseStatus(3000, "Go away!"));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* <p>Performs cleanup and notifies the {@link SockJsHandler}.
|
||||
*/
|
||||
public final void close(CloseStatus status) throws IOException {
|
||||
if (isOpen()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Closing " + this + ", " + status);
|
||||
}
|
||||
try {
|
||||
closeInternal(status);
|
||||
}
|
||||
finally {
|
||||
this.state = State.CLOSED;
|
||||
try {
|
||||
this.handler.afterConnectionClosed(this, status);
|
||||
}
|
||||
catch (Throwable t) {
|
||||
logger.error("Unhandled error for " + this, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void closeInternal(CloseStatus status) throws IOException;
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SockJS session id=" + this.id;
|
||||
}
|
||||
|
||||
|
||||
private enum State { NEW, OPEN, CLOSED }
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs;
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface ConfigurableTransportHandler extends TransportHandler {
|
||||
|
||||
void setSockJsConfiguration(SockJsConfiguration sockJsConfig);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs;
|
||||
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface SockJsConfiguration {
|
||||
|
||||
/**
|
||||
* Streaming transports save responses on the client side and don't free
|
||||
* memory used by delivered messages. Such transports need to recycle the
|
||||
* connection once in a while. This property sets a minimum number of bytes
|
||||
* that can be send over a single HTTP streaming request before it will be
|
||||
* closed. After that client will open a new request. Setting this value to
|
||||
* one effectively disables streaming and will make streaming transports to
|
||||
* behave like polling transports.
|
||||
* <p>
|
||||
* The default value is 128K (i.e. 128 * 1024).
|
||||
*/
|
||||
public int getStreamBytesLimit();
|
||||
|
||||
/**
|
||||
* The amount of time in milliseconds when the server has not sent any
|
||||
* messages and after which the server should send a heartbeat frame to the
|
||||
* client in order to keep the connection from breaking.
|
||||
* <p>
|
||||
* The default value is 25,000 (25 seconds).
|
||||
*/
|
||||
public long getHeartbeatTime();
|
||||
|
||||
/**
|
||||
* A scheduler instance to use for scheduling heart-beat messages.
|
||||
*/
|
||||
public TaskScheduler getTaskScheduler();
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.fasterxml.jackson.core.io.JsonStringEncoder;
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class SockJsFrame {
|
||||
|
||||
private static final SockJsFrame OPEN_FRAME = new SockJsFrame("o");
|
||||
|
||||
private static final SockJsFrame HEARTBEAT_FRAME = new SockJsFrame("h");
|
||||
|
||||
private static final SockJsFrame CLOSE_GO_AWAY_FRAME = closeFrame(3000, "Go away!");
|
||||
|
||||
private static final SockJsFrame CLOSE_ANOTHER_CONNECTION_OPEN = closeFrame(2010, "Another connection still open");
|
||||
|
||||
|
||||
private final String content;
|
||||
|
||||
|
||||
private SockJsFrame(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
|
||||
public static SockJsFrame openFrame() {
|
||||
return OPEN_FRAME;
|
||||
}
|
||||
|
||||
public static SockJsFrame heartbeatFrame() {
|
||||
return HEARTBEAT_FRAME;
|
||||
}
|
||||
|
||||
public static SockJsFrame messageFrame(String... messages) {
|
||||
return new MessageFrame(messages);
|
||||
}
|
||||
|
||||
public static SockJsFrame closeFrameGoAway() {
|
||||
return CLOSE_GO_AWAY_FRAME;
|
||||
}
|
||||
|
||||
public static SockJsFrame closeFrameAnotherConnectionOpen() {
|
||||
return CLOSE_ANOTHER_CONNECTION_OPEN;
|
||||
}
|
||||
|
||||
public static SockJsFrame closeFrame(int code, String reason) {
|
||||
return new SockJsFrame("c[" + code + ",\"" + reason + "\"]");
|
||||
}
|
||||
|
||||
|
||||
public String getContent() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
public byte[] getContentBytes() {
|
||||
return this.content.getBytes(Charset.forName("UTF-8"));
|
||||
}
|
||||
|
||||
/**
|
||||
* See "JSON Unicode Encoding" section of SockJS protocol.
|
||||
*/
|
||||
public static String escapeCharacters(char[] characters) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (char c : characters) {
|
||||
if (isSockJsEscapeCharacter(c)) {
|
||||
result.append('\\').append('u');
|
||||
String hex = Integer.toHexString(c).toLowerCase();
|
||||
for (int i = 0; i < (4 - hex.length()); i++) {
|
||||
result.append('0');
|
||||
}
|
||||
result.append(hex);
|
||||
}
|
||||
else {
|
||||
result.append(c);
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
// See `escapable_by_server` var in SockJS protocol (under "JSON Unicode Encoding")
|
||||
|
||||
private static boolean isSockJsEscapeCharacter(char ch) {
|
||||
return (ch >= '\u0000' && ch <= '\u001F') || (ch >= '\u200C' && ch <= '\u200F')
|
||||
|| (ch >= '\u2028' && ch <= '\u202F') || (ch >= '\u2060' && ch <= '\u206F')
|
||||
|| (ch >= '\uFFF0' && ch <= '\uFFFF') || (ch >= '\uD800' && ch <= '\uDFFF');
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
String result = this.content;
|
||||
if (result.length() > 80) {
|
||||
result = result.substring(0, 80) + "...(truncated)";
|
||||
}
|
||||
return "SockJsFrame content='" + result.replace("\n", "\\n").replace("\r", "\\r") + "'";
|
||||
}
|
||||
|
||||
|
||||
private static class MessageFrame extends SockJsFrame {
|
||||
|
||||
public MessageFrame(String... messages) {
|
||||
super(prepareContent(messages));
|
||||
}
|
||||
|
||||
public static String prepareContent(String... messages) {
|
||||
Assert.notNull(messages, "messages required");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("a[");
|
||||
for (int i=0; i < messages.length; i++) {
|
||||
sb.append('"');
|
||||
// TODO: dependency on Jackson
|
||||
char[] quotedChars = JsonStringEncoder.getInstance().quoteAsString(messages[i]);
|
||||
sb.append(escapeCharacters(quotedChars));
|
||||
sb.append('"');
|
||||
if (i < messages.length - 1) {
|
||||
sb.append(',');
|
||||
}
|
||||
}
|
||||
sb.append(']');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public interface FrameFormat {
|
||||
|
||||
SockJsFrame format(SockJsFrame frame);
|
||||
}
|
||||
|
||||
public static class DefaultFrameFormat implements FrameFormat {
|
||||
|
||||
private final String format;
|
||||
|
||||
public DefaultFrameFormat(String format) {
|
||||
Assert.notNull(format, "format is required");
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param format a String with a single %s formatting character where the
|
||||
* frame content is to be inserted; e.g. "data: %s\r\n\r\n"
|
||||
* @return new SockJsFrame instance with the formatted content
|
||||
*/
|
||||
public SockJsFrame format(SockJsFrame frame) {
|
||||
String content = String.format(this.format, preProcessContent(frame.getContent()));
|
||||
return new SockJsFrame(content);
|
||||
}
|
||||
|
||||
protected String preProcessContent(String content) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.sockjs;
|
||||
|
||||
import org.springframework.core.NestedRuntimeException;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class SockJsRuntimeException extends NestedRuntimeException {
|
||||
|
||||
public SockJsRuntimeException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public SockJsRuntimeException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface SockJsService {
|
||||
|
||||
|
||||
void handleRequest(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler)
|
||||
throws IOException, TransportErrorException;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs;
|
||||
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
|
||||
/**
|
||||
* A factory for creating a SockJS session.
|
||||
*
|
||||
* @param <S> The type of session being created
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface SockJsSessionFactory {
|
||||
|
||||
/**
|
||||
* Create a new SockJS session.
|
||||
* @param sessionId the ID of the session
|
||||
* @param webSocketHandler the underlying {@link WebSocketHandler}
|
||||
* @return a new non-null session
|
||||
*/
|
||||
AbstractSockJsSession createSession(String sessionId, WebSocketHandler webSocketHandler);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs;
|
||||
|
||||
import org.springframework.core.NestedRuntimeException;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
|
||||
|
||||
/**
|
||||
* Raised when a TransportHandler fails during request processing.
|
||||
*
|
||||
* <p>If the underlying exception occurs while sending messages to the client,
|
||||
* the session will have been closed and the {@link WebSocketHandler} notified.
|
||||
*
|
||||
* <p>If the underlying exception occurs while processing an incoming HTTP request
|
||||
* including posted messages, the session will remain open. Only the incoming
|
||||
* request is rejected.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class TransportErrorException extends NestedRuntimeException {
|
||||
|
||||
private final String sockJsSessionId;
|
||||
|
||||
public TransportErrorException(String msg, Throwable cause, String sockJsSessionId) {
|
||||
super(msg, cause);
|
||||
this.sockJsSessionId = sockJsSessionId;
|
||||
}
|
||||
|
||||
public String getSockJsSessionId() {
|
||||
return sockJsSessionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "Transport error for SockJS session id=" + this.sockJsSessionId + ", " + super.getMessage();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs;
|
||||
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface TransportHandler {
|
||||
|
||||
TransportType getTransportType();
|
||||
|
||||
void handleRequest(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSocketHandler handler, AbstractSockJsSession session) throws TransportErrorException;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public enum TransportType {
|
||||
|
||||
WEBSOCKET("websocket", HttpMethod.GET),
|
||||
|
||||
XHR("xhr", HttpMethod.POST, "cors", "jsessionid", "no_cache"),
|
||||
|
||||
XHR_SEND("xhr_send", HttpMethod.POST, "cors", "jsessionid", "no_cache"),
|
||||
|
||||
JSONP("jsonp", HttpMethod.GET, "jsessionid", "no_cache"),
|
||||
|
||||
JSONP_SEND("jsonp_send", HttpMethod.POST, "jsessionid", "no_cache"),
|
||||
|
||||
XHR_STREAMING("xhr_streaming", HttpMethod.POST, "cors", "jsessionid", "no_cache"),
|
||||
|
||||
EVENT_SOURCE("eventsource", HttpMethod.GET, "jsessionid", "no_cache"),
|
||||
|
||||
HTML_FILE("htmlfile", HttpMethod.GET, "jsessionid", "no_cache");
|
||||
|
||||
|
||||
private final String value;
|
||||
|
||||
private final HttpMethod httpMethod;
|
||||
|
||||
private final List<String> headerHints;
|
||||
|
||||
private static final Map<String, TransportType> transportTypes = new HashMap<String, TransportType>();
|
||||
|
||||
static {
|
||||
for (TransportType type : values()) {
|
||||
transportTypes.put(type.value, type);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private TransportType(String value, HttpMethod httpMethod, String... headerHints) {
|
||||
this.value = value;
|
||||
this.httpMethod = httpMethod;
|
||||
this.headerHints = Arrays.asList(headerHints);
|
||||
}
|
||||
|
||||
|
||||
public String value() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The HTTP method for this transport.
|
||||
*/
|
||||
public HttpMethod getHttpMethod() {
|
||||
return this.httpMethod;
|
||||
}
|
||||
|
||||
public boolean setsNoCache() {
|
||||
return this.headerHints.contains("no_cache");
|
||||
}
|
||||
|
||||
public boolean supportsCors() {
|
||||
return this.headerHints.contains("cors");
|
||||
}
|
||||
|
||||
public boolean setsJsessionId() {
|
||||
return this.headerHints.contains("jsessionid");
|
||||
}
|
||||
|
||||
public static TransportType fromValue(String value) {
|
||||
return transportTypes.get(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Common abstractions for the SockJS protocol.
|
||||
*/
|
||||
package org.springframework.web.socket.sockjs;
|
||||
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
|
||||
import org.springframework.http.Cookie;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.DefaultHandshakeHandler;
|
||||
import org.springframework.web.socket.server.HandshakeHandler;
|
||||
import org.springframework.web.socket.server.support.ServerWebSocketSessionInitializer;
|
||||
import org.springframework.web.socket.sockjs.AbstractSockJsService;
|
||||
import org.springframework.web.socket.sockjs.AbstractSockJsSession;
|
||||
import org.springframework.web.socket.sockjs.ConfigurableTransportHandler;
|
||||
import org.springframework.web.socket.sockjs.SockJsService;
|
||||
import org.springframework.web.socket.sockjs.SockJsSessionFactory;
|
||||
import org.springframework.web.socket.sockjs.TransportErrorException;
|
||||
import org.springframework.web.socket.sockjs.TransportHandler;
|
||||
import org.springframework.web.socket.sockjs.TransportType;
|
||||
import org.springframework.web.socket.sockjs.transport.EventSourceTransportHandler;
|
||||
import org.springframework.web.socket.sockjs.transport.HtmlFileTransportHandler;
|
||||
import org.springframework.web.socket.sockjs.transport.JsonpPollingTransportHandler;
|
||||
import org.springframework.web.socket.sockjs.transport.JsonpTransportHandler;
|
||||
import org.springframework.web.socket.sockjs.transport.WebSocketTransportHandler;
|
||||
import org.springframework.web.socket.sockjs.transport.XhrPollingTransportHandler;
|
||||
import org.springframework.web.socket.sockjs.transport.XhrStreamingTransportHandler;
|
||||
import org.springframework.web.socket.sockjs.transport.XhrTransportHandler;
|
||||
|
||||
|
||||
/**
|
||||
* A default implementation of {@link SockJsService} adding support for transport handling
|
||||
* and session management.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class DefaultSockJsService extends AbstractSockJsService {
|
||||
|
||||
private final Map<TransportType, TransportHandler> transportHandlers = new HashMap<TransportType, TransportHandler>();
|
||||
|
||||
private final Map<String, AbstractSockJsSession> sessions = new ConcurrentHashMap<String, AbstractSockJsSession>();
|
||||
|
||||
private final ServerWebSocketSessionInitializer sessionInitializer = new ServerWebSocketSessionInitializer();
|
||||
|
||||
private ScheduledFuture sessionCleanupTask;
|
||||
|
||||
|
||||
/**
|
||||
* Create an instance with default {@link TransportHandler transport handler} types.
|
||||
*
|
||||
* @param taskScheduler a task scheduler for heart-beat messages and removing
|
||||
* timed-out sessions; the provided TaskScheduler should be declared as a
|
||||
* Spring bean to ensure it is initialized at start up and shut down when the
|
||||
* application stops.
|
||||
*/
|
||||
public DefaultSockJsService(TaskScheduler taskScheduler) {
|
||||
this(taskScheduler, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance by overriding or replacing completely the default
|
||||
* {@link TransportHandler transport handler} types.
|
||||
*
|
||||
* @param taskScheduler a task scheduler for heart-beat messages and removing
|
||||
* timed-out sessions; the provided TaskScheduler should be declared as a
|
||||
* Spring bean to ensure it is initialized at start up and shut down when the
|
||||
* application stops.
|
||||
* @param transportHandlers the transport handlers to use (replaces the default ones);
|
||||
* can be {@code null}.
|
||||
* @param transportHandlerOverrides zero or more overrides to the default transport
|
||||
* handler types.
|
||||
*/
|
||||
public DefaultSockJsService(TaskScheduler taskScheduler, Set<TransportHandler> transportHandlers,
|
||||
TransportHandler... transportHandlerOverrides) {
|
||||
|
||||
super(taskScheduler);
|
||||
|
||||
transportHandlers = CollectionUtils.isEmpty(transportHandlers) ? getDefaultTransportHandlers() : transportHandlers;
|
||||
addTransportHandlers(transportHandlers);
|
||||
addTransportHandlers(Arrays.asList(transportHandlerOverrides));
|
||||
}
|
||||
|
||||
protected final Set<TransportHandler> getDefaultTransportHandlers() {
|
||||
Set<TransportHandler> result = new HashSet<TransportHandler>();
|
||||
result.add(new XhrPollingTransportHandler());
|
||||
result.add(new XhrTransportHandler());
|
||||
result.add(new JsonpPollingTransportHandler());
|
||||
result.add(new JsonpTransportHandler());
|
||||
result.add(new XhrStreamingTransportHandler());
|
||||
result.add(new EventSourceTransportHandler());
|
||||
result.add(new HtmlFileTransportHandler());
|
||||
try {
|
||||
result.add(new WebSocketTransportHandler(new DefaultHandshakeHandler()));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("Failed to add default WebSocketTransportHandler: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected void addTransportHandlers(Collection<TransportHandler> handlers) {
|
||||
for (TransportHandler handler : handlers) {
|
||||
if (handler instanceof ConfigurableTransportHandler) {
|
||||
((ConfigurableTransportHandler) handler).setSockJsConfiguration(this);
|
||||
}
|
||||
this.transportHandlers.put(handler.getTransportType(), handler);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<TransportType, TransportHandler> getTransportHandlers() {
|
||||
return Collections.unmodifiableMap(this.transportHandlers);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleRawWebSocketRequest(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSocketHandler webSocketHandler) throws IOException {
|
||||
|
||||
if (isWebSocketEnabled()) {
|
||||
TransportHandler transportHandler = this.transportHandlers.get(TransportType.WEBSOCKET);
|
||||
if (transportHandler != null) {
|
||||
if (transportHandler instanceof HandshakeHandler) {
|
||||
((HandshakeHandler) transportHandler).doHandshake(request, response, webSocketHandler);
|
||||
return;
|
||||
}
|
||||
}
|
||||
logger.warn("No handler for raw WebSocket messages");
|
||||
}
|
||||
response.setStatusCode(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleTransportRequest(ServerHttpRequest request, ServerHttpResponse response,
|
||||
String sessionId, TransportType transportType, WebSocketHandler webSocketHandler)
|
||||
throws IOException, TransportErrorException {
|
||||
|
||||
TransportHandler transportHandler = this.transportHandlers.get(transportType);
|
||||
|
||||
if (transportHandler == null) {
|
||||
logger.debug("Transport handler not found");
|
||||
response.setStatusCode(HttpStatus.NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
HttpMethod supportedMethod = transportType.getHttpMethod();
|
||||
if (!supportedMethod.equals(request.getMethod())) {
|
||||
if (HttpMethod.OPTIONS.equals(request.getMethod()) && transportType.supportsCors()) {
|
||||
response.setStatusCode(HttpStatus.NO_CONTENT);
|
||||
addCorsHeaders(request, response, HttpMethod.OPTIONS, supportedMethod);
|
||||
addCacheHeaders(response);
|
||||
}
|
||||
else {
|
||||
List<HttpMethod> supportedMethods = Arrays.asList(supportedMethod);
|
||||
if (transportType.supportsCors()) {
|
||||
supportedMethods.add(HttpMethod.OPTIONS);
|
||||
}
|
||||
sendMethodNotAllowed(response, supportedMethods);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
AbstractSockJsSession session = getSockJsSession(sessionId, webSocketHandler,
|
||||
transportHandler, request, response);
|
||||
|
||||
if (session != null) {
|
||||
if (transportType.setsNoCache()) {
|
||||
addNoCacheHeaders(response);
|
||||
}
|
||||
|
||||
if (transportType.setsJsessionId() && isJsessionIdCookieRequired()) {
|
||||
Cookie cookie = request.getCookies().getCookie("JSESSIONID");
|
||||
String jsid = (cookie != null) ? cookie.getValue() : "dummy";
|
||||
// TODO: bypass use of Cookie object (causes Jetty to set Expires header)
|
||||
response.getHeaders().set("Set-Cookie", "JSESSIONID=" + jsid + ";path=/");
|
||||
}
|
||||
|
||||
if (transportType.supportsCors()) {
|
||||
addCorsHeaders(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
transportHandler.handleRequest(request, response, webSocketHandler, session);
|
||||
}
|
||||
|
||||
protected AbstractSockJsSession getSockJsSession(String sessionId, WebSocketHandler handler,
|
||||
TransportHandler transportHandler, ServerHttpRequest request, ServerHttpResponse response) {
|
||||
|
||||
AbstractSockJsSession session = this.sessions.get(sessionId);
|
||||
if (session != null) {
|
||||
return session;
|
||||
}
|
||||
|
||||
if (transportHandler instanceof SockJsSessionFactory) {
|
||||
SockJsSessionFactory sessionFactory = (SockJsSessionFactory) transportHandler;
|
||||
|
||||
synchronized (this.sessions) {
|
||||
session = this.sessions.get(sessionId);
|
||||
if (session != null) {
|
||||
return session;
|
||||
}
|
||||
if (this.sessionCleanupTask == null) {
|
||||
scheduleSessionTask();
|
||||
}
|
||||
logger.debug("Creating new session with session id \"" + sessionId + "\"");
|
||||
session = sessionFactory.createSession(sessionId, handler);
|
||||
this.sessionInitializer.initialize(request, response, session);
|
||||
this.sessions.put(sessionId, session);
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void scheduleSessionTask() {
|
||||
this.sessionCleanupTask = getTaskScheduler().scheduleAtFixedRate(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
int count = sessions.size();
|
||||
if (logger.isTraceEnabled() && (count != 0)) {
|
||||
logger.trace("Checking " + count + " session(s) for timeouts [" + getName() + "]");
|
||||
}
|
||||
for (AbstractSockJsSession session : sessions.values()) {
|
||||
if (session.getTimeSinceLastActive() > getDisconnectDelay()) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Removing " + session + " for [" + getName() + "]");
|
||||
}
|
||||
session.close();
|
||||
sessions.remove(session.getId());
|
||||
}
|
||||
}
|
||||
if (logger.isTraceEnabled() && (count != 0)) {
|
||||
logger.trace(sessions.size() + " remaining session(s) [" + getName() + "]");
|
||||
}
|
||||
}
|
||||
catch (Throwable t) {
|
||||
logger.error("Failed to complete session timeout checks for [" + getName() + "]", t);
|
||||
}
|
||||
}
|
||||
}, getDisconnectDelay());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs.support;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.http.server.AsyncServletServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.HttpRequestHandler;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.sockjs.SockJsService;
|
||||
import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator;
|
||||
import org.springframework.web.socket.support.LoggingWebSocketHandlerDecorator;
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class SockJsHttpRequestHandler implements HttpRequestHandler {
|
||||
|
||||
private final SockJsService sockJsService;
|
||||
|
||||
private final WebSocketHandler webSocketHandler;
|
||||
|
||||
|
||||
/**
|
||||
* Class constructor with {@link SockJsHandler} instance ...
|
||||
*/
|
||||
public SockJsHttpRequestHandler(SockJsService sockJsService, WebSocketHandler webSocketHandler) {
|
||||
Assert.notNull(sockJsService, "sockJsService is required");
|
||||
Assert.notNull(webSocketHandler, "webSocketHandler is required");
|
||||
this.sockJsService = sockJsService;
|
||||
this.webSocketHandler = decorateWebSocketHandler(webSocketHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorate the WebSocketHandler provided to the class constructor.
|
||||
* <p>
|
||||
* By default {@link ExceptionWebSocketHandlerDecorator} and
|
||||
* {@link LoggingWebSocketHandlerDecorator} are applied are added.
|
||||
*/
|
||||
protected WebSocketHandler decorateWebSocketHandler(WebSocketHandler handler) {
|
||||
handler = new ExceptionWebSocketHandlerDecorator(handler);
|
||||
return new LoggingWebSocketHandlerDecorator(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
|
||||
throws ServletException, IOException {
|
||||
|
||||
ServerHttpRequest httpRequest = new AsyncServletServerHttpRequest(request, response);
|
||||
ServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
|
||||
this.sockJsService.handleRequest(httpRequest, httpResponse, this.webSocketHandler);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Server-side SockJS classes including a
|
||||
* {@link org.springframework.web.socket.sockjs.support.DefaultSockJsService} implementation
|
||||
* as well as a Spring MVC HandlerMapping mapping SockJS services to incoming requests.
|
||||
*
|
||||
*/
|
||||
package org.springframework.web.socket.sockjs.support;
|
||||
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.sockjs.AbstractSockJsSession;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame;
|
||||
import org.springframework.web.socket.sockjs.SockJsRuntimeException;
|
||||
import org.springframework.web.socket.sockjs.TransportErrorException;
|
||||
import org.springframework.web.socket.sockjs.TransportHandler;
|
||||
import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public abstract class AbstractHttpReceivingTransportHandler implements TransportHandler {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(this.getClass());
|
||||
|
||||
// TODO: the JSON library used must be configurable
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
|
||||
public ObjectMapper getObjectMapper() {
|
||||
return this.objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSocketHandler webSocketHandler, AbstractSockJsSession session)
|
||||
throws TransportErrorException {
|
||||
|
||||
if (session == null) {
|
||||
response.setStatusCode(HttpStatus.NOT_FOUND);
|
||||
logger.warn("Session not found");
|
||||
return;
|
||||
}
|
||||
|
||||
handleRequestInternal(request, response, session);
|
||||
}
|
||||
|
||||
protected void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response,
|
||||
AbstractSockJsSession session) throws TransportErrorException {
|
||||
|
||||
String[] messages = null;
|
||||
try {
|
||||
messages = readMessages(request);
|
||||
}
|
||||
catch (JsonMappingException ex) {
|
||||
logger.error("Failed to read message: ", ex);
|
||||
sendInternalServerError(response, "Payload expected.", session.getId());
|
||||
return;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
logger.error("Failed to read message: ", ex);
|
||||
sendInternalServerError(response, "Broken JSON encoding.", session.getId());
|
||||
return;
|
||||
}
|
||||
catch (Throwable t) {
|
||||
logger.error("Failed to read message: ", t);
|
||||
sendInternalServerError(response, "Failed to process messages", session.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Received message(s): " + Arrays.asList(messages));
|
||||
}
|
||||
|
||||
response.setStatusCode(getResponseStatus());
|
||||
response.getHeaders().setContentType(new MediaType("text", "plain", Charset.forName("UTF-8")));
|
||||
|
||||
try {
|
||||
session.delegateMessages(messages);
|
||||
}
|
||||
catch (Throwable t) {
|
||||
ExceptionWebSocketHandlerDecorator.tryCloseWithError(session, t, logger);
|
||||
throw new SockJsRuntimeException("Unhandled WebSocketHandler error in " + this, t);
|
||||
}
|
||||
}
|
||||
|
||||
protected void sendInternalServerError(ServerHttpResponse response, String error,
|
||||
String sessionId) throws TransportErrorException {
|
||||
|
||||
try {
|
||||
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
response.getBody().write(error.getBytes("UTF-8"));
|
||||
}
|
||||
catch (Throwable t) {
|
||||
throw new TransportErrorException("Failed to send error message to client", t, sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract String[] readMessages(ServerHttpRequest request) throws IOException;
|
||||
|
||||
protected abstract HttpStatus getResponseStatus();
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.sockjs.AbstractSockJsSession;
|
||||
import org.springframework.web.socket.sockjs.ConfigurableTransportHandler;
|
||||
import org.springframework.web.socket.sockjs.SockJsConfiguration;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame;
|
||||
import org.springframework.web.socket.sockjs.SockJsSessionFactory;
|
||||
import org.springframework.web.socket.sockjs.TransportErrorException;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public abstract class AbstractHttpSendingTransportHandler
|
||||
implements ConfigurableTransportHandler, SockJsSessionFactory {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(this.getClass());
|
||||
|
||||
private SockJsConfiguration sockJsConfig;
|
||||
|
||||
|
||||
@Override
|
||||
public void setSockJsConfiguration(SockJsConfiguration sockJsConfig) {
|
||||
this.sockJsConfig = sockJsConfig;
|
||||
}
|
||||
|
||||
public SockJsConfiguration getSockJsConfig() {
|
||||
return this.sockJsConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSocketHandler webSocketHandler, AbstractSockJsSession session)
|
||||
throws TransportErrorException {
|
||||
|
||||
// Set content type before writing
|
||||
response.getHeaders().setContentType(getContentType());
|
||||
|
||||
AbstractHttpServerSockJsSession httpServerSession = (AbstractHttpServerSockJsSession) session;
|
||||
handleRequestInternal(request, response, httpServerSession);
|
||||
}
|
||||
|
||||
protected void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response,
|
||||
AbstractHttpServerSockJsSession httpServerSession) throws TransportErrorException {
|
||||
|
||||
if (httpServerSession.isNew()) {
|
||||
logger.debug("Opening " + getTransportType() + " connection");
|
||||
httpServerSession.setInitialRequest(request, response, getFrameFormat(request));
|
||||
}
|
||||
else if (!httpServerSession.isActive()) {
|
||||
logger.debug("starting " + getTransportType() + " async request");
|
||||
httpServerSession.setLongPollingRequest(request, response, getFrameFormat(request));
|
||||
}
|
||||
else {
|
||||
try {
|
||||
logger.debug("another " + getTransportType() + " connection still open: " + httpServerSession);
|
||||
SockJsFrame closeFrame = SockJsFrame.closeFrameAnotherConnectionOpen();
|
||||
response.getBody().write(getFrameFormat(request).format(closeFrame).getContentBytes());
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new TransportErrorException("Failed to send SockJS close frame", e, httpServerSession.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract MediaType getContentType();
|
||||
|
||||
protected abstract FrameFormat getFrameFormat(ServerHttpRequest request);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
import org.springframework.http.server.AsyncServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.sockjs.AbstractServerSockJsSession;
|
||||
import org.springframework.web.socket.sockjs.SockJsConfiguration;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame;
|
||||
import org.springframework.web.socket.sockjs.TransportErrorException;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat;
|
||||
import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator;
|
||||
|
||||
/**
|
||||
* An abstract base class for use with HTTP-based transports.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public abstract class AbstractHttpServerSockJsSession extends AbstractServerSockJsSession {
|
||||
|
||||
private FrameFormat frameFormat;
|
||||
|
||||
private final BlockingQueue<String> messageCache = new ArrayBlockingQueue<String>(100);
|
||||
|
||||
private AsyncServerHttpRequest asyncRequest;
|
||||
|
||||
private ServerHttpResponse response;
|
||||
|
||||
|
||||
public AbstractHttpServerSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) {
|
||||
super(sessionId, config, handler);
|
||||
}
|
||||
|
||||
public synchronized void setInitialRequest(ServerHttpRequest request, ServerHttpResponse response,
|
||||
FrameFormat frameFormat) throws TransportErrorException {
|
||||
|
||||
try {
|
||||
udpateRequest(request, response, frameFormat);
|
||||
writePrelude();
|
||||
writeFrame(SockJsFrame.openFrame());
|
||||
}
|
||||
catch (Throwable t) {
|
||||
tryCloseWithSockJsTransportError(t, null);
|
||||
throw new TransportErrorException("Failed open SockJS session", t, getId());
|
||||
}
|
||||
try {
|
||||
delegateConnectionEstablished();
|
||||
}
|
||||
catch (Throwable t) {
|
||||
ExceptionWebSocketHandlerDecorator.tryCloseWithError(this, t, logger);
|
||||
}
|
||||
}
|
||||
|
||||
protected void writePrelude() throws IOException {
|
||||
}
|
||||
|
||||
public synchronized void setLongPollingRequest(ServerHttpRequest request, ServerHttpResponse response,
|
||||
FrameFormat frameFormat) throws TransportErrorException {
|
||||
|
||||
try {
|
||||
udpateRequest(request, response, frameFormat);
|
||||
|
||||
if (isClosed()) {
|
||||
logger.debug("connection already closed");
|
||||
try {
|
||||
writeFrame(SockJsFrame.closeFrameGoAway());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new TransportErrorException("Failed to send SockJS close frame", ex, getId());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.asyncRequest.setTimeout(-1);
|
||||
this.asyncRequest.startAsync();
|
||||
|
||||
scheduleHeartbeat();
|
||||
tryFlushCache();
|
||||
}
|
||||
catch (Throwable t) {
|
||||
tryCloseWithSockJsTransportError(t, null);
|
||||
throw new TransportErrorException("Failed to start long running request and flush messages", t, getId());
|
||||
}
|
||||
}
|
||||
|
||||
private void udpateRequest(ServerHttpRequest request, ServerHttpResponse response, FrameFormat frameFormat) {
|
||||
Assert.notNull(request, "expected request");
|
||||
Assert.notNull(response, "expected response");
|
||||
Assert.notNull(frameFormat, "expected frameFormat");
|
||||
Assert.isInstanceOf(AsyncServerHttpRequest.class, request, "Expected AsyncServerHttpRequest");
|
||||
this.asyncRequest = (AsyncServerHttpRequest) request;
|
||||
this.response = response;
|
||||
this.frameFormat = frameFormat;
|
||||
}
|
||||
|
||||
|
||||
public synchronized boolean isActive() {
|
||||
return ((this.asyncRequest != null) && (!this.asyncRequest.isAsyncCompleted()));
|
||||
}
|
||||
|
||||
protected BlockingQueue<String> getMessageCache() {
|
||||
return this.messageCache;
|
||||
}
|
||||
|
||||
protected ServerHttpRequest getRequest() {
|
||||
return this.asyncRequest;
|
||||
}
|
||||
|
||||
protected ServerHttpResponse getResponse() {
|
||||
return this.response;
|
||||
}
|
||||
|
||||
protected final synchronized void sendMessageInternal(String message) throws IOException {
|
||||
this.messageCache.add(message);
|
||||
tryFlushCache();
|
||||
}
|
||||
|
||||
private void tryFlushCache() throws IOException {
|
||||
if (isActive() && !getMessageCache().isEmpty()) {
|
||||
logger.trace("Flushing messages");
|
||||
flushCache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only called if the connection is currently active
|
||||
*/
|
||||
protected abstract void flushCache() throws IOException;
|
||||
|
||||
@Override
|
||||
protected void disconnect(CloseStatus status) {
|
||||
resetRequest();
|
||||
}
|
||||
|
||||
protected synchronized void resetRequest() {
|
||||
updateLastActiveTime();
|
||||
if (isActive() && this.asyncRequest.isAsyncStarted()) {
|
||||
try {
|
||||
logger.debug("Completing async request");
|
||||
this.asyncRequest.completeAsync();
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
logger.error("Failed to complete async request: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
this.asyncRequest = null;
|
||||
this.response = null;
|
||||
}
|
||||
|
||||
protected synchronized void writeFrameInternal(SockJsFrame frame) throws IOException {
|
||||
if (isActive()) {
|
||||
frame = this.frameFormat.format(frame);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Writing " + frame);
|
||||
}
|
||||
this.response.getBody().write(frame.getContentBytes());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.sockjs.TransportType;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame.DefaultFrameFormat;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class EventSourceTransportHandler extends AbstractHttpSendingTransportHandler {
|
||||
|
||||
@Override
|
||||
public TransportType getTransportType() {
|
||||
return TransportType.EVENT_SOURCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MediaType getContentType() {
|
||||
return new MediaType("text", "event-stream", Charset.forName("UTF-8"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamingServerSockJsSession createSession(String sessionId, WebSocketHandler handler) {
|
||||
Assert.notNull(getSockJsConfig(), "This transport requires SockJsConfiguration");
|
||||
return new StreamingServerSockJsSession(sessionId, getSockJsConfig(), handler) {
|
||||
@Override
|
||||
protected void writePrelude() throws IOException {
|
||||
getResponse().getBody().write('\r');
|
||||
getResponse().getBody().write('\n');
|
||||
getResponse().flush();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FrameFormat getFrameFormat(ServerHttpRequest request) {
|
||||
return new DefaultFrameFormat("data: %s\r\n\r\n");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.sockjs.TransportErrorException;
|
||||
import org.springframework.web.socket.sockjs.TransportType;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame.DefaultFrameFormat;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat;
|
||||
import org.springframework.web.util.JavaScriptUtils;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class HtmlFileTransportHandler extends AbstractHttpSendingTransportHandler {
|
||||
|
||||
private static final String PARTIAL_HTML_CONTENT;
|
||||
|
||||
static {
|
||||
StringBuilder sb = new StringBuilder(
|
||||
"<!doctype html>\n" +
|
||||
"<html><head>\n" +
|
||||
" <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n" +
|
||||
" <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" +
|
||||
"</head><body><h2>Don't panic!</h2>\n" +
|
||||
" <script>\n" +
|
||||
" document.domain = document.domain;\n" +
|
||||
" var c = parent.%s;\n" +
|
||||
" c.start();\n" +
|
||||
" function p(d) {c.message(d);};\n" +
|
||||
" window.onload = function() {c.stop();};\n" +
|
||||
" </script>"
|
||||
);
|
||||
|
||||
// Safari needs at least 1024 bytes to parse the website.
|
||||
// http://code.google.com/p/browsersec/wiki/Part2#Survey_of_content_sniffing_behaviors
|
||||
int spaces = 1024 - sb.length();
|
||||
for (int i=0; i < spaces; i++) {
|
||||
sb.append(' ');
|
||||
}
|
||||
|
||||
PARTIAL_HTML_CONTENT = sb.toString();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public TransportType getTransportType() {
|
||||
return TransportType.HTML_FILE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MediaType getContentType() {
|
||||
return new MediaType("text", "html", Charset.forName("UTF-8"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamingServerSockJsSession createSession(String sessionId, WebSocketHandler handler) {
|
||||
Assert.notNull(getSockJsConfig(), "This transport requires SockJsConfiguration");
|
||||
|
||||
return new StreamingServerSockJsSession(sessionId, getSockJsConfig(), handler) {
|
||||
|
||||
@Override
|
||||
protected void writePrelude() throws IOException {
|
||||
// we already validated the parameter..
|
||||
String callback = getRequest().getQueryParams().getFirst("c");
|
||||
|
||||
String html = String.format(PARTIAL_HTML_CONTENT, callback);
|
||||
getResponse().getBody().write(html.getBytes("UTF-8"));
|
||||
getResponse().flush();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response,
|
||||
AbstractHttpServerSockJsSession session) throws TransportErrorException {
|
||||
|
||||
try {
|
||||
String callback = request.getQueryParams().getFirst("c");
|
||||
if (! StringUtils.hasText(callback)) {
|
||||
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
response.getBody().write("\"callback\" parameter required".getBytes("UTF-8"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Throwable t) {
|
||||
throw new TransportErrorException("Failed to send error to client", t, session.getId());
|
||||
}
|
||||
|
||||
super.handleRequestInternal(request, response, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FrameFormat getFrameFormat(ServerHttpRequest request) {
|
||||
return new DefaultFrameFormat("<script>\np(\"%s\");\n</script>\r\n") {
|
||||
@Override
|
||||
protected String preProcessContent(String content) {
|
||||
return JavaScriptUtils.javaScriptEscape(content);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs.transport;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame;
|
||||
import org.springframework.web.socket.sockjs.TransportErrorException;
|
||||
import org.springframework.web.socket.sockjs.TransportType;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat;
|
||||
import org.springframework.web.util.JavaScriptUtils;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class JsonpPollingTransportHandler extends AbstractHttpSendingTransportHandler {
|
||||
|
||||
@Override
|
||||
public TransportType getTransportType() {
|
||||
return TransportType.JSONP;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MediaType getContentType() {
|
||||
return new MediaType("application", "javascript", Charset.forName("UTF-8"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PollingServerSockJsSession createSession(String sessionId, WebSocketHandler handler) {
|
||||
Assert.notNull(getSockJsConfig(), "This transport requires SockJsConfiguration");
|
||||
return new PollingServerSockJsSession(sessionId, getSockJsConfig(), handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response,
|
||||
AbstractHttpServerSockJsSession session) throws TransportErrorException {
|
||||
|
||||
try {
|
||||
String callback = request.getQueryParams().getFirst("c");
|
||||
if (! StringUtils.hasText(callback)) {
|
||||
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
response.getBody().write("\"callback\" parameter required".getBytes("UTF-8"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Throwable t) {
|
||||
throw new TransportErrorException("Failed to send error to client", t, session.getId());
|
||||
}
|
||||
|
||||
super.handleRequestInternal(request, response, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FrameFormat getFrameFormat(ServerHttpRequest request) {
|
||||
|
||||
// we already validated the parameter..
|
||||
String callback = request.getQueryParams().getFirst("c");
|
||||
|
||||
return new SockJsFrame.DefaultFrameFormat(callback + "(\"%s\");\r\n") {
|
||||
@Override
|
||||
protected String preProcessContent(String content) {
|
||||
return JavaScriptUtils.javaScriptEscape(content);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.socket.sockjs.AbstractSockJsSession;
|
||||
import org.springframework.web.socket.sockjs.TransportErrorException;
|
||||
import org.springframework.web.socket.sockjs.TransportType;
|
||||
|
||||
public class JsonpTransportHandler extends AbstractHttpReceivingTransportHandler {
|
||||
|
||||
@Override
|
||||
public TransportType getTransportType() {
|
||||
return TransportType.JSONP_SEND;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response,
|
||||
AbstractSockJsSession sockJsSession) throws TransportErrorException {
|
||||
|
||||
if (MediaType.APPLICATION_FORM_URLENCODED.equals(request.getHeaders().getContentType())) {
|
||||
if (request.getQueryParams().getFirst("d") == null) {
|
||||
sendInternalServerError(response, "Payload expected.", sockJsSession.getId());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
super.handleRequestInternal(request, response, sockJsSession);
|
||||
|
||||
try {
|
||||
response.getBody().write("ok".getBytes("UTF-8"));
|
||||
}
|
||||
catch (Throwable t) {
|
||||
throw new TransportErrorException("Failed to write response body", t, sockJsSession.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] readMessages(ServerHttpRequest request) throws IOException {
|
||||
if (MediaType.APPLICATION_FORM_URLENCODED.equals(request.getHeaders().getContentType())) {
|
||||
String d = request.getQueryParams().getFirst("d");
|
||||
return getObjectMapper().readValue(d, String[].class);
|
||||
}
|
||||
else {
|
||||
return getObjectMapper().readValue(request.getBody(), String[].class);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpStatus getResponseStatus() {
|
||||
return HttpStatus.OK;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.socket.sockjs.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.sockjs.SockJsConfiguration;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame;
|
||||
|
||||
|
||||
public class PollingServerSockJsSession extends AbstractHttpServerSockJsSession {
|
||||
|
||||
public PollingServerSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) {
|
||||
super(sessionId, config, handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void flushCache() throws IOException {
|
||||
cancelHeartbeat();
|
||||
String[] messages = getMessageCache().toArray(new String[getMessageCache().size()]);
|
||||
getMessageCache().clear();
|
||||
writeFrame(SockJsFrame.messageFrame(messages));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeFrame(SockJsFrame frame) throws IOException {
|
||||
super.writeFrame(frame);
|
||||
resetRequest();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs.transport;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.adapter.TextWebSocketHandlerAdapter;
|
||||
import org.springframework.web.socket.sockjs.SockJsConfiguration;
|
||||
|
||||
|
||||
/**
|
||||
* A wrapper around a {@link WebSocketHandler} instance that parses and adds SockJS
|
||||
* messages frames and also sends SockJS heartbeat messages.
|
||||
*
|
||||
* <p>
|
||||
* Implementations of the {@link WebSocketHandler} interface in this class allow
|
||||
* exceptions from the wrapped {@link WebSocketHandler} to propagate. However, any
|
||||
* exceptions resulting from SockJS message handling (e.g. while sending SockJS frames or
|
||||
* heartbeat messages) are caught and treated as transport errors, i.e. routed to the
|
||||
* {@link WebSocketHandler#handleTransportError(WebSocketSession, Throwable)
|
||||
* handleTransportError} method of the wrapped handler and the session closed.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class SockJsWebSocketHandler extends TextWebSocketHandlerAdapter {
|
||||
|
||||
private final SockJsConfiguration sockJsConfig;
|
||||
|
||||
private WebSocketServerSockJsSession session;
|
||||
|
||||
private final AtomicInteger sessionCount = new AtomicInteger(0);
|
||||
|
||||
|
||||
public SockJsWebSocketHandler(SockJsConfiguration config,
|
||||
WebSocketHandler webSocketHandler, WebSocketServerSockJsSession session) {
|
||||
|
||||
Assert.notNull(config, "sockJsConfig is required");
|
||||
Assert.notNull(webSocketHandler, "webSocketHandler is required");
|
||||
Assert.notNull(session, "session is required");
|
||||
|
||||
this.sockJsConfig = config;
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
protected SockJsConfiguration getSockJsConfig() {
|
||||
return this.sockJsConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession wsSession) throws Exception {
|
||||
Assert.isTrue(this.sessionCount.compareAndSet(0, 1), "Unexpected connection");
|
||||
this.session.initWebSocketSession(wsSession);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTextMessage(WebSocketSession wsSession, TextMessage message) throws Exception {
|
||||
this.session.handleMessage(message, wsSession);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession wsSession, CloseStatus status) throws Exception {
|
||||
this.session.delegateConnectionClosed(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportError(WebSocketSession webSocketSession, Throwable exception) throws Exception {
|
||||
this.session.delegateError(exception);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.sockjs.SockJsConfiguration;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame;
|
||||
import org.springframework.web.socket.sockjs.TransportErrorException;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat;
|
||||
|
||||
public class StreamingServerSockJsSession extends AbstractHttpServerSockJsSession {
|
||||
|
||||
private int byteCount;
|
||||
|
||||
|
||||
public StreamingServerSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) {
|
||||
super(sessionId, config, handler);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public synchronized void setInitialRequest(ServerHttpRequest request, ServerHttpResponse response,
|
||||
FrameFormat frameFormat) throws TransportErrorException {
|
||||
|
||||
super.setInitialRequest(request, response, frameFormat);
|
||||
|
||||
// the WebSocketHandler delegate may have closed the session
|
||||
if (!isClosed()) {
|
||||
super.setLongPollingRequest(request, response, frameFormat);
|
||||
}
|
||||
}
|
||||
|
||||
protected void flushCache() throws IOException {
|
||||
|
||||
cancelHeartbeat();
|
||||
|
||||
do {
|
||||
String message = getMessageCache().poll();
|
||||
SockJsFrame frame = SockJsFrame.messageFrame(message);
|
||||
writeFrame(frame);
|
||||
|
||||
this.byteCount += frame.getContentBytes().length + 1;
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(this.byteCount + " bytes written so far, "
|
||||
+ getMessageCache().size() + " more messages not flushed");
|
||||
}
|
||||
if (this.byteCount >= getSockJsConfig().getStreamBytesLimit()) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Streamed bytes limit reached. Recycling current request");
|
||||
}
|
||||
resetRequest();
|
||||
break;
|
||||
}
|
||||
} while (!getMessageCache().isEmpty());
|
||||
|
||||
scheduleHeartbeat();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void resetRequest() {
|
||||
super.resetRequest();
|
||||
this.byteCount = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void writeFrameInternal(SockJsFrame frame) throws IOException {
|
||||
if (isActive()) {
|
||||
super.writeFrameInternal(frame);
|
||||
getResponse().flush();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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.sockjs.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.sockjs.AbstractServerSockJsSession;
|
||||
import org.springframework.web.socket.sockjs.SockJsConfiguration;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class WebSocketServerSockJsSession extends AbstractServerSockJsSession {
|
||||
|
||||
private WebSocketSession webSocketSession;
|
||||
|
||||
// TODO: JSON library used must be configurable
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
|
||||
public WebSocketServerSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) {
|
||||
super(sessionId, config, handler);
|
||||
}
|
||||
|
||||
public void initWebSocketSession(WebSocketSession session) throws Exception {
|
||||
this.webSocketSession = session;
|
||||
try {
|
||||
TextMessage message = new TextMessage(SockJsFrame.openFrame().getContent());
|
||||
this.webSocketSession.sendMessage(message);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
tryCloseWithSockJsTransportError(ex, null);
|
||||
return;
|
||||
}
|
||||
scheduleHeartbeat();
|
||||
delegateConnectionEstablished();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return ((this.webSocketSession != null) && this.webSocketSession.isOpen());
|
||||
}
|
||||
|
||||
public void handleMessage(TextMessage message, WebSocketSession wsSession) throws Exception {
|
||||
String payload = message.getPayload();
|
||||
if (StringUtils.isEmpty(payload)) {
|
||||
logger.trace("Ignoring empty message");
|
||||
return;
|
||||
}
|
||||
String[] messages;
|
||||
try {
|
||||
messages = objectMapper.readValue(payload, String[].class);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
logger.error("Broken data received. Terminating WebSocket connection abruptly", ex);
|
||||
tryCloseWithSockJsTransportError(ex, CloseStatus.BAD_DATA);
|
||||
return;
|
||||
}
|
||||
delegateMessages(messages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessageInternal(String message) throws IOException {
|
||||
cancelHeartbeat();
|
||||
writeFrame(SockJsFrame.messageFrame(message));
|
||||
scheduleHeartbeat();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeFrameInternal(SockJsFrame frame) throws IOException {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Write " + frame);
|
||||
}
|
||||
TextMessage message = new TextMessage(frame.getContent());
|
||||
this.webSocketSession.sendMessage(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void disconnect(CloseStatus status) throws IOException {
|
||||
this.webSocketSession.close(status);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.HandshakeHandler;
|
||||
import org.springframework.web.socket.sockjs.AbstractSockJsSession;
|
||||
import org.springframework.web.socket.sockjs.ConfigurableTransportHandler;
|
||||
import org.springframework.web.socket.sockjs.SockJsConfiguration;
|
||||
import org.springframework.web.socket.sockjs.SockJsSessionFactory;
|
||||
import org.springframework.web.socket.sockjs.TransportErrorException;
|
||||
import org.springframework.web.socket.sockjs.TransportHandler;
|
||||
import org.springframework.web.socket.sockjs.TransportType;
|
||||
|
||||
|
||||
/**
|
||||
* A WebSocket {@link TransportHandler} that delegates to a {@link HandshakeHandler}
|
||||
* passing a SockJS {@link WebSocketHandler}. Also implements {@link HandshakeHandler}
|
||||
* directly in support for raw WebSocket communication at SockJS URL "/websocket".
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class WebSocketTransportHandler implements ConfigurableTransportHandler,
|
||||
HandshakeHandler, SockJsSessionFactory {
|
||||
|
||||
private final HandshakeHandler handshakeHandler;
|
||||
|
||||
private SockJsConfiguration sockJsConfig;
|
||||
|
||||
|
||||
public WebSocketTransportHandler(HandshakeHandler handshakeHandler) {
|
||||
Assert.notNull(handshakeHandler, "handshakeHandler is required");
|
||||
this.handshakeHandler = handshakeHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransportType getTransportType() {
|
||||
return TransportType.WEBSOCKET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSockJsConfiguration(SockJsConfiguration sockJsConfig) {
|
||||
this.sockJsConfig = sockJsConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractSockJsSession createSession(String sessionId, WebSocketHandler webSocketHandler) {
|
||||
return new WebSocketServerSockJsSession(sessionId, this.sockJsConfig, webSocketHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRequest(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSocketHandler webSocketHandler, AbstractSockJsSession session) throws TransportErrorException {
|
||||
|
||||
try {
|
||||
WebSocketServerSockJsSession wsSession = (WebSocketServerSockJsSession) session;
|
||||
WebSocketHandler sockJsWrapper = new SockJsWebSocketHandler(this.sockJsConfig, webSocketHandler, wsSession);
|
||||
this.handshakeHandler.doHandshake(request, response, sockJsWrapper);
|
||||
}
|
||||
catch (Throwable t) {
|
||||
throw new TransportErrorException("Failed to start handshake request", t, session.getId());
|
||||
}
|
||||
}
|
||||
|
||||
// HandshakeHandler methods
|
||||
|
||||
@Override
|
||||
public boolean doHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler)
|
||||
throws IOException {
|
||||
|
||||
return this.handshakeHandler.doHandshake(request, response, handler);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.socket.sockjs.transport;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.sockjs.TransportType;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame.DefaultFrameFormat;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat;
|
||||
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class XhrPollingTransportHandler extends AbstractHttpSendingTransportHandler {
|
||||
|
||||
|
||||
@Override
|
||||
public TransportType getTransportType() {
|
||||
return TransportType.XHR;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MediaType getContentType() {
|
||||
return new MediaType("application", "javascript", Charset.forName("UTF-8"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FrameFormat getFrameFormat(ServerHttpRequest request) {
|
||||
return new DefaultFrameFormat("%s\n");
|
||||
}
|
||||
|
||||
public PollingServerSockJsSession createSession(String sessionId, WebSocketHandler handler) {
|
||||
Assert.notNull(getSockJsConfig(), "This transport requires SockJsConfiguration");
|
||||
return new PollingServerSockJsSession(sessionId, getSockJsConfig(), handler);
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue