package com.limegroup.gnutella.connection;
import java.nio.ByteBuffer;
import java.io.IOException;
import com.limegroup.gnutella.messages.Message;
import com.limegroup.gnutella.io.ChannelWriter;
import com.limegroup.gnutella.io.InterestWriteChannel;
import com.limegroup.gnutella.util.BufferByteArrayOutputStream;
/**
* Writes messages using non-blocking I/O.
*
* Messages are queued via send(Message). When a write can happen, this is notified
* via handleWrite(), which will pull any non-expired messages from the queue, writing
* them. ConnectionStats are kept updated for all should-be-sent messages as well
* as dropped messages (from expiry or buffer overflow), and the SentMessageHandler
* is notified of all succesfully sent messages.
*/
public class MessageWriter implements ChannelWriter, OutputRunner {
/**
* The queue that holds the messages to write. The queue internally can
* expire messages which are old, or purge messages if many become buffered.
*/
private final MessageQueue queue;
/**
* The OutputStream that messages are written to. For efficieny, the stream
* internally uses a ByteBuffer and we get the buffer directly to write to
* our sink channel. This prevents recreation of many byte[]s.
*/
private final BufferByteArrayOutputStream out;
/**
* The statistics object that keeps track of how many messages were sent,
* how many tried to be sent, how many dropped, etc...
*/
private final ConnectionStats stats;
/**
* A callback for handlers who wish to process messages we succesfully sent.
*/
private final SentMessageHandler sendHandler;
/**
* The sink channel we write to & interest ourselves on.
*/
private InterestWriteChannel channel;
/**
* Whether or not we've flipped the data. This is an optimization so
* we don't have to compact (which does array copies) as much.
*/
private boolean flipped = false;
/**
* Whether or not we've shut down. If we have, stop accepting incoming
* messages & stop writing them.
*/
private boolean shutdown = false;
/**
* Constructs a new MessageWriter with the given stats, queue & sendHandler.
* You MUST call setWriteChannel prior to handleWrite.
*/
public MessageWriter(ConnectionStats stats, MessageQueue queue, SentMessageHandler sendHandler) {
this(stats, queue, sendHandler, null);
}
/**
* Constructs a new MessageWriter that writes to the given sink.
*/
public MessageWriter(ConnectionStats stats, MessageQueue queue,
SentMessageHandler sendHandler, InterestWriteChannel sink) {
this.stats = stats;
this.queue = queue;
this.sendHandler = sendHandler;
this.channel = sink;
out = new BufferByteArrayOutputStream();
}
/** The channel we're writing to. */
public synchronized InterestWriteChannel getWriteChannel() {
return channel;
}
/** The channel we're writing to. */
public synchronized void setWriteChannel(InterestWriteChannel channel) {
this.channel = channel;
channel.interest(this, true);
}
/**
* Adds a new message to the queue.
*
* Any messages that were dropped because this was added are calculated
* into the ConnectionStats. The sink channel is notified that we're
* interested in writing.
*/
public synchronized void send(Message m) {
if(shutdown)
return;
stats.addSent();
queue.add(m);
int dropped = queue.resetDropped();
stats.addSentDropped(dropped);
if(channel != null)
channel.interest(this, true);
}
/**
* Writes as many messages as possible to the sink.
*/
public synchronized boolean handleWrite() throws IOException {
if(channel == null)
throw new IllegalStateException("writing with no source.");
// first try to write any leftover data.
if(writeRemaining()) //still have data to send.
return true;
// then loop through and write to the channel till we can't anymore.
while(true) {
Message m = queue.removeNext();
int dropped = queue.resetDropped();
stats.addSentDropped(dropped);
// no more messages to send.
if(m == null) {
channel.interest(this, false);
return false;
}
m.writeQuickly(out);
sendHandler.processSentMessage(m);
if(writeRemaining()) // still have data to send.
return true;
}
}
/**
* Writes any data that was left in the buffer. As an optimization,
* we do not recompact the buffer if more data can be written. Instead,
* we just wait till we can completely write the buffer & then clear it
* entirely. This prevents the need to compact the buffer.
*/
private boolean writeRemaining() throws IOException {
if(shutdown)
throw new IOException("connection shut down.");
// if there was data left in the stream, try writing it.
ByteBuffer buffer = out.buffer();
// write any data that was leftover in the buffer.
if(flipped || buffer.position() > 0) {
// prepare for writing...
if(!flipped) {
buffer.flip();
flipped = true;
}
// write.
channel.write(buffer);
// if we couldn't write everything, exit.
if(buffer.hasRemaining())
return true; // still have data to write.
flipped = false;
buffer.clear();
}
return false; // wrote everything.
}
/**
* Ignored -- we'll shut down from reading.
*
* THIS MUST NOT CLOSE THE CONNECTION. (Connection.close calls this.)
*/
public synchronized void shutdown() {
shutdown = true;
}
/** Unused, Unsupported */
public void handleIOException(IOException x) {
throw new RuntimeException("Unsupported", x);
}
}