ReversibleReader.java

/*
** Module   : ReversibleReader.java
** Abstract : a reader which allows its input to be unread
**
** Copyright (c) 2008-2017, Golden Code Development Corporation.
**
** -#- -I- --Date-- -T- --JPRM-- ----------------Description-----------------
** 001 GES 20081015 NEW   @40316 First version which provides a reader which
**                               can unread its input. This is similar in
**                               function to the J2SE PushbackReader but it
**                               is designed with the ability for an external
**                               user to inspect and obtain unread data
**                               separately from data that has not yet been
**                               read.
*/
/*
** 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.io;

import java.io.*;
import java.util.*;

/**
 * Implements a reader can unread its input. This is similar in function to
 * the J2SE PushbackReader but it is designed with the ability for an external
 * user to inspect and obtain unread data separately from data that has not
 * yet been read.
 */
public class ReversibleReader
extends FilterReader
{
   /** Number of elements by which to grow or shrink the array. */
   private static final int INCREMENT = 1024;
   
   /** Number of empty elements at which the array will be shrunk. */
   private static final int THRESHOLD = INCREMENT * 2;
   
   /** LIFO queue for data that has been "unread". */
   private char[] queue = null;
   
   /** Index of the "front" of the queue (the next char to be read). */
   private int idx = -1;
   
   /**
    * Creates an instance with the specified reader as the primary data
    * source.
    *
    * @param    reader
    *           The primary data source.
    */
   public ReversibleReader(Reader reader)
   {
      super(reader);
   }

   /**
    * Queue the given data on a last-in first-out (LIFO) basis.
    *
    * @param   ch
    *          The character to be unread.
    */
   public void unread(int ch)
   {
      synchronized (lock)
      {
         // allocate space as needed for 1 character
         grow(1);
         
         // copy the character into the array and maintain the index
         queue[++idx] = (char) ch;
      }
   }

   /**
    * Queue the given data on a last-in first-out (LIFO) basis.
    *
    * @param   buf
    *          The data to be unread.
    */
   public void unread(char[] buf)
   {
      synchronized (lock)
      {
         // allocate space as needed
         grow(buf.length);
         
         // copy the data into the array (we must reverse the order of
         // the data to maintain a proper LIFO)
         for (int i = buf.length - 1; i >= 0; i--)
         {
            queue[++idx] = buf[i];
         }
      }
   }
   
   /**
    * Reports on the current amount of queued data.  Queued data is that
    * which has been explicitly {@link #unread} AND which also has not yet
    * been re-read using {@link #read}.
    *
    * @return   The size of the queue.
    */
   public int queueSize()
   {
      synchronized (lock)
      {
         return idx + 1;
      }
   }
   
   /**
    * Obtain the current contents of the queued (unread) data and clear
    * the queue completely.
    *
    * @return   All queued data or <code>null</code> if there is no queued
    *           data.
    */
   public char[] clear()
   {
      char[] buffer = null;
      
      synchronized (lock)
      {
         // check if there is a queue allocated
         if (queue != null)
         {
            // check if there is data in the queue
            if (idx >= 0)
            {
               int sz = idx + 1;
               
               buffer = new char[sz];
               
               // copy the data into the array (we must reverse the order of
               // the data to convert back from LIFO order)
               for (int i = 0; i < sz; i++)
               {
                  buffer[i] = queue[idx--];
               }
            }
            
            // clear the queue
            queue = null;
            idx   = -1;
         }
      }
      
      return buffer;
   }

   /**
    * Reads the next byte from the current stream. If no stream was
    * switched to, returns <code>-1</code> as the indication of the
    * <code>EOF</code> condition.
    *
    * @return   The character read, as an integer in the range 0 to 65535
    *           (0x0000-0xFFFF), or -1 if the end of the stream has been
    *           reached or if no reader was switched to.
    *
    * @throws   IOException
    *           If any error occurs in reading from the underlying reader.
    */
   public int read()
   throws IOException
   {
      int ch = -1;
      
      synchronized (lock)
      {
         if (queue != null && idx >= 0)
         {
            // read queued data and maintain index
            ch = queue[idx--];
            
            // reduce memory usage if we have too much space
            shrink();
         }
         else
         {
            // read from the stream
            if (super.in != null)
            {
               ch = super.read();
            }
         }
      }
      
      return ch;
   }
   
   /**
    * Reads characters into a portion of an array.
    *
    * @param    buf
    *           Destination buffer
    * @param    off
    *           Offset at which to start writing characters
    * @param    len
    *           Maximum number of characters to read
    *
    * @return   The number of characters read, or -1 if the end of the
    *           stream has been reached
    *
    * @throws   IOException
    *           If an I/O error occurs
    */
   public int read(char buf[], int off, int len)
   throws IOException
   {
      int num = 0;
      
      if (len > 0)
      {
         synchronized (lock)
         {
            if (off < 0 || off > buf.length)
            {
               throw new IndexOutOfBoundsException("Invalid offset " + off);
            }
            
            if (off + len > buf.length)
            {
               // limit max number of read chars to the space available
               len = buf.length - off;
            }
            
            int remain = len;
            
            if (queue != null && idx >= 0)
            {
               int start  = 0;
               int amount = 0;
               
               int sz = idx + 1;
               
               if (remain <= sz)
               {
                  // there is enough queued data to satisfy the request
                  start  = sz - remain;
                  amount = remain;
               }
               else
               {
                  // we can only partially satisfy the request so copy
                  // everything
                  start  = 0;
                  amount = sz;
               }
               
               // read queued data
               System.arraycopy(queue, start, buf, off, amount);
               
               // maintain the offset
               off += amount;
               
               // reduce the amount of additional data needed
               remain -= amount;
               
               // remember that we read some data
               num = amount;
               
               // maintain index
               idx -= amount;
               
               if (idx < 0)
               {
                  queue = null;
                  idx   = -1;
               }
               else
               {
                  // reduce memory usage if we have too much space
                  shrink();
               }
            }
            
            if (remain > 0)
            {
               // read from the stream
               int num2 = super.read(buf, off, remain);
               
               if (num2 == -1)
               {
                  // EOF from the backing stream
                  if (num == 0)
                  {
                     // we didn't read any data so return EOF
                     num = -1;
                  }
               }
               else
               {
                  // we read from the stream
                  num += num2;
               }
            }
         }
      }
      
      return num;
   }
   
   /**
    * Tells whether this stream is ready to be read.
    *
    * @return   <code>true</code> if a read request will not block.
    *
    * @throws   IOException
    *           If an I/O error occurs
    */
   public boolean ready()
   throws IOException
   {
      synchronized (lock)
      {
         return (idx >= 0 || super.ready());
      }
   }
   
   /**
    * Closes the stream and releases any system resources associated with
    * it.
    * <p>
    * Closing a previously closed stream has no effect.
    *
    * @throws   IOException
    *           If an I/O error occurs
    */
   public void close()
   throws IOException
   {
      synchronized (lock)
      {
         queue = null;
         idx   = -1;
         super.close();
      }
   }
   
   /**
    * Grow the array by the standard increment amount if it is full. This
    * doesn't change the number of unused elements.
    *
    * @param    amount
    *           The amount of space needed.
    */
   private void grow(int amount)
   {
      if (queue == null)
      {
         // allocate a new array when needed
         queue = new char[nextIncrement(amount)];
         idx   = -1;
      }
      else
      {
         int newsize = idx + 1 + amount;
      
         // is there room?
         if (newsize >= queue.length)
         {
            // reallocate the array to a size sufficient to hold the
            // expected contents AND copy the current contents
            queue = Arrays.copyOf(queue, nextIncrement(newsize));
         }
      }
   }
   
   /**
    * Shrink the array to the smallest multiple of the standard increment
    * amount if there is enough space to reclaim while still leaving some
    * open slots (so new additions won't cause array growth immediately).
    * This doesn't change the number of unused elements.
    */
   private void shrink()
   {
      int size = idx + 1;
      
      if (size < (queue.length - THRESHOLD))
      {
         queue = Arrays.copyOf(queue, nextIncrement(size));
      }
   }
   
   /**
    * Calculate the next multiple of INCREMENT that can hold the given
    * <code>size</code> elements.
    *
    * @param    size
    *           The amount of space needed.
    *
    * @return   The nearest multiple of INCREMENT that is larger than the
    *           given <code>size</code>.
    */
   private int nextIncrement(int size)
   {
      return ((size / INCREMENT) + 1) * INCREMENT;
   }
}