CharReadBuffer.java
- /*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.commons.geometry.io.core.internal;
- import java.io.IOException;
- import java.io.Reader;
- import java.util.Objects;
- /** Class used to buffer characters read from an underlying {@link Reader}.
- * Characters can be consumed from the buffer, examined without being consumed,
- * and pushed back onto the buffer. The internal bufer is resized as needed.
- */
- public class CharReadBuffer {
- /** Constant indicating that the end of the input has been reached. */
- private static final int EOF = -1;
- /** Default initial buffer capacity. */
- private static final int DEFAULT_INITIAL_CAPACITY = 512;
- /** Log 2 constant. */
- private static final double LOG2 = Math.log(2);
- /** Underlying reader instance. */
- private final Reader reader;
- /** Character buffer. */
- private char[] buffer;
- /** The index of the head element in the buffer. */
- private int head;
- /** The number of valid elements in the buffer. */
- private int count;
- /** True when the end of reader content is reached. */
- private boolean reachedEof;
- /** Minimum number of characters to request for each read. */
- private final int minRead;
- /** Construct a new instance that buffers characters from the given reader.
- * @param reader underlying reader instance
- * @throws NullPointerException if {@code reader} is null
- */
- public CharReadBuffer(final Reader reader) {
- this(reader, DEFAULT_INITIAL_CAPACITY);
- }
- /** Construct a new instance that buffers characters from the given reader.
- * @param reader underlying reader instance
- * @param initialCapacity the initial capacity of the internal buffer; the buffer
- * is resized as needed
- * @throws NullPointerException if {@code reader} is null
- * @throws IllegalArgumentException if {@code initialCapacity} is less than one.
- */
- public CharReadBuffer(final Reader reader, final int initialCapacity) {
- this(reader, initialCapacity, (initialCapacity + 1) / 2);
- }
- /** Construct a new instance that buffers characters from the given reader.
- * @param reader underlying reader instance
- * @param initialCapacity the initial capacity of the internal buffer; the buffer
- * is resized as needed
- * @param minRead the minimum number of characters to request from the reader
- * when fetching more characters into the buffer; this can be used to limit the
- * number of calls made to the reader
- * @throws NullPointerException if {@code reader} is null
- * @throws IllegalArgumentException if {@code initialCapacity} or {@code minRead}
- * are less than one.
- */
- public CharReadBuffer(final Reader reader, final int initialCapacity, final int minRead) {
- Objects.requireNonNull(reader, "Reader cannot be null");
- if (initialCapacity < 1) {
- throw new IllegalArgumentException("Initial buffer capacity must be greater than 0; was " +
- initialCapacity);
- }
- if (minRead < 1) {
- throw new IllegalArgumentException("Min read value must be greater than 0; was " +
- minRead);
- }
- this.reader = reader;
- this.buffer = new char[initialCapacity];
- this.minRead = minRead;
- }
- /** Return true if more characters are available from the read buffer.
- * @return true if more characters are available from the read buffer
- * @throws java.io.UncheckedIOException if an I/O error occurs
- */
- public boolean hasMoreCharacters() {
- return makeAvailable(1) > 0;
- }
- /** Attempt to make at least {@code n} characters available in the buffer, reading
- * characters from the underlying reader as needed. The number of characters available
- * is returned.
- * @param n number of characters requested to be available
- * @return number of characters available for immediate use in the buffer
- * @throws java.io.UncheckedIOException if an I/O error occurs
- */
- public int makeAvailable(final int n) {
- final int diff = n - count;
- if (diff > 0) {
- readChars(diff);
- }
- return count;
- }
- /** Remove and return the next character in the buffer.
- * @return the next character in the buffer or {@value #EOF}
- * if the end of the content has been reached
- * @throws java.io.UncheckedIOException if an I/O error occurs
- * @see #peek()
- */
- public int read() {
- final int result = peek();
- charsRemoved(1);
- return result;
- }
- /** Remove and return a string from the buffer. The length of the string will be
- * the number of characters available in the buffer up to {@code len}. Null is
- * returned if no more characters are available.
- * @param len requested length of the string
- * @return a string from the read buffer or null if no more characters are available
- * @throws IllegalArgumentException if {@code len} is less than 0
- * @throws java.io.UncheckedIOException if an I/O error occurs
- * @see #peekString(int)
- */
- public String readString(final int len) {
- final String result = peekString(len);
- if (result != null) {
- charsRemoved(result.length());
- }
- return result;
- }
- /** Return the next character in the buffer without removing it.
- * @return the next character in the buffer or {@value #EOF}
- * if the end of the content has been reached
- * @throws java.io.UncheckedIOException if an I/O error occurs
- * @see #read()
- */
- public int peek() {
- if (makeAvailable(1) < 1) {
- return EOF;
- }
- return buffer[head];
- }
- /** Return a string from the buffer without removing it. The length of the string will be
- * the number of characters available in the buffer up to {@code len}. Null is
- * returned if no more characters are available.
- * @param len requested length of the string
- * @return a string from the read buffer or null if no more characters are available
- * @throws IllegalArgumentException if {@code len} is less than 0
- * @throws java.io.UncheckedIOException if an I/O error occurs
- * @see #readString(int)
- */
- public String peekString(final int len) {
- if (len < 0) {
- throw new IllegalArgumentException("Requested string length cannot be negative; was " + len);
- } else if (len == 0) {
- return hasMoreCharacters() ?
- "" :
- null;
- }
- final int available = makeAvailable(len);
- final int resultLen = Math.min(len, available);
- if (resultLen < 1) {
- return null;
- }
- final int contiguous = Math.min(buffer.length - head, resultLen);
- final int remaining = resultLen - contiguous;
- String result = String.valueOf(buffer, head, contiguous);
- if (remaining > 0) {
- result += String.valueOf(buffer, 0, remaining);
- }
- return result;
- }
- /** Get the character at the given buffer index or {@value #EOF} if the index
- * is past the end of the content. The character is not removed from the buffer.
- * @param index index of the character to receive relative to the buffer start
- * @return the character at the given index of {@code -1} if the character is
- * past the end of the stream content
- * @throws java.io.UncheckedIOException if an I/O exception occurs
- */
- public int charAt(final int index) {
- if (index < 0) {
- throw new IllegalArgumentException("Character index cannot be negative; was " + index);
- }
- final int requiredSize = index + 1;
- if (makeAvailable(requiredSize) < requiredSize) {
- return EOF;
- }
- return buffer[(head + index) % buffer.length];
- }
- /** Skip {@code n} characters from the stream. Characters are first skipped from the buffer
- * and then from the underlying reader using {@link Reader#skip(long)} if needed.
- * @param n number of character to skip
- * @return the number of characters skipped
- * @throws IllegalArgumentException if {@code n} is negative
- * @throws java.io.UncheckedIOException if an I/O error occurs
- */
- public int skip(final int n) {
- if (n < 0) {
- throw new IllegalArgumentException("Character skip count cannot be negative; was " + n);
- }
- // skip buffered content first
- int skipped = Math.min(n, count);
- charsRemoved(skipped);
- // skip from the reader if required
- final int remaining = n - skipped;
- if (remaining > 0) {
- try {
- skipped += (int) reader.skip(remaining);
- } catch (IOException exc) {
- throw GeometryIOUtils.createUnchecked(exc);
- }
- }
- return skipped;
- }
- /** Push a character back onto the read buffer. The argument will
- * be the next character returned by {@link #read()} or {@link #peek()}.
- * @param ch character to push onto the read buffer
- */
- public void push(final char ch) {
- ensureCapacity(count + 1);
- pushCharInternal(ch);
- }
- /** Push a string back onto the read buffer. The first character
- * of the string will be the next character returned by
- * {@link #read()} or {@link #peek()}.
- * @param str string to push onto the read buffer
- */
- public void pushString(final String str) {
- final int len = str.length();
- ensureCapacity(count + len);
- for (int i = len - 1; i >= 0; --i) {
- pushCharInternal(str.charAt(i));
- }
- }
- /** Internal method to push a single character back onto the read
- * buffer. The buffer capacity is <em>not</em> checked.
- * @param ch character to push onto the read buffer
- */
- private void pushCharInternal(final char ch) {
- charsPushed(1);
- buffer[head] = ch;
- }
- /** Read characters from the underlying character stream into
- * the internal buffer.
- * @param n minimum number of characters requested to be placed
- * in the buffer
- * @throws java.io.UncheckedIOException if an I/O error occurs
- */
- private void readChars(final int n) {
- if (!reachedEof) {
- int remaining = Math.max(n, minRead);
- ensureCapacity(count + remaining);
- try {
- int tail;
- int len;
- int read;
- while (remaining > 0) {
- tail = (head + count) % buffer.length;
- len = Math.min(buffer.length - tail, remaining);
- read = reader.read(buffer, tail, len);
- if (read == EOF) {
- reachedEof = true;
- break;
- }
- charsAppended(read);
- remaining -= read;
- }
- } catch (IOException exc) {
- throw GeometryIOUtils.createUnchecked(exc);
- }
- }
- }
- /** Method called to indicate that characters have been removed from
- * the front of the read buffer.
- * @param n number of characters removed
- */
- private void charsRemoved(final int n) {
- head = (head + n) % buffer.length;
- count -= n;
- }
- /** Method called to indicate that characters have been pushed to
- * the front of the read buffer.
- * @param n number of characters pushed
- */
- private void charsPushed(final int n) {
- head = (head + buffer.length - n) % buffer.length;
- count += n;
- }
- /** Method called to indicate that characters have been appended
- * to the end of the read buffer.
- * @param n number of characters appended
- */
- private void charsAppended(final int n) {
- count += n;
- }
- /** Ensure that the current buffer has at least {@code capacity}
- * number of elements. The number of content elements in the buffer
- * is not changed.
- * @param capacity the minimum required capacity of the buffer
- */
- private void ensureCapacity(final int capacity) {
- if (capacity > buffer.length) {
- final double newCapacityPower = Math.ceil(Math.log(capacity) / LOG2);
- final int newCapacity = (int) Math.pow(2, newCapacityPower);
- final char[] newBuffer = new char[newCapacity];
- final int contiguousCount = Math.min(count, buffer.length - head);
- System.arraycopy(buffer, head, newBuffer, 0, contiguousCount);
- if (contiguousCount < count) {
- System.arraycopy(buffer, 0, newBuffer, contiguousCount, count - contiguousCount);
- }
- buffer = newBuffer;
- head = 0;
- }
- }
- }