package net.contrapunctus.rngzip.util;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.HashMap;
import java.util.LinkedList;
/**
* This cless reads multiple logical streams of data from a single
* underlying input stream. Each embedded stream is identified by a
* small integer. To begin reading an embedded stream, you must first
* open it.
*
* <p class='license'>This is free software; you may modify and/or
* redistribute it under the terms of the GNU General Public License,
* but it comes with <b>absolutely no warranty.</b>
*
* @author Christopher League
* @see MultiplexOutputStream
*/
public final class MultiplexInputStream implements Closeable
{
/* The underlying data stream. After this stream is closed, we set
‘in’ to null and forbid any rfurther operations on this or any
embedded streams. */
private DataInputStream in;
private boolean close_p;
private final void check()
{
if(in == null)
throw new IllegalStateException("Stream already closed.");
}
/* The application-defined magic/version number read from the
stream. */
private int magic;
/* Used for decoding block headers. */
private MultiplexBlockRep block;
/* A map from stream ID to an embedded input stream. If it’s not
in this map, it hasn’t been opened yet. */
private HashMap<Integer,EmbeddedIS> map =
new HashMap<Integer,EmbeddedIS>();
private static final boolean DEBUG = false;
private static final boolean TRACE_MEM =
System.getProperty("TRACE_MEM") != null;
private static final PrintStream dbg = System.err;
/**
* The primary constructor for objects of this class.
*
* @param in the underlying data input streams. Embedded streams
* opened with this class are read from ‘in’.
* @param close_p determines whether closing this stream also
* closes the underlying stream.
* @throws IllegalArgumentException if ‘in’ is null.
* @throws IOException if there is a problem reading header
* information from the underlying stream.
* @throws MultiplexFormatException if this does not appear to be a
* valid multiplex input stream.
*/
public MultiplexInputStream(DataInputStream in, boolean close_p)
throws IOException
{
this.in = in;
this.close_p = close_p;
this.block = new MultiplexBlockRep(in);
if(in.readInt() != MultiplexOutputStream.MAGIC)
throw MultiplexFormatException.badMagic();
magic = in.readInt();
}
/**
* Convenience constructor for a normal input stream. It
* constructs a <code>DataInputStream</code> around ‘in’ for you.
*/
public MultiplexInputStream(InputStream in, boolean close_p)
throws IOException
{
this(new DataInputStream(in), close_p);
}
/**
* Convenience constructor for when ‘close_p’ is true (meaning that
* this class <i>is</i> responsible for closing ‘in’.
*/
public MultiplexInputStream(DataInputStream in) throws IOException
{
this(in, true);
}
/**
* Convenience constructor for a normal input stream that also
* automatically sets ‘close_p’ to true.
*/
public MultiplexInputStream(InputStream in) throws IOException
{
this(new DataInputStream(in), true);
}
/**
* Retrieve the application-level magic/version number read from
* the stream. This is the 4-byte ‘magic’ value provided to the
* <code>MultiplexOutputStream</code> class when it was created.
*/
public int magic()
{
return magic;
}
/**
* Open a new embedded stream. Unlike its counterpart in
* <code>MultiplexOutputStream</code>, clients may wrap the
* returned <code>InputStream</code> in whatever filters are needed
* without informing this class.
*
* @param streamID the identifier of the stream to open.
* @return an input stream that will read only those blocks marked
* with ‘streamID’.
* @throws IllegalArgumentException if ‘streamID’ is out of range.
* @throws IllegalStateException if the stream is already closed.
* @see MultiplexBlockRep#MAX_STREAM_ID
* @see MultiplexOutputStream#open(int, OutputStreamFilter)
*/
public InputStream open(int streamID)
{
MultiplexBlockRep.checkStreamID(streamID);
check();
return getStream(streamID);
}
/* Use the map to fetch a particular stream, or create it if it
doesn’t exist yet. */
private EmbeddedIS getStream(int streamID)
{
EmbeddedIS eis = map.get(streamID);
if(eis == null) {
eis = new EmbeddedIS(streamID);
map.put(streamID, eis);
}
return eis;
}
/**
* Close the multiplex stream. This closes the underlying input
* stream if this object was created with ‘close_p’ set to true.
* No further operations on this object will be permitted.
* @throws IllegalStateException if the stream is already closed.
* @throws IOException if there is a problem closing the underlying
* stream.
*/
public void close() throws IOException
{
check();
if(close_p) in.close();
in = null;
block = null;
map.clear();
map = null;
}
private final class EmbeddedIS extends InputStream
{
private int streamID;
/* A queue of blocks read from the underlying stream. */
private LinkedList<byte[]> queue = new LinkedList<byte[]>();
/* The current block is wrapped in this input stream: */
private ByteArrayInputStream bin;
/* Get ready to read data, or return false if nothing remains. */
private boolean prepare() throws IOException
{
if(bin != null && bin.available() > 0) {
/* Some bytes still remain in ‘bin’. */
return true;
}
/* bin is empty; get more data from the queue. */
if(queue.size() < 1) {
/* queue is empty, read from underlying stream */
readMore(streamID);
}
/* Now queue should be ready */
byte[] bs = queue.poll();
if(bs == null || bs.length == 0) {
/* There was nothing left for this stream. */
bin = null;
return false;
}
bin = new ByteArrayInputStream(bs);
return true;
}
private EmbeddedIS(int streamID)
{
this.streamID = streamID;
}
/* This is the InputStream method that reads a single byte,
* or returns -1 on EOF.
*/
public int read() throws IOException
{
if(prepare()) return bin.read();
else return -1; // EOF
}
/* Returns the number of bytes that can be read “without
* blocking”. Here, we interpret this as the number of bytes
* KNOWN to be available (in ‘bin’ and ‘queue’) without going
* down to the underlying stream.
*/
public int available() throws IOException
{
int n = 0;
if(bin != null) {
n = bin.available();
}
for(byte[] bs : queue) {
n += bs.length;
}
return n;
}
/* Subclasses of InputStream are encouraged to override the
* following methods with more efficient versions... so here
* they are. To read an array, we are happy reading up as much
* as possible from ‘bin’. Further data in ‘queue’ or in the
* underlying stream will wait for the next call.
*/
public int read(byte[] b, int off, int len) throws IOException
{
if(prepare()) return bin.read(b, off, len);
else return -1;
}
public long skip(long n) throws IOException
{
if(prepare()) return bin.skip(n);
else return 0;
}
}
/* We need to read blocks from the underlying stream. As they are
* read, queue them onto the appropriate embedded stream queues.
* Stop when we hit a block from streamID, which initiated the
* call.
*/
private void readMore(int streamID) throws IOException
{
check();
if(DEBUG) {
dbg.printf("*** Asked to read more from stream %d%n", streamID);
}
do {
int sz;
try { sz = block.decode(); }
catch(EOFException x) { return; } // this EOF is okay
assert sz > 0;
if(DEBUG) {
dbg.printf("*** Found stream %d with %d bytes.%n",
block.streamID, sz);
}
byte buf[] = new byte[sz];
try { in.readFully(buf); }
catch(EOFException x) { // this EOF indicates an error
throw MultiplexFormatException.endOfStream(sz);
}
getStream(block.streamID).queue.offer(buf);
} while(block.streamID != streamID);
if(TRACE_MEM) reportMemoryStats();
}
private void reportMemoryStats()
{
int streamCount = 0, byteCount = 0;
for(EmbeddedIS is : map.values()) {
streamCount++;
for(byte[] bs : is.queue) {
byteCount += bs.length;
}
}
System.err.printf("MEM: MuxInputStream: %d bytes in %d streams%n",
byteCount, streamCount);
}
}