From c14ba1a0ff72ed11eaa5fc7d808f6f3a3fbe2916 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 21 Apr 2014 13:57:09 -0400 Subject: [PATCH] Add SockJsFrameType enum SPR-10797 --- .../frame/DefaultSockJsFrameFormat.java | 13 ++- .../web/socket/sockjs/frame/SockJsFrame.java | 68 +++++++++++-- .../sockjs/frame/SockJsFrameFormat.java | 13 ++- .../socket/sockjs/frame/SockJsFrameType.java | 29 ++++++ .../AbstractHttpSendingTransportHandler.java | 6 +- .../session/AbstractHttpSockJsSession.java | 6 +- .../socket/sockjs/frame/SockJsFrameTests.java | 99 +++++++++++++++++++ .../HttpSendingTransportHandlerTests.java | 12 +-- 8 files changed, 219 insertions(+), 27 deletions(-) create mode 100644 spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/SockJsFrameType.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/sockjs/frame/SockJsFrameTests.java diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/DefaultSockJsFrameFormat.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/DefaultSockJsFrameFormat.java index 56220f5f46..fbb92f0944 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/DefaultSockJsFrameFormat.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/DefaultSockJsFrameFormat.java @@ -19,6 +19,10 @@ package org.springframework.web.socket.sockjs.frame; import org.springframework.util.Assert; /** + * A default implementation of + * {@link org.springframework.web.socket.sockjs.frame.SockJsFrameFormat} that relies + * on {@link java.lang.String#format(String, Object...)}.. + * * @author Rossen Stoyanchev * @since 4.0 */ @@ -33,14 +37,9 @@ public class DefaultSockJsFrameFormat implements SockJsFrameFormat { } - /** - * @param frame the SockJs frame. - * @return new SockJsFrame instance with the formatted content - */ @Override - public SockJsFrame format(SockJsFrame frame) { - String content = String.format(this.format, preProcessContent(frame.getContent())); - return new SockJsFrame(content); + public String format(SockJsFrame frame) { + return String.format(this.format, preProcessContent(frame.getContent())); } protected String preProcessContent(String content) { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/SockJsFrame.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/SockJsFrame.java index 97a620170d..7e07bf7ced 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/SockJsFrame.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/SockJsFrame.java @@ -18,18 +18,17 @@ package org.springframework.web.socket.sockjs.frame; import java.nio.charset.Charset; -import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** - * Represents a SockJS frame. Provides factory methods to create SockJS frames on - * the server side. + * Represents a SockJS frame. Provides factory methods to create SockJS frames. * * @author Rossen Stoyanchev * @since 4.0 */ public class SockJsFrame { - private static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); + public static final Charset CHARSET = Charset.forName("UTF-8"); private static final SockJsFrame OPEN_FRAME = new SockJsFrame("o"); @@ -40,12 +39,40 @@ public class SockJsFrame { private static final SockJsFrame CLOSE_ANOTHER_CONNECTION_OPEN_FRAME = closeFrame(2010, "Another connection still open"); + private final SockJsFrameType type; + private final String content; + /** + * Create a new instance frame with the given frame content. + * @param content the content, must be a non-empty and represent a valid SockJS frame + */ public SockJsFrame(String content) { - Assert.notNull("Content must not be null"); - this.content = content; + StringUtils.hasText(content); + if ("o".equals(content)) { + this.type = SockJsFrameType.OPEN; + this.content = content; + } + else if ("h".equals(content)) { + this.type = SockJsFrameType.HEARTBEAT; + this.content = content; + } + else if (content.charAt(0) == 'a') { + this.type = SockJsFrameType.MESSAGE; + this.content = (content.length() > 1 ? content : "a[]"); + } + else if (content.charAt(0) == 'm') { + this.type = SockJsFrameType.MESSAGE; + this.content = (content.length() > 1 ? content : "null"); + } + else if (content.charAt(0) == 'c') { + this.type = SockJsFrameType.CLOSE; + this.content = (content.length() > 1 ? content : "c[]"); + } + else { + throw new IllegalArgumentException("Unexpected SockJS frame type in content=\"" + content + "\""); + } } public static SockJsFrame openFrame() { @@ -74,12 +101,39 @@ public class SockJsFrame { } + /** + * Return the SockJS frame type. + */ + public SockJsFrameType getType() { + return this.type; + } + + /** + * Return the SockJS frame content, never {@code null}. + */ public String getContent() { return this.content; } + /** + * Return the SockJS frame content as a byte array. + */ public byte[] getContentBytes() { - return this.content.getBytes(UTF8_CHARSET); + return this.content.getBytes(CHARSET); + } + + /** + * Return data contained in a SockJS "message" and "close" frames. Otherwise + * for SockJS "open" and "close" frames, which do not contain data, return + * {@code null}. + */ + public String getFrameData() { + if (SockJsFrameType.OPEN == getType() || SockJsFrameType.HEARTBEAT == getType()) { + return null; + } + else { + return getContent().substring(1); + } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/SockJsFrameFormat.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/SockJsFrameFormat.java index 858d2476ed..26376e468c 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/SockJsFrameFormat.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/SockJsFrameFormat.java @@ -17,11 +17,22 @@ package org.springframework.web.socket.sockjs.frame; /** + * Applies a transport-specific format to the content of a SockJS frame resulting + * in a content that can be written out. Primarily for use in HTTP server-side + * transports that push data. + * + *

Formatting may vary from simply appending a new line character for XHR + * polling and streaming transports, to a jsonp-style callback function, + * surrounding script tags, and more. + * + *

For the various SockJS frame formats in use, see implementations of + * {@link org.springframework.web.socket.sockjs.transport.handler.AbstractHttpSendingTransportHandler#getFrameFormat(org.springframework.http.server.ServerHttpRequest) AbstractHttpSendingTransportHandler.getFrameFormat} + * * @author Rossen Stoyanchev * @since 4.0 */ public interface SockJsFrameFormat { - SockJsFrame format(SockJsFrame frame); + String format(SockJsFrame frame); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/SockJsFrameType.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/SockJsFrameType.java new file mode 100644 index 0000000000..eb6fc66460 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/SockJsFrameType.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2014 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.frame; + +/** + * SockJS frame types. + * + * @author Rossen Stoyanchev + * @since 4.1 + */ +public enum SockJsFrameType { + + OPEN, HEARTBEAT, MESSAGE, CLOSE + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/AbstractHttpSendingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/AbstractHttpSendingTransportHandler.java index 0bba697e10..0acd733767 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/AbstractHttpSendingTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/AbstractHttpSendingTransportHandler.java @@ -82,12 +82,12 @@ public abstract class AbstractHttpSendingTransportHandler extends AbstractTransp } else { logger.debug("another " + getTransportType() + " connection still open: " + sockJsSession); - SockJsFrame frame = getFrameFormat(request).format(SockJsFrame.closeFrameAnotherConnectionOpen()); + String formattedFrame = getFrameFormat(request).format(SockJsFrame.closeFrameAnotherConnectionOpen()); try { - response.getBody().write(frame.getContentBytes()); + response.getBody().write(formattedFrame.getBytes(SockJsFrame.CHARSET)); } catch (IOException ex) { - throw new SockJsException("Failed to send " + frame, sockJsSession.getId(), ex); + throw new SockJsException("Failed to send " + formattedFrame, sockJsSession.getId(), ex); } } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractHttpSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractHttpSockJsSession.java index f433ea5f27..bd3dccd613 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractHttpSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractHttpSockJsSession.java @@ -351,11 +351,11 @@ public abstract class AbstractHttpSockJsSession extends AbstractSockJsSession { @Override protected void writeFrameInternal(SockJsFrame frame) throws IOException { if (isActive()) { - frame = this.frameFormat.format(frame); + String formattedFrame = this.frameFormat.format(frame); if (logger.isTraceEnabled()) { - logger.trace("Writing " + frame); + logger.trace("Writing " + formattedFrame); } - getResponse().getBody().write(frame.getContentBytes()); + getResponse().getBody().write(formattedFrame.getBytes(SockJsFrame.CHARSET)); } } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/frame/SockJsFrameTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/frame/SockJsFrameTests.java new file mode 100644 index 0000000000..d32b49d51d --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/frame/SockJsFrameTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2014 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.frame; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * Unit tests for {@link org.springframework.web.socket.sockjs.frame.SockJsFrame}. + * + * @author Rossen Stoyanchev + * @since 4.1 + */ +public class SockJsFrameTests { + + + @Test + public void openFrame() { + SockJsFrame frame = SockJsFrame.openFrame(); + + assertEquals("o", frame.getContent()); + assertEquals(SockJsFrameType.OPEN, frame.getType()); + assertNull(frame.getFrameData()); + } + + @Test + public void heartbeatFrame() { + SockJsFrame frame = SockJsFrame.heartbeatFrame(); + + assertEquals("h", frame.getContent()); + assertEquals(SockJsFrameType.HEARTBEAT, frame.getType()); + assertNull(frame.getFrameData()); + } + + @Test + public void messageArrayFrame() { + SockJsFrame frame = SockJsFrame.messageFrame(new Jackson2SockJsMessageCodec(), "m1", "m2"); + + assertEquals("a[\"m1\",\"m2\"]", frame.getContent()); + assertEquals(SockJsFrameType.MESSAGE, frame.getType()); + assertEquals("[\"m1\",\"m2\"]", frame.getFrameData()); + } + + @Test + public void messageArrayFrameEmpty() { + SockJsFrame frame = new SockJsFrame("a"); + + assertEquals("a[]", frame.getContent()); + assertEquals(SockJsFrameType.MESSAGE, frame.getType()); + assertEquals("[]", frame.getFrameData()); + + frame = new SockJsFrame("a[]"); + + assertEquals("a[]", frame.getContent()); + assertEquals(SockJsFrameType.MESSAGE, frame.getType()); + assertEquals("[]", frame.getFrameData()); + } + + @Test + public void closeFrame() { + SockJsFrame frame = SockJsFrame.closeFrame(3000, "Go Away!"); + + assertEquals("c[3000,\"Go Away!\"]", frame.getContent()); + assertEquals(SockJsFrameType.CLOSE, frame.getType()); + assertEquals("[3000,\"Go Away!\"]", frame.getFrameData()); + } + + @Test + public void closeFrameEmpty() { + SockJsFrame frame = new SockJsFrame("c"); + + assertEquals("c[]", frame.getContent()); + assertEquals(SockJsFrameType.CLOSE, frame.getType()); + assertEquals("[]", frame.getFrameData()); + + frame = new SockJsFrame("c[]"); + + assertEquals("c[]", frame.getContent()); + assertEquals(SockJsFrameType.CLOSE, frame.getType()); + assertEquals("[]", frame.getFrameData()); + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/handler/HttpSendingTransportHandlerTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/handler/HttpSendingTransportHandlerTests.java index 3797f9024b..dcab42610e 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/handler/HttpSendingTransportHandlerTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/handler/HttpSendingTransportHandlerTests.java @@ -168,24 +168,24 @@ public class HttpSendingTransportHandlerTests extends AbstractHttpRequestTests SockJsFrame frame = SockJsFrame.openFrame(); SockJsFrameFormat format = new XhrPollingTransportHandler().getFrameFormat(this.request); - SockJsFrame formatted = format.format(frame); - assertEquals(frame.getContent() + "\n", formatted.getContent()); + String formatted = format.format(frame); + assertEquals(frame.getContent() + "\n", formatted); format = new XhrStreamingTransportHandler().getFrameFormat(this.request); formatted = format.format(frame); - assertEquals(frame.getContent() + "\n", formatted.getContent()); + assertEquals(frame.getContent() + "\n", formatted); format = new HtmlFileTransportHandler().getFrameFormat(this.request); formatted = format.format(frame); - assertEquals("\r\n", formatted.getContent()); + assertEquals("\r\n", formatted); format = new EventSourceTransportHandler().getFrameFormat(this.request); formatted = format.format(frame); - assertEquals("data: " + frame.getContent() + "\r\n\r\n", formatted.getContent()); + assertEquals("data: " + frame.getContent() + "\r\n\r\n", formatted); format = new JsonpPollingTransportHandler().getFrameFormat(this.request); formatted = format.format(frame); - assertEquals("callback(\"" + frame.getContent() + "\");\r\n", formatted.getContent()); + assertEquals("callback(\"" + frame.getContent() + "\");\r\n", formatted); } }