Merge branch 'websocket'

This commit is contained in:
Rossen Stoyanchev 2013-05-06 14:46:29 -04:00
commit e7f38e5b17
117 changed files with 9111 additions and 10 deletions

88
README-WEBSOCKET.md Normal file
View File

@ -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`

View File

@ -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"

View File

@ -1 +1 @@
version=4.0.0.BUILD-SNAPSHOT
version=4.0.0.WEBSOCKET-SNAPSHOT

View File

@ -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"

View File

@ -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 ;
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -31,4 +31,9 @@ public interface HttpMessage {
*/
HttpHeaders getHeaders();
/**
* TODO ..
*/
Cookies getCookies();
}

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -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()) {

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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.
*/

View File

@ -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);

View File

@ -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);
}
}
}
}

View File

@ -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 ;
}
}

View File

@ -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;
}
}

View File

@ -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 ;
}
}
}

View File

@ -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;
}
}

View File

@ -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 + "]";
}
}

View File

@ -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());
}
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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
}
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}
}

View File

@ -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()));
}
}

View File

@ -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
}
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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();
}
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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()));
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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
}
}
}

View File

@ -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");
}
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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);
}
};
}

View File

@ -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 }
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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());
}
}
}

View File

@ -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");
}
}

View File

@ -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);
}
};
}
}

View File

@ -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);
}
};
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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