package com.limegroup.gnutella.connection; import java.nio.ByteBuffer; import java.nio.channels.Channel; import java.io.IOException; import java.util.zip.Deflater; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.limegroup.gnutella.io.Shutdownable; import com.limegroup.gnutella.io.ChannelWriter; import com.limegroup.gnutella.io.InterestWriteChannel; import com.limegroup.gnutella.io.WriteObserver; /** * A channel that deflates data written to it & writes the deflated * data to another sink. */ public class DeflaterWriter implements ChannelWriter, InterestWriteChannel { private static final Log LOG = LogFactory.getLog(DeflaterWriter.class); /** The channel to write to & interest on. */ private volatile InterestWriteChannel channel; /** The next observer. */ private volatile WriteObserver observer; /** The buffer used for deflating into. */ private ByteBuffer outgoing; /** The buffer used for writing data into. */ private ByteBuffer incoming; /** The deflater to use */ private Deflater deflater; /** The sync level we're on. 0: not sync, 1: NO_COMPRESSION, 2: DEFAULT */ private int sync = 0; /** An empty byte array to reuse. */ private static final byte[] EMPTY = new byte[0]; /** * Constructs a new DeflaterWriter with the given deflater. * You MUST call setWriteChannel prior to handleWrite. */ public DeflaterWriter(Deflater deflater) { this(deflater, null); } /** * Constructs a new DeflaterWriter with the given deflater & channel. */ public DeflaterWriter(Deflater deflater, InterestWriteChannel channel) { this.deflater = deflater; this.incoming = ByteBuffer.allocate(4 * 1024); this.outgoing = ByteBuffer.allocate(512); outgoing.flip(); this.channel = channel; } /** Retreives the sink. */ public InterestWriteChannel getWriteChannel() { return channel; } /** Sets the sink. */ public void setWriteChannel(InterestWriteChannel channel) { this.channel = channel; channel.interest(this, true); } /** * 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; // just always set interest on. it's easiest & it'll be turned off // immediately once we're notified if we don't wanna do anything. // note that if we did want to do it correctly, we'd have to check // incoming.hasRemaining() || outgoing.hasRemaining(), but since // interest can be called in any thread, we'd have to introduce // locking around incoming & outgoing, which just isn't worth it. InterestWriteChannel source = channel; if(source != null) source.interest(this, true); } /** * Writes data to our internal buffer, if there's room. */ public int write(ByteBuffer buffer) throws IOException { int wrote = 0; if(incoming.hasRemaining()) { int remaining = incoming.remaining(); int adding = buffer.remaining(); if(remaining >= adding) { incoming.put(buffer); wrote = adding; } else { int oldLimit = buffer.limit(); int position = buffer.position(); buffer.limit(position + remaining); incoming.put(buffer); buffer.limit(oldLimit); wrote = remaining; } } return wrote; } /** Closes the underlying channel. */ public void close() throws IOException { Channel source = channel; if(source != null) source.close(); } /** Determines if the underlying channel is open. */ public boolean isOpen() { Channel source = channel; return source != null ? source.isOpen() : false; } /** * Writes as much data as possible to the underlying source. * This tries to write any previously unwritten data, then tries * to deflate any new data, then tries to get more data by telling * its interested-observer to write to it. This continues until * there is no more data to be written or the sink is full. */ public boolean handleWrite() throws IOException { InterestWriteChannel source = channel; if(source == null) throw new IllegalStateException("writing with no source."); while(true) { // Step 1: See if there is any pending deflated data to be written. channel.write(outgoing); if(outgoing.hasRemaining()) return true; // there is still deflated data that is pending a write. while(true) { // Step 2: Try and deflate the existing data. int deflated; try { deflated = deflater.deflate(outgoing.array()); } catch(NullPointerException npe) { // stupid deflater not supporting asynchronous ends.. throw (IOException) new IOException().initCause(npe); } if(deflated > 0) { outgoing.position(0).limit(deflated); break; // we managed to deflate some data, try to write it... } // Step 3: Normal deflate didn't work, try to simulate a Z_SYNC_FLUSH // Note that this requires we tried deflating until deflate returned 0 // above. Otherwise, this setInput call would erase prior input. // We must use different levels of syncing because we have to make sure // that we write everything out of deflate after each level is set. // Otherwise compression doesn't work. try { if(sync == 0) { deflater.setInput(EMPTY); deflater.setLevel(Deflater.NO_COMPRESSION); sync = 1; continue; } else if(sync == 1) { deflater.setLevel(Deflater.DEFAULT_COMPRESSION); sync = 2; continue; } } catch(NullPointerException npe) { // stupid deflater not supporting asynchronous ends.. throw (IOException) new IOException().initCause(npe); } // Step 4: If we have no data, tell any interested parties to add some. if(incoming.position() == 0) { WriteObserver interested = observer; if(interested != null) interested.handleWrite(); // If still no data after that, we've written everything we want -- exit. if (incoming.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) { if (observer == null) // no observer? good, we can turn interest off source.interest(this, false); // else, we've got nothing to write, but our observer might. } return false; } } // Step 5: We've got new data to deflate. try { deflater.setInput(incoming.array(), 0, incoming.position()); } catch(NullPointerException npe) { // stupid deflater not supporting asynchronous ends.. throw (IOException) new IOException().initCause(npe); } incoming.clear(); sync = 0; } } } /** Shuts down the last observer. */ public void shutdown() { Shutdownable listener = observer; if(listener != null) listener.shutdown(); } /** Unused, Unsupported */ public void handleIOException(IOException x) { throw new RuntimeException("Unsupported", x); } }