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}