/** * Copyright (C) 2014-2020 Philip Helger (www.helger.com) * philip[at]helger[dot]com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.helger.commons.io.stream; import java.io.FilterReader; import java.io.IOException; import java.io.Reader; import javax.annotation.Nonnegative; import javax.annotation.Nonnull; import com.helger.commons.ValueEnforcer; /** * A non-synchronized copy of the class {@link java.io.PushbackReader}. * * @author Philip Helger * @see java.io.PushbackReader */ public class NonBlockingPushbackReader extends FilterReader { /** Pushback buffer */ private char [] m_aBuf; /** Current position in buffer */ private int m_nBufPos; /** * Creates a new pushback reader with a pushback buffer of the given size. * * @param aReader * The reader from which characters will be read * @param nSize * The size of the pushback buffer * @exception IllegalArgumentException * if size is ≤ 0 */ public NonBlockingPushbackReader (@Nonnull final Reader aReader, @Nonnegative final int nSize) { super (aReader); ValueEnforcer.isGT0 (nSize, "Size"); m_aBuf = new char [nSize]; m_nBufPos = nSize; } /** * Creates a new pushback reader with a one-character pushback buffer. * * @param aReader * The reader from which characters will be read */ public NonBlockingPushbackReader (@Nonnull final Reader aReader) { this (aReader, 1); } /** * Checks to make sure that the stream has not been closed. */ private void _ensureOpen () throws IOException { if (m_aBuf == null) throw new IOException ("Reader closed"); } /** * @return The number of chars currently in the "unread" buffer. */ @Nonnegative public int getUnreadCount () { return m_aBuf.length - m_nBufPos; } /** * @return <code>true</code> if at least one "unread" char is present. */ public boolean hasUnreadChars () { return m_nBufPos < m_aBuf.length; } /** * Reads a single character. * * @return The character read, or -1 if the end of the stream has been reached * @exception IOException * If an I/O error occurs */ @Override public int read () throws IOException { _ensureOpen (); if (m_nBufPos < m_aBuf.length) return m_aBuf[m_nBufPos++]; return super.read (); } /** * Reads characters into a portion of an array. * * @param aBuf * Destination buffer * @param nOfs * Offset at which to start writing characters * @param nLen * Maximum number of characters to read * @return The number of characters read, or -1 if the end of the stream has * been reached * @exception IOException * If an I/O error occurs */ @Override public int read (@Nonnull final char [] aBuf, @Nonnegative final int nOfs, @Nonnegative final int nLen) throws IOException { ValueEnforcer.isArrayOfsLen (aBuf, nOfs, nLen); _ensureOpen (); if (nLen == 0) return 0; try { int nRealOfs = nOfs; int nRealLen = nLen; int nBufAvail = m_aBuf.length - m_nBufPos; if (nBufAvail > 0) { if (nRealLen < nBufAvail) nBufAvail = nRealLen; System.arraycopy (m_aBuf, m_nBufPos, aBuf, nRealOfs, nBufAvail); m_nBufPos += nBufAvail; nRealOfs += nBufAvail; nRealLen -= nBufAvail; } if (nRealLen > 0) { nRealLen = super.read (aBuf, nRealOfs, nRealLen); if (nRealLen == -1) return (nBufAvail == 0) ? -1 : nBufAvail; return nBufAvail + nRealLen; } return nBufAvail; } catch (final ArrayIndexOutOfBoundsException e) { throw new IndexOutOfBoundsException (); } } /** * Pushes back a single character by copying it to the front of the pushback * buffer. After this method returns, the next character to be read will have * the value <code>(char)c</code>. * * @param c * The int value representing a character to be pushed back * @exception IOException * If the pushback buffer is full, or if some other I/O error * occurs */ public void unread (final int c) throws IOException { _ensureOpen (); if (m_nBufPos == 0) throw new IOException ("Pushback buffer overflow"); m_aBuf[--m_nBufPos] = (char) c; } /** * Pushes back a portion of an array of characters by copying it to the front * of the pushback buffer. After this method returns, the next character to be * read will have the value <code>cbuf[off]</code>, the character after that * will have the value <code>cbuf[off+1]</code>, and so forth. * * @param aBuf * Character array * @param nOfs * Offset of first character to push back * @param nLen * Number of characters to push back * @exception IOException * If there is insufficient room in the pushback buffer, or if some * other I/O error occurs */ public void unread (@Nonnull final char [] aBuf, @Nonnegative final int nOfs, @Nonnegative final int nLen) throws IOException { ValueEnforcer.isArrayOfsLen (aBuf, nOfs, nLen); _ensureOpen (); if (nLen > m_nBufPos) throw new IOException ("Pushback buffer overflow"); m_nBufPos -= nLen; System.arraycopy (aBuf, nOfs, m_aBuf, m_nBufPos, nLen); } /** * Pushes back an array of characters by copying it to the front of the * pushback buffer. After this method returns, the next character to be read * will have the value <code>cbuf[0]</code>, the character after that will * have the value <code>cbuf[1]</code>, and so forth. * * @param aBuf * Character array to push back * @exception IOException * If there is insufficient room in the pushback buffer, or if some * other I/O error occurs */ public void unread (@Nonnull final char [] aBuf) throws IOException { unread (aBuf, 0, aBuf.length); } /** * Tells whether this stream is ready to be read. * * @exception IOException * If an I/O error occurs */ @Override public boolean ready () throws IOException { _ensureOpen (); return (m_nBufPos < m_aBuf.length) || super.ready (); } /** * Marks the present position in the stream. The <code>mark</code> for class * <code>PushbackReader</code> always throws an exception. * * @exception IOException * Always, since mark is not supported */ @Override public void mark (final int readAheadLimit) throws IOException { throw new IOException ("mark/reset not supported"); } /** * Resets the stream. The <code>reset</code> method of * <code>PushbackReader</code> always throws an exception. * * @exception IOException * Always, since reset is not supported */ @Override public void reset () throws IOException { throw new IOException ("mark/reset not supported"); } /** * Tells whether this stream supports the mark() operation, which it does not. */ @Override public boolean markSupported () { return false; } /** * Closes the stream and releases any system resources associated with it. * Once the stream has been closed, further read(), unread(), ready(), or * skip() invocations will throw an IOException. Closing a previously closed * stream has no effect. * * @exception IOException * If an I/O error occurs */ @Override public void close () throws IOException { super.close (); m_aBuf = null; } /** * Skips characters. This method will block until some characters are * available, an I/O error occurs, or the end of the stream is reached. * * @param nSkip * The number of characters to skip. Must be ≥ 0. * @return The number of characters actually skipped * @exception IllegalArgumentException * If <code>n</code> is negative. * @exception IOException * If an I/O error occurs */ @Override public long skip (final long nSkip) throws IOException { ValueEnforcer.isGE0 (nSkip, "SkipValue"); _ensureOpen (); if (nSkip == 0) return 0L; long nRealSkip = nSkip; final int nBufAvail = m_aBuf.length - m_nBufPos; if (nBufAvail > 0) { if (nRealSkip <= nBufAvail) { m_nBufPos += nRealSkip; return nRealSkip; } m_nBufPos = m_aBuf.length; nRealSkip -= nBufAvail; } return nBufAvail + super.skip (nRealSkip); } }