19
19
import java .io .ByteArrayOutputStream ;
20
20
import java .nio .ByteBuffer ;
21
21
import java .nio .charset .Charset ;
22
+ import java .util .ArrayList ;
23
+ import java .util .List ;
22
24
23
25
import org .apache .commons .logging .Log ;
24
26
import org .apache .commons .logging .LogFactory ;
30
32
import org .springframework .util .MultiValueMap ;
31
33
32
34
/**
33
- * Decodes STOMP frames from a {@link ByteBuffer}. If the buffer does not contain
34
- * enough data to form a complete STOMP frame, the buffer is reset and the value
35
- * returned is {@code null} indicating that no message could be read.
35
+ * Decodes one or more STOMP frames from a {@link ByteBuffer}. If the buffer
36
+ * contains any additional (incomplete) data, or perhaps not enough data to
37
+ * form even one Message, the the buffer is reset and the value returned is
38
+ * an empty list indicating that no more message can be read.
36
39
*
37
40
* @author Andy Wilkinson
38
41
* @author Rossen Stoyanchev
@@ -47,21 +50,66 @@ public class StompDecoder {
47
50
private final Log logger = LogFactory .getLog (StompDecoder .class );
48
51
49
52
53
+
54
+ /**
55
+ * Decodes one or more STOMP frames from the given {@code buffer} into a
56
+ * list of {@link Message}s.
57
+ *
58
+ * <p>If the given ByteBuffer contains partial STOMP frame content, or additional
59
+ * content with a partial STOMP frame, the buffer is reset and {@code null} is
60
+ * returned.
61
+ *
62
+ * @param buffer The buffer to decode the STOMP frame from
63
+ *
64
+ * @return the decoded messages or an empty list
65
+ */
66
+ public List <Message <byte []>> decode (ByteBuffer buffer ) {
67
+ return decode (buffer , new LinkedMultiValueMap <String , String >());
68
+ }
69
+
70
+ /**
71
+ * Decodes one or more STOMP frames from the given {@code buffer} into a
72
+ * list of {@link Message}s.
73
+ *
74
+ * <p>If the given ByteBuffer contains partial STOMP frame content, or additional
75
+ * content with a partial STOMP frame, the buffer is reset and {@code null} is
76
+ * returned.
77
+ *
78
+ * @param buffer The buffer to decode the STOMP frame from
79
+ * @param headers an empty map that will be filled with the successfully parsed
80
+ * headers of the last decoded message, or the last attempt at decoding an
81
+ * (incomplete) STOMP frame. This can be useful for detecting 'content-length'.
82
+ *
83
+ * @return the decoded messages or an empty list
84
+ */
85
+ public List <Message <byte []>> decode (ByteBuffer buffer , MultiValueMap <String , String > headers ) {
86
+ List <Message <byte []>> messages = new ArrayList <Message <byte []>>();
87
+ while (buffer .hasRemaining ()) {
88
+ headers .clear ();
89
+ Message <byte []> m = decodeMessage (buffer , headers );
90
+ if (m != null ) {
91
+ messages .add (m );
92
+ }
93
+ else {
94
+ break ;
95
+ }
96
+ }
97
+ return messages ;
98
+ }
99
+
50
100
/**
51
- * Decodes a STOMP frame in the given {@code buffer} into a {@link Message}.
52
- * If the given ByteBuffer contains partial STOMP frame content, the method
53
- * resets the buffer and returns {@code null}.
54
- * @param buffer the buffer to decode the frame from
55
- * @return the decoded message or {@code null}
101
+ * Decode a single STOMP frame from the given {@code buffer} into a {@link Message}.
56
102
*/
57
- public Message <byte []> decode (ByteBuffer buffer ) {
103
+ private Message <byte []> decodeMessage (ByteBuffer buffer , MultiValueMap <String , String > headers ) {
104
+
58
105
Message <byte []> decodedMessage = null ;
59
106
skipLeadingEol (buffer );
60
107
buffer .mark ();
61
108
62
109
String command = readCommand (buffer );
63
110
if (command .length () > 0 ) {
64
- MultiValueMap <String , String > headers = readHeaders (buffer );
111
+
112
+ readHeaders (buffer , headers );
65
113
byte [] payload = readPayload (buffer , headers );
66
114
67
115
if (payload != null ) {
@@ -78,7 +126,7 @@ public Message<byte[]> decode(ByteBuffer buffer) {
78
126
}
79
127
else {
80
128
if (logger .isTraceEnabled ()) {
81
- logger .trace ("Received incomplete frame. Resetting buffer" );
129
+ logger .trace ("Received incomplete frame. Resetting buffer. " );
82
130
}
83
131
buffer .reset ();
84
132
}
@@ -93,27 +141,31 @@ public Message<byte[]> decode(ByteBuffer buffer) {
93
141
return decodedMessage ;
94
142
}
95
143
96
- private void skipLeadingEol (ByteBuffer buffer ) {
144
+
145
+ /**
146
+ * Skip one ore more EOL characters at the start of the given ByteBuffer.
147
+ * Those are STOMP heartbeat frames.
148
+ */
149
+ protected void skipLeadingEol (ByteBuffer buffer ) {
97
150
while (true ) {
98
- if (!isEol (buffer )) {
151
+ if (!tryConsumeEndOfLine (buffer )) {
99
152
break ;
100
153
}
101
154
}
102
155
}
103
156
104
157
private String readCommand (ByteBuffer buffer ) {
105
158
ByteArrayOutputStream command = new ByteArrayOutputStream (256 );
106
- while (buffer .remaining () > 0 && !isEol (buffer )) {
159
+ while (buffer .remaining () > 0 && !tryConsumeEndOfLine (buffer )) {
107
160
command .write (buffer .get ());
108
161
}
109
162
return new String (command .toByteArray (), UTF8_CHARSET );
110
163
}
111
164
112
- private MultiValueMap <String , String > readHeaders (ByteBuffer buffer ) {
113
- MultiValueMap <String , String > headers = new LinkedMultiValueMap <String , String >();
165
+ private void readHeaders (ByteBuffer buffer , MultiValueMap <String , String > headers ) {
114
166
while (true ) {
115
167
ByteArrayOutputStream headerStream = new ByteArrayOutputStream (256 );
116
- while (buffer .remaining () > 0 && !isEol (buffer )) {
168
+ while (buffer .remaining () > 0 && !tryConsumeEndOfLine (buffer )) {
117
169
headerStream .write (buffer .get ());
118
170
}
119
171
if (headerStream .size () > 0 ) {
@@ -135,7 +187,6 @@ private MultiValueMap<String, String> readHeaders(ByteBuffer buffer) {
135
187
break ;
136
188
}
137
189
}
138
- return headers ;
139
190
}
140
191
141
192
private String unescape (String input ) {
@@ -146,16 +197,7 @@ private String unescape(String input) {
146
197
}
147
198
148
199
private byte [] readPayload (ByteBuffer buffer , MultiValueMap <String , String > headers ) {
149
- Integer contentLength = null ;
150
- if (headers .containsKey ("content-length" )) {
151
- String rawContentLength = headers .getFirst ("content-length" );
152
- try {
153
- contentLength = Integer .valueOf (rawContentLength );
154
- }
155
- catch (NumberFormatException ex ) {
156
- logger .warn ("Ignoring invalid content-length header value: '" + rawContentLength + "'" );
157
- }
158
- }
200
+ Integer contentLength = getContentLength (headers );
159
201
if (contentLength != null && contentLength >= 0 ) {
160
202
if (buffer .remaining () > contentLength ) {
161
203
byte [] payload = new byte [contentLength ];
@@ -184,7 +226,25 @@ private byte[] readPayload(ByteBuffer buffer, MultiValueMap<String, String> head
184
226
return null ;
185
227
}
186
228
187
- private boolean isEol (ByteBuffer buffer ) {
229
+ protected Integer getContentLength (MultiValueMap <String , String > headers ) {
230
+ if (headers .containsKey (StompHeaderAccessor .STOMP_CONTENT_LENGTH_HEADER )) {
231
+ String rawContentLength = headers .getFirst (StompHeaderAccessor .STOMP_CONTENT_LENGTH_HEADER );
232
+ try {
233
+ return Integer .valueOf (rawContentLength );
234
+ }
235
+ catch (NumberFormatException ex ) {
236
+ logger .warn ("Ignoring invalid content-length header value: '" + rawContentLength + "'" );
237
+ }
238
+ }
239
+ return null ;
240
+ }
241
+
242
+ /**
243
+ * Try to read an EOL incrementing the buffer position if successful.
244
+ *
245
+ * @return whether an EOL was consumed
246
+ */
247
+ private boolean tryConsumeEndOfLine (ByteBuffer buffer ) {
188
248
if (buffer .remaining () > 0 ) {
189
249
byte b = buffer .get ();
190
250
if (b == '\n' ) {
0 commit comments