package net.sf.thingamablog;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
/**
* Wraps an input stream that blocks indefinitely to simulate timeouts on read(), skip(), and close(). The resulting
* input stream is buffered and supports retrying operations that failed due to an InterruptedIOException. Supports
* resuming partially completed operations after an InterruptedIOException REGARDLESS of whether the underlying stream
* does unless the underlying stream itself generates InterruptedIOExceptions in which case it must also support
* resuming. Check the bytesTransferred field to determine how much of the operation completed; conversely, at what
* point to resume.
*/
public class TimeoutInputStream extends FilterInputStream
{
private final long readTimeout;
private final long closeTimeout;
private boolean closeRequested = false;
private Thread thread;
private byte[] iobuffer;
private int head = 0;
private int length = 0;
private IOException ioe = null;
private boolean waitingForClose = false;
private boolean growWhenFull = false;
/**
* Creates a timeout wrapper for an input stream.
* @param in the underlying input stream
* @param bufferSize the buffer size in bytes; should be large enough to mitigate Thread synchronization and context
* switching overhead
* @param readTimeout the number of milliseconds to block for a read() or skip() before throwing an
* InterruptedIOException; blocks indefinitely
* @param closeTimeout the number of milliseconds to block for a close() before throwing an InterruptedIOException;
* blocks indefinitely, -1 closes the stream in the background
*/
public TimeoutInputStream(InputStream in, int bufferSize, long readTimeout, long closeTimeout)
{
super(in);
this.readTimeout = readTimeout;
this.closeTimeout = closeTimeout;
this.iobuffer = new byte[bufferSize];
thread = new Thread(new Runnable()
{
public void run()
{
runThread();
}
}, "TimeoutInputStream");
thread.setDaemon(true);
thread.start();
}
public TimeoutInputStream(InputStream in, int bufferSize, long readTimeout, long closeTimeout, boolean growWhenFull)
{
this(in, bufferSize, readTimeout, closeTimeout);
this.growWhenFull = growWhenFull;
}
/**
* Wraps the underlying stream's method.
* It may be important to wait for a stream to actually be closed because
* it holds an implicit lock on a system resoure (such as a file) while it is open.
* Closing a stream may take time if the underlying stream is still servicing a previous request.
* @throws InterruptedIOException if the timeout expired
* @throws IOException if an i/o error occurs
*/
public void close() throws IOException
{
Thread oldThread;
synchronized(this)
{
if(thread == null)
return;
oldThread = thread;
closeRequested = true;
thread.interrupt();
checkError();
}
if(closeTimeout == -1)
return;
try
{
oldThread.join(closeTimeout);
}
catch(InterruptedException e)
{
Thread.currentThread().interrupt();
}
synchronized(this)
{
checkError();
if(thread != null)
throw new InterruptedIOException();
}
}
/**
* Returns the number of unread bytes in the buffer.
* @throws IOException if an i/o error occurs
*/
public synchronized int available() throws IOException
{
if(length == 0)
checkError();
return length > 0 ? length : 0;
}
/**
* Reads a byte from the stream.
* @throws InterruptedIOException if the timeout expired and no data was received,
* bytesTransferred will be zero
*
* @throws IOException if an i/o error occurs
*/
public synchronized int read() throws IOException
{
if(!syncFill())
return -1;
int b = iobuffer[head++] & 255;
if(head == iobuffer.length)
head = 0;
length--;
notify();
return b;
}
/**
* Reads multiple bytes from the stream.
* @throws InterruptedIOException if the timeout expired and no data was received,
* bytesTransferred will be zero
* @throws IOException if an i/o error occurs
*/
public synchronized int read(byte[] buffer, int off, int len) throws IOException
{
if(!syncFill())
return -1;
int pos = off;
if(len > length)
len = length;
while(len-- > 0)
{
buffer[pos++] = iobuffer[head++];
if(head == iobuffer.length)
head = 0;
length--;
}
notify();
return pos - off;
}
/**
* Skips multiple bytes in the stream.
* @throws InterruptedIOException if the timeout expired before all of the
* bytes specified have been skipped,
* bytesTransferred may be non-zero
* @throws IOException if an i/o error occurs
*/
public synchronized long skip(long count) throws IOException
{
long amount = 0;
try
{
do
{
if(!syncFill())
break;
int skip = (int)Math.min(count - amount, length);
head = (head + skip) % iobuffer.length;
length -= skip;
amount += skip;
}
while(amount < count);
}
catch (InterruptedIOException e)
{
e.bytesTransferred = (int)amount;
throw e;
}
notify();
return amount;
}
/**
* Mark is not supported by the wrapper even if the underlying stream does, returns false.
*/
public boolean markSupported()
{
return false;
}
/**
* Waits for the buffer to fill if it is empty and the stream has not reached EOF.
* @return true if bytes are available, false if EOF has been reached
* @throws InterruptedIOException if EOF not reached but no bytes are available
*/
private boolean syncFill() throws IOException
{
if(length != 0)
return true;
checkError();
if(waitingForClose)
return false;
notify();
try
{
wait(readTimeout);
}
catch(InterruptedException e)
{
Thread.currentThread().interrupt();
}
if(length != 0)
return true;
checkError();
if(waitingForClose)
return false;
throw new InterruptedIOException();
}
/**
* If an exception is pending, throw it.
*/
private void checkError() throws IOException
{
if(ioe != null)
{
IOException e = ioe;
ioe = null;
throw e;
}
}
/**
* Runs the thread in the background.
*/
private void runThread()
{
try
{
readUntilDone();
}
catch (IOException e)
{
synchronized(this)
{
ioe = e;
}
}
finally
{
waitUntilClosed();
try
{
in.close();
}
catch (IOException e)
{
synchronized(this)
{
ioe = e;
}
}
finally
{
synchronized(this)
{
thread = null;
notify();
}
}
}
}
/**
* Waits until we have been requested to close the stream.
*/
private synchronized void waitUntilClosed()
{
waitingForClose = true;
notify();
while(!closeRequested)
{
try
{
wait();
}
catch (InterruptedException e)
{
closeRequested = true;
}
}
}
/**
* Reads bytes into the buffer until EOF, closed, or error.
*/
private void readUntilDone() throws IOException
{
for(;;)
{
int off;
int len;
synchronized(this)
{
while(isBufferFull())
{
if(closeRequested)
return;
waitForRead();
}
off = (head + length) % iobuffer.length;
len = ((head > off) ? head : iobuffer.length) - off;
}
int count;
try
{
count = in.read(iobuffer, off, len);
if(count == -1)
return;
}
catch(InterruptedIOException e)
{
count = e.bytesTransferred;
}
synchronized(this)
{
length += count;
notify();
}
}
}
private synchronized void waitForRead()
{
try
{
if(growWhenFull)
{
wait(readTimeout);
}
else
{
wait();
}
}
catch (InterruptedException e)
{
closeRequested = true;
}
if(growWhenFull && isBufferFull())
{
growBuffer();
}
}
private synchronized void growBuffer()
{
int newSize = 2 * iobuffer.length;
if(newSize > iobuffer.length)
{
if(true)
{
System.out.println("InputStream growing to " + newSize + " bytes");
}
byte[] newBuffer = new byte[newSize];
int pos = 0;
int len = length;
while(len-- > 0)
{
newBuffer[pos++] = iobuffer[head++];
if(head == iobuffer.length)
head = 0;
}
iobuffer = newBuffer;
head = 0;
}
}
private boolean isBufferFull()
{
return length == iobuffer.length;
}
}