View Javadoc

    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
    19  import java.io.IOException;
    20  import java.io.Reader;
    21  import java.util.Objects;
    22
    23  /** Class used to buffer characters read from an underlying {@link Reader}.
    24   * Characters can be consumed from the buffer, examined without being consumed,
    25   * and pushed back onto the buffer. The internal bufer is resized as needed.
    26   */
    27  public class CharReadBuffer {
    28
    29      /** Constant indicating that the end of the input has been reached. */
    30      private static final int EOF = -1;
    31
    32      /** Default initial buffer capacity. */
    33      private static final int DEFAULT_INITIAL_CAPACITY = 512;
    34
    35      /** Log 2 constant. */
    36      private static final double LOG2 = Math.log(2);
    37
    38      /** Underlying reader instance. */
    39      private final Reader reader;
    40
    41      /** Character buffer. */
    42      private char[] buffer;
    43
    44      /** The index of the head element in the buffer. */
    45      private int head;
    46
    47      /** The number of valid elements in the buffer. */
    48      private int count;
    49
    50      /** True when the end of reader content is reached. */
    51      private boolean reachedEof;
    52
    53      /** Minimum number of characters to request for each read. */
    54      private final int minRead;
    55
    56      /** Construct a new instance that buffers characters from the given reader.
    57       * @param reader underlying reader instance
    58       * @throws NullPointerException if {@code reader} is null
    59       */
    60      public CharReadBuffer(final Reader reader) {
    61          this(reader, DEFAULT_INITIAL_CAPACITY);
    62      }
    63
    64      /** Construct a new instance that buffers characters from the given reader.
    65       * @param reader underlying reader instance
    66       * @param initialCapacity the initial capacity of the internal buffer; the buffer
    67       *      is resized as needed
    68       * @throws NullPointerException if {@code reader} is null
    69       * @throws IllegalArgumentException if {@code initialCapacity} is less than one.
    70       */
    71      public CharReadBuffer(final Reader reader, final int initialCapacity) {
    72          this(reader, initialCapacity, (initialCapacity + 1) / 2);
    73      }
    74
    75      /** Construct a new instance that buffers characters from the given reader.
    76       * @param reader underlying reader instance
    77       * @param initialCapacity the initial capacity of the internal buffer; the buffer
    78       *      is resized as needed
    79       * @param minRead the minimum number of characters to request from the reader
    80       *      when fetching more characters into the buffer; this can be used to limit the
    81       *      number of calls made to the reader
    82       * @throws NullPointerException if {@code reader} is null
    83       * @throws IllegalArgumentException if {@code initialCapacity} or {@code minRead}
    84       *      are less than one.
    85       */
    86      public CharReadBuffer(final Reader reader, final int initialCapacity, final int minRead) {
    87          Objects.requireNonNull(reader, "Reader cannot be null");
    88          if (initialCapacity < 1) {
    89              throw new IllegalArgumentException("Initial buffer capacity must be greater than 0; was " +
    90                      initialCapacity);
    91          }
    92          if (minRead < 1) {
    93              throw new IllegalArgumentException("Min read value must be greater than 0; was " +
    94                      minRead);
    95          }
    96
    97          this.reader = reader;
    98          this.buffer = new char[initialCapacity];
    99          this.minRead = minRead;
    100     }
    101
    102     /** Return true if more characters are available from the read buffer.
    103      * @return true if more characters are available from the read buffer
    104      * @throws java.io.UncheckedIOException if an I/O error occurs
    105      */
    106     public boolean hasMoreCharacters() {
    107         return makeAvailable(1) > 0;
    108     }
    109
    110     /** Attempt to make at least {@code n} characters available in the buffer, reading
    111      * characters from the underlying reader as needed. The number of characters available
    112      * is returned.
    113      * @param n number of characters requested to be available
    114      * @return number of characters available for immediate use in the buffer
    115      * @throws java.io.UncheckedIOException if an I/O error occurs
    116      */
    117     public int makeAvailable(final int n) {
    118         final int diff = n - count;
    119         if (diff > 0) {
    120             readChars(diff);
    121         }
    122         return count;
    123     }
    124
    125     /** Remove and return the next character in the buffer.
    126      * @return the next character in the buffer or {@value #EOF}
    127      *      if the end of the content has been reached
    128      * @throws java.io.UncheckedIOException if an I/O error occurs
    129      * @see #peek()
    130      */
    131     public int read() {
    132         final int result = peek();
    133         charsRemoved(1);
    134
    135         return result;
    136     }
    137
    138     /** Remove and return a string from the buffer. The length of the string will be
    139      * the number of characters available in the buffer up to {@code len}. Null is
    140      * returned if no more characters are available.
    141      * @param len requested length of the string
    142      * @return a string from the read buffer or null if no more characters are available
    143      * @throws IllegalArgumentException if {@code len} is less than 0
    144      * @throws java.io.UncheckedIOException if an I/O error occurs
    145      * @see #peekString(int)
    146      */
    147     public String readString(final int len) {
    148         final String result = peekString(len);
    149         if (result != null) {
    150             charsRemoved(result.length());
    151         }
    152
    153         return result;
    154     }
    155
    156     /** Return the next character in the buffer without removing it.
    157      * @return the next character in the buffer or {@value #EOF}
    158      *      if the end of the content has been reached
    159      * @throws java.io.UncheckedIOException if an I/O error occurs
    160      * @see #read()
    161      */
    162     public int peek() {
    163         if (makeAvailable(1) < 1) {
    164             return EOF;
    165         }
    166         return buffer[head];
    167     }
    168
    169     /** Return a string from the buffer without removing it. The length of the string will be
    170      * the number of characters available in the buffer up to {@code len}. Null is
    171      * returned if no more characters are available.
    172      * @param len requested length of the string
    173      * @return a string from the read buffer or null if no more characters are available
    174      * @throws IllegalArgumentException if {@code len} is less than 0
    175      * @throws java.io.UncheckedIOException if an I/O error occurs
    176      * @see #readString(int)
    177      */
    178     public String peekString(final int len) {
    179         if (len < 0) {
    180             throw new IllegalArgumentException("Requested string length cannot be negative; was " + len);
    181         } else if (len == 0) {
    182             return hasMoreCharacters() ?
    183                     "" :
    184                     null;
    185         }
    186
    187         final int available = makeAvailable(len);
    188         final int resultLen = Math.min(len, available);
    189         if (resultLen < 1) {
    190             return null;
    191         }
    192
    193         final int contiguous = Math.min(buffer.length - head, resultLen);
    194         final int remaining = resultLen - contiguous;
    195
    196         String result = String.valueOf(buffer, head, contiguous);
    197         if (remaining > 0) {
    198             result += String.valueOf(buffer, 0, remaining);
    199         }
    200
    201         return result;
    202     }
    203
    204     /** Get the character at the given buffer index or {@value #EOF} if the index
    205      * is past the end of the content. The character is not removed from the buffer.
    206      * @param index index of the character to receive relative to the buffer start
    207      * @return the character at the given index of {@code -1} if the character is
    208      *      past the end of the stream content
    209      * @throws java.io.UncheckedIOException if an I/O exception occurs
    210      */
    211     public int charAt(final int index) {
    212         if (index < 0) {
    213             throw new IllegalArgumentException("Character index cannot be negative; was " + index);
    214         }
    215         final int requiredSize = index + 1;
    216         if (makeAvailable(requiredSize) < requiredSize) {
    217             return EOF;
    218         }
    219
    220         return buffer[(head + index) % buffer.length];
    221     }
    222
    223     /** Skip {@code n} characters from the stream. Characters are first skipped from the buffer
    224      * and then from the underlying reader using {@link Reader#skip(long)} if needed.
    225      * @param n number of character to skip
    226      * @return the number of characters skipped
    227      * @throws IllegalArgumentException if {@code n} is negative
    228      * @throws java.io.UncheckedIOException if an I/O error occurs
    229      */
    230     public int skip(final int n) {
    231         if (n < 0) {
    232             throw new IllegalArgumentException("Character skip count cannot be negative; was " + n);
    233         }
    234
    235         // skip buffered content first
    236         int skipped = Math.min(n, count);
    237         charsRemoved(skipped);
    238
    239         // skip from the reader if required
    240         final int remaining = n - skipped;
    241         if (remaining > 0) {
    242             try {
    243                 skipped += (int) reader.skip(remaining);
    244             } catch (IOException exc) {
    245                 throw GeometryIOUtils.createUnchecked(exc);
    246             }
    247         }
    248
    249         return skipped;
    250     }
    251
    252     /** Push a character back onto the read buffer. The argument will
    253      * be the next character returned by {@link #read()} or {@link #peek()}.
    254      * @param ch character to push onto the read buffer
    255      */
    256     public void push(final char ch) {
    257         ensureCapacity(count + 1);
    258         pushCharInternal(ch);
    259     }
    260
    261     /** Push a string back onto the read buffer. The first character
    262      * of the string will be the next character returned by
    263      * {@link #read()} or {@link #peek()}.
    264      * @param str string to push onto the read buffer
    265      */
    266     public void pushString(final String str) {
    267         final int len = str.length();
    268
    269         ensureCapacity(count + len);
    270         for (int i = len - 1; i >= 0; --i) {
    271             pushCharInternal(str.charAt(i));
    272         }
    273     }
    274
    275     /** Internal method to push a single character back onto the read
    276      * buffer. The buffer capacity is <em>not</em> checked.
    277      * @param ch character to push onto the read buffer
    278      */
    279     private void pushCharInternal(final char ch) {
    280         charsPushed(1);
    281         buffer[head] = ch;
    282     }
    283
    284     /** Read characters from the underlying character stream into
    285      * the internal buffer.
    286      * @param n minimum number of characters requested to be placed
    287      *      in the buffer
    288      * @throws java.io.UncheckedIOException if an I/O error occurs
    289      */
    290     private void readChars(final int n) {
    291         if (!reachedEof) {
    292             int remaining = Math.max(n, minRead);
    293
    294             ensureCapacity(count + remaining);
    295
    296             try {
    297                 int tail;
    298                 int len;
    299                 int read;
    300                 while (remaining > 0) {
    301                     tail = (head + count) % buffer.length;
    302                     len = Math.min(buffer.length - tail, remaining);
    303
    304                     read = reader.read(buffer, tail, len);
    305                     if (read == EOF) {
    306                         reachedEof = true;
    307                         break;
    308                     }
    309
    310                     charsAppended(read);
    311                     remaining -= read;
    312                 }
    313             } catch (IOException exc) {
    314                 throw GeometryIOUtils.createUnchecked(exc);
    315             }
    316         }
    317     }
    318
    319     /** Method called to indicate that characters have been removed from
    320      * the front of the read buffer.
    321      * @param n number of characters removed
    322      */
    323     private void charsRemoved(final int n) {
    324         head = (head + n) % buffer.length;
    325         count -= n;
    326     }
    327
    328     /** Method called to indicate that characters have been pushed to
    329      * the front of the read buffer.
    330      * @param n number of characters pushed
    331      */
    332     private void charsPushed(final int n) {
    333         head = (head + buffer.length - n) % buffer.length;
    334         count += n;
    335     }
    336
    337     /** Method called to indicate that characters have been appended
    338      * to the end of the read buffer.
    339      * @param n number of characters appended
    340      */
    341     private void charsAppended(final int n) {
    342         count += n;
    343     }
    344
    345     /** Ensure that the current buffer has at least {@code capacity}
    346      * number of elements. The number of content elements in the buffer
    347      * is not changed.
    348      * @param capacity the minimum required capacity of the buffer
    349      */
    350     private void ensureCapacity(final int capacity) {
    351         if (capacity > buffer.length) {
    352             final double newCapacityPower = Math.ceil(Math.log(capacity) / LOG2);
    353             final int newCapacity = (int) Math.pow(2, newCapacityPower);
    354
    355             final char[] newBuffer = new char[newCapacity];
    356
    357             final int contiguousCount = Math.min(count, buffer.length - head);
    358             System.arraycopy(buffer, head, newBuffer, 0, contiguousCount);
    359
    360             if (contiguousCount < count) {
    361                 System.arraycopy(buffer, 0, newBuffer, contiguousCount, count - contiguousCount);
    362             }
    363
    364             buffer = newBuffer;
    365             head = 0;
    366         }
    367     }
    368 }