/*
* Copyright (C) 2004 Stephen Ostermiller
* http://ostermiller.org/contact.pl?regarding=Java+Utilities
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* See COPYING.TXT for details.
*/
import java.io.*;
import java.util.ArrayList;
/**
* An input stream which reads sequentially from multiple sources.
* More information about this class is available from * "http://ostermiller.org/utils/">ostermiller.org.
*
* @author Stephen Ostermiller http://ostermiller.org/contact.pl?regarding=Java+Utilities
* @since ostermillerutils 1.04.00
*/
public class ConcatInputStream extends InputStream {
/**
* Current index to inputStreamQueue
*
* @since ostermillerutils 1.04.01
*/
private int inputStreamQueueIndex = 0;
/**
* Queue of inputStreams that have yet to be read from.
*
* @since ostermillerutils 1.04.01
*/
private ArrayList inputStreamQueue = new ArrayList();
/**
* A cache of the current inputStream from the inputStreamQueue
* to avoid unneeded access to the queue which must
* be synchronized.
*
* @since ostermillerutils 1.04.01
*/
private InputStream currentInputStream = null;
/**
* true iff the client may add more inputStreams.
*
* @since ostermillerutils 1.04.01
*/
private boolean doneAddingInputStreams = false;
/**
* Causes the addInputStream method to throw IllegalStateException
* and read() methods to return -1 (end of stream)
* when there is no more available data.
*
* Calling this method when this class is no longer accepting
* more inputStreams has no effect.
*
* @since ostermillerutils 1.04.01
*/
public void lastInputStreamAdded(){
doneAddingInputStreams = true;
}
/**
* Add the given inputStream to the queue of inputStreams from which to
* concatenate data.
*
* @param in InputStream to add to the concatenation.
* @throws IllegalStateException if more inputStreams can't be added because lastInputStreamAdded() has been called, close() has been called, or a constructor with inputStream parameters was used.
*
* @since ostermillerutils 1.04.01
*/
public void addInputStream(InputStream in){
synchronized(inputStreamQueue){
if (in == null) throw new NullPointerException();
if (closed) throw new IllegalStateException("ConcatInputStream has been closed");
if (doneAddingInputStreams) throw new IllegalStateException("Cannot add more inputStreams - the last inputStream has already been added.");
inputStreamQueue.add(in);
}
}
/**
* Add the given inputStream to the queue of inputStreams from which to
* concatenate data.
*
* @param in InputStream to add to the concatenation.
* @throws IllegalStateException if more inputStreams can't be added because lastInputStreamAdded() has been called, close() has been called, or a constructor with inputStream parameters was used.
* @throws NullPointerException the array of inputStreams, or any of the contents is null.
*
* @since ostermillerutils 1.04.01
*/
public void addInputStreams(InputStream[] in){
for (InputStream element: in) {
addInputStream(element);
}
}
/**
* Gets the current inputStream, looking at the next
* one in the list if the current one is null.
*
* @since ostermillerutils 1.04.01
*/
private InputStream getCurrentInputStream(){
if (currentInputStream == null && inputStreamQueueIndex < inputStreamQueue.size()){
synchronized(inputStreamQueue){
// inputStream queue index is advanced only by the nextInputStream()
// method. Don't do it here.
currentInputStream = inputStreamQueue.get(inputStreamQueueIndex);
}
}
return currentInputStream;
}
/**
* Indicate that we are done with the current inputStream and we should
* advance to the next inputStream.
*
* @since ostermillerutils 1.04.01
*/
private void advanceToNextInputStream(){
currentInputStream = null;
inputStreamQueueIndex++;
}
/**
* True iff this the close() method has been called on this stream.
*
* @since ostermillerutils 1.04.00
*/
private boolean closed = false;
/**
* Create a new input stream that can dynamically accept new sources.
*
* New sources should be added using the addInputStream() method.
* When all sources have been added the lastInputStreamAdded() should
* be called so that read methods can return -1 (end of stream).
*
* Adding new sources may by interleaved with read calls.
*
* @since ostermillerutils 1.04.01
*/
public ConcatInputStream(){
// Empty constructor
}
/**
* Create a new InputStream with one source.
*
* @param in InputStream to use as a source.
*
* @throws NullPointerException if in is null
*
* @since ostermillerutils 1.04.00
*/
public ConcatInputStream(InputStream in){
addInputStream(in);
lastInputStreamAdded();
}
/**
* Create a new InputStream with two sources.
*
* @param in1 first InputStream to use as a source.
* @param in2 second InputStream to use as a source.
*
* @throws NullPointerException if either source is null.
*
* @since ostermillerutils 1.04.00
*/
public ConcatInputStream(InputStream in1, InputStream in2){
addInputStream(in1);
addInputStream(in2);
lastInputStreamAdded();
}
/**
* Create a new InputStream with an arbitrary number of sources.
*
* @param in InputStreams to use as a sources.
*
* @throws NullPointerException if the input array on any element is null.
*
* @since ostermillerutils 1.04.00
*/
public ConcatInputStream(InputStream[] in){
addInputStreams(in);
lastInputStreamAdded();
}
/**
* Reads the next byte of data from the underlying streams. The value byte is
* returned as an int in the range 0 to 255. If no byte is available because
* the end of the stream has been reached, the value -1 is returned. This method
* blocks until input data is available, the end of the stream is detected, or
* an exception is thrown.
*
* If this class in not done accepting inputstreams and the end of the last known
* stream is reached, this method will block forever unless another thread
* adds an inputstream or interrupts.
*
* @return the next byte of data, or -1 if the end of the stream is reached.
*
* @throws IOException if an I/O error occurs.
*/
@Override public int read() throws IOException {
if (closed) throw new IOException("InputStream closed");
int r = -1;
while (r == -1){
InputStream in = getCurrentInputStream();
if (in == null){
if (doneAddingInputStreams) return -1;
try {
Thread.sleep(100);
} catch (InterruptedException iox){
throw new IOException("Interrupted");
}
} else {
r = in.read();
if (r == -1) advanceToNextInputStream();
}
}
return r;
}
/**
* Reads some number of bytes from the underlying streams and stores them into
* the buffer array b. The number of bytes actually read is returned as an
* integer. This method blocks until input data is available, end of file is
* detected, or an exception is thrown.
*
* If the length of b is zero,
* then no bytes are read and 0 is returned; otherwise, there is an attempt
* to read at least one byte.
*
* The read(b) method for class InputStream has the same effect as:
* read(b, 0, b.length)
*
* If this class in not done accepting inputstreams and the end of the last known
* stream is reached, this method will block forever unless another thread
* adds an inputstream or interrupts.
*
* @param b - Destination buffer
* @return The number of bytes read, or -1 if the end of the stream has been reached
*
* @throws IOException - If an I/O error occurs
* @throws NullPointerException - If b is null.
*
* @since ostermillerutils 1.04.00
*/
@Override public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
/**
* Reads up to length bytes of data from the underlying streams into an array of bytes.
* An attempt is made to read as many as length bytes, but a smaller number may be read,
* possibly zero. The number of bytes actually read is returned as an integer.
*
* If length is zero,
* then no bytes are read and 0 is returned; otherwise, there is an attempt
* to read at least one byte.
*
* This method blocks until input data is available
*
* If this class in not done accepting inputstreams and the end of the last known
* stream is reached, this method will block forever unless another thread
* adds an inputstream or interrupts.
*
* @param b Destination buffer
* @param off Offset at which to start storing bytes
* @param len Maximum number of bytes to read
* @return The number of bytes read, or -1 if the end of the stream has been reached
*
* @throws IOException - If an I/O error occurs
* @throws NullPointerException - If b is null.
* @throws IndexOutOfBoundsException - if length or offset are not possible.
*/
@Override public int read(byte[] b, int off, int len) throws IOException {
if (off < 0 || len < 0 || off + len > b.length) throw new IllegalArgumentException();
if (closed) throw new IOException("InputStream closed");
int r = -1;
while (r == -1){
InputStream in = getCurrentInputStream();
if (in == null){
if (doneAddingInputStreams) return -1;
try {
Thread.sleep(100);
} catch (InterruptedException iox){
throw new IOException("Interrupted");
}
} else {
r = in.read(b, off, len);
if (r == -1) advanceToNextInputStream();
}
}
return r;
}
/**
* Skips over and discards n bytes of data from this input stream. The skip method
* may, for a variety of reasons, end up skipping over some smaller number of bytes,
* possibly 0. This may result from any of a number of conditions; reaching end of
* file before n bytes have been skipped is only one possibility. The actual number
* of bytes skipped is returned. If n is negative, no bytes are skipped.
*
* If this class in not done accepting inputstreams and the end of the last known
* stream is reached, this method will block forever unless another thread
* adds an inputstream or interrupts.
*
* @param n he number of characters to skip
* @return The number of characters actually skipped
*
* @throws IOException If an I/O error occurs
*
* @since ostermillerutils 1.04.00
*/
@Override public long skip(long n) throws IOException {
if (closed) throw new IOException("InputStream closed");
if (n <= 0) return 0;
long s = -1;
while (s <= 0){
InputStream in = getCurrentInputStream();
if (in == null){
if (doneAddingInputStreams) return 0;
try {
Thread.sleep(100);
} catch (InterruptedException iox){
throw new IOException("Interrupted");
}
} else {
s = in.skip(n);
// When nothing was skipped it is a bit of a puzzle.
// The most common cause is that the end of the underlying
// stream was reached. In which case calling skip on it
// will always return zero. If somebody were calling skip
// until it skipped everything they needed, there would
// be an infinite loop if we were to return zero here.
// If we get zero, let us try to read one character so
// we can see if we are at the end of the stream. If so,
// we will move to the next.
if (s <= 0) {
// read() will advance to the next stream for us, so don't do it again
s = ((read()==-1)?-1:1);
}
}
}
return s;
}
/**
* Returns the number of bytes that can be read (or skipped over) from this input
* stream without blocking by the next caller of a method for this input stream.
* The next caller might be the same thread or or another thread.
*
* @throws IOException If an I/O error occurs
*
* @since ostermillerutils 1.04.00
*/
@Override public int available() throws IOException {
if (closed) throw new IOException("InputStream closed");
InputStream in = getCurrentInputStream();
if (in == null) return 0;
return in.available();
}
/**
* Closes this input stream and releases any system resources associated with the stream.
*
* @since ostermillerutils 1.04.00
*/
@Override public void close() throws IOException {
if (closed) return;
for (Object element: inputStreamQueue) {
((InputStream)element).close();
}
closed = true;
}
/**
* Mark not supported
*
* @since ostermillerutils 1.04.00
*/
@Override public void mark(int readlimit){
// Mark not supported -- do nothing
}
/**
* Reset not supported.
*
* @throws IOException because reset is not supported.
*
* @since ostermillerutils 1.04.00
*/
@Override public void reset() throws IOException {
throw new IOException("Reset not supported");
}
/**
* Does not support mark.
*
* @return false
*
* @since ostermillerutils 1.04.00
*/
@Override public boolean markSupported(){
return false;
}
}