package eu.hgross.blaubot.messaging; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import eu.hgross.blaubot.core.BlaubotConstants; import eu.hgross.blaubot.core.IBlaubotConnection; import eu.hgross.blaubot.util.Log; /** * The message object that is sent through connections */ public class BlaubotMessage { private static final String LOG_TAG = "BlaubotMessage"; public final static int VERSION_FIELD_LENGTH = 1; public final static int TYPE_FIELD_LENGTH = 1; public final static int PRIORITY_FIELD_LENGTH = 1; public final static int CHANNEL_FIELD_LENGTH = 2; public final static int PAYLOAD_LENGTH_FIELD_LENGTH = 2; public final static int CHUNK_ID_FIELD_LENGTH = 2; public final static int CHUNK_NO_FIELD_LENGTH = 2; public final static int FULL_HEADER_LENGTH = VERSION_FIELD_LENGTH + TYPE_FIELD_LENGTH + PRIORITY_FIELD_LENGTH + CHANNEL_FIELD_LENGTH + CHUNK_ID_FIELD_LENGTH + CHUNK_NO_FIELD_LENGTH + PAYLOAD_LENGTH_FIELD_LENGTH; /** * Creates chunks of this message containing the given chunkId. * The chunk messages are numbered. The numbers can be retrieved via #getChunkNumber(). * To signal a potential receiver that a chunk message is the last message of a chunkId, * a chunk message with less than BlaubotConstants.MAX_PAYLOAD_SIZE payload size is send. * If the last message's payload is equal to BlaubotConstants.MAX_PAYLOAD_SIZE the last message * will not contain any payload. * * @param chunkId the chunk id to identify this message * @return the ordered list of chunks * @throws IllegalArgumentException iff the message contains too much payload to chunk (more than Short.MAX_VALUE resulting chunks) */ public List<BlaubotMessage> createChunks(short chunkId) { final int payloadLength = getPayload().length; if (!(payloadLength > 0)) { throw new IllegalStateException("createChunks() was called for a message without any payload!"); } final int maxChunkSize = BlaubotConstants.MAX_PAYLOAD_SIZE; final int numChunks = (payloadLength + maxChunkSize - 1) / maxChunkSize; // rounded up if (numChunks > BlaubotConstants.USHORT_MAX_VALUE) { // unsigned short max value throw new IllegalArgumentException("The message contains " + payloadLength + "bytes payload which results in " + numChunks + " chunks. The number of chunks exceeds the message header field (short, 2 bytes) and is therefore too big"); } final ByteBuffer byteBuffer = ByteBuffer.wrap(getPayload()).order(BlaubotConstants.BYTE_ORDER); final List<BlaubotMessage> chunks = new ArrayList<>(); for (int chunkNo = 1; chunkNo <= numChunks; chunkNo += 1) { // create chunk messages final byte[] chunkPayload; if (byteBuffer.remaining() >= maxChunkSize) { chunkPayload = new byte[maxChunkSize]; } else { chunkPayload = new byte[byteBuffer.remaining()]; } byteBuffer.get(chunkPayload); // build the message object BlaubotMessage chunk = new BlaubotMessage(); chunk.setMessageType(BlaubotMessageType.copy(messageType)); chunk.getMessageType().setIsChunk(true); chunk.setChunkId(chunkId); chunk.setChunkNo((short) chunkNo); chunk.setPriority(priority); chunk.channelId = channelId; chunk.setPayload(chunkPayload); chunks.add(chunk); } // now we check if the last message equals our maxChunkSize and we therefore have to add an "end marker" message BlaubotMessage lastMessage = chunks.get(chunks.size()-1); if (lastMessage.getPayload().length == maxChunkSize) { // we add an empty message to signal that this is the last chunk BlaubotMessage chunk = new BlaubotMessage(); chunk.setMessageType(BlaubotMessageType.copy(messageType)); chunk.getMessageType().setIsChunk(true); chunk.setChunkId(chunkId); chunk.setChunkNo((short) (chunks.size() + 1)); chunk.setPriority(priority); chunk.channelId = channelId; chunk.setPayload(new byte[0]); chunks.add(chunk); } return chunks; } /** * Creates a message from multiple chunks * @param chunks the complete list of chunks with the same chunkId. Must not be ordered. * @return the message */ public static BlaubotMessage fromChunks(List<BlaubotMessage> chunks) { // sort by chunkNo Collections.sort(chunks, new Comparator<BlaubotMessage>() { @Override public int compare(BlaubotMessage o1, BlaubotMessage o2) { return Short.valueOf(o1.chunkNo).compareTo(Short.valueOf(o2.chunkNo)); } }); int chunkId = -1; int totalSize = 0; int i = 0; Priority p = null; short channelId = -1; IBlaubotConnection originator = null; for (BlaubotMessage chunk : chunks) { // validate id on the run if (i++ == 0) { // initially set up some vars chunkId = chunk.getChunkId(); channelId = chunk.getChannelId(); p = chunk.getPriority(); originator = chunk.getLastOriginatorConnection(); } else if (chunkId != chunk.getChunkId()) { throw new IllegalArgumentException("The list contained chunk messages of multiple chunkIds. "); } if (chunk.getMessageType().containsPayload()) { totalSize += chunk.getPayload().length; } } // create the combined payload byte array final ByteBuffer byteBuffer = ByteBuffer.allocate(totalSize).order(BlaubotConstants.BYTE_ORDER); for (BlaubotMessage chunk : chunks) { if (chunk.getMessageType().containsPayload()) { byteBuffer.put(chunk.getPayload()); } } byteBuffer.flip(); final byte[] payload = byteBuffer.array(); // build the message BlaubotMessage out = new BlaubotMessage(); out.setMessageType(BlaubotMessageType.copy(chunks.get(0).messageType)); out.getMessageType().setIsChunk(false); out.setPriority(p); out.setChannelId(channelId); out.setLastOriginatorConnection(originator); out.setPayload(payload); return out; } /** * Priority for BlaubotMessages. */ public enum Priority { ADMIN ((byte) 1), ADMIN_LOW ((byte) 2), HIGH ((byte) 3), NORMAL ((byte) 4), LOW ((byte) 5); Priority(byte value) { this.value = value; } public final byte value; public static Priority fromByte(byte val) { for (Priority p : Priority.values()) { if (p.value == val) { return p; } } throw new RuntimeException("Unknown priority: " + val); } } private byte protocolVersion; private BlaubotMessageType messageType; private Priority priority; private short channelId; private short chunkId; private short chunkNo; private byte[] payload; /** * An attribute that is not sent via the connection * Only used to send messages with the same priority in the order they were * queued. */ protected int sequenceNumber; private IBlaubotConnection lastOriginatorConnection; /** * Constructs a default message, which sends data on a default channel */ public BlaubotMessage() { this.messageType = new BlaubotMessageType(); this.priority = Priority.NORMAL; this.channelId = BlaubotConstants.DEFAULT_CHANNEL_ID; this.payload = new byte[0]; } /** * Gets the last originators connection. This is the connection over which this message was * received. * Note that this only represents the origin of the message's last hop and NOT the * connection from the original sender. * @return the originator connection or null, if not received from a connection */ protected IBlaubotConnection getLastOriginatorConnection() { return lastOriginatorConnection; } protected void setLastOriginatorConnection(IBlaubotConnection originatorConnection) { this.lastOriginatorConnection = originatorConnection; } public byte getProtocolVersion() { return protocolVersion; } protected void setProtocolVersion(byte protocolVersion) { this.protocolVersion = protocolVersion; } public BlaubotMessageType getMessageType() { return messageType; } protected void setMessageType(BlaubotMessageType messageType) { this.messageType = messageType; } public Priority getPriority() { return priority; } public void setPriority(Priority priority) { this.priority = priority; } /** * Retrieves the channelId for which this message was designated. * There are some cases in which no channel is involved like: * - AdminMessages * - KeepAliveMessages * for which this method will return -1; * * @return channelId or -1, if no channel is involved */ public short getChannelId() { return channelId; } protected void setChannelId(short channelId) { // some assertions to avoid mistakes if(messageType.isAdminMessage() && channelId >=0) { throw new IllegalArgumentException("You are trying something weird. Message is an admin message but you tried to set a channelId ("+channelId+"). AdminMessages don't involve any channels."); } else if(messageType.isKeepAliveMessage() && channelId >= 0) { throw new IllegalArgumentException("You are trying something weird. Message is a keep alive message but you tried to set a channelId ("+channelId+"). Keep alives don't involve any channels."); } this.channelId = channelId; } /** * Retrieve this message's payload * @return payload as byte array (max 65535 bytes) */ public byte[] getPayload() { return payload; } /** * Set the payload of this message. * @param payload the payload bytes @throws java.lang.IllegalArgumentException if the payload length exeeds 65535 bytes */ public void setPayload(byte[] payload) { // validate payload length to avoid buffer overflows // if(payload.length > BlaubotConstants.MAX_PAYLOAD_SIZE) { // max unsigned short value // throw new IllegalArgumentException("Payload is to large. Max size is " + BlaubotConstants.MAX_PAYLOAD_SIZE + ", but got " + payload.length + " bytes. Consider chunking your messages."); // } else // Messages are now chunked by MessageReceivers and MessageSenders, if too large! if(payload != null && payload.length > 0) { this.messageType.setContainsPayload(true); } else { this.messageType.setContainsPayload(false); } this.payload = payload; } /** * Applies all data from the message schema except the payload, which has to be set afterwards * by setPayload(), to this instance. * The payload's length is returned. * * @param headerBytes the byte array containing all header informations * @return the payloads length in bytes - 0 if no payload at all. */ public int applyBytes(byte[] headerBytes) { // VERSION ByteBuffer byteBuffer = ByteBuffer.wrap(headerBytes).order(BlaubotConstants.BYTE_ORDER); byte version = byteBuffer.get(); setProtocolVersion(version); // MessageType byte type = byteBuffer.get(); BlaubotMessageType messageType = BlaubotMessageType.fromByte(type); setMessageType(messageType); // PRIORITY byte priority = byteBuffer.get(); setPriority(Priority.fromByte(priority)); // Now we check if we have to deal with channels if (messageType.isAdminMessage() || messageType.isKeepAliveMessage()) { // -- no channel needed setChannelId((byte) -1); } else { // CHANNEL ID short channelId = byteBuffer.getShort(); setChannelId(channelId); } // Now check if this is a chunked message if (messageType.isChunk()) { // read chunk id short chunkId = byteBuffer.getShort(); // read chunk no short chunkNo = byteBuffer.getShort(); setChunkId(chunkId); setChunkNo(chunkNo); } else { // we ignore this fields } // Check if there is any payload if (messageType.containsPayload()) { // PAYLOAD_LENGTH short payloadLength = byteBuffer.getShort(); int unsignedPayloadLength = payloadLength & 0xffff; return unsignedPayloadLength; } else { // no payload return 0; } } /** * Calculates a messages total header length by a given BlaubotMessageType * @param messageType the message type * @return the length of all header fields excluding the payload bytes. */ protected static int calculateHeaderLength(BlaubotMessageType messageType) { boolean isChannelFieldRelevant = !messageType.isAdminMessage() && !messageType.isKeepAliveMessage(); boolean containsPayload = messageType.containsPayload(); boolean isChunkMessage = messageType.isChunk(); // calculate the total header length needed int totalLength = FULL_HEADER_LENGTH; if (!containsPayload) { totalLength -= PAYLOAD_LENGTH_FIELD_LENGTH; } if (!isChannelFieldRelevant) { totalLength -= CHANNEL_FIELD_LENGTH; } if (!isChunkMessage) { totalLength -= CHUNK_ID_FIELD_LENGTH + CHUNK_NO_FIELD_LENGTH; } return totalLength; } /** * Serializes the message to a byte array. * The resulting bytes contain the header as well as the payload (if any). * * @return byte array containing the message's header as well as payload (if any) */ public byte[] toBytes() { int headerLength = calculateHeaderLength(messageType); int totalLength = headerLength + (messageType.containsPayload() ? payload.length : 0); // allocate and encode attributes ByteBuffer bb = ByteBuffer.allocate(totalLength).order(BlaubotConstants.BYTE_ORDER); // encode version, type and priority bb.put(protocolVersion); bb.put(messageType.toByte()); bb.put(priority.value); // encode channel, if relevant final boolean isChannelRelevant = !messageType.isAdminMessage() && !messageType.isKeepAliveMessage(); if (isChannelRelevant) { bb.putShort(channelId); } // chunked message fields, if relevant final boolean isChunkedMessage = messageType.isChunk(); if (isChunkedMessage) { bb.putShort(getChunkId()); bb.putShort(getChunkNo()); } // append payload, if relevant if (messageType.containsPayload()) { // note the cast to short which is effectively: (intValue) & 0xffff // so the result could be a negative short! bb.putShort((short) payload.length); bb.put(payload); } // retrieve byte array from buffer byte[] bytes = new byte[totalLength]; bb.clear(); bb.get(bytes); return bytes; } /** * Deserializes a BlaubotMessage from a byte array. * The byte array should contain the header and payload. * * @param messageBytes byte array containing header and payload * @return the deserialized blaubot message */ public static BlaubotMessage fromByteArray(byte[] messageBytes) { int headerLength = BlaubotMessage.FULL_HEADER_LENGTH; byte[] headerBuffer = new byte[headerLength]; ByteBuffer headerByteBuffer = ByteBuffer.wrap(headerBuffer).order(BlaubotConstants.BYTE_ORDER); ByteBuffer messageByteBuffer = ByteBuffer.wrap(messageBytes).order(BlaubotConstants.BYTE_ORDER); // Partially read version, type and type, then decide how much bytes we need to read int partialHeaderLength = BlaubotMessage.VERSION_FIELD_LENGTH + BlaubotMessage.TYPE_FIELD_LENGTH; messageByteBuffer.get(headerBuffer, 0, partialHeaderLength); // assert a compatible message schema byte messageSchemaVersion = headerByteBuffer.get(); if (messageSchemaVersion != BlaubotConstants.MESSAGE_SCHEMA_VERSION) { // TODO: maybe close connection, see TODO at methods begin throw new RuntimeException("Incompatible Blaubot message schema version: " + messageSchemaVersion); } // pre-create the MessageType to determine, how much header length is left to be read byte typeInfo = headerByteBuffer.get(); BlaubotMessageType messageType = BlaubotMessageType.fromByte(typeInfo); // we are now able to calculate, how many bytes of this message's header we need to read. // we already read some header bytes, now get the rest needed for this message int totalHeaderLength = BlaubotMessage.calculateHeaderLength(messageType); int outstandingHeaderBytes = totalHeaderLength - partialHeaderLength; messageByteBuffer.get(headerBuffer, partialHeaderLength, outstandingHeaderBytes); // construct the message with all header informations BlaubotMessage message = new BlaubotMessage(); int payloadLength = message.applyBytes(headerBuffer); // check if there is any payload to retrieve if (message.getMessageType().containsPayload()) { if (payloadLength > 0) { // create buffer - Note: intentionally no reuse of buffers - faster because of javas memory management byte[] payloadBuffer = new byte[payloadLength]; messageByteBuffer.get(payloadBuffer, 0, payloadLength); byte[] orderedPayload = new byte[payloadLength]; // byte ordered ByteBuffer.wrap(payloadBuffer).order(BlaubotConstants.BYTE_ORDER).get(orderedPayload); message.setPayload(payloadBuffer); } } return message; } /** * If this message is a chunk message, returns the chunk number. * @return the chunk number */ public short getChunkNo() { return chunkNo; } /** * Sets the chunk number * @param chunkNo has to be 1-based */ public void setChunkNo(short chunkNo) { if (chunkNo == 0) { throw new IllegalArgumentException("chunkNo is 1-based"); } this.chunkNo = chunkNo; } /** * If this message is a chunk message, gets the chunk id. * @return the chunk id */ public short getChunkId() { return chunkId; } /** * sets the chunk id * @param chunkId the chunk id */ public void setChunkId(short chunkId) { this.chunkId = chunkId; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; BlaubotMessage that = (BlaubotMessage) o; if (channelId != that.channelId) return false; if (protocolVersion != that.protocolVersion) return false; if (messageType != null ? !messageType.equals(that.messageType) : that.messageType != null) return false; if (!Arrays.equals(payload, that.payload)) return false; if (priority != that.priority) return false; return true; } @Override public int hashCode() { int result = (int) protocolVersion; result = 31 * result + (messageType != null ? messageType.hashCode() : 0); result = 31 * result + (priority != null ? priority.hashCode() : 0); result = 31 * result + (int) channelId; result = 31 * result + (payload != null ? Arrays.hashCode(payload) : 0); return result; } @Override public String toString() { final StringBuffer sb = new StringBuffer("BlaubotMessage{"); sb.append("protocolVersion=").append(protocolVersion); sb.append(", messageType=").append(messageType); sb.append(", priority=").append(priority); sb.append(", channelId=").append(channelId); sb.append(", chunkId=").append(chunkId); sb.append(", chunkNo=").append(chunkNo); sb.append(", payload="); if (payload == null) sb.append("null"); else { sb.append(payload.length + " bytes"); } sb.append(", sequenceNumber=").append(sequenceNumber); sb.append(", lastOriginatorConnection=").append(lastOriginatorConnection); sb.append('}'); return sb.toString(); } /** * Reads a message from a given connection (readFully) * @param connection the connection * @return the message * @throws java.io.IOException if something goes wrong */ public static BlaubotMessage readFromBlaubotConnection(IBlaubotConnection connection) throws IOException { byte[] headerBuffer, payloadBuffer; headerBuffer = new byte[BlaubotMessage.FULL_HEADER_LENGTH]; ByteBuffer headerByteBuffer = ByteBuffer.wrap(headerBuffer).order(BlaubotConstants.BYTE_ORDER); return readFromBlaubotConnection(connection, headerByteBuffer, headerBuffer); } /** * Reads a message from a given connection and reuses buffers * @param blaubotConnection the connection * @param headerByteBuffer the byte buffer around header buffer * @param headerBuffer the header buffer * @return the message * @throws IOException if something goes wrong */ public static BlaubotMessage readFromBlaubotConnection(IBlaubotConnection blaubotConnection, ByteBuffer headerByteBuffer, byte[] headerBuffer) throws IOException { byte[] payloadBuffer; headerByteBuffer.clear(); // Partially read version and type, then decide how much bytes we need to read int partialHeaderLength = BlaubotMessage.VERSION_FIELD_LENGTH + BlaubotMessage.TYPE_FIELD_LENGTH; blaubotConnection.readFully(headerBuffer, 0, partialHeaderLength); // assert a compatible message schema byte messageSchemaVersion = headerByteBuffer.get(); if (messageSchemaVersion != BlaubotConstants.MESSAGE_SCHEMA_VERSION) { final String errorMsg = "Error reading BlaubotMessage from connection " + blaubotConnection + ". Either the remote device is using a different Blaubot message schema version (" + messageSchemaVersion + ") or the byte stream got corrupted."; if (Log.logErrorMessages()) { Log.e(LOG_TAG, errorMsg); } // the connection is useless now, so we close it blaubotConnection.disconnect(); // something went wrong, so we throw an io exception as promised throw new IOException(errorMsg); } // pre-create the MessageType to determine, how much header length is left to be read byte typeInfo = headerByteBuffer.get(); BlaubotMessageType messageType = BlaubotMessageType.fromByte(typeInfo); // we are now able to calculate, how many bytes of this message's header we need to read. // we already read some header bytes, now get the rest needed for this message int totalHeaderLength = BlaubotMessage.calculateHeaderLength(messageType); int outstandingHeaderBytes = totalHeaderLength - partialHeaderLength; blaubotConnection.readFully(headerBuffer, partialHeaderLength, outstandingHeaderBytes); // construct the message with all header informations BlaubotMessage message = new BlaubotMessage(); int payloadLength = message.applyBytes(headerBuffer); // check if there is any payload to retrieve if (message.getMessageType().containsPayload()) { if (payloadLength > 0) { // create buffer - Note: intentionally no reuse of buffers - faster because of javas memory management payloadBuffer = new byte[payloadLength]; blaubotConnection.readFully(payloadBuffer, 0, payloadLength); byte[] orderedPayload = new byte[payloadLength]; // byte ordered ByteBuffer.wrap(payloadBuffer).order(BlaubotConstants.BYTE_ORDER).get(orderedPayload); message.setPayload(payloadBuffer); } } // set the originator connection message.setLastOriginatorConnection(blaubotConnection); return message; } public static void main(String[] args) { BlaubotMessage msg = new BlaubotMessage(); msg.setPayload("blabla".getBytes()); final byte[] bytes = msg.toBytes(); final byte[] headerBytes = new byte[calculateHeaderLength(msg.getMessageType())]; ByteBuffer bb = ByteBuffer.wrap(bytes).order(BlaubotConstants.BYTE_ORDER); System.out.println(bytes.length); bb.get(headerBytes); BlaubotMessage msg2 = new BlaubotMessage(); int payloadLength = msg2.applyBytes(headerBytes); byte[] payloadBytes = new byte[payloadLength]; bb.get(payloadBytes); msg2.setPayload(payloadBytes); System.out.println("msg, msg2: " + msg + ", " + msg2); System.out.println("msg == msg2: " + msg.equals(msg2)); BlaubotMessage msg3 = fromByteArray(msg2.toBytes()); System.out.println("msg2, msg3: " + msg2 + ", " + msg3); System.out.println("msg2 == msg3: " + msg2.equals(msg3)); int x = 33000; short x1 = (short)x; System.out.println(x1); int i1 = (int) (x1 & 0xffff); System.out.println(i1); System.out.println("################################"); BlaubotMessage bmsg = new BlaubotMessage(); payloadBytes = new byte[(int) (BlaubotConstants.MAX_PAYLOAD_SIZE * 1.5f)]; bmsg.setPayload(payloadBytes); List<BlaubotMessage> chunks = bmsg.createChunks((short)1); System.out.println(bmsg); System.out.println(chunks); System.out.println(fromChunks(chunks)); } }