diff --git a/build.gradle b/build.gradle index 899f2bf8e9..da2f7e84be 100644 --- a/build.gradle +++ b/build.gradle @@ -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 { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsService.java index 479226d31c..49593d538c 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsService.java @@ -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. - * - *

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. + *

+ * 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/**"). + *

+ * 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 diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsSession.java index c5e35a6f20..31d392b06e 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsSession.java @@ -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 diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsConfiguration.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsConfiguration.java index ceba86f242..f7cc5fd797 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsConfiguration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsConfiguration.java @@ -38,7 +38,7 @@ public interface SockJsConfiguration { * *

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

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(); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsFrame.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsFrame.java index b7620d2348..4c1a374ea7 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsFrame.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsFrame.java @@ -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); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsMessageCodec.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsMessageCodec.java new file mode 100644 index 0000000000..a4fabe891a --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsMessageCodec.java @@ -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: + * + *

+ * a["message1","message2"]
+ * 
+ * + * @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; + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/TransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/TransportHandler.java index b1c2e258ef..70449d59ff 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/TransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/TransportHandler.java @@ -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; diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsMessageCodec.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsMessageCodec.java new file mode 100644 index 0000000000..893113a058 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsMessageCodec.java @@ -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'); + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/Jackson2SockJsMessageCodec.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/Jackson2SockJsMessageCodec.java new file mode 100644 index 0000000000..b9b5617cd3 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/Jackson2SockJsMessageCodec.java @@ -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); + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/JacksonSockJsMessageCodec.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/JacksonSockJsMessageCodec.java new file mode 100644 index 0000000000..34f37c5f2f --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/JacksonSockJsMessageCodec.java @@ -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); + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/package-info.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/package-info.java index 2b3cc8d734..2359960d30 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/package-info.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/package-info.java @@ -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; diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpReceivingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpReceivingTransportHandler.java index ce98eb55f2..8320969e15 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpReceivingTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpReceivingTransportHandler.java @@ -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, diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpSendingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpSendingTransportHandler.java index 0dac595b0f..8d58423714 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpSendingTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpSendingTransportHandler.java @@ -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, diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/DefaultSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/DefaultSockJsService.java similarity index 87% rename from spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/DefaultSockJsService.java rename to spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/DefaultSockJsService.java index 57a3d34415..2be0f0f790 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/DefaultSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/DefaultSockJsService.java @@ -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 transportHandlers = new HashMap(); + private SockJsMessageCodec messageCodec; + private final Map sessions = new ConcurrentHashMap(); 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 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 getTransportHandlers() { return Collections.unmodifiableMap(this.transportHandlers); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/EventSourceTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/EventSourceTransportHandler.java index 39b5d83a9a..ea986bf55d 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/EventSourceTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/EventSourceTransportHandler.java @@ -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(); + } + } + } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/HtmlFileTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/HtmlFileTransportHandler.java index e3dfd012bb..ed76baf698 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/HtmlFileTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/HtmlFileTransportHandler.java @@ -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(); + } + } + } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/JsonpPollingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/JsonpPollingTransportHandler.java index 4ebf1751fe..0b134f442c 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/JsonpPollingTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/JsonpPollingTransportHandler.java @@ -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); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/JsonpTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/JsonpTransportHandler.java index 585fcb2272..98777f3c6e 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/JsonpTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/JsonpTransportHandler.java @@ -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 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()); } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/PollingSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/PollingSockJsSession.java index b35bd89d5e..13f04c822b 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/PollingSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/PollingSockJsSession.java @@ -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 diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/StreamingSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/StreamingSockJsSession.java index 33a9f23da7..3ce2ec9b2d 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/StreamingSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/StreamingSockJsSession.java @@ -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; diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/ConfigurableTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandlerSupport.java similarity index 53% rename from spring-websocket/src/main/java/org/springframework/web/socket/sockjs/ConfigurableTransportHandler.java rename to spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandlerSupport.java index f4b2fb51d1..3dfe2e6a06 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/ConfigurableTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandlerSupport.java @@ -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); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/WebSocketServerSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/WebSocketServerSockJsSession.java index 639bfbeba0..d1f873b433 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/WebSocketServerSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/WebSocketServerSockJsSession.java @@ -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(); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/WebSocketTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/WebSocketTransportHandler.java index 9bb00e4563..5b53f430ee 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/WebSocketTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/WebSocketTransportHandler.java @@ -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) { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrPollingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrPollingTransportHandler.java index 8c784a2e83..3f26872a47 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrPollingTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrPollingTransportHandler.java @@ -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); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrStreamingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrStreamingTransportHandler.java index e0e7881168..2e38c136fe 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrStreamingTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrStreamingTransportHandler.java @@ -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(); + } + } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrTransportHandler.java index eff26d2611..fdbc5f8893 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrTransportHandler.java @@ -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 diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/package-info.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/package-info.java index a9ea15cede..55c92a22eb 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/package-info.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/package-info.java @@ -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; diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/AbstractSockJsServiceTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/AbstractSockJsServiceTests.java index 13cb2e87e4..d72b3934a0 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/AbstractSockJsServiceTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/AbstractSockJsServiceTests.java @@ -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 { diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/StubSockJsConfig.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/StubSockJsConfig.java index 050c920540..095f1470b3 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/StubSockJsConfig.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/StubSockJsConfig.java @@ -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; + } + } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/DefaultSockJsServiceTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/DefaultSockJsServiceTests.java similarity index 96% rename from spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/DefaultSockJsServiceTests.java rename to spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/DefaultSockJsServiceTests.java index 31fb40c6c1..2849f1f6ee 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/DefaultSockJsServiceTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/DefaultSockJsServiceTests.java @@ -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 { diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/HttpReceivingTransportHandlerTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/HttpReceivingTransportHandlerTests.java index ee184d0fd6..816bda130a 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/HttpReceivingTransportHandlerTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/HttpReceivingTransportHandlerTests.java @@ -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());