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 }