Skip to content

Commit dad7115

Browse files
committed
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
1 parent 2af8916 commit dad7115

31 files changed

+517
-218
lines changed

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,8 @@ project("spring-websocket") {
507507
}
508508
optional("org.eclipse.jetty.websocket:websocket-server:9.0.4.v20130625")
509509
optional("org.eclipse.jetty.websocket:websocket-client:9.0.4.v20130625")
510-
optional("com.fasterxml.jackson.core:jackson-databind:2.2.0") // required for SockJS support currently
510+
optional("com.fasterxml.jackson.core:jackson-databind:2.2.0")
511+
optional("org.codehaus.jackson:jackson-mapper-asl:1.9.12")
511512
}
512513

513514
repositories {

spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsService.java

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,19 @@
4646
import org.springframework.web.socket.WebSocketHandler;
4747

4848
/**
49-
* An abstract class for {@link SockJsService} implementations. Provides configuration
50-
* support, SockJS path resolution, and processing for static SockJS requests (e.g.
51-
* "/info", "/iframe.html", etc). Sub-classes are responsible for handling transport
52-
* requests.
53-
*
54-
* <p>It is expected that this service is mapped correctly to one or more prefixes such as
55-
* "/echo" including all sub-URLs (e.g. "/echo/**"). A SockJS service itself is generally
56-
* unaware of request mapping details but nevertheless must be able to extract the SockJS
57-
* path, which is the portion of the request path following the prefix. In most cases,
58-
* this class can auto-detect the SockJS path but you can also explicitly configure the
59-
* list of valid prefixes with {@link #setValidSockJsPrefixes(String...)}.
49+
* An abstract base class for {@link SockJsService} implementations that provides SockJS
50+
* path resolution and handling of static SockJS requests (e.g. "/info", "/iframe.html",
51+
* etc). Transport-specific requests are left as abstract methods.
52+
* <p>
53+
* This service can be integrated into any HTTP request handling mechanism (e.g. plain
54+
* Servlet, Spring MVC, or other). It is expected that it will be mapped correctly to a
55+
* prefix (e.g. "/echo") and will also handle all sub-URLs (i.e. "/echo/**").
56+
* <p>
57+
* The service itself is unaware of the underlying mapping mechanism but nevertheless must
58+
* be able to extract the SockJS path, i.e. the portion of the request path following the
59+
* prefix. In most cases, this class can auto-detect the SockJS path but it is also
60+
* possible to configure explicitly the prefixes via
61+
* {@link #setValidSockJsPrefixes(String...)}.
6062
*
6163
* @author Rossen Stoyanchev
6264
* @since 4.0

spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsSession.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,13 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess
7373
* @param config the sockJS configuration
7474
* @param webSocketHandler the recipient of SockJS messages
7575
*/
76-
public AbstractSockJsSession(String sessionId, SockJsConfiguration config,
77-
WebSocketHandler webSocketHandler) {
78-
Assert.notNull(sessionId, "sessionId must not be null");
79-
Assert.notNull(webSocketHandler, "webSocketHandler must not be null");
76+
public AbstractSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler webSocketHandler) {
77+
Assert.notNull(sessionId, "sessionId is required");
78+
Assert.notNull(config, "sockJsConfig is required");
79+
Assert.notNull(webSocketHandler, "webSocketHandler is required");
8080
this.id = sessionId;
81-
this.handler = webSocketHandler;
8281
this.sockJsConfig = config;
82+
this.handler = webSocketHandler;
8383
}
8484

8585
@Override

spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsConfiguration.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public interface SockJsConfiguration {
3838
*
3939
* <p>The default value is 128K (i.e. 128 * 1024).
4040
*/
41-
public int getStreamBytesLimit();
41+
int getStreamBytesLimit();
4242

4343
/**
4444
* The amount of time in milliseconds when the server has not sent any
@@ -47,11 +47,17 @@ public interface SockJsConfiguration {
4747
*
4848
* <p>The default value is 25,000 (25 seconds).
4949
*/
50-
public long getHeartbeatTime();
50+
long getHeartbeatTime();
5151

5252
/**
5353
* A scheduler instance to use for scheduling heart-beat messages.
5454
*/
55-
public TaskScheduler getTaskScheduler();
55+
TaskScheduler getTaskScheduler();
56+
57+
/**
58+
* The codec to use for encoding and decoding SockJS messages.
59+
* @exception IllegalStateException if no {@link SockJsMessageCodec} is available
60+
*/
61+
SockJsMessageCodec getMessageCodecRequired();
5662

5763
}

spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsFrame.java

Lines changed: 12 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020

2121
import org.springframework.util.Assert;
2222

23-
import com.fasterxml.jackson.core.io.JsonStringEncoder;
24-
2523
/**
2624
* Represents a SockJS frames. Provides methods for access to commonly used message
2725
* frames.
@@ -31,42 +29,43 @@
3129
*/
3230
public class SockJsFrame {
3331

34-
private static final SockJsFrame OPEN_FRAME = new SockJsFrame("o");
32+
private static final SockJsFrame openFrame = new SockJsFrame("o");
3533

36-
private static final SockJsFrame HEARTBEAT_FRAME = new SockJsFrame("h");
34+
private static final SockJsFrame heartbeatFrame = new SockJsFrame("h");
3735

38-
private static final SockJsFrame CLOSE_GO_AWAY_FRAME = closeFrame(3000, "Go away!");
36+
private static final SockJsFrame closeGoAwayFrame = closeFrame(3000, "Go away!");
3937

40-
private static final SockJsFrame CLOSE_ANOTHER_CONNECTION_OPEN = closeFrame(2010, "Another connection still open");
38+
private static final SockJsFrame closeAnotherConnectionOpenFrame = closeFrame(2010, "Another connection still open");
4139

4240

4341
private final String content;
4442

4543

4644
private SockJsFrame(String content) {
47-
Assert.notNull("content must not be null");
45+
Assert.notNull("content is required");
4846
this.content = content;
4947
}
5048

5149

5250
public static SockJsFrame openFrame() {
53-
return OPEN_FRAME;
51+
return openFrame;
5452
}
5553

5654
public static SockJsFrame heartbeatFrame() {
57-
return HEARTBEAT_FRAME;
55+
return heartbeatFrame;
5856
}
5957

60-
public static SockJsFrame messageFrame(String... messages) {
61-
return new MessageFrame(messages);
58+
public static SockJsFrame messageFrame(SockJsMessageCodec codec, String... messages) {
59+
String encoded = codec.encode(messages);
60+
return new SockJsFrame(encoded);
6261
}
6362

6463
public static SockJsFrame closeFrameGoAway() {
65-
return CLOSE_GO_AWAY_FRAME;
64+
return closeGoAwayFrame;
6665
}
6766

6867
public static SockJsFrame closeFrameAnotherConnectionOpen() {
69-
return CLOSE_ANOTHER_CONNECTION_OPEN;
68+
return closeAnotherConnectionOpenFrame;
7069
}
7170

7271
public static SockJsFrame closeFrame(int code, String reason) {
@@ -82,35 +81,6 @@ public byte[] getContentBytes() {
8281
return this.content.getBytes(Charset.forName("UTF-8"));
8382
}
8483

85-
/**
86-
* See "JSON Unicode Encoding" section of SockJS protocol.
87-
*/
88-
public static String escapeCharacters(char[] characters) {
89-
StringBuilder result = new StringBuilder();
90-
for (char c : characters) {
91-
if (isSockJsEscapeCharacter(c)) {
92-
result.append('\\').append('u');
93-
String hex = Integer.toHexString(c).toLowerCase();
94-
for (int i = 0; i < (4 - hex.length()); i++) {
95-
result.append('0');
96-
}
97-
result.append(hex);
98-
}
99-
else {
100-
result.append(c);
101-
}
102-
}
103-
return result.toString();
104-
}
105-
106-
// See `escapable_by_server` var in SockJS protocol (under "JSON Unicode Encoding")
107-
108-
private static boolean isSockJsEscapeCharacter(char ch) {
109-
return (ch >= '\u0000' && ch <= '\u001F') || (ch >= '\u200C' && ch <= '\u200F')
110-
|| (ch >= '\u2028' && ch <= '\u202F') || (ch >= '\u2060' && ch <= '\u206F')
111-
|| (ch >= '\uFFF0' && ch <= '\uFFFF') || (ch >= '\uD800' && ch <= '\uDFFF');
112-
}
113-
11484
@Override
11585
public String toString() {
11686
String result = this.content;
@@ -137,31 +107,6 @@ public boolean equals(Object other) {
137107
}
138108

139109

140-
private static class MessageFrame extends SockJsFrame {
141-
142-
public MessageFrame(String... messages) {
143-
super(prepareContent(messages));
144-
}
145-
146-
public static String prepareContent(String... messages) {
147-
Assert.notNull(messages, "messages must not be null");
148-
StringBuilder sb = new StringBuilder();
149-
sb.append("a[");
150-
for (int i=0; i < messages.length; i++) {
151-
sb.append('"');
152-
// TODO: dependency on Jackson
153-
char[] quotedChars = JsonStringEncoder.getInstance().quoteAsString(messages[i]);
154-
sb.append(escapeCharacters(quotedChars));
155-
sb.append('"');
156-
if (i < messages.length - 1) {
157-
sb.append(',');
158-
}
159-
}
160-
sb.append(']');
161-
return sb.toString();
162-
}
163-
}
164-
165110
public interface FrameFormat {
166111

167112
SockJsFrame format(SockJsFrame frame);
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.socket.sockjs;
18+
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
22+
23+
/**
24+
* A contract for encoding and decoding of messages to and from a SockJS message frame,
25+
* which is essentially an array of JSON-encoded messages. For example:
26+
*
27+
* <pre>
28+
* a["message1","message2"]
29+
* </pre>
30+
*
31+
* @author Rossen Stoyanchev
32+
* @since 4.0
33+
*/
34+
public interface SockJsMessageCodec {
35+
36+
37+
/**
38+
* Encode the given messages as a SockJS message frame. Aside from applying standard
39+
* JSON quoting to each message, there are some additional JSON Unicode escaping
40+
* rules. See the "JSON Unicode Encoding" section of SockJS protocol (i.e. the
41+
* protocol test suite).
42+
*
43+
* @param messages the messages to encode
44+
* @return the content for a SockJS message frame, never {@code null}
45+
*/
46+
String encode(String[] messages);
47+
48+
/**
49+
* Decode the given SockJS message frame.
50+
*
51+
* @param content the SockJS message frame
52+
* @return an array of messages or {@code null}
53+
* @throws IOException if the content could not be parsed
54+
*/
55+
String[] decode(String content) throws IOException;
56+
57+
/**
58+
* Decode the given SockJS message frame.
59+
*
60+
* @param content the SockJS message frame
61+
* @return an array of messages or {@code null}
62+
* @throws IOException if the content could not be parsed
63+
*/
64+
String[] decodeInputStream(InputStream content) throws IOException;
65+
66+
}

spring-websocket/src/main/java/org/springframework/web/socket/sockjs/TransportHandler.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public interface TransportHandler {
3636

3737
TransportType getTransportType();
3838

39+
void setSockJsConfiguration(SockJsConfiguration sockJsConfig);
40+
3941
void handleRequest(ServerHttpRequest request, ServerHttpResponse response,
4042
WebSocketHandler handler, AbstractSockJsSession session) throws TransportErrorException;
4143

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.socket.sockjs.support;
18+
19+
import org.springframework.util.Assert;
20+
import org.springframework.web.socket.sockjs.SockJsMessageCodec;
21+
22+
23+
/**
24+
* An base class for SockJS message codec that provides an implementation of
25+
* {@link #encode(String[])}.
26+
*
27+
* @author Rossen Stoyanchev
28+
* @since 4.0
29+
*/
30+
public abstract class AbstractSockJsMessageCodec implements SockJsMessageCodec {
31+
32+
33+
@Override
34+
public String encode(String[] messages) {
35+
Assert.notNull(messages, "messages must not be null");
36+
StringBuilder sb = new StringBuilder();
37+
sb.append("a[");
38+
for (int i=0; i < messages.length; i++) {
39+
sb.append('"');
40+
char[] quotedChars = applyJsonQuoting(messages[i]);
41+
sb.append(escapeSockJsSpecialChars(quotedChars));
42+
sb.append('"');
43+
if (i < messages.length - 1) {
44+
sb.append(',');
45+
}
46+
}
47+
sb.append(']');
48+
return sb.toString();
49+
}
50+
51+
/**
52+
* Apply standard JSON string quoting (see http://www.json.org/).
53+
*/
54+
protected abstract char[] applyJsonQuoting(String content);
55+
56+
/**
57+
* See "JSON Unicode Encoding" section of SockJS protocol.
58+
*/
59+
private String escapeSockJsSpecialChars(char[] characters) {
60+
StringBuilder result = new StringBuilder();
61+
for (char c : characters) {
62+
if (isSockJsSpecialChar(c)) {
63+
result.append('\\').append('u');
64+
String hex = Integer.toHexString(c).toLowerCase();
65+
for (int i = 0; i < (4 - hex.length()); i++) {
66+
result.append('0');
67+
}
68+
result.append(hex);
69+
}
70+
else {
71+
result.append(c);
72+
}
73+
}
74+
return result.toString();
75+
}
76+
77+
/**
78+
* See `escapable_by_server` variable in the SockJS protocol test suite.
79+
*/
80+
private boolean isSockJsSpecialChar(char ch) {
81+
return (ch >= '\u0000' && ch <= '\u001F') || (ch >= '\u200C' && ch <= '\u200F')
82+
|| (ch >= '\u2028' && ch <= '\u202F') || (ch >= '\u2060' && ch <= '\u206F')
83+
|| (ch >= '\uFFF0' && ch <= '\uFFFF') || (ch >= '\uD800' && ch <= '\uDFFF');
84+
}
85+
86+
}

0 commit comments

Comments
 (0)