
com.gc.iotools.stream.reader.TeeReaderWriter Maven / Gradle / Ivy
Show all versions of easystream Show documentation
package com.gc.iotools.stream.reader;
/*
* Copyright (c) 2008, 2015 Gabriele Contini. This source code is released
* under the BSD License.
*/
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.Arrays;
import com.gc.iotools.stream.base.EasyStreamConstants;
/**
*
* Copies the data from the underlying Reader
to the
* Writer(s)
passed in the constructor. The data copied are
* similar to the underlying Reader
.
*
*
* When the method {@link #close()}
is invoked all the bytes
* remaining in the underlying Reader
are copied to the
* Writer(s)
. This behavior is different from this class and
* {@code TeeInputStream} in Apache commons-io.
*
*
* Bytes skipped are in any case copied to the Writer
. Mark and
* reset of the outer Reader
doesn't affect the data copied to
* the Writer(s)
, that remain similar to the Reader
* passed in the constructor.
*
*
* It also calculate some statistics on the read/write operations.
* {@link #getWriteTime()} returns the time spent writing to the Writers,
* {@link #getReadTime()} returns the time spent reading from the Reader and
* {@link #getWriteSize()} returns the amount of data written
* to a single Writer
until now.
*
*
* Sample usage:
*
*
*
* Reader source=... //some data to be read.
* StringWriter destination1= new StringWriter();
* StringWriter destination2= new StringWriter();
*
* TeeReaderWriter tee = new TeeReaderWriter(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 TeeReaderWriter while in
* //destination2 they were copied by IOUtils.
* StringBuffer buffer=destination1.getBuffer();
*
*
* @see org.apache.commons.io.input.TeeReader
* @author dvd.smnt
* @since 1.2.7
* @version $Id: TeeReaderWriter.java 576 2015-03-28 00:03:33Z gcontini $
*/
public class TeeReaderWriter extends Reader {
/**
* 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 Writer
s where data is written.
*/
protected final Writer[] destinations;
private long markPosition = 0;
private long readTime = 0;
/**
* The source Reader
where the data comes from.
*/
protected final Reader source;
private long sourcePosition = 0;
private final long[] writeTime;
/**
*
* Creates a TeeInputStreamWriter
and saves its argument, the
* input stream source
and the output stream
* destination
for later use.
*
*
* This constructor allow to specify multiple Writer
to which
* the data will be copied.
*
*
* @since 1.2.7
* @param source
* The underlying Reader
* @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
* Writer
.
*/
public TeeReaderWriter(final Reader source, final boolean closeStreams,
final Writer... destinations) {
this.source = source;
if (destinations == null) {
throw new IllegalArgumentException(
"Destinations Writer can't be null");
}
if (destinations.length == 0) {
throw new IllegalArgumentException(
"At least one destination Writer must be specified");
}
for (final Writer destination : destinations) {
if (destination == null) {
throw new IllegalArgumentException(
"One of the Writers 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 TeeReaderWriter
and saves its argument, the
* input stream source
and the Writer
* 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 Reader
* @param destination
* Data read from source
are also written to this
* Writer
.
*/
public TeeReaderWriter(final Reader source, final Writer destination) {
this(source, destination, true);
}
/**
* Creates a TeeReaderWriter
and saves its argument, the
* input stream source
and the output stream
* destination
for later use.
*
* @since 1.2.7
* @param source
* The underlying Reader
* @param destination
* Data read from source
are also written to this
* Writer
.
* @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 TeeReaderWriter(final Reader source, final Writer destination,
final boolean closeStreams) {
this(source, closeStreams, new Writer[] { destination });
}
/**
* {@inheritDoc}
*
*
* This method is called when the method {@link #close()} is invoked. It
* copies all the data eventually remaining in the source
* Reader
passed in the constructor to the destination
* Writer
.
*
*
* The standard behavior is to close both the underlying
* Reader
and Writer(s)
. When the class was
* constructed with the parameter {@link #closeStreams}
* set to false the underlying streams must be closed
* externally.
* @see #close()
*/
@Override
public void close() throws IOException {
IOException e1 = null;
try {
final char[] buffer = new char[EasyStreamConstants.SKIP_BUFFER_SIZE];
while (read(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 "
+ "Writer(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 Writer 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 number of milliseconds spent reading from the
* source
Reader
.
*
*
* @return number of milliseconds spent reading from the
* source
.
* @since 1.2.7
*/
public long getReadTime() {
return this.readTime;
}
/**
*
* Returns the number of bytes written until now to a single destination
* Writer
.
*
*
* This number is not affected by any of the mark and reset that are made
* on this {@linkplain TeeReaderWriter} and reflects only the number of
* bytes written.
*
*
* @return number of bytes written until now to a single
* destination
.
* @since 1.2.7
*/
public long getWriteSize() {
return this.destinationPosition;
}
/**
*
* Return the time spent writing on the destination Writer(s)
* in milliseconds.
*
*
* The returned array has one element for each Writer
passed
* in the constructor.
*
*
* @return time spent writing on the destination Writers
.
*/
public long[] getWriteTime() {
return this.writeTime;
}
/**
* {@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.Reader#mark(int)
* @since 1.2.7
*/
@Override
public void mark(final int readLimit) throws IOException {
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} */
@Override
public int read(final char[] 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}
*
*
* 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 Writer
until the position where
* reset
was issued is reached again. This ensures the data
* copied to the destination Writer
reflects the data
* contained in the source Reader (the one passed in the constructor).
*
* @see #mark(int)
* @see java.io.Reader#reset()
* @exception IOException
* If the source stream has an exception in calling
* reset().
* @since 1.2.7
*/
@Override
public synchronized void reset() throws IOException {
this.source.reset();
this.sourcePosition = this.markPosition;
}
}