Added encode/decode for Publish message

This commit is contained in:
andrea
2013-09-12 12:38:31 +02:00
parent 4dd633cef8
commit 28b37f68ea
10 changed files with 503 additions and 4 deletions
@@ -20,6 +20,7 @@ public class MQTTDecoder extends ByteToMessageDecoder {
MQTTDecoder() {
m_decoderMap.put(AbstractMessage.CONNECT, new ConnectDecoder());
m_decoderMap.put(AbstractMessage.CONNACK, new ConnAckDecoder());
m_decoderMap.put(AbstractMessage.PUBLISH, new PublishDecoder());
}
@Override
@@ -19,6 +19,7 @@ public class MQTTEncoder extends MessageToByteEncoder<AbstractMessage> {
MQTTEncoder() {
m_encoderMap.put(AbstractMessage.CONNECT, new ConnectEncoder());
m_encoderMap.put(AbstractMessage.CONNACK, new ConnAckEncoder());
m_encoderMap.put(AbstractMessage.PUBLISH, new PublishEncoder());
}
@Override
@@ -0,0 +1,61 @@
package org.dna.mqtt.moquette.parser.netty;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import java.util.List;
import org.dna.mqtt.moquette.proto.messages.AbstractMessage;
import org.dna.mqtt.moquette.proto.messages.PublishMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author andrea
*/
class PublishDecoder extends DemuxDecoder {
private static Logger LOG = LoggerFactory.getLogger(PublishDecoder.class);
@Override
void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
LOG.debug("decode invoked with buffer " + in);
int startPos = in.readerIndex();
in.resetReaderIndex();
//Common decoding part
PublishMessage message = new PublishMessage();
if (!decodeCommonHeader(message, in)) {
LOG.info("decode ask for more data after " + in);
in.resetReaderIndex();
return;
}
int remainingLength = message.getRemainingLength();
//Topic name
String topic = Utils.decodeString(in);
if (topic == null) {
in.resetReaderIndex();
return;
}
message.setTopicName(topic);
if (message.getQos() == AbstractMessage.QOSType.LEAST_ONE ||
message.getQos() == AbstractMessage.QOSType.EXACTLY_ONCE) {
message.setMessageID((int)in.readShort());
}
int stopPos = in.readerIndex();
//read the payload
int payloadSize = remainingLength - (stopPos - startPos - 2) + (Utils.numBytesToEncode(remainingLength) - 1);
if (in.readableBytes() < payloadSize) {
in.resetReaderIndex();
return;
}
byte[] b = new byte[payloadSize];
in.readBytes(b);
message.setPayload(b);
out.add(message);
}
}
@@ -0,0 +1,45 @@
package org.dna.mqtt.moquette.parser.netty;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import org.dna.mqtt.moquette.proto.messages.AbstractMessage;
import org.dna.mqtt.moquette.proto.messages.PublishMessage;
/**
*
* @author andrea
*/
class PublishEncoder extends DemuxEncoder<PublishMessage> {
@Override
protected void encode(ChannelHandlerContext ctx, PublishMessage message, ByteBuf out) {
if (message.getQos() == AbstractMessage.QOSType.RESERVED) {
throw new IllegalArgumentException("Found a message with RESERVED Qos");
}
if (message.getTopicName() == null || message.getTopicName().isEmpty()) {
throw new IllegalArgumentException("Found a message with empty or null topic name");
}
ByteBuf variableHeaderBuff = ctx.alloc().buffer(2);
variableHeaderBuff.writeBytes(Utils.encodeString(message.getTopicName()));
if (message.getQos() == AbstractMessage.QOSType.LEAST_ONE ||
message.getQos() == AbstractMessage.QOSType.EXACTLY_ONCE ) {
if (message.getMessageID() == null) {
throw new IllegalArgumentException("Found a message with QOS 1 or 2 and not MessageID setted");
}
variableHeaderBuff.writeShort(message.getMessageID());
}
variableHeaderBuff.writeBytes(message.getPayload());
int variableHeaderSize = variableHeaderBuff.readableBytes();
byte flags = Utils.encodeFlags(message);
ByteBuf buff = ctx.alloc().buffer(2 + variableHeaderSize);
buff.writeByte(AbstractMessage.PUBLISH << 4 | flags);
buff.writeBytes(Utils.encodeRemainingLength(variableHeaderSize));
buff.writeBytes(variableHeaderBuff);
out.writeBytes(buff);
}
}
@@ -4,6 +4,7 @@ import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.CorruptedFrameException;
import java.io.UnsupportedEncodingException;
import org.dna.mqtt.moquette.proto.messages.AbstractMessage;
import org.slf4j.LoggerFactory;
/**
@@ -131,4 +132,28 @@ public class Utils {
out.writeBytes(raw);
return out;
}
/**
* Return the number of bytes to encode the given remaining length value
*/
static int numBytesToEncode(int len) {
if (0 <= len && len <= 127) return 1;
if (128 <= len && len <= 16383) return 2;
if (16384 <= len && len <= 2097151) return 3;
if (2097152 <= len && len <= 268435455) return 4;
throw new IllegalArgumentException("value shoul be in the range [0..268435455]");
}
static byte encodeFlags(AbstractMessage message) {
byte flags = 0;
if (message.isDupFlag()) {
flags |= 0x08;
}
if (message.isRetainFlag()) {
flags |= 0x01;
}
flags |= ((message.getQos().ordinal() & 0x03) << 1);
return flags;
}
}
@@ -32,10 +32,6 @@ public class ConnectEncoderTest {
msg.setWillFlag(false);
msg.setCleanSession(true);
msg.setKeepAlive(512);
//mock the ChannelHandlerContext to return an UnpooledAllocator
/*ChannelHandlerContext mockedContext = mock(ChannelHandlerContext.class);
ByteBufAllocator allocator = UnpooledByteBufAllocator.DEFAULT;
when(mockedContext.alloc()).thenReturn(allocator);*/
ByteBuf out = Unpooled.buffer();
//Exercise
@@ -0,0 +1,167 @@
package org.dna.mqtt.moquette.parser.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.util.ArrayList;
import java.util.List;
import org.dna.mqtt.moquette.proto.messages.AbstractMessage;
import org.dna.mqtt.moquette.proto.messages.PublishMessage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import org.junit.Before;
import org.junit.Test;
/**
*
* @author andrea
*/
public class PublishDecoderTest {
ByteBuf m_buff;
PublishDecoder m_msgdec;
List<Object> m_results;
@Before
public void setUp() {
m_msgdec = new PublishDecoder();
m_results = new ArrayList<Object >();
}
@Test
public void testHeader() throws Exception {
m_buff = Unpooled.buffer(14);
initHeader(m_buff);
//Excercise
m_msgdec.decode(null, m_buff, m_results);
assertFalse(m_results.isEmpty());
PublishMessage message = (PublishMessage)m_results.get(0);
assertNotNull(message);
assertEquals("Fake Topic", message.getTopicName());
assertNull(message.getMessageID());
assertEquals(AbstractMessage.PUBLISH, message.getMessageType());
}
@Test
public void testHeaderWithMessageID() throws Exception {
m_buff = Unpooled.buffer(14);
int messageID = 123;
initHeaderWithMessageID(m_buff, messageID);
//Exercise
m_msgdec.decode(null, m_buff, m_results);
assertFalse(m_results.isEmpty());
PublishMessage message = (PublishMessage)m_results.get(0);
assertNotNull(message);
assertEquals("Fake Topic", message.getTopicName());
assertEquals(messageID, (int) message.getMessageID());
}
@Test
public void testHeaderWithMessageID_Payload() throws Exception {
m_buff = Unpooled.buffer(14);
int messageID = 123;
byte[] payload = new byte[]{0x0A, 0x0B, 0x0C};
initHeaderWithMessageID_Payload(m_buff, messageID, payload);
//Exercise
m_msgdec.decode(null, m_buff, m_results);
assertFalse(m_results.isEmpty());
PublishMessage message = (PublishMessage)m_results.get(0);
assertNotNull(message);
assertEquals("Fake Topic", message.getTopicName());
assertEquals(messageID, (int) message.getMessageID());
TestUtils.verifyEquals(payload, message.getPayload());
}
@Test
public void testBugOnRealCase() throws Exception {
byte[] overallMessage = new byte[]{0x30, 0x17, //fixed header, 25 byte lenght
0x00, 0x06, 0x2f, 0x74, 0x6f, 0x70, 0x69, 0x63, //[/topic] string 2 len + 6 content
0x54, 0x65, 0x73, 0x74, 0x20, 0x6d, 0x79, // [Test my payload] encoding
0x20, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64};
m_buff = Unpooled.buffer(overallMessage.length);
m_buff.writeBytes(overallMessage);
//Exercise
m_msgdec.decode(null, m_buff, m_results);
assertFalse(m_results.isEmpty());
PublishMessage message = (PublishMessage)m_results.get(0);
assertNotNull(message);
}
@Test
public void testDecodeBigContent() throws Exception {
int size = 129;
ByteBuf payload = TestUtils.generateRandomPayload(size);
ByteBuf firstPublish = generatePublishQoS0(payload);
ByteBuf secondPublish = generatePublishQoS0(TestUtils.generateRandomPayload(size));
ByteBuf doubleMessageBuf = Unpooled.buffer(size * 2);
doubleMessageBuf.writeBytes(firstPublish).writeBytes(secondPublish);
//Exercise
m_msgdec.decode(null, doubleMessageBuf, m_results);
assertFalse(m_results.isEmpty());
PublishMessage message = (PublishMessage)m_results.get(0);
assertNotNull(message);
m_results.clear();
m_msgdec.decode(null, doubleMessageBuf, m_results);
assertFalse(m_results.isEmpty());
message = (PublishMessage)m_results.get(0);
assertNotNull(message);
}
private void initHeader(ByteBuf buff) throws IllegalAccessException {
ByteBuf tmp = Unpooled.buffer(4).writeBytes(Utils.encodeString("Fake Topic"));
buff.clear().writeByte(AbstractMessage.PUBLISH << 4).writeBytes(Utils.encodeRemainingLength(tmp.readableBytes()));
//topic name
buff.writeBytes(tmp);
}
private void initHeaderWithMessageID(ByteBuf buff, int messageID) throws IllegalAccessException {
ByteBuf tmp = Unpooled.buffer(4).writeBytes(Utils.encodeString("Fake Topic"));
tmp.writeShort(messageID);
buff.clear().writeByte(AbstractMessage.PUBLISH << 4 | 0x02) //set Qos to 1
.writeBytes(Utils.encodeRemainingLength(tmp.readableBytes()));
//topic name
buff.writeBytes(tmp);
}
private void initHeaderWithMessageID_Payload(ByteBuf buff, int messageID, byte[] payload) throws IllegalAccessException {
ByteBuf tmp = Unpooled.buffer(4).writeBytes(Utils.encodeString("Fake Topic"));
tmp.writeShort(messageID);
tmp.writeBytes(payload);
buff.clear().writeByte(AbstractMessage.PUBLISH << 4 | 0x02) //set Qos to 1
.writeBytes(Utils.encodeRemainingLength(tmp.readableBytes()));
//topic name
buff.writeBytes(tmp);
}
private ByteBuf generatePublishQoS0(ByteBuf payload) throws IllegalAccessException {
int size = payload.capacity();
ByteBuf messageBody = Unpooled.buffer(size);
messageBody.writeBytes(Utils.encodeString("/topic"));
//ONLY for QoS > 1 Utils.writeWord(messageBody, messageID);
messageBody.writeBytes(payload);
ByteBuf completeMsg = Unpooled.buffer(size);
completeMsg.clear().writeByte(AbstractMessage.PUBLISH << 4 | 0x00) //set Qos to 0
.writeBytes(Utils.encodeRemainingLength(messageBody.readableBytes()));
completeMsg.writeBytes(messageBody);
return completeMsg;
}
}
@@ -0,0 +1,175 @@
package org.dna.mqtt.moquette.parser.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.dna.mqtt.moquette.parser.netty.TestUtils.*;
import org.dna.mqtt.moquette.proto.messages.AbstractMessage.QOSType;
import org.dna.mqtt.moquette.proto.messages.PublishMessage;
/**
*
* @author andrea
*/
public class PublishEncoderTest {
PublishEncoder m_encoder = new PublishEncoder();
ChannelHandlerContext m_mockedContext;
ByteBuf m_out;
@Before
public void setUp() {
//mock the ChannelHandlerContext to return an UnpooledAllocator
m_mockedContext = mockChannelHandler();
m_out = Unpooled.buffer();
}
@Test
public void testEncodeWithQos_0_noMessageID() throws Exception {
String topic = "/photos";
PublishMessage msg = new PublishMessage();
msg.setQos(QOSType.MOST_ONE);
msg.setTopicName(topic);
//variable part
byte[] payload = new byte[]{0x0A, 0x0B, 0x0C};
msg.setPayload(payload);
//Exercise
m_encoder.encode(m_mockedContext, msg, m_out);
//Verify
assertEquals(0x30, m_out.readByte()); //1 byte
//(2+7) topic + 3 payload
assertEquals(12, m_out.readByte()); //remaining length
//Variable part
verifyString(topic, m_out);
verifyBuff(payload.length, payload, m_out);
}
@Test
public void testEncodeWithQos_1_MessageID() throws Exception {
String topic = "/photos";
PublishMessage msg = new PublishMessage();
msg.setQos(QOSType.LEAST_ONE);
msg.setMessageID(1);
msg.setTopicName(topic);
//variable part
byte[] payload = new byte[]{0x0A, 0x0B, 0x0C};
msg.setPayload(payload);
//Exercise
m_encoder.encode(m_mockedContext, msg, m_out);
//Verify
assertEquals(0x32, m_out.readByte()); //1 byte
//(2+7) topic + 2 messageID + 3 payload
assertEquals(14, m_out.readByte()); //remaining length
//Variable part
verifyString(topic, m_out);
assertEquals(0, m_out.readByte()); //MessageID MSB
assertEquals(1, m_out.readByte()); //MessageID LSB
verifyBuff(payload.length, payload, m_out);
}
@Test(expected = IllegalArgumentException.class)
public void testEncodeWithQos_1_noMessageID_fail() throws Exception {
String topic = "pictures/photos";
PublishMessage msg = new PublishMessage();
msg.setQos(QOSType.LEAST_ONE);
msg.setTopicName(topic);
//Exercise
m_encoder.encode(m_mockedContext, msg, m_out);
}
@Test(expected = IllegalArgumentException.class)
public void testEncode_empty_topic() throws Exception {
PublishMessage msg = new PublishMessage();
msg.setQos(QOSType.LEAST_ONE);
msg.setMessageID(1);
ByteBuf out = Unpooled.buffer();
//Exercise
m_encoder.encode(m_mockedContext, msg, out);
}
@Test(expected = IllegalArgumentException.class)
public void testEncode_null_topic() throws Exception {
PublishMessage msg = new PublishMessage();
msg.setQos(QOSType.LEAST_ONE);
msg.setMessageID(1);
msg.setTopicName(null);
//Exercise
m_encoder.encode(m_mockedContext, msg, m_out);
}
@Test(expected = IllegalArgumentException.class)
public void testEncode_qos_reserved() throws Exception {
PublishMessage msg = new PublishMessage();
msg.setQos(QOSType.RESERVED);
msg.setMessageID(1);
msg.setTopicName(null);
//variable part
byte[] payload = new byte[]{0x0A, 0x0B, 0x0C};
msg.setPayload(payload);
ByteBuf out = Unpooled.buffer();
//Exercise
m_encoder.encode(m_mockedContext, msg, out);
}
@Test
public void testBugMissedMessageIDEncoding() throws Exception{
String topic = "/topic";
PublishMessage msg = new PublishMessage();
msg.setQos(QOSType.MOST_ONE);
msg.setTopicName(topic);
//variable part
byte[] payload = "Test my payload".getBytes();
msg.setPayload(payload);
//Exercise
m_encoder.encode(m_mockedContext, msg, m_out);
//Verify
//2 byte header + (2+6) topic name + [2 message ID if QoQ <> 0]+ payload
int size = m_out.readableBytes();
assertEquals(10+payload.length, size);
assertEquals(0x30, m_out.readByte()); //1 byte
assertEquals(23, m_out.readByte()); //1 byte the length
}
@Test
public void testEncodeBigMessage() throws Exception {
byte[] payload = new byte[256];
TestUtils.generateRandomPayload(256).readBytes(payload);
String topic = "/topic";
PublishMessage msg = new PublishMessage();
msg.setQos(QOSType.MOST_ONE);
msg.setTopicName(topic);
//variable part
msg.setPayload(payload);
//Exercise
m_encoder.encode(m_mockedContext, msg, m_out);
int size = m_out.readableBytes();
assertEquals(11+payload.length, size);
assertEquals(0x30, m_out.readByte()); //1 byte
}
}
@@ -56,4 +56,20 @@ public class TestUtils {
when(m_mockedContext.alloc()).thenReturn(allocator);
return m_mockedContext;
}
static void verifyEquals(byte[] expected, byte[] found) {
assertEquals(expected.length, found.length);
for (int i = 0; i < expected.length; i++) {
assertEquals(expected[i], found[i]);
}
}
public static ByteBuf generateRandomPayload(int size) {
ByteBuf payloadBuffer = Unpooled.buffer(size);
for (int i = 0; i < size; i++) {
payloadBuffer.writeByte((byte) (Math.random() * 255));
}
return payloadBuffer;
}
}
@@ -7,6 +7,8 @@ import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.dna.mqtt.moquette.parser.netty.TestUtils.*;
import org.dna.mqtt.moquette.proto.messages.AbstractMessage;
import org.dna.mqtt.moquette.proto.messages.UnsubscribeMessage;
/**
*
@@ -78,4 +80,14 @@ public class UtilsTest {
verifyBuff(4, new byte[]{(byte)0x80, (byte)0x80, (byte)0x80, 0x01}, Utils.encodeRemainingLength(2097152));
verifyBuff(4, new byte[]{(byte)0xFF, (byte)0xFF, (byte)0xFF, 0x7F}, Utils.encodeRemainingLength(268435455));
}
@Test
public void testEncodeFlags() {
UnsubscribeMessage msg = new UnsubscribeMessage();
msg.setRetainFlag(true);
msg.setQos(AbstractMessage.QOSType.MOST_ONE);
//Exercise
assertEquals(1, Utils.encodeFlags(msg));
}
}