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