package org.limewire.nio;
import java.io.IOException;
import java.io.InputStream;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import org.limewire.nio.channel.ChannelReadObserver;
import org.limewire.nio.channel.InterestReadableByteChannel;
import org.limewire.nio.channel.InterestScatteringByteChannel;
import org.limewire.nio.observer.Shutdownable;
import org.limewire.nio.timeout.ReadTimeout;
import org.limewire.nio.timeout.SoTimeout;
import org.limewire.util.BufferUtils;
/**
* Manages reading data from the network & piping it to a blocking input stream.
*
* This uses a BufferInputStream that waits on a lock when no data is available.
* The stream exposes a BufferLock that should be notified when data is available
* to be read.
*
* InterestReadChannel is implemented so that future ReadObservers can take over
* reading and use this NIOInputStream as a source channel to read any buffered
* data.
*/
class NIOInputStream implements ChannelReadObserver, InterestScatteringByteChannel, ReadTimeout {
private final Shutdownable shutdownHandler;
private final SoTimeout soTimeoutHandler;
private InterestReadableByteChannel channel;
private BufferInputStream source;
private volatile Object bufferLock;
private ByteBuffer buffer;
private boolean shutdown;
private boolean lastFilled = false;
/**
* Constructs a class that will allow asynchronous read events to be
* piped to an InputStream.
*
* @param soTimeouter Socket object to use to retrieve the soTimeout for
* the input stream timing out while reading.
* @param shutdowner Object to shutdown when the InputStream is closed.
* @param channel Channel to do reads from.
*/
NIOInputStream(SoTimeout soTimeouter, Shutdownable shutdowner, InterestReadableByteChannel channel) {
this.soTimeoutHandler = soTimeouter;
this.shutdownHandler = shutdowner;
this.channel = channel;
}
/**
* Creates the pipes & buffer.
*/
synchronized NIOInputStream init() throws IOException {
if(buffer != null)
throw new IllegalStateException("already init'd!");
if(shutdown)
throw new IOException("Already closed!");
buffer = NIODispatcher.instance().getBufferCache().getHeap();
source = new BufferInputStream(buffer, this, shutdownHandler, channel, this);
bufferLock = source.getBufferLock();
return this;
}
/**
* Reads from this' channel (which is the temporary ByteBuffer,
* not the SocketChannel) into the given buffer.
*/
public int read(ByteBuffer toBuffer) {
return BufferUtils.transfer(buffer, toBuffer);
}
public long read(ByteBuffer[] dst, int offset, int length) {
return BufferUtils.transfer(buffer,dst, offset, length, true);
}
public long read(ByteBuffer [] dst) {
return read(dst,0, dst.length);
}
/**
* Retrieves the InputStream to read from.
*/
synchronized InputStream getInputStream() throws IOException {
if(buffer == null)
init();
return source;
}
/**
* Notification that a read can happen on the SocketChannel.
*/
public void handleRead() throws IOException {
// This is a hack to allow TLSNIOSockets to handshake
// without init'ing the whole InputStream.
if(bufferLock == null) {
int read = channel.read(BufferUtils.getEmptyBuffer());
// We're trying to read into an empty buffer -- not any real data,
// so if we read EOF, that means this connection is closed.
// TODO: Make sure that this is correct! Maybe a better way is to turn
// read interest off, or do nothing and leave the responsibility to the
// channel we're reading from? Read interest shouldn't even be on unless
// that channel requested it, or this was initialized...
if(read == -1)
throw new ClosedChannelException();
} else {
synchronized(bufferLock) {
int read = 0;
// read everything we can.
while(buffer.hasRemaining() && (read = channel.read(buffer)) > 0);
if(read == -1)
source.finished();
// If there's data in the buffer, we're interested in writing.
if(buffer.position() > 0 || read == -1)
bufferLock.notify();
// if there's room in the buffer, we're interested in more reading ...
// if not, we're not interested in more reading, but we should remember
// that we just filled it up, so if someone notifies us they want to read,
// we can immediately trigger a read.
if(!buffer.hasRemaining()) {
lastFilled = true;
channel.interestRead(false);
} else {
lastFilled = false;
}
// if we read EOF, no more stuff to read.
if(read == -1) {
channel.interestRead(false);
}
}
}
}
/** Notification from BufferInputStream that we want to try to read. */
void readHappening() {
synchronized(bufferLock) {
if(lastFilled) {
NIODispatcher.instance().getScheduledExecutorService().execute(new Runnable() {
public void run() {
try {
handleRead();
} catch(IOException iox) {
channel.interestRead(true);
shutdownHandler.shutdown();
}
}
});
}
}
}
/**
* Shuts down all internal channels.
* The SocketChannel should be shut by NIOSocket.
*/
public synchronized void shutdown() {
if(shutdown)
return;
shutdown = true;
if(source != null)
source.shutdown();
if(buffer != null) {
NIODispatcher.instance().getScheduledExecutorService().execute(new Runnable() {
public void run() {
NIODispatcher.instance().getBufferCache().release(buffer);
}
});
}
}
/** Unused */
public void handleIOException(IOException iox) {
throw new RuntimeException("unsupported operation", iox);
}
/**
* Does nothing, since this is implemented for ReadableByteChannel,
* and that is used for reading from the temporary buffer --
* there is no buffer to close in this case.
*/
public void close() throws IOException {
}
/**
* Always returns true, since this is implemented for ReadableByteChannel,
* and the Buffer is always available for reading.
*/
public boolean isOpen() {
return true;
}
/**
* Does nothing.
*/
public void interestRead(boolean status) {
channel.interestRead(status);
}
public InterestReadableByteChannel getReadChannel() {
return channel;
}
public void setReadChannel(InterestReadableByteChannel newChannel) {
synchronized(bufferLock) {
this.channel = newChannel;
source.setReadChannel(newChannel);
}
}
public long getReadTimeout() {
try {
return soTimeoutHandler.getSoTimeout();
} catch(SocketException se) {
return -1;
}
}
}