001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.socket.sockjs.frame;
018
019import java.nio.charset.Charset;
020import java.nio.charset.StandardCharsets;
021
022import org.springframework.lang.Nullable;
023import org.springframework.util.Assert;
024import org.springframework.util.StringUtils;
025
026/**
027 * Represents a SockJS frame. Provides factory methods to create SockJS frames.
028 *
029 * @author Rossen Stoyanchev
030 * @since 4.0
031 */
032public class SockJsFrame {
033
034        /**
035         * The charset used by SockJS.
036         */
037        public static final Charset CHARSET = StandardCharsets.UTF_8;
038
039        private static final SockJsFrame OPEN_FRAME = new SockJsFrame("o");
040
041        private static final SockJsFrame HEARTBEAT_FRAME = new SockJsFrame("h");
042
043        private static final SockJsFrame CLOSE_GO_AWAY_FRAME = closeFrame(3000, "Go away!");
044
045        private static final SockJsFrame CLOSE_ANOTHER_CONNECTION_OPEN_FRAME =
046                        closeFrame(2010, "Another connection still open");
047
048
049        private final SockJsFrameType type;
050
051        private final String content;
052
053
054        /**
055         * Create a new instance frame with the given frame content.
056         * @param content the content (must be a non-empty and represent a valid SockJS frame)
057         */
058        public SockJsFrame(String content) {
059                Assert.hasText(content, "Content must not be empty");
060                if ("o".equals(content)) {
061                        this.type = SockJsFrameType.OPEN;
062                        this.content = content;
063                }
064                else if ("h".equals(content)) {
065                        this.type = SockJsFrameType.HEARTBEAT;
066                        this.content = content;
067                }
068                else if (content.charAt(0) == 'a') {
069                        this.type = SockJsFrameType.MESSAGE;
070                        this.content = (content.length() > 1 ? content : "a[]");
071                }
072                else if (content.charAt(0) == 'm') {
073                        this.type = SockJsFrameType.MESSAGE;
074                        this.content = (content.length() > 1 ? content : "null");
075                }
076                else if (content.charAt(0) == 'c') {
077                        this.type = SockJsFrameType.CLOSE;
078                        this.content = (content.length() > 1 ? content : "c[]");
079                }
080                else {
081                        throw new IllegalArgumentException("Unexpected SockJS frame type in content \"" + content + "\"");
082                }
083        }
084
085
086        /**
087         * Return the SockJS frame type.
088         */
089        public SockJsFrameType getType() {
090                return this.type;
091        }
092
093        /**
094         * Return the SockJS frame content (never {@code null}).
095         */
096        public String getContent() {
097                return this.content;
098        }
099
100        /**
101         * Return the SockJS frame content as a byte array.
102         */
103        public byte[] getContentBytes() {
104                return this.content.getBytes(CHARSET);
105        }
106
107        /**
108         * Return data contained in a SockJS "message" and "close" frames. Otherwise
109         * for SockJS "open" and "close" frames, which do not contain data, return
110         * {@code null}.
111         */
112        @Nullable
113        public String getFrameData() {
114                if (getType() == SockJsFrameType.OPEN || getType() == SockJsFrameType.HEARTBEAT) {
115                        return null;
116                }
117                else {
118                        return getContent().substring(1);
119                }
120        }
121
122
123        @Override
124        public boolean equals(@Nullable Object other) {
125                if (this == other) {
126                        return true;
127                }
128                if (!(other instanceof SockJsFrame)) {
129                        return false;
130                }
131                SockJsFrame otherFrame = (SockJsFrame) other;
132                return (this.type.equals(otherFrame.type) && this.content.equals(otherFrame.content));
133        }
134
135        @Override
136        public int hashCode() {
137                return this.content.hashCode();
138        }
139
140        @Override
141        public String toString() {
142                String result = this.content;
143                if (result.length() > 80) {
144                        result = result.substring(0, 80) + "...(truncated)";
145                }
146                result = StringUtils.replace(result, "\n", "\\n");
147                result = StringUtils.replace(result, "\r", "\\r");
148                return "SockJsFrame content='" + result + "'";
149        }
150
151
152        public static SockJsFrame openFrame() {
153                return OPEN_FRAME;
154        }
155
156        public static SockJsFrame heartbeatFrame() {
157                return HEARTBEAT_FRAME;
158        }
159
160        public static SockJsFrame messageFrame(SockJsMessageCodec codec, String... messages) {
161                String encoded = codec.encode(messages);
162                return new SockJsFrame(encoded);
163        }
164
165        public static SockJsFrame closeFrameGoAway() {
166                return CLOSE_GO_AWAY_FRAME;
167        }
168
169        public static SockJsFrame closeFrameAnotherConnectionOpen() {
170                return CLOSE_ANOTHER_CONNECTION_OPEN_FRAME;
171        }
172
173        public static SockJsFrame closeFrame(int code, @Nullable String reason) {
174                return new SockJsFrame("c[" + code + ",\"" + (reason != null ? reason : "") + "\"]");
175        }
176
177}