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 }