Browse Source
Previously, the broker relay's TCP client used Reactor's built in delimited codec as part of its parsing of STOMP frames. \0 was used as the delimiter. This worked for most STOMP frames but, crucially, not for frames with a body that contained \0: when such a frame was received it would be truncated. This commit adds a custom codec that parses STOMP frames more intelligently. It honours the content-length header allowing it to correctly parse frames with a body that contains \0. The codec largely delegates to two new classes: StompEncoder and StompDecoder. For consistency, code that previously used StompMessageConverter has been reworked to use these new encoder and decoder classes. Issue: SPR-10818pull/364/merge
Andy Wilkinson
11 years ago
committed by
Rossen Stoyanchev
10 changed files with 598 additions and 432 deletions
@ -0,0 +1,68 @@
@@ -0,0 +1,68 @@
|
||||
/* |
||||
* Copyright 2002-2013 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.messaging.simp.stomp; |
||||
|
||||
import org.springframework.messaging.Message; |
||||
|
||||
import reactor.function.Consumer; |
||||
import reactor.function.Function; |
||||
import reactor.io.Buffer; |
||||
import reactor.tcp.encoding.Codec; |
||||
|
||||
/** |
||||
* A Reactor TCP {@link Codec} for sending and receiving STOMP messages |
||||
* |
||||
* @author Andy Wilkinson |
||||
* @since 4.0 |
||||
*/ |
||||
public class StompCodec implements Codec<Buffer, Message<byte[]>, Message<byte[]>> { |
||||
|
||||
private static final StompDecoder DECODER = new StompDecoder(); |
||||
|
||||
private static final Function<Message<byte[]>, Buffer> ENCODER_FUNCTION = new Function<Message<byte[]>, Buffer>() { |
||||
|
||||
private final StompEncoder encoder = new StompEncoder(); |
||||
|
||||
@Override |
||||
public Buffer apply(Message<byte[]> message) { |
||||
return Buffer.wrap(this.encoder.encode(message)); |
||||
} |
||||
}; |
||||
|
||||
@Override |
||||
public Function<Buffer, Message<byte[]>> decoder(final Consumer<Message<byte[]>> next) { |
||||
return new Function<Buffer, Message<byte[]>>() { |
||||
|
||||
@Override |
||||
public Message<byte[]> apply(Buffer buffer) { |
||||
while (buffer.remaining() > 0) { |
||||
Message<byte[]> message = DECODER.decode(buffer.byteBuffer()); |
||||
if (message != null) { |
||||
next.accept(message); |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
}; |
||||
} |
||||
|
||||
@Override |
||||
public Function<Message<byte[]>, Buffer> encoder() { |
||||
return ENCODER_FUNCTION; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,157 @@
@@ -0,0 +1,157 @@
|
||||
/* |
||||
* Copyright 2002-2013 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.messaging.simp.stomp; |
||||
|
||||
import java.io.ByteArrayOutputStream; |
||||
import java.nio.ByteBuffer; |
||||
import java.nio.charset.Charset; |
||||
|
||||
import org.springframework.messaging.Message; |
||||
import org.springframework.messaging.support.MessageBuilder; |
||||
import org.springframework.util.LinkedMultiValueMap; |
||||
import org.springframework.util.MultiValueMap; |
||||
|
||||
/** |
||||
* A decoder for STOMP frames |
||||
* |
||||
* @author awilkinson |
||||
* @since 4.0 |
||||
*/ |
||||
public class StompDecoder { |
||||
|
||||
private static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); |
||||
|
||||
|
||||
/** |
||||
* Decodes a STOMP frame in the given {@code buffer} into a {@link Message}. |
||||
* |
||||
* @param buffer The buffer to decode the frame from |
||||
* @return The decoded message |
||||
*/ |
||||
public Message<byte[]> decode(ByteBuffer buffer) { |
||||
skipLeadingEol(buffer); |
||||
String command = readCommand(buffer); |
||||
if (command.length() > 0) { |
||||
MultiValueMap<String, String> headers = readHeaders(buffer); |
||||
byte[] payload = readPayload(buffer, headers); |
||||
|
||||
return MessageBuilder.withPayloadAndHeaders(payload, |
||||
StompHeaderAccessor.create(StompCommand.valueOf(command), headers)).build(); |
||||
} |
||||
else { |
||||
// Heartbeat
|
||||
return null; |
||||
} |
||||
|
||||
} |
||||
|
||||
private String readCommand(ByteBuffer buffer) { |
||||
ByteArrayOutputStream command = new ByteArrayOutputStream(); |
||||
while (buffer.remaining() > 0 && !isEol(buffer)) { |
||||
command.write(buffer.get()); |
||||
} |
||||
return new String(command.toByteArray(), UTF8_CHARSET); |
||||
} |
||||
|
||||
private MultiValueMap<String, String> readHeaders(ByteBuffer buffer) { |
||||
MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>(); |
||||
while (true) { |
||||
ByteArrayOutputStream headerStream = new ByteArrayOutputStream(); |
||||
while (buffer.remaining() > 0 && !isEol(buffer)) { |
||||
headerStream.write(buffer.get()); |
||||
} |
||||
if (headerStream.size() > 0) { |
||||
String header = new String(headerStream.toByteArray(), UTF8_CHARSET); |
||||
int colonIndex = header.indexOf(':'); |
||||
if (colonIndex <= 0 || colonIndex == header.length() - 1) { |
||||
throw new StompConversionException( |
||||
"Illegal header: '" + header + "'. A header must be of the form <name>:<value"); |
||||
} |
||||
else { |
||||
String headerName = unescape(header.substring(0, colonIndex)); |
||||
String headerValue = unescape(header.substring(colonIndex + 1)); |
||||
headers.add(headerName, headerValue); |
||||
} |
||||
} |
||||
else { |
||||
break; |
||||
} |
||||
} |
||||
return headers; |
||||
} |
||||
|
||||
private String unescape(String input) { |
||||
return input.replaceAll("\\\\n", "\n") |
||||
.replaceAll("\\\\r", "\r") |
||||
.replaceAll("\\\\c", ":") |
||||
.replaceAll("\\\\\\\\", "\\\\"); |
||||
} |
||||
|
||||
private byte[] readPayload(ByteBuffer buffer, MultiValueMap<String, String> headers) { |
||||
String contentLengthString = headers.getFirst("content-length"); |
||||
if (contentLengthString != null) { |
||||
int contentLength = Integer.valueOf(contentLengthString); |
||||
byte[] payload = new byte[contentLength]; |
||||
buffer.get(payload); |
||||
if (buffer.remaining() < 1 || buffer.get() != 0) { |
||||
throw new StompConversionException("Frame must be terminated with a null octect"); |
||||
} |
||||
return payload; |
||||
} |
||||
else { |
||||
ByteArrayOutputStream payload = new ByteArrayOutputStream(); |
||||
while (buffer.remaining() > 0) { |
||||
byte b = buffer.get(); |
||||
if (b == 0) { |
||||
return payload.toByteArray(); |
||||
} |
||||
else { |
||||
payload.write(b); |
||||
} |
||||
} |
||||
} |
||||
|
||||
throw new StompConversionException("Frame must be terminated with a null octect"); |
||||
} |
||||
|
||||
private void skipLeadingEol(ByteBuffer buffer) { |
||||
while (true) { |
||||
if (!isEol(buffer)) { |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
private boolean isEol(ByteBuffer buffer) { |
||||
if (buffer.remaining() > 0) { |
||||
byte b = buffer.get(); |
||||
if (b == '\n') { |
||||
return true; |
||||
} |
||||
else if (b == '\r') { |
||||
if (buffer.remaining() > 0 && buffer.get() == '\n') { |
||||
return true; |
||||
} |
||||
else { |
||||
throw new StompConversionException("'\\r' must be followed by '\\n'"); |
||||
} |
||||
} |
||||
buffer.position(buffer.position() - 1); |
||||
} |
||||
return false; |
||||
} |
||||
} |
@ -0,0 +1,115 @@
@@ -0,0 +1,115 @@
|
||||
/* |
||||
* Copyright 2002-2013 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.messaging.simp.stomp; |
||||
|
||||
import java.io.ByteArrayOutputStream; |
||||
import java.io.DataOutputStream; |
||||
import java.io.IOException; |
||||
import java.nio.charset.Charset; |
||||
import java.util.List; |
||||
import java.util.Map.Entry; |
||||
|
||||
import org.springframework.messaging.Message; |
||||
|
||||
/** |
||||
* An encoder for STOMP frames |
||||
* |
||||
* @author Andy Wilkinson |
||||
* @since 4.0 |
||||
*/ |
||||
public final class StompEncoder { |
||||
|
||||
private static final byte LF = '\n'; |
||||
|
||||
private static final byte COLON = ':'; |
||||
|
||||
private static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); |
||||
|
||||
|
||||
/** |
||||
* Encodes the given STOMP {@code message} into a {@code byte[]} |
||||
* |
||||
* @param message The message to encode |
||||
* |
||||
* @return The encoded message |
||||
*/ |
||||
public byte[] encode(Message<byte[]> message) { |
||||
try { |
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
||||
DataOutputStream output = new DataOutputStream(baos); |
||||
|
||||
StompHeaderAccessor headers = StompHeaderAccessor.wrap(message); |
||||
|
||||
writeCommand(headers, output); |
||||
writeHeaders(headers, message, output); |
||||
output.write(LF); |
||||
writeBody(message, output); |
||||
output.write((byte)0); |
||||
|
||||
return baos.toByteArray(); |
||||
} |
||||
catch (IOException e) { |
||||
throw new StompConversionException("Failed to encode STOMP frame", e); |
||||
} |
||||
} |
||||
|
||||
private void writeCommand(StompHeaderAccessor headers, DataOutputStream output) throws IOException { |
||||
output.write(headers.getCommand().toString().getBytes(UTF8_CHARSET)); |
||||
output.write(LF); |
||||
} |
||||
|
||||
private void writeHeaders(StompHeaderAccessor headers, Message<byte[]> message, DataOutputStream output) |
||||
throws IOException { |
||||
|
||||
for (Entry<String, List<String>> entry : headers.toStompHeaderMap().entrySet()) { |
||||
byte[] key = getUtf8BytesEscapingIfNecessary(entry.getKey(), headers); |
||||
for (String value : entry.getValue()) { |
||||
output.write(key); |
||||
output.write(COLON); |
||||
output.write(getUtf8BytesEscapingIfNecessary(value, headers)); |
||||
output.write(LF); |
||||
} |
||||
} |
||||
if (headers.getCommand() == StompCommand.SEND || |
||||
headers.getCommand() == StompCommand.MESSAGE || |
||||
headers.getCommand() == StompCommand.ERROR) { |
||||
output.write("content-length:".getBytes(UTF8_CHARSET)); |
||||
output.write(Integer.toString(message.getPayload().length).getBytes(UTF8_CHARSET)); |
||||
output.write(LF); |
||||
} |
||||
} |
||||
|
||||
private void writeBody(Message<byte[]> message, DataOutputStream output) throws IOException { |
||||
output.write(message.getPayload()); |
||||
} |
||||
|
||||
private byte[] getUtf8BytesEscapingIfNecessary(String input, StompHeaderAccessor headers) { |
||||
if (headers.getCommand() != StompCommand.CONNECT && headers.getCommand() != StompCommand.CONNECTED) { |
||||
return escape(input).getBytes(UTF8_CHARSET); |
||||
} |
||||
else { |
||||
return input.getBytes(UTF8_CHARSET); |
||||
} |
||||
} |
||||
|
||||
private String escape(String input) { |
||||
return input.replaceAll("\\\\", "\\\\\\\\") |
||||
.replaceAll(":", "\\\\c") |
||||
.replaceAll("\n", "\\\\n") |
||||
.replaceAll("\r", "\\\\r"); |
||||
} |
||||
} |
@ -1,231 +0,0 @@
@@ -1,231 +0,0 @@
|
||||
/* |
||||
* Copyright 2002-2013 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.messaging.simp.stomp; |
||||
|
||||
import java.io.ByteArrayOutputStream; |
||||
import java.io.IOException; |
||||
import java.nio.charset.Charset; |
||||
import java.util.List; |
||||
import java.util.Map.Entry; |
||||
|
||||
import org.springframework.messaging.Message; |
||||
import org.springframework.messaging.support.MessageBuilder; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.LinkedMultiValueMap; |
||||
import org.springframework.util.MultiValueMap; |
||||
|
||||
|
||||
/** |
||||
* @author Gary Russell |
||||
* @author Rossen Stoyanchev |
||||
* @since 4.0 |
||||
*/ |
||||
public class StompMessageConverter { |
||||
|
||||
private static final Charset STOMP_CHARSET = Charset.forName("UTF-8"); |
||||
|
||||
public static final byte LF = 0x0a; |
||||
|
||||
public static final byte CR = 0x0d; |
||||
|
||||
private static final byte COLON = ':'; |
||||
|
||||
/** |
||||
* @param stompContent a complete STOMP message (without the trailing 0x00) as byte[] or String. |
||||
*/ |
||||
public Message<?> toMessage(Object stompContent) { |
||||
|
||||
byte[] byteContent = null; |
||||
if (stompContent instanceof String) { |
||||
byteContent = ((String) stompContent).getBytes(STOMP_CHARSET); |
||||
} |
||||
else if (stompContent instanceof byte[]){ |
||||
byteContent = (byte[]) stompContent; |
||||
} |
||||
else { |
||||
throw new IllegalArgumentException( |
||||
"stompContent is neither String nor byte[]: " + stompContent.getClass()); |
||||
} |
||||
|
||||
int totalLength = byteContent.length; |
||||
if (byteContent[totalLength-1] == 0) { |
||||
totalLength--; |
||||
} |
||||
|
||||
int payloadIndex = findIndexOfPayload(byteContent); |
||||
if (payloadIndex == 0) { |
||||
throw new StompConversionException("No command found"); |
||||
} |
||||
|
||||
String headerContent = new String(byteContent, 0, payloadIndex, STOMP_CHARSET); |
||||
Parser parser = new Parser(headerContent); |
||||
|
||||
StompCommand command = StompCommand.valueOf(parser.nextToken(LF).trim()); |
||||
Assert.notNull(command, "No command found"); |
||||
|
||||
MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>(); |
||||
while (parser.hasNext()) { |
||||
String header = parser.nextToken(COLON); |
||||
if (header != null) { |
||||
if (parser.hasNext()) { |
||||
String value = parser.nextToken(LF); |
||||
headers.add(header, value); |
||||
} |
||||
else { |
||||
throw new StompConversionException("Parse exception for " + headerContent); |
||||
} |
||||
} |
||||
} |
||||
|
||||
byte[] payload = new byte[totalLength - payloadIndex]; |
||||
System.arraycopy(byteContent, payloadIndex, payload, 0, totalLength - payloadIndex); |
||||
StompHeaderAccessor stompHeaders = StompHeaderAccessor.create(command, headers); |
||||
return MessageBuilder.withPayloadAndHeaders(payload, stompHeaders).build(); |
||||
} |
||||
|
||||
private int findIndexOfPayload(byte[] bytes) { |
||||
int i; |
||||
// ignore any leading EOL from the previous message
|
||||
for (i = 0; i < bytes.length; i++) { |
||||
if (bytes[i] != '\n' && bytes[i] != '\r') { |
||||
break; |
||||
} |
||||
bytes[i] = ' '; |
||||
} |
||||
int index = 0; |
||||
for (; i < bytes.length - 1; i++) { |
||||
if (bytes[i] == LF && bytes[i+1] == LF) { |
||||
index = i + 2; |
||||
break; |
||||
} |
||||
if ((i < (bytes.length - 3)) && |
||||
(bytes[i] == CR && bytes[i+1] == LF && bytes[i+2] == CR && bytes[i+3] == LF)) { |
||||
index = i + 4; |
||||
break; |
||||
} |
||||
} |
||||
if (i >= bytes.length) { |
||||
throw new StompConversionException("No end of headers found"); |
||||
} |
||||
return index; |
||||
} |
||||
|
||||
public byte[] fromMessage(Message<?> message) { |
||||
|
||||
byte[] payload; |
||||
if (message.getPayload() instanceof byte[]) { |
||||
payload = (byte[]) message.getPayload(); |
||||
} |
||||
else { |
||||
throw new IllegalArgumentException( |
||||
"stompContent is not byte[]: " + message.getPayload().getClass()); |
||||
} |
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(); |
||||
StompHeaderAccessor stompHeaders = StompHeaderAccessor.wrap(message); |
||||
|
||||
try { |
||||
out.write(stompHeaders.getCommand().toString().getBytes("UTF-8")); |
||||
out.write(LF); |
||||
for (Entry<String, List<String>> entry : stompHeaders.toStompHeaderMap().entrySet()) { |
||||
String key = entry.getKey(); |
||||
key = replaceAllOutbound(key); |
||||
for (String value : entry.getValue()) { |
||||
out.write(key.getBytes("UTF-8")); |
||||
out.write(COLON); |
||||
value = replaceAllOutbound(value); |
||||
out.write(value.getBytes("UTF-8")); |
||||
out.write(LF); |
||||
} |
||||
} |
||||
out.write(LF); |
||||
out.write(payload); |
||||
out.write(0); |
||||
return out.toByteArray(); |
||||
} |
||||
catch (IOException e) { |
||||
throw new StompConversionException("Failed to serialize " + message, e); |
||||
} |
||||
} |
||||
|
||||
private String replaceAllOutbound(String key) { |
||||
return key.replaceAll("\\\\", "\\\\") |
||||
.replaceAll(":", "\\\\c") |
||||
.replaceAll("\n", "\\\\n") |
||||
.replaceAll("\r", "\\\\r"); |
||||
} |
||||
|
||||
|
||||
private class Parser { |
||||
|
||||
private final String content; |
||||
|
||||
private int offset; |
||||
|
||||
public Parser(String content) { |
||||
this.content = content; |
||||
} |
||||
|
||||
public boolean hasNext() { |
||||
return this.offset < this.content.length(); |
||||
} |
||||
|
||||
public String nextToken(byte delimiter) { |
||||
if (this.offset >= this.content.length()) { |
||||
return null; |
||||
} |
||||
int delimAt = this.content.indexOf(delimiter, this.offset); |
||||
if (delimAt == -1) { |
||||
if (this.offset == this.content.length() - 1 && delimiter == COLON && |
||||
this.content.charAt(this.offset) == LF) { |
||||
this.offset++; |
||||
return null; |
||||
} |
||||
else if (this.offset == this.content.length() - 2 && delimiter == COLON && |
||||
this.content.charAt(this.offset) == CR && |
||||
this.content.charAt(this.offset + 1) == LF) { |
||||
this.offset += 2; |
||||
return null; |
||||
} |
||||
else { |
||||
throw new StompConversionException("No delimiter found at offset " + offset + " in " + this.content); |
||||
} |
||||
} |
||||
int escapeAt = this.content.indexOf('\\', this.offset); |
||||
String token = this.content.substring(this.offset, delimAt + 1); |
||||
this.offset += token.length(); |
||||
if (escapeAt >= 0 && escapeAt < delimAt) { |
||||
char escaped = this.content.charAt(escapeAt + 1); |
||||
if (escaped == 'n' || escaped == 'c' || escaped == '\\') { |
||||
token = token.replaceAll("\\\\n", "\n") |
||||
.replaceAll("\\\\r", "\r") |
||||
.replaceAll("\\\\c", ":") |
||||
.replaceAll("\\\\\\\\", "\\\\"); |
||||
} |
||||
else { |
||||
throw new StompConversionException("Invalid escape sequence \\" + escaped); |
||||
} |
||||
} |
||||
int length = token.length(); |
||||
if (delimiter == LF && length > 1 && token.charAt(length - 2) == CR) { |
||||
return token.substring(0, length - 2); |
||||
} |
||||
else { |
||||
return token.substring(0, length - 1); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,212 @@
@@ -0,0 +1,212 @@
|
||||
/* |
||||
* Copyright 2002-2013 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.messaging.simp.stomp; |
||||
|
||||
import java.io.UnsupportedEncodingException; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
import org.junit.Test; |
||||
import org.springframework.messaging.Message; |
||||
import org.springframework.messaging.support.MessageBuilder; |
||||
|
||||
import reactor.function.Consumer; |
||||
import reactor.function.Function; |
||||
import reactor.io.Buffer; |
||||
|
||||
import static org.junit.Assert.*; |
||||
|
||||
|
||||
/** |
||||
* |
||||
* @author awilkinson |
||||
*/ |
||||
public class StompCodecTests { |
||||
|
||||
private final ArgumentCapturingConsumer<Message<byte[]>> consumer = new ArgumentCapturingConsumer<Message<byte[]>>(); |
||||
|
||||
private final Function<Buffer, Message<byte[]>> decoder = new StompCodec().decoder(consumer); |
||||
|
||||
@Test |
||||
public void decodeFrameWithCrLfEols() { |
||||
Message<byte[]> frame = decode("DISCONNECT\r\n\r\n\0"); |
||||
StompHeaderAccessor headers = StompHeaderAccessor.wrap(frame); |
||||
|
||||
assertEquals(StompCommand.DISCONNECT, headers.getCommand()); |
||||
assertEquals(0, headers.toStompHeaderMap().size()); |
||||
assertEquals(0, frame.getPayload().length); |
||||
} |
||||
|
||||
@Test |
||||
public void decodeFrameWithNoHeadersAndNoBody() { |
||||
Message<byte[]> frame = decode("DISCONNECT\n\n\0"); |
||||
StompHeaderAccessor headers = StompHeaderAccessor.wrap(frame); |
||||
|
||||
assertEquals(StompCommand.DISCONNECT, headers.getCommand()); |
||||
assertEquals(0, headers.toStompHeaderMap().size()); |
||||
assertEquals(0, frame.getPayload().length); |
||||
} |
||||
|
||||
@Test |
||||
public void decodeFrameWithNoBody() { |
||||
String accept = "accept-version:1.1\n"; |
||||
String host = "host:github.org\n"; |
||||
|
||||
Message<byte[]> frame = decode("CONNECT\n" + accept + host + "\n\0"); |
||||
StompHeaderAccessor headers = StompHeaderAccessor.wrap(frame); |
||||
|
||||
assertEquals(StompCommand.CONNECT, headers.getCommand()); |
||||
|
||||
assertEquals(2, headers.toStompHeaderMap().size()); |
||||
assertEquals("1.1", headers.getFirstNativeHeader("accept-version")); |
||||
assertEquals("github.org", headers.getHost()); |
||||
|
||||
assertEquals(0, frame.getPayload().length); |
||||
} |
||||
|
||||
@Test |
||||
public void decodeFrame() throws UnsupportedEncodingException { |
||||
Message<byte[]> frame = decode("SEND\ndestination:test\n\nThe body of the message\0"); |
||||
StompHeaderAccessor headers = StompHeaderAccessor.wrap(frame); |
||||
|
||||
assertEquals(StompCommand.SEND, headers.getCommand()); |
||||
|
||||
assertEquals(1, headers.toStompHeaderMap().size()); |
||||
assertEquals("test", headers.getDestination()); |
||||
|
||||
String bodyText = new String(frame.getPayload()); |
||||
assertEquals("The body of the message", bodyText); |
||||
} |
||||
|
||||
@Test |
||||
public void decodeFrameWithContentLength() { |
||||
Message<byte[]> frame = decode("SEND\ncontent-length:23\n\nThe body of the message\0"); |
||||
StompHeaderAccessor headers = StompHeaderAccessor.wrap(frame); |
||||
|
||||
assertEquals(StompCommand.SEND, headers.getCommand()); |
||||
|
||||
assertEquals(1, headers.toStompHeaderMap().size()); |
||||
assertEquals(Integer.valueOf(23), headers.getContentLength()); |
||||
|
||||
String bodyText = new String(frame.getPayload()); |
||||
assertEquals("The body of the message", bodyText); |
||||
} |
||||
|
||||
@Test |
||||
public void decodeFrameWithNullOctectsInTheBody() { |
||||
Message<byte[]> frame = decode("SEND\ncontent-length:23\n\nThe b\0dy \0f the message\0"); |
||||
StompHeaderAccessor headers = StompHeaderAccessor.wrap(frame); |
||||
|
||||
assertEquals(StompCommand.SEND, headers.getCommand()); |
||||
|
||||
assertEquals(1, headers.toStompHeaderMap().size()); |
||||
assertEquals(Integer.valueOf(23), headers.getContentLength()); |
||||
|
||||
String bodyText = new String(frame.getPayload()); |
||||
assertEquals("The b\0dy \0f the message", bodyText); |
||||
} |
||||
|
||||
@Test |
||||
public void decodeFrameWithEscapedHeaders() { |
||||
Message<byte[]> frame = decode("DISCONNECT\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\0"); |
||||
StompHeaderAccessor headers = StompHeaderAccessor.wrap(frame); |
||||
|
||||
assertEquals(StompCommand.DISCONNECT, headers.getCommand()); |
||||
|
||||
assertEquals(1, headers.toStompHeaderMap().size()); |
||||
assertEquals("alpha:bravo\r\n\\", headers.getFirstNativeHeader("a:\r\n\\b")); |
||||
} |
||||
|
||||
@Test |
||||
public void decodeMultipleFramesFromSameBuffer() { |
||||
String frame1 = "SEND\ndestination:test\n\nThe body of the message\0"; |
||||
String frame2 = "DISCONNECT\n\n\0"; |
||||
|
||||
Buffer buffer = Buffer.wrap(frame1 + frame2); |
||||
|
||||
final List<Message<byte[]>> messages = new ArrayList<Message<byte[]>>(); |
||||
new StompCodec().decoder(new Consumer<Message<byte[]>>() { |
||||
@Override |
||||
public void accept(Message<byte[]> message) { |
||||
messages.add(message); |
||||
} |
||||
}).apply(buffer); |
||||
|
||||
assertEquals(2, messages.size()); |
||||
assertEquals(StompCommand.SEND, StompHeaderAccessor.wrap(messages.get(0)).getCommand()); |
||||
assertEquals(StompCommand.DISCONNECT, StompHeaderAccessor.wrap(messages.get(1)).getCommand()); |
||||
} |
||||
|
||||
@Test |
||||
public void encodeFrameWithNoHeadersAndNoBody() { |
||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT); |
||||
|
||||
Message<byte[]> frame = MessageBuilder.withPayloadAndHeaders(new byte[0], headers).build(); |
||||
|
||||
assertEquals("DISCONNECT\n\n\0", new StompCodec().encoder().apply(frame).asString()); |
||||
} |
||||
|
||||
@Test |
||||
public void encodeFrameWithHeaders() { |
||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT); |
||||
headers.setAcceptVersion("1.2"); |
||||
headers.setHost("github.org"); |
||||
|
||||
Message<byte[]> frame = MessageBuilder.withPayloadAndHeaders(new byte[0], headers).build(); |
||||
|
||||
String frameString = new StompCodec().encoder().apply(frame).asString(); |
||||
|
||||
assertTrue(frameString.equals("CONNECT\naccept-version:1.2\nhost:github.org\n\n\0") || |
||||
frameString.equals("CONNECT\nhost:github.org\naccept-version:1.2\n\n\0")); |
||||
} |
||||
|
||||
@Test |
||||
public void encodeFrameWithHeadersThatShouldBeEscaped() { |
||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT); |
||||
headers.addNativeHeader("a:\r\n\\b", "alpha:bravo\r\n\\"); |
||||
|
||||
Message<byte[]> frame = MessageBuilder.withPayloadAndHeaders(new byte[0], headers).build(); |
||||
|
||||
assertEquals("DISCONNECT\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\0", new StompCodec().encoder().apply(frame).asString()); |
||||
} |
||||
|
||||
@Test |
||||
public void encodeFrameWithHeadersBody() { |
||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); |
||||
headers.addNativeHeader("a", "alpha"); |
||||
|
||||
Message<byte[]> frame = MessageBuilder.withPayloadAndHeaders("Message body".getBytes(), headers).build(); |
||||
|
||||
assertEquals("SEND\na:alpha\ncontent-length:12\n\nMessage body\0", new StompCodec().encoder().apply(frame).asString()); |
||||
} |
||||
|
||||
private Message<byte[]> decode(String stompFrame) { |
||||
this.decoder.apply(Buffer.wrap(stompFrame)); |
||||
return consumer.arguments.get(0); |
||||
} |
||||
|
||||
private static final class ArgumentCapturingConsumer<T> implements Consumer<T> { |
||||
|
||||
private final List<T> arguments = new ArrayList<T>(); |
||||
|
||||
@Override |
||||
public void accept(T t) { |
||||
arguments.add(t); |
||||
} |
||||
|
||||
} |
||||
} |
@ -1,153 +0,0 @@
@@ -1,153 +0,0 @@
|
||||
/* |
||||
* Copyright 2002-2013 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.messaging.simp.stomp; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.Map; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import org.springframework.messaging.Message; |
||||
import org.springframework.messaging.MessageHeaders; |
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor; |
||||
import org.springframework.messaging.simp.SimpMessageType; |
||||
import org.springframework.web.socket.TextMessage; |
||||
|
||||
import static org.junit.Assert.*; |
||||
|
||||
/** |
||||
* @author Gary Russell |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
public class StompMessageConverterTests { |
||||
|
||||
private StompMessageConverter converter; |
||||
|
||||
|
||||
@Before |
||||
public void setup() { |
||||
this.converter = new StompMessageConverter(); |
||||
} |
||||
|
||||
@Test |
||||
public void connectFrame() throws Exception { |
||||
|
||||
String accept = "accept-version:1.1"; |
||||
String host = "host:github.org"; |
||||
|
||||
TextMessage textMessage = StompTextMessageBuilder.create(StompCommand.CONNECT) |
||||
.headers(accept, host).build(); |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
Message<byte[]> message = (Message<byte[]>) this.converter.toMessage(textMessage.getPayload()); |
||||
|
||||
assertEquals(0, message.getPayload().length); |
||||
|
||||
MessageHeaders headers = message.getHeaders(); |
||||
StompHeaderAccessor stompHeaders = StompHeaderAccessor.wrap(message); |
||||
Map<String, Object> map = stompHeaders.toMap(); |
||||
assertEquals(5, map.size()); |
||||
assertNotNull(stompHeaders.getId()); |
||||
assertNotNull(stompHeaders.getTimestamp()); |
||||
assertEquals(SimpMessageType.CONNECT, stompHeaders.getMessageType()); |
||||
assertEquals(StompCommand.CONNECT, stompHeaders.getCommand()); |
||||
assertNotNull(map.get(SimpMessageHeaderAccessor.NATIVE_HEADERS)); |
||||
|
||||
assertEquals(Collections.singleton("1.1"), stompHeaders.getAcceptVersion()); |
||||
assertEquals("github.org", stompHeaders.getHost()); |
||||
|
||||
assertEquals(SimpMessageType.CONNECT, stompHeaders.getMessageType()); |
||||
assertEquals(StompCommand.CONNECT, stompHeaders.getCommand()); |
||||
assertNotNull(headers.get(MessageHeaders.ID)); |
||||
assertNotNull(headers.get(MessageHeaders.TIMESTAMP)); |
||||
|
||||
String convertedBack = new String(this.converter.fromMessage(message), "UTF-8"); |
||||
|
||||
assertEquals("CONNECT\n", convertedBack.substring(0,8)); |
||||
assertTrue(convertedBack.contains(accept)); |
||||
assertTrue(convertedBack.contains(host)); |
||||
} |
||||
|
||||
@Test |
||||
public void connectWithEscapes() throws Exception { |
||||
|
||||
String accept = "accept-version:1.1"; |
||||
String host = "ho\\c\\ns\\rt:st\\nomp.gi\\cthu\\b.org"; |
||||
|
||||
TextMessage textMessage = StompTextMessageBuilder.create(StompCommand.CONNECT) |
||||
.headers(accept, host).build(); |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
Message<byte[]> message = (Message<byte[]>) this.converter.toMessage(textMessage.getPayload()); |
||||
|
||||
assertEquals(0, message.getPayload().length); |
||||
|
||||
StompHeaderAccessor stompHeaders = StompHeaderAccessor.wrap(message); |
||||
assertEquals(Collections.singleton("1.1"), stompHeaders.getAcceptVersion()); |
||||
assertEquals("st\nomp.gi:thu\\b.org", stompHeaders.toNativeHeaderMap().get("ho:\ns\rt").get(0)); |
||||
|
||||
String convertedBack = new String(this.converter.fromMessage(message), "UTF-8"); |
||||
|
||||
assertEquals("CONNECT\n", convertedBack.substring(0,8)); |
||||
assertTrue(convertedBack.contains(accept)); |
||||
assertTrue(convertedBack.contains(host)); |
||||
} |
||||
|
||||
@Test |
||||
public void connectCR12() throws Exception { |
||||
|
||||
String accept = "accept-version:1.2\n"; |
||||
String host = "host:github.org\n"; |
||||
String test = "CONNECT\r\n" + accept.replaceAll("\n", "\r\n") + host.replaceAll("\n", "\r\n") + "\r\n"; |
||||
@SuppressWarnings("unchecked") |
||||
Message<byte[]> message = (Message<byte[]>) this.converter.toMessage(test.getBytes("UTF-8")); |
||||
|
||||
assertEquals(0, message.getPayload().length); |
||||
|
||||
StompHeaderAccessor stompHeaders = StompHeaderAccessor.wrap(message); |
||||
assertEquals(Collections.singleton("1.2"), stompHeaders.getAcceptVersion()); |
||||
assertEquals("github.org", stompHeaders.getHost()); |
||||
|
||||
String convertedBack = new String(this.converter.fromMessage(message), "UTF-8"); |
||||
|
||||
assertEquals("CONNECT\n", convertedBack.substring(0,8)); |
||||
assertTrue(convertedBack.contains(accept)); |
||||
assertTrue(convertedBack.contains(host)); |
||||
} |
||||
|
||||
@Test |
||||
public void connectWithEscapesAndCR12() throws Exception { |
||||
|
||||
String accept = "accept-version:1.1\n"; |
||||
String host = "ho\\c\\ns\\rt:st\\nomp.gi\\cthu\\b.org\n"; |
||||
String test = "\n\n\nCONNECT\r\n" + accept.replaceAll("\n", "\r\n") + host.replaceAll("\n", "\r\n") + "\r\n"; |
||||
@SuppressWarnings("unchecked") |
||||
Message<byte[]> message = (Message<byte[]>) this.converter.toMessage(test.getBytes("UTF-8")); |
||||
|
||||
assertEquals(0, message.getPayload().length); |
||||
|
||||
StompHeaderAccessor stompHeaders = StompHeaderAccessor.wrap(message); |
||||
assertEquals(Collections.singleton("1.1"), stompHeaders.getAcceptVersion()); |
||||
assertEquals("st\nomp.gi:thu\\b.org", stompHeaders.toNativeHeaderMap().get("ho:\ns\rt").get(0)); |
||||
|
||||
String convertedBack = new String(this.converter.fromMessage(message), "UTF-8"); |
||||
|
||||
assertEquals("CONNECT\n", convertedBack.substring(0,8)); |
||||
assertTrue(convertedBack.contains(accept)); |
||||
assertTrue(convertedBack.contains(host)); |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue