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;
   }
}