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.juneau.marshall.parser; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020import static org.apache.juneau.commons.utils.StringUtils.*; 021 022import java.io.*; 023import java.util.*; 024 025import org.apache.juneau.commons.io.*; 026 027/** 028 * Similar to a {@link PushbackReader} with a pushback buffer of 1 character. 029 * 030 * <p> 031 * Code is optimized to work with a 1 character buffer. 032 * 033 * <p> 034 * Additionally keeps track of current line and column number, and provides the ability to set mark points and capture 035 * characters from the previous mark point. 036 * 037 * <h5 class='section'>Notes:</h5><ul> 038 * <li class='warn'>This class is not thread safe. 039 * </ul> 040 * 041 * <h5 class='section'>See Also:</h5><ul> 042 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SerializersAndParsers">Serializers and Parsers</a> 043 044 * </ul> 045 */ 046@SuppressWarnings({ 047 "java:S115", // Constants use UPPER_snakeCase convention (e.g., MSG_bufferUnderflow) 048 "java:S1192", // Duplicated literals read more clearly inline than as constants. 049 "resource" // ParserReader is managed by caller 050}) 051public class ParserReader extends Reader implements Positionable { 052 053 /** 054 * Maximum number of previously-read characters preserved as unread lookback when the buffer is 055 * refilled <em>without</em> an active {@link #mark()}. Originally 10, which was sufficient for 056 * JSON's short tokens but too small for YAML's indent-aware parser: a deeply-nested OpenAPI 3.1 057 * document can have indent depth well past 10 chars on a single line, and the YAML parser's 058 * {@code skipBlanksAndCountIndent} / {@code unreadSpaces} pair (and similar peek-then-unread 059 * patterns in block-scalar handling) tries to unread the entire indent at once. When the indent 060 * read crossed the buffer boundary, the rest underflowed. 256 chars covers any realistic YAML 061 * document indent depth (128 levels at 2-space indent) while still leaving 768 chars per 062 * 1024-char refill for forward-progress reads. 063 */ 064 private static final int UNMARKED_LOOKBACK_CHARS = 256; 065 066 /** Wrapped reader */ 067 protected final Reader r; 068 069 private char[] buff; // Internal character buffer 070 private int line = 1; // Current line number 071 private int column; // Current column number 072 private int iCurrent; // Current pointer into character buffer 073 private int iMark = -1; // Mark position in buffer 074 private int iEnd; // The last good character position in the buffer 075 private boolean endReached; 076 private BitSet holes; // Deleted positions (relative to iMark); null unless an escape was collapsed in the current mark 077 private final boolean unbuffered; 078 079 /** 080 * Constructor. 081 * 082 * @param pipe The parser input. 083 * <br>Must not be <jk>null</jk>. 084 * @throws IOException Thrown by underlying stream. 085 */ 086 public ParserReader(ParserPipe pipe) throws IOException { 087 this.unbuffered = pipe.unbuffered; 088 if (pipe.isString()) { 089 String in = pipe.getInputAsString(); 090 this.r = new CharSequenceReader(in); 091 this.buff = new char[in.length() < 1024 ? in.length() : 1024]; 092 } else { 093 Reader reader = pipe.getReader(); 094 if (reader instanceof ParserReader reader2) 095 this.r = reader2.r; 096 else 097 this.r = reader; 098 this.buff = new char[1024]; 099 } 100 pipe.setPositionable(this); 101 } 102 103 /** 104 * No-op. 105 * 106 * <p> 107 * Input readers are closed in the {@link ParserPipe} class. 108 * 109 * @throws IOException If a problem occurred trying to read from the reader. 110 */ 111 @Override /* Overridden from Reader */ 112 public void close() throws IOException { 113 // No-op 114 } 115 116 /** 117 * Trims off the last character in the marking buffer. 118 * 119 * <p> 120 * Useful for removing escape characters from sequences. 121 * 122 * @return This object. 123 */ 124 public final ParserReader delete() { 125 return delete(1); 126 } 127 128 /** 129 * Trims off the specified number of last characters in the marking buffer. 130 * Useful for removing escape characters from sequences. 131 * 132 * @param count The number of characters to delete. 133 * @return This object. 134 */ 135 public final ParserReader delete(int count) { 136 markHoles(iCurrent - count, iCurrent); 137 return this; 138 } 139 140 /** 141 * Returns the contents of the reusable character buffer as a string, and resets the buffer for next usage. 142 * 143 * @return The contents of the reusable character buffer as a string. 144 */ 145 public final String getMarked() { return getMarked(0, 0); } 146 147 /** 148 * Same as {@link #getMarked()} except allows you to specify offsets into the buffer. 149 * 150 * <p> 151 * For example, to return the marked string, but trim the first and last characters, call the following: 152 * <p class='bjava'> 153 * getFromMarked(1, -1); 154 * </p> 155 * 156 * @param offsetStart The offset of the start position. 157 * @param offsetEnd The offset of the end position. 158 * @return The contents of the reusable character buffer as a string. 159 */ 160 public final String getMarked(int offsetStart, int offsetEnd) { 161 int offset = 0; 162 163 // Holes are positions removed by delete()/replace() that we need to get rid of now. 164 // They are tracked by position (not by sentinel character) so real DEL characters are preserved. 165 if (holes != null) { 166 for (var i = iMark; i < iCurrent; i++) { 167 if (holes.get(i - iMark)) 168 offset++; 169 else 170 buff[i - offset] = buff[i]; 171 } 172 holes = null; 173 } 174 int start = iMark + offsetStart; 175 int len = iCurrent - iMark + offsetEnd - offsetStart - offset; 176 var s = new String(buff, start, len); 177 iMark = -1; 178 return s; 179 } 180 181 @Override /* Overridden from Positionable */ 182 public Position getPosition() { return new Position(line, column); } 183 184 /** 185 * Start buffering the calls to read() so that the text can be gathered from the mark point on calling {@code getFromMarked()}. 186 */ 187 public final void mark() { 188 iMark = iCurrent; 189 holes = null; 190 } 191 192 /** 193 * Reads a numeric string from the specified reader. 194 * 195 * @return The parsed number string. 196 * @throws IOException Thrown by underlying stream. 197 */ 198 public String readNumberString() throws IOException { 199 mark(); 200 int c = 0; 201 while (true) { 202 c = read(); 203 if (c == -1 || ! isNumberChar((char)c)) { 204 if (c != -1) 205 unread(); 206 break; 207 } 208 } 209 return getMarked(); 210 } 211 212 /** 213 * Peeks the next character in the stream. 214 * 215 * <p> 216 * This is equivalent to doing a {@code read()} followed by an {@code unread()}. 217 * 218 * @return The peeked character, or (char)-1 if the end of the stream has been reached. 219 * @throws IOException If a problem occurred trying to read from the reader. 220 */ 221 public final int peek() throws IOException { 222 int c = read(); 223 if (c != -1) 224 unread(); 225 return c; 226 } 227 228 /** 229 * Same as {@link #peek()} but skips over any whitespace characters. 230 * 231 * <p> 232 * This is equivalent to doing a {@code read()} followed by an {@code unread()}. 233 * 234 * @return The peeked character, or (char)-1 if the end of the stream has been reached. 235 * @throws IOException If a problem occurred trying to read from the reader. 236 */ 237 public final int peekSkipWs() throws IOException { 238 while (true) { 239 var c = read(); 240 var isWs = Character.isWhitespace(c); 241 if (c != -1 && ! isWs) 242 unread(); 243 if (! isWs) 244 return c; 245 } 246 } 247 248 /** 249 * Reads a single character. 250 * 251 * <p> 252 * Note that this method does NOT process extended unicode characters (i.e. characters above 0x10000), but rather 253 * returns them as two <jk>char</jk>s. 254 * Use {@link #readCodePoint()} to ensure proper handling of extended unicode. 255 * 256 * @return The character read, or -1 if the end of the stream has been reached. 257 * @throws IOException If a problem occurred trying to read from the reader. 258 */ 259 @Override /* Overridden from Reader */ 260 public final int read() throws IOException { 261 int c = readFromBuff(); 262 if (c == -1) 263 return -1; 264 if (c == '\n') { 265 line++; 266 column = 0; 267 } else { 268 column++; 269 } 270 return c; 271 } 272 273 /** 274 * Subclasses can override this method to provide additional filtering. 275 * 276 * <p> 277 * Default implementation simply calls the same method on the underlying reader. 278 */ 279 @Override /* Overridden from Reader */ 280 public int read(char[] cbuf, int off, int len) throws IOException { 281 return r.read(cbuf, off, unbuffered ? 1 : len); 282 } 283 284 /** 285 * Read the specified number of characters off the stream. 286 * 287 * @param num The number of characters to read. 288 * @return The characters packaged as a String. 289 * @throws IOException If a problem occurred trying to read from the reader. 290 */ 291 public final String read(int num) throws IOException { 292 var c = new char[num]; 293 for (var i = 0; i < num; i++) { 294 var c2 = read(); 295 if (c2 == -1) 296 return new String(c, 0, i); 297 c[i] = (char)c2; 298 } 299 return new String(c); 300 } 301 302 /** 303 * Same as {@link #read()} but detects and combines extended unicode characters (characters above 0x10000). 304 * 305 * @return The character read, or -1 if the end of the stream has been reached. 306 * @throws IOException If a problem occurred trying to read from the reader. 307 */ 308 public final int readCodePoint() throws IOException { 309 int c = read(); 310 311 // Characters that take up 2 chars. 312 if (c >= 0xd800 && c <= 0xdbff) { 313 var low = read(); 314 if (low >= 0xdc00 && low <= 0xdfff) 315 c = 0x10000 + ((c - 0xd800) << 10) + (low - 0xdc00); 316 } 317 318 return c; 319 } 320 321 /** 322 * Same as {@link #read()} but skips over any whitespace characters. 323 * 324 * @return The first non-whitespace character, or -1 if the end of stream reached. 325 * @throws IOException Thrown by underlying stream. 326 */ 327 public final int readSkipWs() throws IOException { 328 while (true) { 329 var c = read(); 330 if (c == -1 || ! Character.isWhitespace(c)) 331 return c; 332 } 333 } 334 335 /** 336 * Replace the last read character in the buffer with the specified character. 337 * 338 * @param c The new character. 339 * @return This object. 340 * @throws IOException Thrown by underlying stream. 341 */ 342 public final ParserReader replace(char c) throws IOException { 343 return replace(c, 1); 344 } 345 346 /** 347 * Replaces the last character in the marking buffer with the specified character. 348 * 349 * <p> 350 * <c>offset</c> must be at least <c>1</c> for normal characters, and <c>2</c> for extended 351 * unicode characters in order for the replacement to fit into the buffer. 352 * 353 * @param c The new character. 354 * @param offset The offset. 355 * @return This object. 356 * @throws IOException Thrown by underlying stream. 357 */ 358 public final ParserReader replace(int c, int offset) throws IOException { 359 if (c < 0x10000) { 360 if (offset < 1) 361 throw ioex("Buffer underflow."); 362 clearHoles(iCurrent - offset, iCurrent - offset + 1); 363 buff[iCurrent - offset] = (char)c; 364 } else { 365 if (offset < 2) 366 throw ioex("Buffer underflow."); 367 c -= 0x10000; 368 clearHoles(iCurrent - offset, iCurrent - offset + 2); 369 buff[iCurrent - offset] = (char)(0xd800 + (c >> 10)); 370 buff[iCurrent - offset + 1] = (char)(0xdc00 + (c & 0x3ff)); 371 offset--; 372 } 373 // Mark the gap as deleted. 374 if (offset > 1) 375 markHoles(iCurrent - offset + 1, iCurrent); 376 return this; 377 } 378 379 /* 380 * Un-deletes [from,to) because a replacement character is being written there. 381 */ 382 private void clearHoles(int from, int to) { 383 if (holes != null && iMark >= 0) 384 holes.clear(Math.max(from - iMark, 0), Math.max(to - iMark, 0)); 385 } 386 387 /* 388 * Records [from,to) as deleted; positions are stored relative to iMark so they survive buffer shifts. 389 */ 390 private void markHoles(int from, int to) { 391 if (iMark < 0) 392 return; 393 if (holes == null) 394 holes = new BitSet(); 395 holes.set(Math.max(from - iMark, 0), Math.max(to - iMark, 0)); 396 } 397 398 /** 399 * Pushes the last read character back into the stream. 400 * 401 * @return This object. 402 * @throws IOException If a problem occurred trying to read from the reader. 403 */ 404 public ParserReader unread() throws IOException { 405 if (iCurrent <= 0) 406 throw ioex("Buffer underflow."); 407 iCurrent--; 408 if (column == 0) 409 line--; 410 else 411 column--; 412 return this; 413 } 414 415 @SuppressWarnings({ 416 "java:S3776" // Cognitive complexity acceptable for buffer reading logic 417 }) 418 private final int readFromBuff() throws IOException { 419 while (iCurrent >= iEnd) { 420 if (endReached) 421 return -1; 422 423 // If there's still space at the end of this buffer, fill it. 424 // Make sure there's at least 2 character spaces free for extended unicode characters. 425 if (iEnd + 1 < buff.length) { 426 int x = read(buff, iCurrent, buff.length - iEnd); 427 if (x == -1) { 428 endReached = true; 429 return -1; 430 } 431 iEnd += x; 432 433 } else { 434 // If we're currently marking, then we want to copy from the current mark point 435 // to the beginning of the buffer and then fill in the remainder of buffer. 436 if (iMark >= 0) { 437 438 // If we're marking from the beginning of the array, we double the size of the 439 // buffer. This isn't likely to occur often. 440 if (iMark == 0) { 441 var buff2 = new char[buff.length << 1]; 442 System.arraycopy(buff, 0, buff2, 0, buff.length); 443 buff = buff2; 444 445 // Otherwise, we copy what's currently marked to the beginning of the buffer. 446 } else { 447 int copyBuff = iMark; 448 System.arraycopy(buff, copyBuff, buff, 0, buff.length - copyBuff); 449 iCurrent -= copyBuff; 450 iMark -= copyBuff; 451 } 452 int expected = buff.length - iCurrent; 453 454 int x = read(buff, iCurrent, expected); 455 if (x == -1) { 456 endReached = true; 457 iEnd = iCurrent; 458 return -1; 459 } 460 iEnd = iCurrent + x; 461 } else { 462 // Copy the last UNMARKED_LOOKBACK_CHARS chars in the buffer to the beginning so 463 // callers can still unread() that far across a buffer-boundary refill. See the 464 // constant's javadoc for why 256 (not 10) — widened to handle YAML's deep-indent 465 // peek-then-unread patterns where a single line's indent can exceed 10 chars. 466 int copyBuff = Math.min(iCurrent, UNMARKED_LOOKBACK_CHARS); 467 System.arraycopy(buff, iCurrent - copyBuff, buff, 0, copyBuff); 468 469 // Number of characters we expect to copy on the next read. 470 int expected = buff.length - copyBuff; 471 int x = read(buff, copyBuff, expected); 472 iCurrent = copyBuff; 473 if (x == -1) { 474 endReached = true; 475 iEnd = iCurrent; 476 return -1; 477 } 478 iEnd = iCurrent + x; 479 } 480 } 481 } 482 return buff[iCurrent++]; 483 } 484}