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:
Rossen Stoyanchev
2013-07-31 16:21:52 -04:00
parent 2af891683e
commit dad7115c23
31 changed files with 517 additions and 218 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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