
com.gc.iotools.stream.os.OutputStreamToInputStream Maven / Gradle / Ivy
package com.gc.iotools.stream.os;
/*
* Copyright (c) 2008,2009 Davide Simonetti.
* This source code is released under the BSD License.
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.commons.io.input.CloseShieldInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gc.iotools.stream.base.EasyStreamConstants;
import com.gc.iotools.stream.base.ExecutionModel;
import com.gc.iotools.stream.base.ExecutorServiceFactory;
/**
*
* This class allow to read from an InputStream
the data who has
* been written to an OutputStream
(performs an
* OutputStream
-> InputStream
conversion).
*
*
* More detailiy it is an OutputStream
that, when extended, allows
* to read the data written to it from the InputStream
inside the
* method {@linkplain #doRead()}.
*
*
* To use this class you must extend it and implement the method
* {@linkplain #doRead(InputStream)}. Inside this method place the logic that
* needs to read the data from the InputStream
. Then the data can
* be written to this class that implements OutputStream
. When
* {@linkplain #close()} method is called on the outer OutputStream
* an EOF is generated in the InputStream
passed in the
* {@linkplain #doRead(InputStream)}.
*
*
* The {@linkplain #doRead(InputStream)} call executes in another thread, so
* there is no warranty on when it will start and when it will end. Special care
* must be taken in passing variables to it: all the arguments must be final and
* inside {@linkplain #doRead(InputStream)} you shouldn't change the variables
* of the outer class.
*
*
* Any Exception threw inside the {@linkplain #doRead(InputStream)} method is
* propagated to the outer OutputStream
on the next
* write
operation.
*
*
* The method {@link #getResults()} suspend the outer thread and wait for the
* read from the internal stream is over. It returns when the
* doRead()
terminates and has produced its result.
*
*
* Some sample code:
*
*
*
* OutputStreamToInputStream<String> oStream2IStream =
* new OutputStreamToInputStream<String>() {
* protected String doRead(final InputStream istream) throws Exception {
* // Users of this class should place all the code that need to read data
* // from the InputStream in this method. Data available through the
* // InputStream passed as a parameter is the data that is written to the
* // OutputStream oStream2IStream through its write method.
* final String result = IOUtils.toString(istream);
* return result + " was processed.";
* }
* };
* try {
* // some data is written to the OutputStream, will be passed to the method
* // doRead(InputStream i) above and after close() is called the results
* // will be available through the getResults() method.
* oStream2IStream.write("test".getBytes());
* } finally {
* // don't miss the close (or a thread would not terminate correctly).
* oStream2IStream.close();
* }
* String result = oStream2IStream.getResults();
* //result now contains the string "test was processed."
*
*
* @param
* Type returned by the method {@link #getResults()} after the thread
* has finished.
* @since 1.0
* @author dvd.smnt
*/
public abstract class OutputStreamToInputStream extends OutputStream {
/**
* This class executes in the second thread.
*
* @author dvd.smnt
*
*/
private final class DataConsumer implements Callable {
private final InputStream inputstream;
DataConsumer(final InputStream istream) {
this.inputstream = istream;
}
public synchronized T call() throws Exception {
T processResult;
try {
// avoid the internal class close the stream.
final CloseShieldInputStream istream = new CloseShieldInputStream(
this.inputstream);
processResult = doRead(istream);
} catch (final Exception e) {
OutputStreamToInputStream.this.abort = true;
throw e;
} finally {
emptyInputStream();
this.inputstream.close();
}
return processResult;
}
private void emptyInputStream() {
try {
final byte[] buffer = new byte[EasyStreamConstants.SKIP_BUFFER_SIZE];
while (this.inputstream.read(buffer) >= 0) {
;
// empty block: just throw bytes away
}
} catch (final IOException e) {
if ((e.getMessage() != null)
&& (e.getMessage().indexOf("closed") > 0)) {
OutputStreamToInputStream.LOG
.debug("Stream already closed");
} else {
OutputStreamToInputStream.LOG.error(
"IOException while empty InputStream a "
+ "thread can be locked", e);
}
} catch (final Throwable e) {
OutputStreamToInputStream.LOG.error(
"IOException while empty InputStream a "
+ "thread can be locked", e);
}
}
}
/**
* Extends PipedInputStream to allow set the default buffer size.
*
*/
private final class MyPipedInputStream extends PipedInputStream {
MyPipedInputStream(final int bufferSize) {
super.buffer = new byte[bufferSize];
}
}
// Default timeout in milliseconds.
private static final int DEFAULT_TIMEOUT = 15 * 60 * 1000;
/**
* The default pipe buffer size for the newly created pipes.
*/
private static int defaultPipeSize = EasyStreamConstants.DEFAULT_PIPE_SIZE;
private static final Logger LOG = LoggerFactory
.getLogger(OutputStreamToInputStream.class);
/**
*
* Set the size for the pipe circular buffer. This setting has effect for
* the newly created OutputStreamToInputStream
. Default is 4096
* bytes.
*
*
*
* Will be removed in the 1.3 release. Use {@link #setDefaultPipeSize(int)}
* instead.
*
*
* @since 1.2.0
* @param defaultPipeSize
* The default pipe buffer size in bytes.
* @deprecated
* @see #setDefaultPipeSize(int)
*/
@Deprecated
public static void setDefaultBufferSize(final int defaultPipeSize) {
OutputStreamToInputStream.defaultPipeSize = defaultPipeSize;
}
/**
* Set the size for the pipe circular buffer. This setting has effect for
* the newly created OutputStreamToInputStream
. Default is 4096
* bytes.
*
* @since 1.2.3
* @param defaultPipeSize
* The default pipe buffer size in bytes.
*/
public static void setDefaultPipeSize(final int defaultPipeSize) {
OutputStreamToInputStream.defaultPipeSize = defaultPipeSize;
}
private boolean closeCalled = false;
private boolean abort = false;
private final boolean joinOnClose;
private final PipedOutputStream pipedOs;
private final Future writingResult;
/**
*
* Creates a new OutputStreamToInputStream
. It uses the default
* {@link ExecutionModel#THREAD_PER_INSTANCE} thread instantiation strategy.
* This means that a new thread is created for every instance of
* OutputStreamToInputStream
.
*
*
* When the {@linkplain #close()} method is called this class wait for the
* internal thread to terminate.
*
*
* @throws IOException
* Exception thrown if pipe can't be created.
*/
public OutputStreamToInputStream() throws IOException {
this(true, ExecutionModel.THREAD_PER_INSTANCE);
}
/**
*
* Creates a new OutputStreamToInputStream
. It let the user
* specify the thread instantiation strategy and what will happen upon the
* invocation of close()
method.
*
*
* If joinOnClose
is true
when the
* close()
method is invoked this class will wait for the
* internal thread to terminate.
*
*
* @see ExecutionModel
* @param joinOnClose
* if true
the internal thread will be joined when
* close is invoked.
* @param executionModel
* Strategy for allocating threads.
* @throws IOException
* Exception thrown if pipe can't be created.
*/
public OutputStreamToInputStream(final boolean joinOnClose,
final ExecutionModel executionModel) throws IOException {
this(joinOnClose, ExecutorServiceFactory.getExecutor(executionModel));
}
/**
*
* @param joinOnClose
* if true
the internal thread will be joined when
* close is invoked.
* @param executorService
* Service for executing the internal thread.
* @throws IOException
* Exception thrown if pipe can't be created.
*/
public OutputStreamToInputStream(final boolean joinOnClose,
final ExecutorService executorService) throws IOException {
if (executorService == null) {
throw new IllegalArgumentException(
"executor service can't be null");
}
this.pipedOs = new PipedOutputStream();
final PipedInputStream pipedIS = new MyPipedInputStream(
defaultPipeSize);
pipedIS.connect(this.pipedOs);
final DataConsumer executingProcess = new DataConsumer(pipedIS);
this.joinOnClose = joinOnClose;
this.writingResult = executorService.submit(executingProcess);
}
/**
* {@inheritDoc}
*/
@Override
public final void close() throws IOException {
internalClose(this.joinOnClose, TimeUnit.MILLISECONDS,
DEFAULT_TIMEOUT);
}
/**
* When this method is called the internal thread is always waited for
* completion.
*
* @param timeout
* maximum time to wait for the internal thread to finish.
* @param tu
* Time unit for the timeout.
* @throws IOException
* Threw if some problem (timeout or internal exception) occurs.
* see the getCause()
method for the explanation.
*/
public final void close(final long timeout, final TimeUnit tu)
throws IOException {
internalClose(true, tu, timeout);
}
/**
* {@inheritDoc}
*/
@Override
public final void flush() throws IOException {
if (this.abort) {
// internal thread is already aborting. wait for short time.
internalClose(true, TimeUnit.SECONDS, 1);
} else {
this.pipedOs.flush();
}
}
/**
*
* This method returns the result of the method {@link #doRead(InputStream)}
* and ensure the previous method is over.
*
*
* This method suspend the calling thread and waits for the function
* {@link #doRead(InputStream)} to finish. It returns when the
* doRead()
terminates and has produced its result.
*
*
* It must be called after the method {@link #close()} otherwise a
* IllegalStateException
is thrown.
*
*
* @exception InterruptedException
* Thrown when the thread is interrupted.
* @exception ExecutionException
* Thrown if the method {@linkplain #doRead(InputStream)}
* threw an Exception. The getCause()
returns
* the original Exception.
* @throws IllegalStateException
* When it is called before the method {@link #close()} has been
* called.
* @return the object returned from the doRead() method.
*/
public final T getResults() throws InterruptedException,
ExecutionException {
if (!this.closeCalled) {
throw new IllegalStateException("Method close() must be called"
+ " before getResults");
}
return this.writingResult.get();
}
/**
* {@inheritDoc}
*/
@Override
public final void write(final byte[] bytes) throws IOException {
if (this.abort) {
// internal thread is already aborting. wait for short time.
internalClose(true, TimeUnit.SECONDS, 1);
} else {
this.pipedOs.write(bytes);
}
}
/**
* {@inheritDoc}
*/
@Override
public final void write(final byte[] bytes, final int offset,
final int length) throws IOException {
if (this.abort) {
// internal thread is already aborting. wait for short time.
internalClose(true, TimeUnit.SECONDS, 1);
} else {
this.pipedOs.write(bytes, offset, length);
}
}
/**
* {@inheritDoc}
*/
@Override
public final void write(final int bytetowr) throws IOException {
if (this.abort) {
// internal thread is already aborting. wait for short time.
internalClose(true, TimeUnit.SECONDS, 1);
} else {
this.pipedOs.write(bytetowr);
}
}
private void internalClose(final boolean join, final TimeUnit timeUnit,
final long timeout) throws IOException {
if (!this.closeCalled) {
this.closeCalled = true;
this.pipedOs.close();
if (join) {
// waiting for thread to finish..
try {
this.writingResult.get(timeout, timeUnit);
} catch (final ExecutionException e) {
final IOException e1 = new IOException(
"The doRead() threw exception. Use "
+ "getCause() for details.");
e1.initCause(e.getCause());
throw e1;
} catch (final InterruptedException e) {
final IOException e1 = new IOException(
"Waiting of the thread has been interrupted");
e1.initCause(e);
throw e1;
} catch (final TimeoutException e) {
if (!this.writingResult.isDone()) {
this.writingResult.cancel(true);
}
final IOException e1 = new IOException(
"Waiting for the internal "
+ "thread to finish took more than ["
+ timeout + "] " + timeUnit);
e1.initCause(e);
throw e1;
}
}
}
}
/**
*
* This method has to be implemented to use this class. It allows to
* retrieve the data written to the outer OutputStream
from the
* InputStream
passed as a parameter.
*
*
* Any exception eventually threw inside this method will be propagated to
* the external OutputStream
. When the next {@linkplain
* write(byte[])} operation is called an IOException
will be
* thrown and the original exception can be accessed calling the getCause()
* method on the IOException. It will also be available by calling the
* method {@link #getResults()}.
*
*
* @param istream
* The InputStream where the data can be retrieved.
* @return Optionally returns a result of the elaboration.
* @throws Exception
* If an Exception
occurs during the elaboration it
* can be thrown. It will be propagated to the external
* OutputStream
and will be available calling the
* method {@link #getResults()}.
*/
protected abstract T doRead(InputStream istream) throws Exception;
}