# InputStreamWrapper.java

    /*
    ** Module : InputStreamWrapper.java
    ** Public : creates a Java InputStream which accesses a P2J Stream instance
    **          (which may be on a remote node)
    **
    ** Copyright (c) 2011-2021, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
    ** 001 GES 20110812 Created initial version.
    ** 002 CA  20210709 Refactored to read pristine bytes and not decoded characters.
    **     CA  20211001 Refactored read(byte[], off, len) to read the remote stream in chunks, as very large files
    **                  can OOME the FWD client.
    */
    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.util;

    import java.io.*;

    /**
     * Provides a Java <code>InputStream</code> which accesses a contained
     * {@link Stream} instance.  This allows classes that use the standard Java
     * I/O features (in package <code>java.io.*</code>) to transparently use
     * a P2J stream instead.  That stream may be on a remote node.
     */
    public class InputStreamWrapper
    extends InputStream
    {
       /**
        * The chunk to read at once from the remote {@link #stream}, via in the {@link #read(byte[], int, int)}
        * implementation.
        */
       private static final int READ_CHUNK_SIZE = 1024 * 1024; // 1MB

       /** The contained stream that does our real work. */
       private Stream stream = null;

       /** The marked position in the stream. */
       private long mark = -1;

       /**
        * Construct an instance which wrappers the given stream.
        *
        * @param    stream
        *           The P2J stream to wrap.
        */
       public InputStreamWrapper(Stream stream)
       {
          this.stream = stream;
       }

       /**
        * Get the wrapped FWD stream.
        *
        * @return   The {@link #stream}.
        */
       public Stream getStream()
       {
          return stream;
       }

       /**
        * Reads the next byte of data from the input stream. Although the byte is
        * returned as a 32-bit <code>int</code>, the actual value will always be
        * between 0 and 255 inclusive. When the end of the stream has been reached,
        * -1 is returned.
        * <p>
        * The underlying stream subclass determines the content of the result.
        * Byte oriented streams such as pipes or files will return a byte while
        * streams that generate keystrokes or characters may return a DBCS or
        * Unicode character. Any data that is greater in size than a byte will
        * be truncated!
        * <p>
        * This method blocks waiting for the data, the end of the stream or some
        * exceptional condition.
        *
        * @return   The next byte of data or -1 when the end of the stream is
        *           reached.
        */
       public int read()
       throws IOException
       {
          if (stream == null)
          {
             return -1;
          }

          int result = stream.readByte();

          switch (result)
          {
             case -1:
                // error
                throw new IOException("Failure during stream read.");
             case -2:
                // EOF
                result = -1;
                break;
          }

          return result;
       }

       /**
        * Read a chunk of bytes from the current read position in the stream to the
        * offset calculated by given length or to the <code>EOF</code>, whichever
        * comes first. The <code>EOF</code> character is not returned.
        *
        * @param    array
        *           The array in which to save bytes.
        * @param    off
        *           The offset into the array at which to start saving bytes.
        * @param    len
        *           The maximum number of bytes to read.
        *
        * @return   The number of bytes read or -1 if <code>EOF</code> has been
        *           reached). The number of bytes read will be the smaller
        *           of the <code>len</code> parameter or the actual bytes left
        *           before the <code>EOF</code>.
        *
        * @throws   IOException
        *           If an I/O error occurs.
        */
       public int read(byte[] array, int off, int len)
       throws IOException
       {
          if (stream == null)
          {
             return -1;
          }

          ByteArrayOutputStream baos = new ByteArrayOutputStream(len);

          int remaining = len;
          while (remaining > 0)
          {
             // read at least 1MB of data and at most the remaining bytes
             byte[] chunk = stream.readBytes(Math.min(remaining, READ_CHUNK_SIZE));
             if (chunk == null)
             {
                if (remaining == len)
                {
                   // couldn't read anything
                   return -1;
                }

                // something was read, EOF reached
                break;
             }

             remaining -= chunk.length;
             baos.write(chunk);
          }

          byte[] data = baos.toByteArray();
          System.arraycopy(data, 0, array, off, data.length);

          return data.length;
       }

       /**
        * Obtains an estimate of the number of bytes that can be read from this
        * stream without blocking.  This number is not reliable in an absolute
        * sense, but it does specify a minimum that can be used.
        *
        * @return   The minimum number of bytes immediately.
        */
       public int available()
       throws IOException
       {
          if (stream == null)
          {
             throw new IOException("Stream is not open.");
          }

          return (int) stream.available();
       }

       /**
        * Closes the input stream and releases OS resources associated with it.
        */
       public void close()
       {
          if (stream != null)
          {
             stream.closeIn();
             stream = null;
          }
       }

       /**
        * Mark the current position in the stream.
        *
        * @param    readlimit
        *           This argument is not used.
        */
       @Override
       public synchronized void mark(int readlimit)
       {
          try
          {
             mark = stream == null ? -1l : stream.getPos();
          }
          catch (UnsupportedOperationException |
                 IOException e)
          {
             mark = -1;
          }
       }

       /**
        * Reset the stream to the marked position.
        */
       @Override
       public synchronized void reset()
       throws IOException
       {
          if (stream == null)
          {
             throw new IOException("Stream is not open.");
          }

          if (mark == -1)
          {
             throw new IOException("Stream is not marked.");
          }

          stream.setPos(mark);
       }

       /**
        * Tests if this stream supports the mark mode.
        *
        * @return  Always<code>true</code>.
        */
       @Override
       public boolean markSupported()
       {
          return true;
       }
    }