package com.limegroup.gnutella.io; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.limegroup.gnutella.io.ChannelWriter; import com.limegroup.gnutella.io.InterestWriteChannel; import com.limegroup.gnutella.io.Shutdownable; import com.limegroup.gnutella.io.WriteObserver; /** * A Writer that stores data within a buffer and writes it out after some delay, * or if the buffer fills up. */ public class DelayedBufferWriter implements ChannelWriter, InterestWriteChannel { private static final Log LOG = LogFactory.getLog(DelayedBufferWriter.class); /** The delay time to use before forcing a flush */ private final static int MAX_TIME = 200; /** The channel to write to & interest on. */ private volatile InterestWriteChannel sink; /** The next observer. */ private volatile WriteObserver observer; /** * The buffer where we store delayed data. Most of the time it will be * written to, so we keep it in compacted state by default. */ private final ByteBuffer buf; /** The last time we flushed, so we don't flush again too soon. */ private long lastFlushTime; /** Constructs a new DelayedBufferWriter whose buffer is the given size. */ public DelayedBufferWriter(int size) { buf = ByteBuffer.allocate(size); } /** * Used by an observer to interest themselves in when something can * write to this. * * We must synchronize interest setting so that in the writing loop * we can ensure that interest isn't turned on between the time we * get the interested party, check for null, and turn off interest * (if it was null). */ public synchronized void interest(WriteObserver observer, boolean status) { this.observer = status ? observer : null; InterestWriteChannel source = sink; if(source != null) source.interest(this, true); } /** Closes the underlying channel. */ public void close() throws IOException { Channel chan = sink; if(chan != null) chan.close(); } /** Determines if the underlying channel is open. */ public boolean isOpen() { Channel chan = sink; return chan != null ? chan.isOpen() : false; } /** Retreives the sink. */ public InterestWriteChannel getWriteChannel() { return sink; } /** Sets the sink. */ public void setWriteChannel(InterestWriteChannel newChannel) { sink = newChannel; newChannel.interest(this,true); } /** Unused, Unsupported */ public void handleIOException(IOException iox) { throw new RuntimeException("Unsupported", iox); } /** Shuts down the last observer. */ public void shutdown() { Shutdownable listener = observer; if(listener != null) listener.shutdown(); } /** * Writes data into the internal buffer. * * If the internal buffer gets filled, it tries flushing some data out * to the sink. If some data can be flushed, this continues filling the * internal buffer. This continues forever until either the incoming * buffer is emptied or no data can be written to the sink. */ public int write(ByteBuffer buffer) throws IOException { int originalPos = buffer.position(); while(buffer.hasRemaining()) { if(buf.hasRemaining()) { int remaining = buf.remaining(); int adding = buffer.remaining(); if(remaining >= adding) { buf.put(buffer); } else { int oldLimit = buffer.limit(); int position = buffer.position(); buffer.limit(position + remaining); buf.put(buffer); buffer.limit(oldLimit); } } else { flush(System.currentTimeMillis()); if (!buf.hasRemaining()) break; } } return buffer.position() - originalPos; } /** * Notification that a write can happen. The observer is informed of the event * in order to try filling our internal buffer. If our last flush was too long * ago, we force a flush to occur. */ public boolean handleWrite() throws IOException { WriteObserver upper = observer; if (upper != null) upper.handleWrite(); long now = System.currentTimeMillis(); if (lastFlushTime == 0) lastFlushTime = now; if (now - lastFlushTime > MAX_TIME) flush(now); // If still no data after that, we've written everything we want -- exit. if (buf.position() == 0) { // We have nothing left to write, however, it is possible // that between the above check for interested.handleWrite & here, // we got pre-empted and another thread turned on interest. synchronized(this) { upper = observer; if (upper == null) sink.interest(this,false); } return false; } return true; } /** * Writes data to the underlying channel, remembering the time we did this * if anything was written. THIS DOES NOT BLOCK, NOR DOES IT ENFORCE * THAT ALL DATA WILL BE WRITTEN, UNLIKE OutputStream.flush(). */ private void flush(long now) throws IOException { buf.flip(); InterestWriteChannel chan = sink; chan.write(buf); // if we wrote anything, consider this flushed if (buf.position() > 0) { lastFlushTime = now; if (buf.hasRemaining()) buf.compact(); else buf.clear(); } else { buf.position(buf.limit()).limit(buf.capacity()); } } }