package eu.hgross.blaubot.messaging;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import eu.hgross.blaubot.core.BlaubotConstants;
import eu.hgross.blaubot.core.IActionListener;
import eu.hgross.blaubot.core.IBlaubotConnection;
import eu.hgross.blaubot.util.Log;
/**
* A message receiver handles incoming data streams from an IBlaubotConnection.
* It converts the byte stream into BlaubotMessage instances and notifies it's
* listeners when a message was completely read.
*
* Message listeners can be activated and deactivated.
*/
public class BlaubotMessageReceiver {
/*
* TODO the current deactivate/activate implementation will stack new blocking threads on each
* deactivate/activate cycle if no message is received in between.
* Shouldn't be a problem in almost every use case but is still not pretty.
*/
private static final String LOG_TAG = "BlaubotMessageReceiver";
/**
* Locks the access to the chunked message mappings
*/
private final Object chunkLock = new Object();
private final Map<Short, Boolean> receivedLastChunkMapping;
private final Map<Short, List<BlaubotMessage>> receivedChunks;
private final IBlaubotConnection blaubotConnection;
private final CopyOnWriteArrayList<IBlaubotMessageListener> messageListeners;
private volatile MessageReceivingThread messageReceivingThread;
private boolean forwardChunks = false;
/**
* Monitor to avoid execution of two MessageReceivingThreads at the same time on this instance.
* (could happen on fast activate/deactivate calls)
*/
private final Object receiverMonitor = new Object();
private long receivedMessages = 0;
private long receivedPayloadBytes = 0;
private long receivedChunkMessages = 0;
public BlaubotMessageReceiver(IBlaubotConnection blaubotConnection) {
this.blaubotConnection = blaubotConnection;
this.messageListeners = new CopyOnWriteArrayList<>();
this.receivedLastChunkMapping = new HashMap<>();
this.receivedChunks = new HashMap<>();
}
public void addMessageListener(IBlaubotMessageListener messageListener) {
this.messageListeners.add(messageListener);
}
public void removeMessageListener(IBlaubotMessageListener messageListener) {
this.messageListeners.remove(messageListener);
}
/**
* The blaubot connection used to receive messages.
*
* @return the connection object used to receive messages
*/
public IBlaubotConnection getBlaubotConnection() {
return blaubotConnection;
}
/**
* Activates the message receiver (reading from the connection).
* If the receiver was already started, it will start a new consumer-thread that will sequentially
* take over the work from the previous thread.
*/
public void activate() {
MessageReceivingThread mrt = new MessageReceivingThread();
mrt.setName("msg-receiver-" + blaubotConnection.getRemoteDevice().getUniqueDeviceID() + ", " + mrt.getId());
synchronized (activationLock) {
// we don't interrupt a possibly already running receive thread here, see comment in BlaubotMessageManager
// deactivate() method.
messageReceivingThread = mrt;
}
mrt.start();
}
/**
* Deactivates the message receiver (completes current message readings, if any and then shuts down)
*
* @param actionListener callback to be informed when the receiver was closed (thread finished), can be null
*/
public void deactivate(final IActionListener actionListener) {
final MessageReceivingThread mrt;
synchronized (activationLock) {
mrt = messageReceivingThread;
messageReceivingThread = null;
// replacing the consumer thread is sufficient, we call the listener
if (actionListener != null) actionListener.onFinished();
}
if (mrt != null) {
//mrt.interrupt(); // we don't interrupt the thread, because we could end up in a out of sync bytestream this way
}
// clear chunk mappings
synchronized (chunkLock) {
receivedLastChunkMapping.clear();
}
}
/**
* Monitor for activation/deactivation calls.
*/
private Object activationLock = new Object();
/**
* @return number of received messages so far (including chunks)
*/
public long getReceivedMessages() {
return receivedMessages;
}
/**
* @return number of received payload bytes so far (including chunks)
*/
public long getReceivedPayloadBytes() {
return receivedPayloadBytes;
}
/**
* If set to true, the receiver will not collect chunks and dispatch the whole (pieced together)
* message to the listeners, but simply forward the chunks to the listeners.
* Default: false
*
* @param forwardChunks iff true, chunk forwarding is active
*/
public void setForwardChunks(boolean forwardChunks) {
this.forwardChunks = forwardChunks;
}
/**
* @return number of received chunk messages (chunks themselves)
*/
public long getReceivedChunkMessages() {
return receivedChunkMessages;
}
/**
* Called by the receiving thread if a chunk message was received.
*
* @param chunkMessage the message
*/
private void onChunkMessageReceived(BlaubotMessage chunkMessage) {
/*
* Chunked messages are processed as follows:
* - we store a mapping chunkId -> List of received messages regarding this chunk id
* - additionally we store a mapping chunkId -> Boolean which indicates, that we received the last chunk, which is determined by receiving a chunked message with the chunkId, that has less than BlaubotConstants.MAX_PAYLOAD_SIZE bytes as payload
* - if we receive a chunked message, we check if the last message was received.
* - if no: do nothing
* - if yes: check if we have all the chunks (all numbers from 0 to chunkNo of the last message)
* - if no: do nothin
* - if yes:
* - built the resulting message from the chunked messages and notify our listeners
* - clear the mapping
*/
final short chunkId = chunkMessage.getChunkId();
List<BlaubotMessage> completeListOfChunks = null;
synchronized (chunkLock) {
final boolean contained = receivedChunks.containsKey(chunkId);
if (!contained) {
receivedChunks.put(chunkId, new ArrayList<BlaubotMessage>());
}
final List<BlaubotMessage> messageList = receivedChunks.get(chunkId);
messageList.add(chunkMessage);
boolean isLastChunkMessage = chunkMessage.getPayload().length < BlaubotConstants.MAX_PAYLOAD_SIZE;
if (isLastChunkMessage) {
receivedLastChunkMapping.put(chunkId, true);
}
if (isLastChunkMessage || receivedLastChunkMapping.get(chunkId) != null) {
int sum = 0;
int maxChunkNo = -1;
for (BlaubotMessage chunk : messageList) {
final int chunkNo = chunk.getChunkNo() & 0xffff; // unsigned shorts
sum += chunkNo;
if (chunkNo > maxChunkNo) {
maxChunkNo = chunkNo;
}
}
// check if we have all chunkNo n * (n+1)/2 == sum (Gauss)
boolean weHaveAllChunks = (maxChunkNo * (maxChunkNo + 1) / 2) == sum;
if (weHaveAllChunks) {
// fill the completeListOfChunks variable and forget about everything
completeListOfChunks = messageList;
receivedLastChunkMapping.remove(chunkId);
receivedChunks.remove(chunkId);
}
}
}
if (completeListOfChunks != null) {
BlaubotMessage msg = BlaubotMessage.fromChunks(completeListOfChunks);
notifyListeners(msg);
}
}
/**
* Consumes the connection's byte stream and deserializes BlaubotMessages from it.
*/
class MessageReceivingThread extends Thread {
/**
* Milliseconds to wait if an io exception happens on read to not block the whole system in this cases and
* let the listeners do their onConnectionClosed magic a little faster.
*/
private static final long SLEEP_TIME_ON_IO_FAILURE = 350;
private final String LOG_TAG = "MessageReceivingThread";
@Override
public void run() {
synchronized (receiverMonitor) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Started receiver for connection: " + blaubotConnection);
}
byte[] headerBuffer;
int headerLength = BlaubotMessage.FULL_HEADER_LENGTH;
headerBuffer = new byte[headerLength];
ByteBuffer headerByteBuffer = ByteBuffer.wrap(headerBuffer).order(BlaubotConstants.BYTE_ORDER);
// Keep listening to the InputStream until an exception occurs
while (messageReceivingThread == this && !isInterrupted()) {
// Read from the InputStream
try {
BlaubotMessage message = BlaubotMessage.readFromBlaubotConnection(blaubotConnection, headerByteBuffer, headerBuffer);
// maintain stats
receivedMessages += 1;
receivedPayloadBytes += message.getPayload().length;
// check if we need to process a chunked message
boolean isChunk = message.getMessageType().isChunk();
if (isChunk) {
receivedChunkMessages += 1;
}
if (!forwardChunks && isChunk) {
onChunkMessageReceived(message);
} else {
// notify all listeners
notifyListeners(message);
}
} catch (IOException e) {
// on connection failure the message receiver will transition to an inactive state
// failed connection are handled by the connection manager and this receiver will
// get a deactivate() call
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "IOException (" + e.getMessage() + ") while reading from connection: " + blaubotConnection);
}
try {
Thread.sleep(SLEEP_TIME_ON_IO_FAILURE);
} catch (InterruptedException e1) {
break; // got interrupted, we exit
}
}
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Stopped receiver for connection: " + blaubotConnection);
}
}
}
}
/**
* Notifies all attached message listeners about a newly received message
*
* @param message the message to be dispatched to the listening parties.
*/
private void notifyListeners(BlaubotMessage message) {
// notify listeners about new message
for (IBlaubotMessageListener listener : messageListeners) {
listener.onMessage(message);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BlaubotMessageReceiver receiver = (BlaubotMessageReceiver) o;
if (blaubotConnection != null ? !blaubotConnection.equals(receiver.blaubotConnection) : receiver.blaubotConnection != null)
return false;
return true;
}
@Override
public int hashCode() {
return blaubotConnection != null ? blaubotConnection.hashCode() : 0;
}
@Override
public String toString() {
return "BlaubotMessageReceiver{" +
"blaubotConnection=" + blaubotConnection +
'}';
}
}