CharReadBuffer.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.geometry.io.core.internal;

  18. import java.io.IOException;
  19. import java.io.Reader;
  20. import java.util.Objects;

  21. /** Class used to buffer characters read from an underlying {@link Reader}.
  22.  * Characters can be consumed from the buffer, examined without being consumed,
  23.  * and pushed back onto the buffer. The internal bufer is resized as needed.
  24.  */
  25. public class CharReadBuffer {

  26.     /** Constant indicating that the end of the input has been reached. */
  27.     private static final int EOF = -1;

  28.     /** Default initial buffer capacity. */
  29.     private static final int DEFAULT_INITIAL_CAPACITY = 512;

  30.     /** Log 2 constant. */
  31.     private static final double LOG2 = Math.log(2);

  32.     /** Underlying reader instance. */
  33.     private final Reader reader;

  34.     /** Character buffer. */
  35.     private char[] buffer;

  36.     /** The index of the head element in the buffer. */
  37.     private int head;

  38.     /** The number of valid elements in the buffer. */
  39.     private int count;

  40.     /** True when the end of reader content is reached. */
  41.     private boolean reachedEof;

  42.     /** Minimum number of characters to request for each read. */
  43.     private final int minRead;

  44.     /** Construct a new instance that buffers characters from the given reader.
  45.      * @param reader underlying reader instance
  46.      * @throws NullPointerException if {@code reader} is null
  47.      */
  48.     public CharReadBuffer(final Reader reader) {
  49.         this(reader, DEFAULT_INITIAL_CAPACITY);
  50.     }

  51.     /** Construct a new instance that buffers characters from the given reader.
  52.      * @param reader underlying reader instance
  53.      * @param initialCapacity the initial capacity of the internal buffer; the buffer
  54.      *      is resized as needed
  55.      * @throws NullPointerException if {@code reader} is null
  56.      * @throws IllegalArgumentException if {@code initialCapacity} is less than one.
  57.      */
  58.     public CharReadBuffer(final Reader reader, final int initialCapacity) {
  59.         this(reader, initialCapacity, (initialCapacity + 1) / 2);
  60.     }

  61.     /** Construct a new instance that buffers characters from the given reader.
  62.      * @param reader underlying reader instance
  63.      * @param initialCapacity the initial capacity of the internal buffer; the buffer
  64.      *      is resized as needed
  65.      * @param minRead the minimum number of characters to request from the reader
  66.      *      when fetching more characters into the buffer; this can be used to limit the
  67.      *      number of calls made to the reader
  68.      * @throws NullPointerException if {@code reader} is null
  69.      * @throws IllegalArgumentException if {@code initialCapacity} or {@code minRead}
  70.      *      are less than one.
  71.      */
  72.     public CharReadBuffer(final Reader reader, final int initialCapacity, final int minRead) {
  73.         Objects.requireNonNull(reader, "Reader cannot be null");
  74.         if (initialCapacity < 1) {
  75.             throw new IllegalArgumentException("Initial buffer capacity must be greater than 0; was " +
  76.                     initialCapacity);
  77.         }
  78.         if (minRead < 1) {
  79.             throw new IllegalArgumentException("Min read value must be greater than 0; was " +
  80.                     minRead);
  81.         }

  82.         this.reader = reader;
  83.         this.buffer = new char[initialCapacity];
  84.         this.minRead = minRead;
  85.     }

  86.     /** Return true if more characters are available from the read buffer.
  87.      * @return true if more characters are available from the read buffer
  88.      * @throws java.io.UncheckedIOException if an I/O error occurs
  89.      */
  90.     public boolean hasMoreCharacters() {
  91.         return makeAvailable(1) > 0;
  92.     }

  93.     /** Attempt to make at least {@code n} characters available in the buffer, reading
  94.      * characters from the underlying reader as needed. The number of characters available
  95.      * is returned.
  96.      * @param n number of characters requested to be available
  97.      * @return number of characters available for immediate use in the buffer
  98.      * @throws java.io.UncheckedIOException if an I/O error occurs
  99.      */
  100.     public int makeAvailable(final int n) {
  101.         final int diff = n - count;
  102.         if (diff > 0) {
  103.             readChars(diff);
  104.         }
  105.         return count;
  106.     }

  107.     /** Remove and return the next character in the buffer.
  108.      * @return the next character in the buffer or {@value #EOF}
  109.      *      if the end of the content has been reached
  110.      * @throws java.io.UncheckedIOException if an I/O error occurs
  111.      * @see #peek()
  112.      */
  113.     public int read() {
  114.         final int result = peek();
  115.         charsRemoved(1);

  116.         return result;
  117.     }

  118.     /** Remove and return a string from the buffer. The length of the string will be
  119.      * the number of characters available in the buffer up to {@code len}. Null is
  120.      * returned if no more characters are available.
  121.      * @param len requested length of the string
  122.      * @return a string from the read buffer or null if no more characters are available
  123.      * @throws IllegalArgumentException if {@code len} is less than 0
  124.      * @throws java.io.UncheckedIOException if an I/O error occurs
  125.      * @see #peekString(int)
  126.      */
  127.     public String readString(final int len) {
  128.         final String result = peekString(len);
  129.         if (result != null) {
  130.             charsRemoved(result.length());
  131.         }

  132.         return result;
  133.     }

  134.     /** Return the next character in the buffer without removing it.
  135.      * @return the next character in the buffer or {@value #EOF}
  136.      *      if the end of the content has been reached
  137.      * @throws java.io.UncheckedIOException if an I/O error occurs
  138.      * @see #read()
  139.      */
  140.     public int peek() {
  141.         if (makeAvailable(1) < 1) {
  142.             return EOF;
  143.         }
  144.         return buffer[head];
  145.     }

  146.     /** Return a string from the buffer without removing it. The length of the string will be
  147.      * the number of characters available in the buffer up to {@code len}. Null is
  148.      * returned if no more characters are available.
  149.      * @param len requested length of the string
  150.      * @return a string from the read buffer or null if no more characters are available
  151.      * @throws IllegalArgumentException if {@code len} is less than 0
  152.      * @throws java.io.UncheckedIOException if an I/O error occurs
  153.      * @see #readString(int)
  154.      */
  155.     public String peekString(final int len) {
  156.         if (len < 0) {
  157.             throw new IllegalArgumentException("Requested string length cannot be negative; was " + len);
  158.         } else if (len == 0) {
  159.             return hasMoreCharacters() ?
  160.                     "" :
  161.                     null;
  162.         }

  163.         final int available = makeAvailable(len);
  164.         final int resultLen = Math.min(len, available);
  165.         if (resultLen < 1) {
  166.             return null;
  167.         }

  168.         final int contiguous = Math.min(buffer.length - head, resultLen);
  169.         final int remaining = resultLen - contiguous;

  170.         String result = String.valueOf(buffer, head, contiguous);
  171.         if (remaining > 0) {
  172.             result += String.valueOf(buffer, 0, remaining);
  173.         }

  174.         return result;
  175.     }

  176.     /** Get the character at the given buffer index or {@value #EOF} if the index
  177.      * is past the end of the content. The character is not removed from the buffer.
  178.      * @param index index of the character to receive relative to the buffer start
  179.      * @return the character at the given index of {@code -1} if the character is
  180.      *      past the end of the stream content
  181.      * @throws java.io.UncheckedIOException if an I/O exception occurs
  182.      */
  183.     public int charAt(final int index) {
  184.         if (index < 0) {
  185.             throw new IllegalArgumentException("Character index cannot be negative; was " + index);
  186.         }
  187.         final int requiredSize = index + 1;
  188.         if (makeAvailable(requiredSize) < requiredSize) {
  189.             return EOF;
  190.         }

  191.         return buffer[(head + index) % buffer.length];
  192.     }

  193.     /** Skip {@code n} characters from the stream. Characters are first skipped from the buffer
  194.      * and then from the underlying reader using {@link Reader#skip(long)} if needed.
  195.      * @param n number of character to skip
  196.      * @return the number of characters skipped
  197.      * @throws IllegalArgumentException if {@code n} is negative
  198.      * @throws java.io.UncheckedIOException if an I/O error occurs
  199.      */
  200.     public int skip(final int n) {
  201.         if (n < 0) {
  202.             throw new IllegalArgumentException("Character skip count cannot be negative; was " + n);
  203.         }

  204.         // skip buffered content first
  205.         int skipped = Math.min(n, count);
  206.         charsRemoved(skipped);

  207.         // skip from the reader if required
  208.         final int remaining = n - skipped;
  209.         if (remaining > 0) {
  210.             try {
  211.                 skipped += (int) reader.skip(remaining);
  212.             } catch (IOException exc) {
  213.                 throw GeometryIOUtils.createUnchecked(exc);
  214.             }
  215.         }

  216.         return skipped;
  217.     }

  218.     /** Push a character back onto the read buffer. The argument will
  219.      * be the next character returned by {@link #read()} or {@link #peek()}.
  220.      * @param ch character to push onto the read buffer
  221.      */
  222.     public void push(final char ch) {
  223.         ensureCapacity(count + 1);
  224.         pushCharInternal(ch);
  225.     }

  226.     /** Push a string back onto the read buffer. The first character
  227.      * of the string will be the next character returned by
  228.      * {@link #read()} or {@link #peek()}.
  229.      * @param str string to push onto the read buffer
  230.      */
  231.     public void pushString(final String str) {
  232.         final int len = str.length();

  233.         ensureCapacity(count + len);
  234.         for (int i = len - 1; i >= 0; --i) {
  235.             pushCharInternal(str.charAt(i));
  236.         }
  237.     }

  238.     /** Internal method to push a single character back onto the read
  239.      * buffer. The buffer capacity is <em>not</em> checked.
  240.      * @param ch character to push onto the read buffer
  241.      */
  242.     private void pushCharInternal(final char ch) {
  243.         charsPushed(1);
  244.         buffer[head] = ch;
  245.     }

  246.     /** Read characters from the underlying character stream into
  247.      * the internal buffer.
  248.      * @param n minimum number of characters requested to be placed
  249.      *      in the buffer
  250.      * @throws java.io.UncheckedIOException if an I/O error occurs
  251.      */
  252.     private void readChars(final int n) {
  253.         if (!reachedEof) {
  254.             int remaining = Math.max(n, minRead);

  255.             ensureCapacity(count + remaining);

  256.             try {
  257.                 int tail;
  258.                 int len;
  259.                 int read;
  260.                 while (remaining > 0) {
  261.                     tail = (head + count) % buffer.length;
  262.                     len = Math.min(buffer.length - tail, remaining);

  263.                     read = reader.read(buffer, tail, len);
  264.                     if (read == EOF) {
  265.                         reachedEof = true;
  266.                         break;
  267.                     }

  268.                     charsAppended(read);
  269.                     remaining -= read;
  270.                 }
  271.             } catch (IOException exc) {
  272.                 throw GeometryIOUtils.createUnchecked(exc);
  273.             }
  274.         }
  275.     }

  276.     /** Method called to indicate that characters have been removed from
  277.      * the front of the read buffer.
  278.      * @param n number of characters removed
  279.      */
  280.     private void charsRemoved(final int n) {
  281.         head = (head + n) % buffer.length;
  282.         count -= n;
  283.     }

  284.     /** Method called to indicate that characters have been pushed to
  285.      * the front of the read buffer.
  286.      * @param n number of characters pushed
  287.      */
  288.     private void charsPushed(final int n) {
  289.         head = (head + buffer.length - n) % buffer.length;
  290.         count += n;
  291.     }

  292.     /** Method called to indicate that characters have been appended
  293.      * to the end of the read buffer.
  294.      * @param n number of characters appended
  295.      */
  296.     private void charsAppended(final int n) {
  297.         count += n;
  298.     }

  299.     /** Ensure that the current buffer has at least {@code capacity}
  300.      * number of elements. The number of content elements in the buffer
  301.      * is not changed.
  302.      * @param capacity the minimum required capacity of the buffer
  303.      */
  304.     private void ensureCapacity(final int capacity) {
  305.         if (capacity > buffer.length) {
  306.             final double newCapacityPower = Math.ceil(Math.log(capacity) / LOG2);
  307.             final int newCapacity = (int) Math.pow(2, newCapacityPower);

  308.             final char[] newBuffer = new char[newCapacity];

  309.             final int contiguousCount = Math.min(count, buffer.length - head);
  310.             System.arraycopy(buffer, head, newBuffer, 0, contiguousCount);

  311.             if (contiguousCount < count) {
  312.                 System.arraycopy(buffer, 0, newBuffer, contiguousCount, count - contiguousCount);
  313.             }

  314.             buffer = newBuffer;
  315.             head = 0;
  316.         }
  317.     }
  318. }