Browse Source
* broker-relay: Polish Improve handling of missed heartbeats Upgrade to Reactor 1.0.0.M3 Add heart-beat support to STOMP broker relay Remove CONNECT-related message buffer from STOMP relay Add StompCodecpull/364/merge
16 changed files with 955 additions and 616 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,171 @@
@@ -0,0 +1,171 @@
|
||||
/* |
||||
* 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.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
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"); |
||||
|
||||
private static final byte[] HEARTBEAT_PAYLOAD = new byte[] {'\n'}; |
||||
|
||||
private final Log logger = LogFactory.getLog(StompDecoder.class); |
||||
|
||||
|
||||
/** |
||||
* 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); |
||||
|
||||
Message<byte[]> decodedMessage = MessageBuilder.withPayloadAndHeaders(payload, |
||||
StompHeaderAccessor.create(StompCommand.valueOf(command), headers)).build(); |
||||
|
||||
if (logger.isTraceEnabled()) { |
||||
logger.trace("Decoded " + decodedMessage); |
||||
} |
||||
|
||||
return decodedMessage; |
||||
} |
||||
else { |
||||
if (logger.isTraceEnabled()) { |
||||
logger.trace("Decoded heartbeat"); |
||||
} |
||||
return MessageBuilder.withPayload(HEARTBEAT_PAYLOAD).build(); |
||||
} |
||||
|
||||
} |
||||
|
||||
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,129 @@
@@ -0,0 +1,129 @@
|
||||
/* |
||||
* 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.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
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"); |
||||
|
||||
private final Log logger = LogFactory.getLog(StompEncoder.class); |
||||
|
||||
/** |
||||
* 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 { |
||||
if (logger.isTraceEnabled()) { |
||||
logger.trace("Encoding " + message); |
||||
} |
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
||||
DataOutputStream output = new DataOutputStream(baos); |
||||
|
||||
StompHeaderAccessor headers = StompHeaderAccessor.wrap(message); |
||||
|
||||
if (isHeartbeat(headers)) { |
||||
output.write(message.getPayload()); |
||||
} else { |
||||
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 boolean isHeartbeat(StompHeaderAccessor headers) { |
||||
return headers.getCommand() == null; |
||||
} |
||||
|
||||
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