package org.limewire.http.reactor; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ByteChannel; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.http.nio.reactor.IOEventDispatch; import org.limewire.nio.channel.ChannelReadObserver; import org.limewire.nio.channel.ChannelWriter; import org.limewire.nio.channel.InterestReadableByteChannel; import org.limewire.nio.channel.InterestWritableByteChannel; import org.limewire.util.BufferUtils; import org.limewire.util.StringUtils; /** * A read/write channel implementation that forwards all requests received from * LimeWire's NIO layer to HttpCore's {@link IOEventDispatch}. */ public class HttpChannel implements ByteChannel, ChannelReadObserver, ChannelWriter { private static final Log LOG = LogFactory.getLog(HttpChannel.class); private final HttpIOSession session; private final IOEventDispatch eventDispatch; private AtomicBoolean closed = new AtomicBoolean(false); private InterestReadableByteChannel readSource; private volatile InterestWritableByteChannel writeSource; private boolean writeInterest; private boolean readInterest; private ByteBuffer methodBuffer; private volatile boolean pendingClose = false; private final HttpBandwidthTracker up, down; /** * Constructs a channel optionally pushing back a string that will be read * first. LimeWire's acceptor eats the first word of a connection to * determine the type. If a non-null value is passed as <code>method</code> * this word can be pushed back into the channel and will be the first data * returned by {@link #read(ByteBuffer)}. * * @param session the IO session * @param eventDispatch the IO event dispatcher that * @param method if != null, the content will be pushed back into the * channel * @param up bandwidth tracker tracking upstream bandwidth * @param down bandwidth tracker tracking downstream bandwidth */ public HttpChannel(HttpIOSession session, IOEventDispatch eventDispatch, String method, HttpBandwidthTracker up, HttpBandwidthTracker down) { if (session == null) { throw new IllegalArgumentException("session must not be null"); } if (eventDispatch == null) { throw new IllegalArgumentException("eventDispatch must not be null"); } this.session = session; this.eventDispatch = eventDispatch; if (method != null) { byte[] asciiBytes = StringUtils.toAsciiBytes(method); this.methodBuffer = ByteBuffer.wrap(asciiBytes); } this.up = up; this.down = down; } public HttpChannel(HttpIOSession session, IOEventDispatch eventDispatch, String method) { this(session, eventDispatch, method, new HttpBandwidthTracker(), new HttpBandwidthTracker()); } /** * Constructs a channel that does not push back a method. * * @see #HttpChannel(HttpIOSession, IOEventDispatch) */ public HttpChannel(HttpIOSession session, IOEventDispatch eventDispatch) { this(session, eventDispatch, null); } public int read(ByteBuffer buffer) throws IOException { if (methodBuffer != null) { int read = BufferUtils.transfer(methodBuffer, buffer, false); if (methodBuffer.hasRemaining()) { downCount(read); return read; } methodBuffer = null; read = read + readSource.read(buffer); downCount(read); return read; } int read = readSource.read(buffer); downCount(read); return read; } public void close() throws IOException { shutdown(); } public void closeWhenBufferedOutputHasBeenFlushed() { InterestWritableByteChannel source = writeSource; if (source != null) { if (!source.hasBufferedOutput()) { session.shutdown(); } else { pendingClose = true; requestWrite(true); } } else { session.shutdown(); } } public boolean isOpen() { return !closed.get(); } public int write(ByteBuffer buffer) throws IOException { int written = writeSource.write(buffer); upCount(written); return written; } public void handleRead() throws IOException { if (!readInterest) { LOG .error("Unexpected call to HttpChannel.handleRead(), turning read interest back off"); readSource.interestRead(false); return; } eventDispatch.inputReady(session); } public void handleIOException(IOException e) { LOG.error("Unexpected exception", e); } /** * Invoked in case of a read timeout or when the socket is closed. */ public void shutdown() { if (!closed.getAndSet(true)) { session.getIoExecutor().execute(new Runnable() { public void run() { eventDispatch.disconnected(session); } }); } } public InterestReadableByteChannel getReadChannel() { return readSource; } public void setReadChannel(InterestReadableByteChannel source) { this.readSource = source; if (this.readSource != null) { this.readSource.interestRead(readInterest); } } public synchronized InterestWritableByteChannel getWriteChannel() { return writeSource; } public synchronized void setWriteChannel(InterestWritableByteChannel channel) { this.writeSource = channel; if (this.writeSource != null) { this.writeSource.interestWrite(this, writeInterest); } } public boolean handleWrite() throws IOException { if (pendingClose) { if (!writeSource.hasBufferedOutput()) { session.shutdown(); } return false; } if (!writeInterest) { // HttpIOSession turns off read interest before switching channels // using AbstractNBSocket#setWriteObserver(), do not delegate to // httpcore in this case return false; } eventDispatch.outputReady(session); return session.hasBufferedOutput(); } public void requestRead(boolean status) { if (pendingClose) return; this.readInterest = status; if (readSource != null) { readSource.interestRead(status); } } public void requestWrite(boolean status) { if (pendingClose) { status = true; } this.writeInterest = status; if (writeSource != null) { writeSource.interestWrite(this, status); } } public boolean isWriteInterest() { return writeInterest; } public boolean isReadInterest() { return readInterest; } private void downCount(int read) { if (read > 0) { down.count(read); } } private void upCount(int written) { if (written > 0) { up.count(written); } } }