001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.geometry.io.core.internal;
018
019import java.io.IOException;
020import java.io.Reader;
021import java.util.Objects;
022
023/** Class used to buffer characters read from an underlying {@link Reader}.
024 * Characters can be consumed from the buffer, examined without being consumed,
025 * and pushed back onto the buffer. The internal bufer is resized as needed.
026 */
027public class CharReadBuffer {
028
029    /** Constant indicating that the end of the input has been reached. */
030    private static final int EOF = -1;
031
032    /** Default initial buffer capacity. */
033    private static final int DEFAULT_INITIAL_CAPACITY = 512;
034
035    /** Log 2 constant. */
036    private static final double LOG2 = Math.log(2);
037
038    /** Underlying reader instance. */
039    private final Reader reader;
040
041    /** Character buffer. */
042    private char[] buffer;
043
044    /** The index of the head element in the buffer. */
045    private int head;
046
047    /** The number of valid elements in the buffer. */
048    private int count;
049
050    /** True when the end of reader content is reached. */
051    private boolean reachedEof;
052
053    /** Minimum number of characters to request for each read. */
054    private final int minRead;
055
056    /** Construct a new instance that buffers characters from the given reader.
057     * @param reader underlying reader instance
058     * @throws NullPointerException if {@code reader} is null
059     */
060    public CharReadBuffer(final Reader reader) {
061        this(reader, DEFAULT_INITIAL_CAPACITY);
062    }
063
064    /** Construct a new instance that buffers characters from the given reader.
065     * @param reader underlying reader instance
066     * @param initialCapacity the initial capacity of the internal buffer; the buffer
067     *      is resized as needed
068     * @throws NullPointerException if {@code reader} is null
069     * @throws IllegalArgumentException if {@code initialCapacity} is less than one.
070     */
071    public CharReadBuffer(final Reader reader, final int initialCapacity) {
072        this(reader, initialCapacity, (initialCapacity + 1) / 2);
073    }
074
075    /** Construct a new instance that buffers characters from the given reader.
076     * @param reader underlying reader instance
077     * @param initialCapacity the initial capacity of the internal buffer; the buffer
078     *      is resized as needed
079     * @param minRead the minimum number of characters to request from the reader
080     *      when fetching more characters into the buffer; this can be used to limit the
081     *      number of calls made to the reader
082     * @throws NullPointerException if {@code reader} is null
083     * @throws IllegalArgumentException if {@code initialCapacity} or {@code minRead}
084     *      are less than one.
085     */
086    public CharReadBuffer(final Reader reader, final int initialCapacity, final int minRead) {
087        Objects.requireNonNull(reader, "Reader cannot be null");
088        if (initialCapacity < 1) {
089            throw new IllegalArgumentException("Initial buffer capacity must be greater than 0; was " +
090                    initialCapacity);
091        }
092        if (minRead < 1) {
093            throw new IllegalArgumentException("Min read value must be greater than 0; was " +
094                    minRead);
095        }
096
097        this.reader = reader;
098        this.buffer = new char[initialCapacity];
099        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}