Add SockJsMessageCodec
A SockJS message frame is an array of JSON-encoded messages and before this change the use of the Jackson 2 library was hard-coded. A Jackson 2 and Jackson 1.x implementations are provided and automatically used if those libraries are present on the classpath. Issue: SPR-10800
This commit is contained in:
@@ -507,7 +507,8 @@ project("spring-websocket") {
|
||||
}
|
||||
optional("org.eclipse.jetty.websocket:websocket-server:9.0.4.v20130625")
|
||||
optional("org.eclipse.jetty.websocket:websocket-client:9.0.4.v20130625")
|
||||
optional("com.fasterxml.jackson.core:jackson-databind:2.2.0") // required for SockJS support currently
|
||||
optional("com.fasterxml.jackson.core:jackson-databind:2.2.0")
|
||||
optional("org.codehaus.jackson:jackson-mapper-asl:1.9.12")
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
||||
@@ -46,17 +46,19 @@ 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...)}.
|
||||
* An abstract base class for {@link SockJsService} implementations that provides SockJS
|
||||
* path resolution and handling of static SockJS requests (e.g. "/info", "/iframe.html",
|
||||
* etc). Transport-specific requests are left as abstract methods.
|
||||
* <p>
|
||||
* This service can be integrated into any HTTP request handling mechanism (e.g. plain
|
||||
* Servlet, Spring MVC, or other). It is expected that it will be mapped correctly to a
|
||||
* prefix (e.g. "/echo") and will also handle all sub-URLs (i.e. "/echo/**").
|
||||
* <p>
|
||||
* The service itself is unaware of the underlying mapping mechanism but nevertheless must
|
||||
* be able to extract the SockJS path, i.e. the portion of the request path following the
|
||||
* prefix. In most cases, this class can auto-detect the SockJS path but it is also
|
||||
* possible to configure explicitly the prefixes via
|
||||
* {@link #setValidSockJsPrefixes(String...)}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
|
||||
@@ -73,13 +73,13 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess
|
||||
* @param config the sockJS configuration
|
||||
* @param webSocketHandler the recipient of SockJS messages
|
||||
*/
|
||||
public AbstractSockJsSession(String sessionId, SockJsConfiguration config,
|
||||
WebSocketHandler webSocketHandler) {
|
||||
Assert.notNull(sessionId, "sessionId must not be null");
|
||||
Assert.notNull(webSocketHandler, "webSocketHandler must not be null");
|
||||
public AbstractSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler webSocketHandler) {
|
||||
Assert.notNull(sessionId, "sessionId is required");
|
||||
Assert.notNull(config, "sockJsConfig is required");
|
||||
Assert.notNull(webSocketHandler, "webSocketHandler is required");
|
||||
this.id = sessionId;
|
||||
this.handler = webSocketHandler;
|
||||
this.sockJsConfig = config;
|
||||
this.handler = webSocketHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -38,7 +38,7 @@ public interface SockJsConfiguration {
|
||||
*
|
||||
* <p>The default value is 128K (i.e. 128 * 1024).
|
||||
*/
|
||||
public int getStreamBytesLimit();
|
||||
int getStreamBytesLimit();
|
||||
|
||||
/**
|
||||
* The amount of time in milliseconds when the server has not sent any
|
||||
@@ -47,11 +47,17 @@ public interface SockJsConfiguration {
|
||||
*
|
||||
* <p>The default value is 25,000 (25 seconds).
|
||||
*/
|
||||
public long getHeartbeatTime();
|
||||
long getHeartbeatTime();
|
||||
|
||||
/**
|
||||
* A scheduler instance to use for scheduling heart-beat messages.
|
||||
*/
|
||||
public TaskScheduler getTaskScheduler();
|
||||
TaskScheduler getTaskScheduler();
|
||||
|
||||
/**
|
||||
* The codec to use for encoding and decoding SockJS messages.
|
||||
* @exception IllegalStateException if no {@link SockJsMessageCodec} is available
|
||||
*/
|
||||
SockJsMessageCodec getMessageCodecRequired();
|
||||
|
||||
}
|
||||
|
||||
@@ -20,8 +20,6 @@ import java.nio.charset.Charset;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.fasterxml.jackson.core.io.JsonStringEncoder;
|
||||
|
||||
/**
|
||||
* Represents a SockJS frames. Provides methods for access to commonly used message
|
||||
* frames.
|
||||
@@ -31,42 +29,43 @@ import com.fasterxml.jackson.core.io.JsonStringEncoder;
|
||||
*/
|
||||
public class SockJsFrame {
|
||||
|
||||
private static final SockJsFrame OPEN_FRAME = new SockJsFrame("o");
|
||||
private static final SockJsFrame openFrame = new SockJsFrame("o");
|
||||
|
||||
private static final SockJsFrame HEARTBEAT_FRAME = new SockJsFrame("h");
|
||||
private static final SockJsFrame heartbeatFrame = new SockJsFrame("h");
|
||||
|
||||
private static final SockJsFrame CLOSE_GO_AWAY_FRAME = closeFrame(3000, "Go away!");
|
||||
private static final SockJsFrame closeGoAwayFrame = closeFrame(3000, "Go away!");
|
||||
|
||||
private static final SockJsFrame CLOSE_ANOTHER_CONNECTION_OPEN = closeFrame(2010, "Another connection still open");
|
||||
private static final SockJsFrame closeAnotherConnectionOpenFrame = closeFrame(2010, "Another connection still open");
|
||||
|
||||
|
||||
private final String content;
|
||||
|
||||
|
||||
private SockJsFrame(String content) {
|
||||
Assert.notNull("content must not be null");
|
||||
Assert.notNull("content is required");
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
|
||||
public static SockJsFrame openFrame() {
|
||||
return OPEN_FRAME;
|
||||
return openFrame;
|
||||
}
|
||||
|
||||
public static SockJsFrame heartbeatFrame() {
|
||||
return HEARTBEAT_FRAME;
|
||||
return heartbeatFrame;
|
||||
}
|
||||
|
||||
public static SockJsFrame messageFrame(String... messages) {
|
||||
return new MessageFrame(messages);
|
||||
public static SockJsFrame messageFrame(SockJsMessageCodec codec, String... messages) {
|
||||
String encoded = codec.encode(messages);
|
||||
return new SockJsFrame(encoded);
|
||||
}
|
||||
|
||||
public static SockJsFrame closeFrameGoAway() {
|
||||
return CLOSE_GO_AWAY_FRAME;
|
||||
return closeGoAwayFrame;
|
||||
}
|
||||
|
||||
public static SockJsFrame closeFrameAnotherConnectionOpen() {
|
||||
return CLOSE_ANOTHER_CONNECTION_OPEN;
|
||||
return closeAnotherConnectionOpenFrame;
|
||||
}
|
||||
|
||||
public static SockJsFrame closeFrame(int code, String reason) {
|
||||
@@ -82,35 +81,6 @@ public class SockJsFrame {
|
||||
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');
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String result = this.content;
|
||||
@@ -137,31 +107,6 @@ public class SockJsFrame {
|
||||
}
|
||||
|
||||
|
||||
private static class MessageFrame extends SockJsFrame {
|
||||
|
||||
public MessageFrame(String... messages) {
|
||||
super(prepareContent(messages));
|
||||
}
|
||||
|
||||
public static String prepareContent(String... messages) {
|
||||
Assert.notNull(messages, "messages must not be null");
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
|
||||
/**
|
||||
* A contract for encoding and decoding of messages to and from a SockJS message frame,
|
||||
* which is essentially an array of JSON-encoded messages. For example:
|
||||
*
|
||||
* <pre>
|
||||
* a["message1","message2"]
|
||||
* </pre>
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface SockJsMessageCodec {
|
||||
|
||||
|
||||
/**
|
||||
* Encode the given messages as a SockJS message frame. Aside from applying standard
|
||||
* JSON quoting to each message, there are some additional JSON Unicode escaping
|
||||
* rules. See the "JSON Unicode Encoding" section of SockJS protocol (i.e. the
|
||||
* protocol test suite).
|
||||
*
|
||||
* @param messages the messages to encode
|
||||
* @return the content for a SockJS message frame, never {@code null}
|
||||
*/
|
||||
String encode(String[] messages);
|
||||
|
||||
/**
|
||||
* Decode the given SockJS message frame.
|
||||
*
|
||||
* @param content the SockJS message frame
|
||||
* @return an array of messages or {@code null}
|
||||
* @throws IOException if the content could not be parsed
|
||||
*/
|
||||
String[] decode(String content) throws IOException;
|
||||
|
||||
/**
|
||||
* Decode the given SockJS message frame.
|
||||
*
|
||||
* @param content the SockJS message frame
|
||||
* @return an array of messages or {@code null}
|
||||
* @throws IOException if the content could not be parsed
|
||||
*/
|
||||
String[] decodeInputStream(InputStream content) throws IOException;
|
||||
|
||||
}
|
||||
@@ -36,6 +36,8 @@ public interface TransportHandler {
|
||||
|
||||
TransportType getTransportType();
|
||||
|
||||
void setSockJsConfiguration(SockJsConfiguration sockJsConfig);
|
||||
|
||||
void handleRequest(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSocketHandler handler, AbstractSockJsSession session) throws TransportErrorException;
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 org.springframework.util.Assert;
|
||||
import org.springframework.web.socket.sockjs.SockJsMessageCodec;
|
||||
|
||||
|
||||
/**
|
||||
* An base class for SockJS message codec that provides an implementation of
|
||||
* {@link #encode(String[])}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public abstract class AbstractSockJsMessageCodec implements SockJsMessageCodec {
|
||||
|
||||
|
||||
@Override
|
||||
public String encode(String[] messages) {
|
||||
Assert.notNull(messages, "messages must not be null");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("a[");
|
||||
for (int i=0; i < messages.length; i++) {
|
||||
sb.append('"');
|
||||
char[] quotedChars = applyJsonQuoting(messages[i]);
|
||||
sb.append(escapeSockJsSpecialChars(quotedChars));
|
||||
sb.append('"');
|
||||
if (i < messages.length - 1) {
|
||||
sb.append(',');
|
||||
}
|
||||
}
|
||||
sb.append(']');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply standard JSON string quoting (see http://www.json.org/).
|
||||
*/
|
||||
protected abstract char[] applyJsonQuoting(String content);
|
||||
|
||||
/**
|
||||
* See "JSON Unicode Encoding" section of SockJS protocol.
|
||||
*/
|
||||
private String escapeSockJsSpecialChars(char[] characters) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (char c : characters) {
|
||||
if (isSockJsSpecialChar(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` variable in the SockJS protocol test suite.
|
||||
*/
|
||||
private boolean isSockJsSpecialChar(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');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.io.InputStream;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.fasterxml.jackson.core.io.JsonStringEncoder;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
|
||||
/**
|
||||
* A Jackson 2 codec for encoding and decoding SockJS messages.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class Jackson2SockJsMessageCodec extends AbstractSockJsMessageCodec {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
|
||||
public Jackson2SockJsMessageCodec() {
|
||||
this.objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
public Jackson2SockJsMessageCodec(ObjectMapper objectMapper) {
|
||||
Assert.notNull(objectMapper, "objectMapper is required");
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] decode(String content) throws IOException {
|
||||
return this.objectMapper.readValue(content, String[].class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] decodeInputStream(InputStream content) throws IOException {
|
||||
return this.objectMapper.readValue(content, String[].class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected char[] applyJsonQuoting(String content) {
|
||||
return JsonStringEncoder.getInstance().quoteAsString(content);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.io.InputStream;
|
||||
|
||||
import org.codehaus.jackson.io.JsonStringEncoder;
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
|
||||
/**
|
||||
* A Jackson 1.x codec for encoding and decoding SockJS messages.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class JacksonSockJsMessageCodec extends AbstractSockJsMessageCodec {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
|
||||
public JacksonSockJsMessageCodec() {
|
||||
this.objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
public JacksonSockJsMessageCodec(ObjectMapper objectMapper) {
|
||||
Assert.notNull(objectMapper, "objectMapper is required");
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] decode(String content) throws IOException {
|
||||
return this.objectMapper.readValue(content, String[].class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] decodeInputStream(InputStream content) throws IOException {
|
||||
return this.objectMapper.readValue(content, String[].class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected char[] applyJsonQuoting(String content) {
|
||||
return JsonStringEncoder.getInstance().quoteAsString(content);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -15,10 +15,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Key server-side SockJS implementation 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.
|
||||
*
|
||||
* Support classes for the SockJS implementation.
|
||||
*/
|
||||
package org.springframework.web.socket.sockjs.support;
|
||||
|
||||
|
||||
@@ -20,8 +20,6 @@ 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;
|
||||
@@ -33,7 +31,6 @@ 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;
|
||||
|
||||
/**
|
||||
* Base class for HTTP-based transports that read input messages from HTTP requests.
|
||||
@@ -41,17 +38,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public abstract class AbstractHttpReceivingTransportHandler implements TransportHandler {
|
||||
public abstract class AbstractHttpReceivingTransportHandler
|
||||
extends TransportHandlerSupport 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,
|
||||
|
||||
@@ -18,19 +18,16 @@ 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.SockJsFrame.FrameFormat;
|
||||
import org.springframework.web.socket.sockjs.AbstractSockJsSession;
|
||||
import org.springframework.web.socket.sockjs.SockJsSessionFactory;
|
||||
import org.springframework.web.socket.sockjs.TransportErrorException;
|
||||
import org.springframework.web.socket.sockjs.TransportHandler;
|
||||
|
||||
/**
|
||||
* Base class for HTTP-based transports that send messages over HTTP.
|
||||
@@ -38,22 +35,9 @@ import org.springframework.web.socket.sockjs.TransportErrorException;
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public abstract class AbstractHttpSendingTransportHandler
|
||||
implements ConfigurableTransportHandler, SockJsSessionFactory {
|
||||
public abstract class AbstractHttpSendingTransportHandler extends TransportHandlerSupport
|
||||
implements TransportHandler, 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,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs.support;
|
||||
package org.springframework.web.socket.sockjs.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
@@ -34,6 +34,8 @@ 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.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
@@ -42,20 +44,13 @@ 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.SockJsMessageCodec;
|
||||
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;
|
||||
import org.springframework.web.socket.sockjs.support.Jackson2SockJsMessageCodec;
|
||||
|
||||
|
||||
/**
|
||||
@@ -68,8 +63,17 @@ import org.springframework.web.socket.sockjs.transport.XhrTransportHandler;
|
||||
*/
|
||||
public class DefaultSockJsService extends AbstractSockJsService {
|
||||
|
||||
private static final boolean jackson2Present = ClassUtils.isPresent(
|
||||
"com.fasterxml.jackson.databind.ObjectMapper", DefaultSockJsService.class.getClassLoader());
|
||||
|
||||
private static final boolean jacksonPresent = ClassUtils.isPresent(
|
||||
"org.codehaus.jackson.map.ObjectMapper", DefaultSockJsService.class.getClassLoader());
|
||||
|
||||
|
||||
private final Map<TransportType, TransportHandler> transportHandlers = new HashMap<TransportType, TransportHandler>();
|
||||
|
||||
private SockJsMessageCodec messageCodec;
|
||||
|
||||
private final Map<String, AbstractSockJsSession> sessions = new ConcurrentHashMap<String, AbstractSockJsSession>();
|
||||
|
||||
private final ServerWebSocketSessionInitializer sessionInitializer = new ServerWebSocketSessionInitializer();
|
||||
@@ -88,6 +92,16 @@ public class DefaultSockJsService extends AbstractSockJsService {
|
||||
public DefaultSockJsService(TaskScheduler taskScheduler) {
|
||||
super(taskScheduler);
|
||||
addTransportHandlers(getDefaultTransportHandlers());
|
||||
initMessageCodec();
|
||||
}
|
||||
|
||||
protected void initMessageCodec() {
|
||||
if (jackson2Present) {
|
||||
this.messageCodec = new Jackson2SockJsMessageCodec();
|
||||
}
|
||||
else if (jacksonPresent) {
|
||||
this.messageCodec = new Jackson2SockJsMessageCodec();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,6 +122,8 @@ public class DefaultSockJsService extends AbstractSockJsService {
|
||||
|
||||
super(taskScheduler);
|
||||
|
||||
initMessageCodec();
|
||||
|
||||
if (!CollectionUtils.isEmpty(transportHandlers)) {
|
||||
addTransportHandlers(transportHandlers);
|
||||
}
|
||||
@@ -143,13 +159,27 @@ public class DefaultSockJsService extends AbstractSockJsService {
|
||||
|
||||
protected void addTransportHandlers(Collection<TransportHandler> handlers) {
|
||||
for (TransportHandler handler : handlers) {
|
||||
if (handler instanceof ConfigurableTransportHandler) {
|
||||
((ConfigurableTransportHandler) handler).setSockJsConfiguration(this);
|
||||
}
|
||||
handler.setSockJsConfiguration(this);
|
||||
this.transportHandlers.put(handler.getTransportType(), handler);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void setMessageCodec(SockJsMessageCodec messageCodec) {
|
||||
this.messageCodec = messageCodec;
|
||||
}
|
||||
|
||||
public SockJsMessageCodec getMessageCodec() {
|
||||
return this.messageCodec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SockJsMessageCodec getMessageCodecRequired() {
|
||||
Assert.state(this.messageCodec != null, "A SockJsMessageCodec is required but not available."
|
||||
+ " Either add Jackson 2 or Jackson 1.x to the classpath, or configure a SockJsMessageCode");
|
||||
return this.messageCodec;
|
||||
}
|
||||
|
||||
public Map<TransportType, TransportHandler> getTransportHandlers() {
|
||||
return Collections.unmodifiableMap(this.transportHandlers);
|
||||
}
|
||||
@@ -21,8 +21,8 @@ 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.SockJsConfiguration;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame.DefaultFrameFormat;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat;
|
||||
import org.springframework.web.socket.sockjs.TransportType;
|
||||
@@ -36,6 +36,7 @@ import org.springframework.web.socket.sockjs.TransportType;
|
||||
*/
|
||||
public class EventSourceTransportHandler extends AbstractHttpSendingTransportHandler {
|
||||
|
||||
|
||||
@Override
|
||||
public TransportType getTransportType() {
|
||||
return TransportType.EVENT_SOURCE;
|
||||
@@ -48,15 +49,7 @@ public class EventSourceTransportHandler extends AbstractHttpSendingTransportHan
|
||||
|
||||
@Override
|
||||
public StreamingSockJsSession createSession(String sessionId, WebSocketHandler handler) {
|
||||
Assert.state(getSockJsConfig() != null, "This transport requires SockJsConfiguration");
|
||||
return new StreamingSockJsSession(sessionId, getSockJsConfig(), handler) {
|
||||
@Override
|
||||
protected void writePrelude() throws IOException {
|
||||
getResponse().getBody().write('\r');
|
||||
getResponse().getBody().write('\n');
|
||||
getResponse().flush();
|
||||
}
|
||||
};
|
||||
return new EventSourceStreamingSockJsSession(sessionId, getSockJsConfig(), handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -64,4 +57,19 @@ public class EventSourceTransportHandler extends AbstractHttpSendingTransportHan
|
||||
return new DefaultFrameFormat("data: %s\r\n\r\n");
|
||||
}
|
||||
|
||||
|
||||
private final class EventSourceStreamingSockJsSession extends StreamingSockJsSession {
|
||||
|
||||
private EventSourceStreamingSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) {
|
||||
super(sessionId, config, handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writePrelude() throws IOException {
|
||||
getResponse().getBody().write('\r');
|
||||
getResponse().getBody().write('\n');
|
||||
getResponse().flush();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@ 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.SockJsConfiguration;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame.DefaultFrameFormat;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat;
|
||||
import org.springframework.web.socket.sockjs.TransportErrorException;
|
||||
@@ -85,20 +85,7 @@ public class HtmlFileTransportHandler extends AbstractHttpSendingTransportHandle
|
||||
|
||||
@Override
|
||||
public StreamingSockJsSession createSession(String sessionId, WebSocketHandler handler) {
|
||||
Assert.state(getSockJsConfig() != null, "This transport requires SockJsConfiguration");
|
||||
|
||||
return new StreamingSockJsSession(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();
|
||||
}
|
||||
};
|
||||
return new HtmlFileStreamingSockJsSession(sessionId, getSockJsConfig(), handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -116,7 +103,6 @@ public class HtmlFileTransportHandler extends AbstractHttpSendingTransportHandle
|
||||
catch (Throwable t) {
|
||||
throw new TransportErrorException("Failed to send error to client", t, session.getId());
|
||||
}
|
||||
|
||||
super.handleRequestInternal(request, response, session);
|
||||
}
|
||||
|
||||
@@ -130,4 +116,22 @@ public class HtmlFileTransportHandler extends AbstractHttpSendingTransportHandle
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private final class HtmlFileStreamingSockJsSession extends StreamingSockJsSession {
|
||||
|
||||
private HtmlFileStreamingSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) {
|
||||
super(sessionId, config, 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ 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;
|
||||
@@ -51,7 +50,6 @@ public class JsonpPollingTransportHandler extends AbstractHttpSendingTransportHa
|
||||
|
||||
@Override
|
||||
public PollingSockJsSession createSession(String sessionId, WebSocketHandler handler) {
|
||||
Assert.state(getSockJsConfig() != null, "This transport requires SockJsConfiguration");
|
||||
return new PollingSockJsSession(sessionId, getSockJsConfig(), handler);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.socket.sockjs.AbstractSockJsSession;
|
||||
import org.springframework.web.socket.sockjs.SockJsMessageCodec;
|
||||
import org.springframework.web.socket.sockjs.TransportErrorException;
|
||||
import org.springframework.web.socket.sockjs.TransportHandler;
|
||||
import org.springframework.web.socket.sockjs.TransportType;
|
||||
@@ -61,14 +62,17 @@ public class JsonpTransportHandler extends AbstractHttpReceivingTransportHandler
|
||||
|
||||
@Override
|
||||
protected String[] readMessages(ServerHttpRequest request) throws IOException {
|
||||
|
||||
SockJsMessageCodec messageCodec = getSockJsConfig().getMessageCodecRequired();
|
||||
|
||||
MediaType contentType = request.getHeaders().getContentType();
|
||||
if ((contentType != null) && MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType)) {
|
||||
MultiValueMap<String, String> map = this.formConverter.read(null, request);
|
||||
String d = map.getFirst("d");
|
||||
return (StringUtils.hasText(d)) ? getObjectMapper().readValue(d, String[].class) : null;
|
||||
return (StringUtils.hasText(d)) ? messageCodec.decode(d) : null;
|
||||
}
|
||||
else {
|
||||
return getObjectMapper().readValue(request.getBody(), String[].class);
|
||||
return messageCodec.decodeInputStream(request.getBody());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.io.IOException;
|
||||
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.SockJsMessageCodec;
|
||||
|
||||
/**
|
||||
* A SockJS session for use with polling HTTP transports.
|
||||
@@ -36,10 +37,14 @@ public class PollingSockJsSession extends AbstractHttpSockJsSession {
|
||||
|
||||
@Override
|
||||
protected void flushCache() throws IOException {
|
||||
|
||||
cancelHeartbeat();
|
||||
String[] messages = getMessageCache().toArray(new String[getMessageCache().size()]);
|
||||
getMessageCache().clear();
|
||||
writeFrame(SockJsFrame.messageFrame(messages));
|
||||
|
||||
SockJsMessageCodec messageCodec = getSockJsConfig().getMessageCodecRequired();
|
||||
SockJsFrame frame = SockJsFrame.messageFrame(messageCodec, messages);
|
||||
writeFrame(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -24,6 +24,7 @@ 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.SockJsFrame.FrameFormat;
|
||||
import org.springframework.web.socket.sockjs.SockJsMessageCodec;
|
||||
import org.springframework.web.socket.sockjs.TransportErrorException;
|
||||
|
||||
/**
|
||||
@@ -60,7 +61,8 @@ public class StreamingSockJsSession extends AbstractHttpSockJsSession {
|
||||
|
||||
do {
|
||||
String message = getMessageCache().poll();
|
||||
SockJsFrame frame = SockJsFrame.messageFrame(message);
|
||||
SockJsMessageCodec messageCodec = getSockJsConfig().getMessageCodecRequired();
|
||||
SockJsFrame frame = SockJsFrame.messageFrame(messageCodec, message);
|
||||
writeFrame(frame);
|
||||
|
||||
this.byteCount += frame.getContentBytes().length + 1;
|
||||
|
||||
@@ -14,16 +14,31 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs;
|
||||
package org.springframework.web.socket.sockjs.transport;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.web.socket.sockjs.SockJsConfiguration;
|
||||
|
||||
|
||||
/**
|
||||
* A {@link TransportHandler} that requires access to SockJS configuration options.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
* @sicne 4.0
|
||||
*/
|
||||
public interface ConfigurableTransportHandler extends TransportHandler {
|
||||
public abstract class TransportHandlerSupport {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(this.getClass());
|
||||
|
||||
private SockJsConfiguration sockJsConfig;
|
||||
|
||||
|
||||
public void setSockJsConfiguration(SockJsConfiguration sockJsConfig) {
|
||||
this.sockJsConfig = sockJsConfig;
|
||||
}
|
||||
|
||||
public SockJsConfiguration getSockJsConfig() {
|
||||
return this.sockJsConfig;
|
||||
}
|
||||
|
||||
void setSockJsConfiguration(SockJsConfiguration sockJsConfig);
|
||||
|
||||
}
|
||||
@@ -26,8 +26,7 @@ import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.sockjs.AbstractSockJsSession;
|
||||
import org.springframework.web.socket.sockjs.SockJsConfiguration;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.web.socket.sockjs.SockJsMessageCodec;
|
||||
|
||||
/**
|
||||
* A SockJS session for use with the WebSocket transport.
|
||||
@@ -39,9 +38,6 @@ public class WebSocketServerSockJsSession extends AbstractSockJsSession {
|
||||
|
||||
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);
|
||||
@@ -89,7 +85,7 @@ public class WebSocketServerSockJsSession extends AbstractSockJsSession {
|
||||
}
|
||||
String[] messages;
|
||||
try {
|
||||
messages = this.objectMapper.readValue(payload, String[].class);
|
||||
messages = getSockJsConfig().getMessageCodecRequired().decode(payload);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
logger.error("Broken data received. Terminating WebSocket connection abruptly", ex);
|
||||
@@ -102,7 +98,9 @@ public class WebSocketServerSockJsSession extends AbstractSockJsSession {
|
||||
@Override
|
||||
public void sendMessageInternal(String message) throws IOException {
|
||||
cancelHeartbeat();
|
||||
writeFrame(SockJsFrame.messageFrame(message));
|
||||
SockJsMessageCodec messageCodec = getSockJsConfig().getMessageCodecRequired();
|
||||
SockJsFrame frame = SockJsFrame.messageFrame(messageCodec, message);
|
||||
writeFrame(frame);
|
||||
scheduleHeartbeat();
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@ 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;
|
||||
@@ -41,13 +39,11 @@ import org.springframework.web.socket.sockjs.TransportType;
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class WebSocketTransportHandler implements ConfigurableTransportHandler,
|
||||
HandshakeHandler, SockJsSessionFactory {
|
||||
public class WebSocketTransportHandler extends TransportHandlerSupport
|
||||
implements TransportHandler, SockJsSessionFactory, HandshakeHandler {
|
||||
|
||||
private final HandshakeHandler handshakeHandler;
|
||||
|
||||
private SockJsConfiguration sockJsConfig;
|
||||
|
||||
|
||||
public WebSocketTransportHandler(HandshakeHandler handshakeHandler) {
|
||||
Assert.notNull(handshakeHandler, "handshakeHandler must not be null");
|
||||
@@ -60,14 +56,9 @@ public class WebSocketTransportHandler implements ConfigurableTransportHandler,
|
||||
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);
|
||||
return new WebSocketServerSockJsSession(sessionId, getSockJsConfig(), webSocketHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -76,7 +67,7 @@ public class WebSocketTransportHandler implements ConfigurableTransportHandler,
|
||||
|
||||
try {
|
||||
WebSocketServerSockJsSession wsSession = (WebSocketServerSockJsSession) session;
|
||||
WebSocketHandler sockJsWrapper = new SockJsWebSocketHandler(this.sockJsConfig, webSocketHandler, wsSession);
|
||||
WebSocketHandler sockJsWrapper = new SockJsWebSocketHandler(getSockJsConfig(), webSocketHandler, wsSession);
|
||||
this.handshakeHandler.doHandshake(request, response, sockJsWrapper);
|
||||
}
|
||||
catch (Throwable t) {
|
||||
|
||||
@@ -20,7 +20,6 @@ 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.SockJsFrame.DefaultFrameFormat;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat;
|
||||
@@ -52,7 +51,6 @@ public class XhrPollingTransportHandler extends AbstractHttpSendingTransportHand
|
||||
|
||||
@Override
|
||||
public PollingSockJsSession createSession(String sessionId, WebSocketHandler handler) {
|
||||
Assert.state(getSockJsConfig() != null, "This transport requires SockJsConfiguration");
|
||||
return new PollingSockJsSession(sessionId, getSockJsConfig(), handler);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ 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.SockJsConfiguration;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame.DefaultFrameFormat;
|
||||
import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat;
|
||||
import org.springframework.web.socket.sockjs.TransportHandler;
|
||||
@@ -36,6 +36,7 @@ import org.springframework.web.socket.sockjs.TransportType;
|
||||
*/
|
||||
public class XhrStreamingTransportHandler extends AbstractHttpSendingTransportHandler {
|
||||
|
||||
|
||||
@Override
|
||||
public TransportType getTransportType() {
|
||||
return TransportType.XHR_STREAMING;
|
||||
@@ -48,19 +49,7 @@ public class XhrStreamingTransportHandler extends AbstractHttpSendingTransportHa
|
||||
|
||||
@Override
|
||||
public StreamingSockJsSession createSession(String sessionId, WebSocketHandler handler) {
|
||||
Assert.state(getSockJsConfig() != null, "This transport requires SockJsConfiguration");
|
||||
|
||||
return new StreamingSockJsSession(sessionId, getSockJsConfig(), handler) {
|
||||
|
||||
@Override
|
||||
protected void writePrelude() throws IOException {
|
||||
for (int i=0; i < 2048; i++) {
|
||||
getResponse().getBody().write('h');
|
||||
}
|
||||
getResponse().getBody().write('\n');
|
||||
getResponse().flush();
|
||||
}
|
||||
};
|
||||
return new XhrStreamingSockJsSession(sessionId, getSockJsConfig(), handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -68,4 +57,20 @@ public class XhrStreamingTransportHandler extends AbstractHttpSendingTransportHa
|
||||
return new DefaultFrameFormat("%s\n");
|
||||
}
|
||||
|
||||
|
||||
private final class XhrStreamingSockJsSession extends StreamingSockJsSession {
|
||||
|
||||
private XhrStreamingSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) {
|
||||
super(sessionId, config, handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writePrelude() throws IOException {
|
||||
for (int i=0; i < 2048; i++) {
|
||||
getResponse().getBody().write('h');
|
||||
}
|
||||
getResponse().getBody().write('\n');
|
||||
getResponse().flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.springframework.web.socket.sockjs.TransportType;
|
||||
*/
|
||||
public class XhrTransportHandler extends AbstractHttpReceivingTransportHandler {
|
||||
|
||||
|
||||
@Override
|
||||
public TransportType getTransportType() {
|
||||
return TransportType.XHR_SEND;
|
||||
@@ -37,7 +38,7 @@ public class XhrTransportHandler extends AbstractHttpReceivingTransportHandler {
|
||||
|
||||
@Override
|
||||
protected String[] readMessages(ServerHttpRequest request) throws IOException {
|
||||
return getObjectMapper().readValue(request.getBody(), String[].class);
|
||||
return getSockJsConfig().getMessageCodecRequired().decodeInputStream(request.getBody());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
/**
|
||||
* Server-side support for SockJS transports including
|
||||
* {@link org.springframework.web.socket.sockjs.TransportHandler} implementations
|
||||
* for processing incoming requests and their
|
||||
* for processing incoming requests, their
|
||||
* {@link org.springframework.web.socket.sockjs.AbstractSockJsSession session} counterparts for
|
||||
* sending messages over the various transports.
|
||||
* sending messages over the various transports, and
|
||||
* {@link org.springframework.web.socket.sockjs.transport.DefaultSockJsService}.
|
||||
*/
|
||||
package org.springframework.web.socket.sockjs.transport;
|
||||
|
||||
|
||||
@@ -224,6 +224,11 @@ public class AbstractSockJsServiceTests extends AbstractHttpRequestTests {
|
||||
super(scheduler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SockJsMessageCodec getMessageCodecRequired() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleRawWebSocketRequest(ServerHttpRequest request,
|
||||
ServerHttpResponse response, WebSocketHandler handler) throws IOException {
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.web.socket.sockjs;
|
||||
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.web.socket.sockjs.support.Jackson2SockJsMessageCodec;
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
@@ -30,6 +31,8 @@ public class StubSockJsConfig implements SockJsConfiguration {
|
||||
|
||||
private TaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
|
||||
|
||||
private SockJsMessageCodec messageCodec = new Jackson2SockJsMessageCodec();
|
||||
|
||||
|
||||
@Override
|
||||
public int getStreamBytesLimit() {
|
||||
@@ -58,4 +61,17 @@ public class StubSockJsConfig implements SockJsConfiguration {
|
||||
this.taskScheduler = taskScheduler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SockJsMessageCodec getMessageCodecRequired() {
|
||||
return this.messageCodec;
|
||||
}
|
||||
|
||||
public SockJsMessageCodec getMessageCodec() {
|
||||
return messageCodec;
|
||||
}
|
||||
|
||||
public void setMessageCodec(SockJsMessageCodec messageCodec) {
|
||||
this.messageCodec = messageCodec;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.socket.sockjs.support;
|
||||
package org.springframework.web.socket.sockjs.transport;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
@@ -30,6 +30,7 @@ import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.web.socket.AbstractHttpRequestTests;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.sockjs.AbstractSockJsSession;
|
||||
import org.springframework.web.socket.sockjs.SockJsConfiguration;
|
||||
import org.springframework.web.socket.sockjs.SockJsSessionFactory;
|
||||
import org.springframework.web.socket.sockjs.StubSockJsConfig;
|
||||
import org.springframework.web.socket.sockjs.TestSockJsSession;
|
||||
@@ -164,6 +165,10 @@ public class DefaultSockJsServiceTests extends AbstractHttpRequestTests {
|
||||
return TransportType.XHR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSockJsConfiguration(SockJsConfiguration sockJsConfig) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRequest(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSocketHandler handler, AbstractSockJsSession session) throws TransportErrorException {
|
||||
@@ -176,6 +181,7 @@ public class DefaultSockJsServiceTests extends AbstractHttpRequestTests {
|
||||
public AbstractSockJsSession createSession(String sessionId, WebSocketHandler webSocketHandler) {
|
||||
return new TestSockJsSession(sessionId, new StubSockJsConfig(), webSocketHandler);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class StubXhrSendTransportHandler implements TransportHandler {
|
||||
@@ -185,6 +191,10 @@ public class DefaultSockJsServiceTests extends AbstractHttpRequestTests {
|
||||
return TransportType.XHR_SEND;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSockJsConfiguration(SockJsConfiguration sockJsConfig) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRequest(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSocketHandler handler, AbstractSockJsSession session) throws TransportErrorException {
|
||||
@@ -105,16 +105,20 @@ public class HttpReceivingTransportHandlerTests extends AbstractHttpRequestTest
|
||||
@Test
|
||||
public void delegateMessageException() throws Exception {
|
||||
|
||||
StubSockJsConfig sockJsConfig = new StubSockJsConfig();
|
||||
|
||||
this.servletRequest.setContent("[\"x\"]".getBytes("UTF-8"));
|
||||
|
||||
WebSocketHandler webSocketHandler = mock(WebSocketHandler.class);
|
||||
TestSockJsSession session = new TestSockJsSession("1", new StubSockJsConfig(), webSocketHandler);
|
||||
TestSockJsSession session = new TestSockJsSession("1", sockJsConfig, webSocketHandler);
|
||||
session.delegateConnectionEstablished();
|
||||
|
||||
doThrow(new Exception()).when(webSocketHandler).handleMessage(session, new TextMessage("x"));
|
||||
|
||||
try {
|
||||
new XhrTransportHandler().handleRequest(this.request, this.response, webSocketHandler, session);
|
||||
XhrTransportHandler transportHandler = new XhrTransportHandler();
|
||||
transportHandler.setSockJsConfiguration(sockJsConfig);
|
||||
transportHandler.handleRequest(this.request, this.response, webSocketHandler, session);
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (TransportErrorException ex) {
|
||||
@@ -129,6 +133,7 @@ public class HttpReceivingTransportHandlerTests extends AbstractHttpRequestTest
|
||||
WebSocketHandler webSocketHandler = mock(WebSocketHandler.class);
|
||||
AbstractSockJsSession session = new TestSockJsSession("1", new StubSockJsConfig(), webSocketHandler);
|
||||
|
||||
transportHandler.setSockJsConfiguration(new StubSockJsConfig());
|
||||
transportHandler.handleRequest(this.request, this.response, webSocketHandler, session);
|
||||
|
||||
assertEquals("text/plain;charset=UTF-8", this.response.getHeaders().getContentType().toString());
|
||||
|
||||
Reference in New Issue
Block a user