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 }