package com.tryge.xocotl.io;
import com.tryge.xocotl.util.internal.FutureImpl;
import com.tryge.xocotl.util.Preconditions;
import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* The StreamedDuplexChannel sends and receives messages from a stream that
* provides an input stream and an output stream. All messages are sent
* asynchronously therefore the send methods return immediately. However, if
* you need to wait for a response you can wait using the returned future.
* If you don't care about a response you can just sendAndForget the message,
* however, keep in mind that you won't have any indication that the message
* has really been sent.
*
* @author michael.zehender@me.com
*/
class StreamedDuplexChannel implements DuplexChannel {
static final Message POISON = new PoisonMessage();
private final AtomicReference<ChannelListener> channelListener = new AtomicReference<ChannelListener>(new NopChannelListener());
private final AtomicReference<MessageListener> messageListener = new AtomicReference<MessageListener>(new NopChannelListener());
private final ThreadFactory factory;
private final Responder responder;
private final MessageDecoder decoder;
private final StreamSource source;
private final int receiveBufferSize;
private final LinkedBlockingQueue<Message> messages = new LinkedBlockingQueue<Message>();
private volatile Stream stream;
private volatile Thread readerThread;
private volatile Thread writerThread;
private volatile boolean closing;
private volatile boolean closed;
private volatile boolean open;
StreamedDuplexChannel(ThreadFactory factory, Responder responder, MessageDecoder decoder, StreamSource source, int receiveBufferSize) {
Preconditions.notNull(factory, "thread factory mustn't be null.");
Preconditions.notNull(factory, "responder mustn't be null");
Preconditions.notNull(decoder, "message decoder mustn't be null");
Preconditions.notNull(source, "stream source mustn't be null.");
Preconditions.checkPositive(receiveBufferSize, "receive buffer size must be greater than 0");
this.factory = factory;
this.responder = responder;
this.decoder = decoder;
this.source = source;
this.receiveBufferSize = receiveBufferSize;
}
@Override
public void setChannelListener(ChannelListener listener) {
Preconditions.notNull(listener, "channel listener mustn't be null - you could use NopListener instead.");
this.channelListener.set(listener);
}
@Override
public void setMessageListener(MessageListener listener) {
Preconditions.notNull(listener, "message listener mustn't be null - you could use NopListener instead.");
this.messageListener.set(listener);
}
@Override
public void close() throws IOException {
if (!open || closed) {
// silently ignore close on closed channel
return;
}
closing = true;
readerThread.interrupt();
writerThread.interrupt();
try {
stream.close();
} finally {
try {
sendAndForget(POISON);
if (Thread.currentThread() != readerThread) {
readerThread.join();
}
if (Thread.currentThread() != writerThread) {
writerThread.join();
}
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
} finally {
stream = null;
closed = true;
channelListener.get().onClose(this);
responder.destroyed(this);
}
}
}
private void closeSilently() {
try {
if (!closing) {
close();
}
} catch (Exception e) {
// ignore
}
}
@Override
public void open() throws IOException {
Preconditions.checkState(stream == null, "channel is already open");
Preconditions.checkState(!closed, "channel has already been closed");
stream = source.open();
readerThread = factory.newThread(reader);
writerThread = factory.newThread(writer);
open = true;
readerThread.start();
writerThread.start();
}
@Override
public boolean isClosed() {
return stream == null;
}
@Override
public boolean isOpen() {
return stream != null;
}
@Override
public Future<Message> send(Message msg) {
Preconditions.notNull(msg, "can't send null message");
Preconditions.checkState(stream != null, "channel is closed");
ResponderMessage message = responder.register(this, msg);
FutureImpl<Message> future = message.future();
if (!messages.offer(message)) {
future.failed(new IllegalStateException("could not enqueue message"));
}
return future;
}
@Override
public Future<Void> sendEx(Message msg) {
Preconditions.notNull(msg, "can't send null message");
Preconditions.checkState(stream != null, "channel is closed");
SuccessMessage message = new SuccessMessage(msg);
FutureImpl<Void> future = message.future();
if(!messages.offer(message)) {
future.failed(new IllegalStateException("could not enqueue message"));
}
return future;
}
@Override
public void sendAndForget(Message msg) {
Preconditions.notNull(msg, "can't send null message");
Preconditions.checkState(stream != null, "channel is closed");
try {
messages.add(msg);
} catch(Exception e) {
// couldn't queue message but caller doesn't care.
}
}
final Runnable reader = new Reader();
class Reader implements Runnable {
@Override
public void run() {
try {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(receiveBufferSize);
while(!Thread.currentThread().isInterrupted()) {
byte[] buffer = new byte[Math.min(128, receiveBufferSize/2)];
try {
int read = stream.getInputStream().read(buffer);
if (read == -1) {
break;
}
byteBuffer.put(buffer, 0, read);
// sets the limit to the current position and then the position to zero
byteBuffer.flip();
byteBuffer.mark();
for(Message msg = decoder.decode(byteBuffer); msg != null; msg = decoder.decode(byteBuffer)) {
if (msg.isResponse()) {
responder.responseReceived(StreamedDuplexChannel.this, msg);
} else {
messageListener.get().onMessage(msg);
}
byteBuffer.mark();
}
// reset to the position of the last mark (end of last message)
byteBuffer.reset();
if (byteBuffer.position() != 0) {
byteBuffer.compact();
} else {
byteBuffer.position(byteBuffer.limit());
}
// increase limit to capacity
byteBuffer.limit(byteBuffer.capacity());
} catch (IOException e) {
break;
} catch (Throwable th) {
// something went awfully wrong here
th.printStackTrace();
break;
}
}
} finally {
closeSilently();
}
}
}
final Runnable writer = new Writer();
class Writer implements Runnable {
@Override
public void run() {
try {
while(!Thread.currentThread().isInterrupted()) {
Stream stream = StreamedDuplexChannel.this.stream;
if (stream == null) {
return;
}
try {
Message message = messages.poll(1, TimeUnit.SECONDS);
if (message != null) {
message.writeTo(stream.getOutputStream());
stream.getOutputStream().flush();
}
} catch (InterruptedException e) {
break;
} catch(EOFException eof) {
break;
} catch (IOException e) {
break;
} catch (Throwable th) {
// something went awfully wrong here
th.printStackTrace();
break;
}
}
} finally {
closeSilently();
}
}
}
}