/* This code is part of Freenet. It is distributed under the GNU General
* Public License, version 2 (or at your option any later version). See
* http://www.gnu.org/ for further details of the GPL. */
package freenet.support.compress;
import static java.util.concurrent.TimeUnit.MINUTES;
import freenet.support.LogThresholdCallback;
import freenet.support.TimeUtil;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.List;
import java.util.ArrayDeque;
import java.util.Queue;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.io.Closer;
/** Creates and manages decompressor threads. This class is
* given all decompressors which should be applied to an
* InputStream via addDecompressor. The decompressors will be
* strung together and executed when the execute method is called.
* This class also stores any errors which may arise.
* @author sajack
*/
public class DecompressorThreadManager {
final Queue<DecompressorThread> threads;
PipedInputStream input;
PipedOutputStream output = new PipedOutputStream();
final long maxLen;
private boolean finished = false;
private Throwable error = null;
private static volatile boolean logMINOR;
static {
Logger.registerLogThresholdCallback(new LogThresholdCallback(){
@Override
public void shouldUpdate(){
logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
}
});
}
/** Creates a new DecompressorThreadManager
* @param inputStream The stream that will be decompressed, if compressed
* @param maxLen The maximum number of bytes to extract
*/
public DecompressorThreadManager(PipedInputStream inputStream, List<? extends Compressor> decompressors, long maxLen) throws IOException {
threads = new ArrayDeque<DecompressorThread>(decompressors.size());
this.maxLen = maxLen;
if(inputStream == null) {
IOException e = new IOException("Input stream may not be null");
onFailure(e);
throw e;
}
input = inputStream;
while(!decompressors.isEmpty()) {
Compressor compressor = decompressors.remove(decompressors.size()-1);
if(logMINOR) Logger.minor(this, "Decompressing with "+compressor);
DecompressorThread thread = new DecompressorThread(compressor, this, input, output, maxLen);
threads.add(thread);
input = new PipedInputStream(output);
output = new PipedOutputStream();
}
}
/** Creates and executes a new thread for each decompressor,
* chaining the output of the previous to the next.
* @return An InputStream from which uncompressed data may be read from
*/
public synchronized PipedInputStream execute() throws Throwable {
if(error != null) throw error;
if(threads.isEmpty()) {
onFinish();
return input;
}
try {
int count = 0;
while(!threads.isEmpty()){
if(getError() != null) throw getError();
DecompressorThread threadRunnable = threads.remove();
if(threads.isEmpty()) threadRunnable.setLast();
Thread t = new Thread(threadRunnable, "DecompressorThread"+count);
t.start();
if(logMINOR) Logger.minor(this, "Started decompressor thread "+t);
count++;
}
output.close();
} catch(Throwable t) {
onFailure(t);
throw t;
} finally {
Closer.close(output);
}
return input;
}
/** Informs the manager that a nonrecoverable exception has occured in the
* decompression threads
* @param e The thrown exception
*/
public synchronized void onFailure(Throwable t) {
error = t;
onFinish();
}
/** Marks that the decompression of the stream has finished and wakes
* threads blocking on completion */
public synchronized void onFinish() {
finished = true;
notifyAll();
}
/** Blocks until all threads have finished executing and cleaning up.*/
public synchronized void waitFinished() throws Throwable {
long start = System.currentTimeMillis();
while(!finished) {
try {
// FIXME remove the timeout here.
// Something wierd is happening...
//wait(0)
wait(MINUTES.toMillis(20));
long time = System.currentTimeMillis()-start;
if(time > MINUTES.toMillis(20))
Logger.error(this, "Still waiting for decompressor chain after "+TimeUtil.formatTime(time));
} catch(InterruptedException e) {
//Do nothing
}
}
if(error != null) throw error;
}
/** Returns an exception which was thrown during decompression
* @return Returns an exception which was caught during the decompression
*/
public synchronized Throwable getError() {
return error;
}
/**Represents a thread which invokes a decompressor upon an
* input stream. These threads should be instantiated by a
* <code>DecompressorThreadManager</code>
* @author sajack
*/
class DecompressorThread implements Runnable {
/**The compressor whose decompress method will be invoked*/
final Compressor compressor;
/**The stream compressed data will be read from*/
private InputStream input;
/**The stream decompressed data will be written*/
private OutputStream output;
/**A upper limit to how much data may be decompressed. This is passed to the decompressor*/
final long maxLen;
/**The manager which created the thread*/
final DecompressorThreadManager manager;
/**Whether or not this thread should signal the manager that decompression has finished*/
boolean isLast = false;
public DecompressorThread(Compressor compressor, DecompressorThreadManager manager, InputStream input, PipedOutputStream output, long maxLen) {
this.compressor = compressor;
this.input = new BufferedInputStream(input);
this.output = new BufferedOutputStream(output);
this.maxLen = maxLen;
this.manager = manager;
}
/**Begins the decompression */
@Override
public void run() {
if(logMINOR) Logger.minor(this, "Decompressing...");
try {
if(manager.getError() == null) {
compressor.decompress(input, output, maxLen, maxLen * 4);
input.close();
output.close();
// Avoid relatively expensive repeated close on normal completion
input = null;
output = null;
if(isLast) manager.onFinish();
}
if(logMINOR) Logger.minor(this, "Finished decompressing...");
} catch (Exception e) {
manager.onFailure(e);
} finally {
Closer.close(input);
Closer.close(output);
}
}
/** Should be called before executing the thread when there
* are no further decompressors pending*/
public void setLast() {
isLast = true;
}
}
}