
com.gc.iotools.stream.is.TeeInputStreamOutputStream Maven / Gradle / Ivy
Show all versions of easystream Show documentation
package com.gc.iotools.stream.is;
/*
* Copyright (c) 2008, 2015 Gabriele Contini. This source code is released
* under the BSD License.
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import com.gc.iotools.stream.base.AbstractInputStreamWrapper;
import com.gc.iotools.stream.base.EasyStreamConstants;
/**
*
* Copies the data from the underlying InputStream
to the
* OutputStream(s)
passed in the constructor. The data copied are
* similar to the underlying InputStream
.
*
*
* When the method {@link #close()}
is invoked all the bytes
* remaining in the underlying InputStream
are copied to the
* OutputStream(s)
. This behavior is different from this class
* and {@code TeeInputStream} in Apache commons-io.
*
*
* Bytes skipped are in any case copied to the OutputStream
. Mark
* and reset of the outer InputStream
doesn't affect the data
* copied to the OutputStream(s)
, that remain similar to the
* InputStream
passed in the constructor.
*
*
* It also calculate some statistics on the read/write operations.
* {@link #getWriteTime()} returns the time spent writing to the
* OutputStreams, {@link #getReadTime()} returns the time spent reading from
* the InputStream and {@link TeeInputStreamOutputStream#getWriteSize()}
* returns the amount of data written to a single OutputStream
* until now.
*
*
* Sample usage:
*
*
*
* InputStream source=... //some data to be read.
* ByteArrayOutputStream destination1= new ByteArrayOutputStream();
* ByteArrayOutputStream destination2= new ByteArrayOutputStream();
*
* TeeInputStreamOutputStream tee=
* new TeeInputStreamOutputStream(source,destination1);
* org.apache.commons.io.IOUtils.copy(tee,destination2);
* tee.close();
* //at this point both destination1 and destination2 contains the same bytes
* //in destination1 were put by TeeInputStreamOutputStream while in
* //destination2 they were copied by IOUtils.
* byte[] bytes=destination1.getBytes();
*
*
* @see org.apache.commons.io.input.TeeInputStream
* @author dvd.smnt
* @since 1.0.6
* @version $Id: TeeInputStreamOutputStream.java 576 2015-03-28 00:03:33Z gcontini $
*/
public class TeeInputStreamOutputStream extends AbstractInputStreamWrapper {
/**
* If true
source
and destination
* streams are closed when {@link #close()} is invoked.
*/
protected final boolean closeStreams;
protected final boolean[] copyEnabled;
private long destinationPosition = 0;
/**
* The destination OutputStream
where data is written.
*/
protected final OutputStream[] destinations;
private long markPosition = 0;
private long readTime = 0;
private long sourcePosition = 0;
private final long[] writeTime;
/**
*
* Creates a TeeInputStreamOutputStream
and saves its
* argument, the input stream source
and the output stream
* destination
for later use.
*
*
* This constructor allow to specify multiple OutputStream
to
* which the data will be copied.
*
*
* @since 1.2.3
* @param source
* The underlying InputStream
* @param closeStreams
* if true
the destination
will be
* closed when the {@link #close()} method is invoked. If
* false
the close method on the underlying
* streams will not be called (it must be invoked externally).
* @param destinations
* Data read from source
are also written to this
* OutputStream
.
*/
public TeeInputStreamOutputStream(final InputStream source,
final boolean closeStreams, final OutputStream... destinations) {
super(source);
if (destinations == null) {
throw new IllegalArgumentException(
"Destinations OutputStream can't be null");
}
if (destinations.length == 0) {
throw new IllegalArgumentException(
"At least one destination OutputStream must be specified");
}
for (final OutputStream destination : destinations) {
if (destination == null) {
throw new IllegalArgumentException(
"One of the outputstreams in the array is null");
}
}
this.writeTime = new long[destinations.length];
this.destinations = destinations;
this.closeStreams = closeStreams;
this.copyEnabled = new boolean[destinations.length];
Arrays.fill(this.copyEnabled, true);
}
/**
*
* Creates a TeeOutputStream
and saves its argument, the
* input stream source
and the OutputStream
* destination
for later use.
*
*
* When the method {@link #close()} it is invoked the remaining content of
* the source
stream is copied to the
* destination
and the source
and
* destination
streams are closed.
*
*
* @param source
* The underlying InputStream
* @param destination
* Data read from source
are also written to this
* OutputStream
.
*/
public TeeInputStreamOutputStream(final InputStream source,
final OutputStream destination) {
this(source, destination, true);
}
/**
* Creates a TeeOutputStream
and saves its argument, the
* input stream source
and the output stream
* destination
for later use.
*
* @since 1.2.0
* @param source
* The underlying InputStream
* @param destination
* Data read from source
are also written to this
* OutputStream
.
* @param closeStreams
* if true
the destination
will be
* closed when the {@link #close()} method is invoked. If
* false
the close method on the underlying
* streams will not be called (it must be invoked externally).
*/
public TeeInputStreamOutputStream(final InputStream source,
final OutputStream destination, final boolean closeStreams) {
this(source, closeStreams, new OutputStream[] { destination });
}
/** {@inheritDoc} */
@Override
public int available() throws IOException {
return this.source.available();
}
/**
* {@inheritDoc}
*
*
* This method is called when the method {@link #close()} is invoked. It
* copies all the data eventually remaining in the source
* InputStream
passed in the constructor to the destination
* OutputStream
.
*
*
* The standard behavior is to close both the underlying
* InputStream
and OutputStream(s)
. When the
* class was constructed with the parameter
* {@link TeeInputStreamOutputStream#closeCalled closeCalled} set to false
* the underlying streams must be closed externally.
* @see #close()
*/
@Override
public void closeOnce() throws IOException {
IOException e1 = null;
try {
final byte[] buffer = new byte[EasyStreamConstants.SKIP_BUFFER_SIZE];
while (innerRead(buffer, 0, buffer.length) > 0) {
// empty block: just throw bytes away
}
} catch (final IOException e) {
e1 = new IOException(
"Incomplete data was written to the destination "
+ "OutputStream(s).");
e1.initCause(e);
}
if (this.closeStreams) {
final long startr = System.currentTimeMillis();
this.source.close();
this.readTime += System.currentTimeMillis() - startr;
for (int i = 0; i < this.destinations.length; i++) {
final long start = System.currentTimeMillis();
this.destinations[i].close();
this.writeTime[i] += System.currentTimeMillis() - start;
}
}
if (e1 != null) {
throw e1;
}
}
/**
*
* Allow to switch off the copy to the underlying
* OutputStream
s. Setting the parameter to false will disable
* the copy to all the underlying streams at once.
*
*
* If you need more fine grained control you should use
* {@link #enableCopy(boolean[])} .
*
*
* @since 1.2.9
* @param enable
* whether to copy or not the bytes to the underlying stream.
*/
public final void enableCopy(final boolean enable) {
Arrays.fill(this.copyEnabled, enable);
}
/**
*
* Allow to switch off the copy to the underlying
* OutputStream
s, selectively enabling or disabling copy to
* some specific stream.
*
*
* The copy is enabled by default. Each element in the array correspond to
* an OutputStream
passed in the constructor. If the
* correspondent element in the array passed as a parameter is set to
* true
the copy will be enabled. It can be invoked multiple
* times.
*
*
* @since 1.2.9
* @param enable
* whether to copy or not the bytes to the underlying
* OutputStream
s.
*/
public final void enableCopy(final boolean[] enable) {
if (enable == null) {
throw new IllegalArgumentException("Enable array can't be null");
}
if (enable.length != this.copyEnabled.length) {
throw new IllegalArgumentException("Enable array must be of "
+ "the same size of the OutputStream array passed "
+ "in the constructor. Array size [" + enable.length
+ "] streams [" + this.copyEnabled.length + "]");
}
for (int i = 0; i < enable.length; i++) {
this.copyEnabled[i] = enable[i];
}
}
/**
*
* Returns the OutputStream
(s) passed in the constructor.
*
*
* @since 1.2.9
* @return Array of OutputStream passed in the constructor.
*/
public final OutputStream[] getDestinationStreams() {
return this.destinations;
}
/**
*
* Returns the number of milliseconds spent reading from the
* source
InputStream
.
*
*
* @return number of milliseconds spent reading from the
* source
.
* @since 1.2.5
*/
public long getReadTime() {
return this.readTime;
}
/**
*
* Returns the number of bytes written until now to a single destination
* OutputStream
.
*
*
* This number is not affected by any of the mark and reset that are made
* on this {@linkplain TeeInputStreamOutputStream} and reflects only the
* number of bytes written.
*
*
* @return number of bytes written until now to a single
* destination
.
* @since 1.2.5
*/
public long getWriteSize() {
return this.destinationPosition;
}
/**
*
* Return the time spent writing on the destination
* OutputStream(s)
in milliseconds.
*
*
* The returned array has one element for each OutputStream
* passed in the constructor.
*
*
* @return time spent writing on the destination
* OutputStreams
.
*/
public long[] getWriteTime() {
return this.writeTime;
}
/** {@inheritDoc} */
@Override
public int innerRead(final byte[] b, final int off, final int len)
throws IOException {
final long startr = System.currentTimeMillis();
final int result = this.source.read(b, off, len);
this.readTime += System.currentTimeMillis() - startr;
if (result > 0) {
if (this.sourcePosition + result > this.destinationPosition) {
final int newLen = (int) (this.sourcePosition + result - this.destinationPosition);
final int newOff = off + (result - newLen);
for (int i = 0; i < this.destinations.length; i++) {
if (this.copyEnabled[i]) {
final long start = System.currentTimeMillis();
this.destinations[i].write(b, newOff, newLen);
getWriteTime()[i] += System.currentTimeMillis()
- start;
}
}
this.destinationPosition += newLen;
}
this.sourcePosition += result;
}
return result;
}
/**
* {@inheritDoc}
*
*
* Marks the current position in this input stream. A subsequent call to
* the reset
method repositions this stream at the last
* marked position so that subsequent reads re-read the same bytes.
*
* @see #reset()
* @see java.io.InputStream#mark(int)
* @since 1.2.0
*/
@Override
public void mark(final int readLimit) {
this.source.mark(readLimit);
this.markPosition = this.sourcePosition;
}
/** {@inheritDoc} */
@Override
public boolean markSupported() {
return this.source.markSupported();
}
/** {@inheritDoc} */
@Override
public int read() throws IOException {
final long startr = System.currentTimeMillis();
final int result = this.source.read();
this.readTime += System.currentTimeMillis() - startr;
if (result >= 0) {
this.sourcePosition++;
if (this.sourcePosition > this.destinationPosition) {
for (int i = 0; i < this.destinations.length; i++) {
if (this.copyEnabled[i]) {
final long start = System.currentTimeMillis();
this.destinations[i].write(result);
getWriteTime()[i] += System.currentTimeMillis()
- start;
}
}
this.destinationPosition++;
}
}
return result;
}
/**
* {@inheritDoc}
*
*
* Repositions this stream to the position at the time the
* mark
method was last called on this input stream.
*
*
* After reset()
method is called the data is not copied
* anymore to the destination OutputStream
until the position
* where reset
was issued is reached again. This ensures the
* data copied to the destination OutputStream
reflects the
* data contained in the source InputStream (the one passed in the
* constructor).
*
* @see #mark(int)
* @see java.io.InputStream#reset()
* @exception IOException
* If the source stream has an exception in calling
* reset().
* @since 1.2.0
*/
@Override
public synchronized void reset() throws IOException {
this.source.reset();
this.sourcePosition = this.markPosition;
}
}