package org.limewire.nio;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.nio.channel.ChannelReadObserver;
import org.limewire.nio.channel.ChannelReader;
import org.limewire.nio.channel.ChannelWriter;
import org.limewire.nio.channel.InterestReadableByteChannel;
import org.limewire.nio.channel.InterestWritableByteChannel;
import org.limewire.nio.channel.NIOMultiplexor;
import org.limewire.nio.observer.ConnectObserver;
import org.limewire.nio.observer.ReadObserver;
import org.limewire.nio.observer.ReadWriteObserver;
import org.limewire.nio.observer.Shutdownable;
import org.limewire.nio.observer.WriteObserver;
import org.limewire.nio.timeout.ReadTimeout;
import org.limewire.nio.timeout.SoTimeout;
/**
* Implements all common functionality that a non-blocking socket must contain.
* Specifically, <code>AbstractNBSocket</code> handles
* the multiplexing aspect of handing off reading, writing and connecting to
* other Observers (<code>org.limewire.nio.observer</code>).
* <p>
* Additionally, <code>AbstractNBSocket</code> traverses the chain of readers
* and writers to read leftover data and ensure remaining data is written.
* <p>
* <code>AbstractNBSocket</code> also exposes a common blocking input and output
* stream.
*/
public abstract class AbstractNBSocket extends NBSocket implements ConnectObserver, ReadWriteObserver,
NIOMultiplexor, ReadTimeout, SoTimeout{
private static final Log LOG = LogFactory.getLog(AbstractNBSocket.class);
/** Lock for shutting down. */
private final Object LOCK = new Object();
/** The reader. */
private volatile ReadObserver reader;
/** The writer. */
private volatile WriteObserver writer;
/** The NIOOutputStream object, if we're using blocking writing. */
private volatile NIOOutputStream nioOutputStream;
/** The connecter. */
private volatile ConnectObserver connecter;
/** An observer for being shutdown. */
private volatile Shutdownable shutdownObserver;
/** Whether or not we've shutdown the socket. */
private boolean shutdown = false;
/**
* Retrieves the channel which should be used as the base channel
* for all reading operations.
*/
protected abstract InterestReadableByteChannel getBaseReadChannel();
/**
* Retrieves the channel which should be used as the base channel
* for all writing operations.
* <p>
* If the base write channel is chained (that is, if there are multiple
* writing layers that will always be used) then this must return
* the top-most layer. That layer will be installed beneath the
* bottom layer that is set on the Socket. All layers except the last
* must implement ChannelWriter, so they can be iterated over in order
* to set the last writer.
*/
protected abstract InterestWritableByteChannel getBaseWriteChannel();
/**
* Performs any operations required for shutting down this socket.
* <code>shutdownImpl</code> method will only be called once per Socket.
*/
protected abstract void shutdownImpl();
/**
* Sets the initial reader value.
*/
public final void setInitialReader() {
reader = new NIOInputStream(this, this, getBaseReadChannel());
}
/**
* Sets the initial writer value.
*/
public final void setInitialWriter() {
InterestWritableByteChannel base = getBaseWriteChannel();
writer = getBottomFromChain(base);
nioOutputStream = new NIOOutputStream(this, base);
}
private InterestWritableByteChannel getBottomFromChain(InterestWritableByteChannel top) {
if(top instanceof ChannelWriter) {
ChannelWriter lastChannel = (ChannelWriter)top;
while(lastChannel.getWriteChannel() instanceof ChannelWriter)
lastChannel = (ChannelWriter)lastChannel.getWriteChannel();
return (InterestWritableByteChannel)lastChannel;
} else {
return top;
}
}
/**
* Sets the <code>Shutdown</code> observer.
* This observer is useful for when the Socket is created,
* but connect has not been called yet. This observer will be
* notified when the socket is shutdown.
*/
@Override
public final void setShutdownObserver(Shutdownable observer) {
shutdownObserver = observer;
}
/**
* Sets the new <code>ReadObserver</code>.
* <p>
* The deepest <code>ChannelReader</code> in the chain first has its source
* set to the prior reader (assuming it implemented <code>ReadableByteChannel</code>)
* and a read is notified, in order to read any buffered data.
* The source is then set to the Socket's channel and interest
* in reading is turned on.
*/
public final void setReadObserver(final ChannelReadObserver newReader) {
NIODispatcher.instance().getScheduledExecutorService().execute(new Runnable() {
public void run() {
ReadObserver oldReader = reader;
try {
synchronized(LOCK) {
if(shutdown) {
newReader.shutdown();
return;
}
reader = newReader;
}
// At this point, if the socket gets shutdown, we know the
// reader is going to be notified of the shutdown.
ChannelReader lastChannel = newReader;
// go down the chain of ChannelReaders and find the last one to set our source
while(lastChannel.getReadChannel() instanceof ChannelReader)
lastChannel = (ChannelReader)lastChannel.getReadChannel();
if(lastChannel instanceof RequiresSelectionKeyAttachment)
((RequiresSelectionKeyAttachment)lastChannel).setAttachment(AbstractNBSocket.this);
if(oldReader instanceof InterestReadableByteChannel && oldReader != newReader) {
lastChannel.setReadChannel((InterestReadableByteChannel)oldReader);
reader.handleRead(); // read up any buffered data from the old reader chain.
oldReader.shutdown(); // shutdown the now unused reader.
}
InterestReadableByteChannel source = getBaseReadChannel();
lastChannel.setReadChannel(source);
source.interestRead(true);
// If the socket is still connected, read up any buffered data from the current chain.
// This is only done if we know the dispatcher is not going to immediately call
// handleRead on the socket. If this were not done, buffered data within the chain
// would not be read until future incoming data triggered another handleRead.
if(isConnected() && !NIODispatcher.instance().isReadReadyThisIteration(getChannel()))
reader.handleRead();
} catch(IOException iox) {
shutdown();
oldReader.shutdown(); // in case we lost it.
}
}
});
}
/**
* Sets the new <code>WriteObserver</code>.
*<p>
* If a <code>ThrottleWriter</code> is one of the <code>ChannelWriters</code>,
* the attachment of the <code>ThrottleWriter</code> is set to be this.
* <p>
* The deepest <code>ChannelWriter<code> in the chain has its source set to be
* a new <code>InterestWriteChannel</code>, which will be used as the hub to receive
* and forward interest events from/to the channel.
* <p>
* If this is called while the existing <code>WriteObserver</code> still has data left to
* write, then an <code>IllegalStateException</code> is thrown.
*/
public final void setWriteObserver(final ChannelWriter newWriter) {
NIODispatcher.instance().getScheduledExecutorService().execute(new Runnable() {
public void run() {
try {
if(writer.handleWrite())
throw new IllegalStateException("data still in old writer!");
writer.shutdown();
// Guarantee the NIOOutputStream is closed, if it existed.
if(nioOutputStream != null)
nioOutputStream.shutdown();
ChannelWriter lastChannel = newWriter;
while(lastChannel.getWriteChannel() instanceof ChannelWriter) {
lastChannel = (ChannelWriter)lastChannel.getWriteChannel();
if(lastChannel instanceof RequiresSelectionKeyAttachment)
((RequiresSelectionKeyAttachment)lastChannel).setAttachment(AbstractNBSocket.this);
}
InterestWritableByteChannel source = getBaseWriteChannel();
synchronized(LOCK) {
lastChannel.setWriteChannel(source);
if(shutdown) {
source.shutdown();
return;
}
nioOutputStream = null;
writer = getBottomFromChain(source);
}
} catch(IOException iox) {
shutdown();
newWriter.shutdown(); // in case we hadn't set it yet.
}
}
});
}
/**
* Notification that a connect can occur.
* <p>
* This passes it off on to the delegating connecter and then forgets the
* connecter for the duration of the connection.
*/
public final void handleConnect(Socket s) throws IOException {
// Clear out connector prior to calling handleConnect.
// This is so that if handleConnect throws an IOX, the
// observer won't be confused by having both handleConnect &
// shutdown called. It'll be one or the other.
ConnectObserver observer = connecter;
connecter = null;
observer.handleConnect(this);
}
/**
* Notification that a read can occur.
* <p>
* This passes it off to the delegating reader.
*/
public final void handleRead() throws IOException {
reader.handleRead();
}
/**
* Notification that a write can occur.
* <p>
* This passes it off to the delegating writer.
*/
public final boolean handleWrite() throws IOException {
return writer.handleWrite();
}
/** Closes the socket & all streams, waking up any waiting locks. */
@Override
public final void close() {
shutdown();
}
/** Connects to <code>addr</code> with no timeout. */
@Override
public final void connect(SocketAddress addr) throws IOException {
connect(addr, 0);
}
/** Connects to <code>addr</code> with the given timeout (in milliseconds). */
@Override
public final void connect(SocketAddress addr, int timeout) throws IOException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout must not be < 0");
}
final CountDownLatch connectLatch = new CountDownLatch(1);
ConnectObserver connecter = new ConnectObserver() {
public void handleConnect(Socket s) { connectLatch.countDown(); }
public void shutdown() { connectLatch.countDown(); }
// unused
public void handleIOException(IOException e) { }
};
if(!connect(addr, timeout, connecter)) {
long then = System.currentTimeMillis();
try {
if (timeout == 0) {
connectLatch.await();
} else {
// wait a little extra to allow other threads to notify
connectLatch.await(timeout + 1000, TimeUnit.MILLISECONDS);
}
} catch(InterruptedException ie) {
shutdown();
throw new InterruptedIOException(ie);
}
if(!isConnected()) {
shutdown();
long now = System.currentTimeMillis();
if(timeout != 0 && now - then >= timeout)
throw new SocketTimeoutException("operation timed out (" + timeout + ")");
else
throw new ConnectException("Unable to connect!");
}
}
}
/**
* Connects to the specified address within the given timeout (in milliseconds).
* The given <code>ConnectObserver</code> will be notified of success or failure.
* In the event of success, <code>observer.handleConnect</code> is called. In a failure,
* <code>observer.shutdown</code> is called. <code>observer.handleIOException</code>
* is never called.
* <p>
* Returns true if this was able to connect immediately. The observer is still
* notified about the success even it it was immediate.
*/
@Override
public boolean connect(SocketAddress addr, int timeout, final ConnectObserver observer) {
synchronized(LOCK) {
if(shutdown) {
observer.shutdown();
return false;
}
// Set the connectObserver within the lock so that the connecter
// will not be set away from null after shutdown is called.
this.connecter = observer;
}
// At this point, we know that if shutdown is called, the observer
// will be notified of the shutdown.
try {
InetSocketAddress iaddr = (InetSocketAddress)addr;
if (iaddr.isUnresolved())
throw new IOException("unresolved: " + addr);
if(getChannel().connect(addr)) {
// Make sure connecting callbacks are always on the NIO thread.
NIODispatcher.instance().getScheduledExecutorService().execute(new Runnable() {
public void run() {
// ensure it's registered in the selector, so it can be notified
// for reading|writing, and polled for readiness
NIODispatcher.instance().register(getChannel(), AbstractNBSocket.this);
try {
observer.handleConnect(AbstractNBSocket.this);
} catch(IOException iox) {
NIODispatcher.instance().executeLaterAlways(new Runnable() {
public void run() {
shutdown();
}
});
}
}
});
return true;
} else {
NIODispatcher.instance().registerConnect(getChannel(), this, timeout);
return false;
}
} catch(IOException failed) {
NIODispatcher.instance().executeLaterAlways(new Runnable() {
public void run() {
shutdown();
}
});
return false;
}
}
/**
* Returns the <code>InputStream</code> from the <code>NIOInputStream</code>.
* <p>
* Internally, this is a blocking Pipe from the non-blocking <code>SocketChannel</code>.
*/
@Override
public final InputStream getInputStream() throws IOException {
// Unlocked check real quickly.
if(isClosed() || isShutdown())
throw new IOException("Socket closed.");
ReadObserver localReader;
synchronized(LOCK) {
if(isShutdown())
throw new IOException("Socket closed.");
localReader = reader;
}
if(localReader instanceof NIOInputStream) {
NIOInputStream nis = (NIOInputStream)localReader;
// Ensure the stream is initialized before we interest it.
InputStream stream = nis.getInputStream();
nis.interestRead(true);
return stream;
} else {
Callable<InputStream> callable = new Callable<InputStream>() {
public InputStream call() throws IOException {
NIOInputStream stream = new NIOInputStream(AbstractNBSocket.this, AbstractNBSocket.this, null).init();
setReadObserver(stream);
return stream.getInputStream();
}
};
Future<InputStream> future = NIODispatcher.instance().getScheduledExecutorService().submit(callable);
try {
return future.get();
} catch(ExecutionException ee) {
throw (IOException)new IOException().initCause(ee.getCause());
} catch (InterruptedException ie) {
throw (IOException)new IOException().initCause(ie.getCause());
}
}
}
/**
* Returns the <code>OutputStream</code> from the <code>NIOOutputStream</code>.
* <p>
* Internally, this is a blocking Pipe from the non-blocking SocketChannel.
*/
@Override
public final OutputStream getOutputStream() throws IOException {
// Unlocked check real quickly.
if(isClosed() || isShutdown())
throw new IOException("Socket closed.");
// Grab a handle to the stream, to ensure it can't become null.
NIOOutputStream output = nioOutputStream;
if(output != null)
return output.getOutputStream();
else
throw new IllegalStateException("blocking I/O not in use!");
}
/** Gets the read timeout for this socket. */
public long getReadTimeout() {
if(reader instanceof NIOInputStream) {
return 0; // NIOInputStream handles its own timeouts.
} else {
try {
return getSoTimeout();
} catch(SocketException se) {
return 0;
}
}
}
/**
* Notification that an <code>IOException</code> occurred while processing a
* read, connect, or write.
*/
public final void handleIOException(IOException iox) {
if (LOG.isDebugEnabled())
LOG.debug(this, iox);
shutdown();
}
/**
* Shuts down this socket & all its streams.
*/
public final void shutdown() {
synchronized(LOCK) {
if(shutdown)
return;
shutdown = true;
}
if(LOG.isDebugEnabled())
LOG.debug("Shutting down socket & streams for: " + this);
// NOTE: We assume >= Java 1.5.0_10.
// Otherwise we'd need to workaround bugid: 4744057.
shutdownSocketAndChannels();
shutdownObservers();
NIODispatcher.instance().getScheduledExecutorService().execute(new Runnable() {
public void run() {
if(nioOutputStream != null)
nioOutputStream.shutdown();
nioOutputStream = null;
reader = new NoOpReader();
writer = new NoOpWriter();
connecter = null;
shutdownObserver = null;
}
});
}
/** Shuts down all observers. */
protected void shutdownObservers() {
reader.shutdown();
writer.shutdown();
if(connecter != null)
connecter.shutdown();
if(shutdownObserver != null)
shutdownObserver.shutdown();
}
/** Shuts down the socket and channels. */
private void shutdownSocketAndChannels() {
shutdownImpl();
try {
getChannel().close();
} catch(IOException ignored) {}
}
private boolean isShutdown() {
synchronized(LOCK) {
return shutdown;
}
}
}