/*
* Copyright 2014-2016 CyberVision, Inc.
*
* 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.kaaproject.kaa.common.channels.protocols.kaatcp.messages;
import org.kaaproject.kaa.common.channels.protocols.kaatcp.KaaTcpProtocolException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
/**
* Basic Mqtt message.
* Fixed header format
* bit 7 6 5 4 3 2 1 0
* byte 1 Message Type Dup flag QoS level RETAIN
* byte 2 Remaining length
*
* @author Andrey Panasenko
*/
public abstract class MqttFrame {
public static final Logger LOG = LoggerFactory //NOSONAR
.getLogger(MqttFrame.class);
public static final int MQTT_FIXED_HEADER_LEGTH = 2;
protected ByteBuffer buffer;
protected boolean frameDecodeComplete = false;
protected int remainingLength = 0;
protected int multiplier = 1;
protected FrameParsingState currentState = FrameParsingState.NONE;
/*
* If adding any filed, don't forget to update MqttFrame(MqttFrame old)
* to clone all fileds.
*/
private MessageType messageType;
protected MqttFrame() {
}
protected MqttFrame(MqttFrame old) {
this.messageType = old.getMessageType();
this.buffer = old.getBuffer();
this.frameDecodeComplete = old.frameDecodeComplete;
this.remainingLength = old.remainingLength;
this.multiplier = old.multiplier;
this.currentState = old.currentState;
}
public MessageType getMessageType() {
return messageType;
}
protected void setMessageType(MessageType messageType) {
this.messageType = messageType;
}
/**
* Return mqtt Frame.
*
* @return ByteBuffer mqtt frame
*/
public ByteBuffer getFrame() {
if (buffer == null) {
int remainingLegth = getRemainingLegth();
byte[] kaaTcpHeader = new byte[6];
int headerSize = fillFixedHeader(remainingLegth, kaaTcpHeader);
int remainingSize = remainingLegth + headerSize;
LOG.trace("Allocating buffer size = {}", remainingSize);
buffer = ByteBuffer.allocate(remainingSize);
buffer.put(kaaTcpHeader, 0, headerSize);
pack();
buffer.position(0);
}
return buffer;
}
/**
* Pack message into mqtt frame.
*/
protected abstract void pack();
/**
* Return remaining length of mqtt frame, necessary for ByteBuffer size calculation.
*
* @return remaining length of mqtt frame
*/
protected int getRemainingLegth() {
return remainingLength;
}
/**
* Decode message from mqttFrame ByteBuffer.
*
* @throws KaaTcpProtocolException the kaa tcp protocol exception
*/
protected abstract void decode() throws KaaTcpProtocolException;
/**
* Check if this Mqtt frame should be last frame on connection and connection should be closed.
*
* @return boolean 'true' if connection should be closed after frame transmition.
*/
public abstract boolean isNeedCloseConnection();
/**
* Fill mqtt frame fixed header.
*
* @param remainingLegth the remaining legth
* @param dst the dst
* @return number of packet bytes
*/
private int fillFixedHeader(int remainingLegth, byte[] dst) {
int size = 1;
byte byte1 = getMessageType().getType();
byte1 = (byte) (byte1 & (byte) 0x0F);
byte1 = (byte) (byte1 << 4);
dst[0] = byte1;
byte digit = 0x00;
do {
digit = (byte) (remainingLegth % 0x00000080);
remainingLegth /= 0x00000080;
// if there are more digits to encode, set the top bit of this digit
if (remainingLegth > 0) {
digit = (byte) (digit | 0x80);
}
dst[size] = digit;
++size;
}
while (remainingLegth > 0);
return size;
}
protected ByteBuffer getBuffer() {
return buffer;
}
private void onFrameDone() throws KaaTcpProtocolException {
LOG.trace("Frame ({}): payload processed", getMessageType());
if (buffer != null) {
buffer.position(0);
}
decode();
frameDecodeComplete = true;
}
private void processByte(byte value) throws KaaTcpProtocolException {
if (currentState.equals(FrameParsingState.PROCESSING_LENGTH)) {
remainingLength += ((value & 0xFF) & 127) * multiplier;
multiplier *= 128;
if (((value & 0xFF) & 128) == 0) {
LOG.trace("Frame ({}): payload length = {}", getMessageType(), remainingLength);
if (remainingLength != 0) {
buffer = ByteBuffer.allocate(remainingLength);
currentState = FrameParsingState.PROCESSING_PAYLOAD;
} else {
onFrameDone();
}
}
}
}
/**
* Push bytes of frame.
*
* @param bytes the bytes array
* @param position the position in buffer
* @return int used bytes from buffer
* @throws KaaTcpProtocolException the kaa tcp protocol exception
*/
public int push(byte[] bytes, int position) throws KaaTcpProtocolException {
int pos = position;
if (currentState.equals(FrameParsingState.NONE)) {
remainingLength = 0;
currentState = FrameParsingState.PROCESSING_LENGTH;
}
while (pos < bytes.length && !frameDecodeComplete) {
if (currentState.equals(FrameParsingState.PROCESSING_PAYLOAD)) {
int bytesToCopy = (remainingLength > bytes.length - pos) ? bytes.length - pos :
remainingLength;
buffer.put(bytes, pos, bytesToCopy);
pos += bytesToCopy;
remainingLength -= bytesToCopy;
LOG.trace("Frame ({}): copied {} bytes of payload. {} bytes left", getMessageType(),
bytesToCopy, remainingLength);
if (remainingLength == 0) {
onFrameDone();
}
} else {
processByte(bytes[pos]);
++pos;
}
}
return pos - position;
}
/**
* Test if Mqtt frame decode complete.
*
* @return boolean 'true' if decode complete
*/
public boolean decodeComplete() {
return frameDecodeComplete;
}
/**
* Used in case if Frame Class should be changed during frame decode,
* Used for migrate from KaaSync() general frame to specific classes like Sync, Bootstrap.
* Default implementation is to return this.
*
* @return new MqttFrame as specific class.
* @throws KaaTcpProtocolException the kaa tcp protocol exception
*/
public MqttFrame upgradeFrame() throws KaaTcpProtocolException {
return this;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("MqttFrame [messageType=");
builder.append(messageType);
builder.append(", currentState=");
builder.append(currentState);
builder.append("]");
return builder.toString();
}
protected enum FrameParsingState {
NONE,
PROCESSING_LENGTH,
PROCESSING_PAYLOAD,
}
}