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.json; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.io.*; 022import java.lang.reflect.*; 023 024import org.apache.juneau.commons.utils.*; 025import org.apache.juneau.marshall.*; 026import org.apache.juneau.marshall.parser.*; 027import org.apache.juneau.marshall.stream.*; 028 029/** 030 * Reference implementation of the public {@link TokenReader} surface for the JSON format. 031 * 032 * <p> 033 * Backed by the existing internal {@link ParserReader} plumbing. Operates as a hand-rolled state 034 * machine over the JSON grammar — structural tokens (<c>{</c>, <c>}</c>, <c>[</c>, <c>]</c>, 035 * <c>,</c>, <c>:</c>) and the four scalar shapes (string, number, boolean, null). 036 * 037 * <p> 038 * Pipe-level settings (charset, auto-close, unbuffered) are configured on the 039 * {@link ParserPipe} the caller hands in. Cursor-level behavior is driven by a small 040 * {@link Settings} bundle — currently just <c>trimStrings</c>. 041 * 042 * <h5 class='section'>Notes:</h5><ul> 043 * <li class='warn'>This class is not thread safe. 044 * <li>Whitespace and JSON comments are tolerated — comments are silently skipped to mirror 045 * Juneau's existing JSON parser behavior. 046 * <li>The cursor is purely structural; object swaps and {@code @Schema} annotations have no 047 * effect. Use the POJO databind path on {@link JsonParser} for those. 048 * </ul> 049 */ 050@SuppressWarnings({ 051 "java:S115", // Message-key constant uses UPPER_camelCase convention. 052 "java:S1192", // Duplicated literals read more clearly inline than as constants. 053 "java:S3776", // Cognitive complexity acceptable for JSON tokenizer state machine 054 "java:S6541", // Brain method acceptable for tokenizer dispatch 055 "resource" // The cursor's underlying ParserPipe is owned by the caller via try-with-resources on the cursor itself; Eclipse JDT flags the inner pipe as unclosed but that's by design. 056}) 057public class JsonTokenReader implements TokenReader { 058 059 /** 060 * Cursor-level settings honored by {@link JsonTokenReader}. 061 * 062 * <p> 063 * Pipe-level concerns (auto-close, unbuffered, charsets) are configured on the 064 * {@link ParserPipe} itself before construction; only behaviors that the cursor applies to 065 * already-decoded tokens live here. 066 * 067 * @param trimStrings If <jk>true</jk>, leading/trailing whitespace is trimmed from 068 * {@link TokenType#VALUE_STRING} and {@link TokenType#FIELD_NAME} values returned by the 069 * cursor. 070 */ 071 public record Settings(boolean trimStrings) { 072 073 /** Default canonical setting: no string trimming. */ 074 public static final Settings DEFAULT = new Settings(false); 075 } 076 077 // ============================================================================================== 078 // Subclass extension surface. 079 // 080 // The fields and methods below are part of JsonTokenReader's documented subclass-extension 081 // contract. Cross-package subclasses (Json5TokenReader in marshall.json5, JsonlTokenReader in 082 // marshall.jsonl) need access to the cursor's internal state machine and per-token scratch 083 // buffers in order to implement format dialects (single-quoted strings, bare identifiers, 084 // trailing commas, missing values, etc.). 085 // 086 // Fields are protected (rather than private + accessor) because the dispatch methods 087 // (readValueStart, readFieldNameOrEndObject, readCommaOrEndArray, ...) directly mutate the 088 // per-token scratch and the state machine variable. Wrapping them in get/set pairs would add 089 // indirection without improving safety. In return, subclasses MUST treat them as 090 // implementation-coupled: Juneau reserves the right to evolve them across major versions. 091 // ============================================================================================== 092 093 /** 094 * State-machine state: at root, after <c>[</c>, after <c>:</c>, or after <c>,</c> inside an 095 * array. The next read should be a JSON value (object, array, or scalar). 096 */ 097 protected static final int S00_expectValue = 0; 098 099 /** 100 * State-machine state: after <c>{</c> or after <c>,</c> inside an object. The next read 101 * should be a field name (string). 102 */ 103 protected static final int S01_expectFieldName = 1; 104 105 /** 106 * State-machine state: just emitted a {@link TokenType#FIELD_NAME}. The next char should be 107 * <c>:</c>. 108 */ 109 protected static final int S02_expectColon = 2; 110 111 /** 112 * State-machine state: just emitted a value inside an object. The next char should be 113 * <c>,</c> (followed by another field name) or <c>}</c>. 114 */ 115 protected static final int S03_expectCommaOrEndObject = 3; 116 117 /** 118 * State-machine state: just emitted a value inside an array. The next char should be 119 * <c>,</c> (followed by another value) or <c>]</c>. 120 */ 121 protected static final int S04_expectCommaOrEndArray = 4; 122 123 /** 124 * State-machine state: the root document has been fully consumed. The next call to 125 * {@link #next()} returns {@link TokenType#END_OF_STREAM}. 126 */ 127 protected static final int S05_end = 5; 128 129 /** Underlying character-level reader; subclasses use this for direct {@code read}/{@code unread}/{@code mark}/{@code getMarked} calls during dialect-specific tokenization. */ 130 protected final ParserReader r; 131 132 private final ParserPipe pipe; 133 134 /** Cursor-level settings (currently just <c>trimStrings</c>). */ 135 protected final Settings settings; 136 137 private final JsonParserSession session; 138 139 /** The token most recently emitted by {@link #next()}. Subclasses set this when overriding state-machine dispatch methods. */ 140 protected TokenType currentToken = TokenType.NOT_AVAILABLE; 141 142 /** Current state-machine state; one of the {@code STATE_*} constants. */ 143 protected int state = S00_expectValue; 144 145 // Container stack: 0 bits = object, 1 bits = array. Limited to 64 levels of nesting 146 // (tracked via {@link #depth}); deeper documents fall back to a heap-allocated overflow array. 147 private long containerBits; 148 private boolean[] containerOverflow; 149 150 /** Current container nesting depth (0 = root). */ 151 protected int depth; 152 153 /** Field name buffer for the current {@link TokenType#FIELD_NAME} token. */ 154 protected String currentFieldName; 155 156 /** String value buffer for the current {@link TokenType#VALUE_STRING} token. */ 157 protected String currentString; 158 159 /** Numeric lexeme buffer for the current {@link TokenType#VALUE_NUMBER} token (preserved verbatim from the input). */ 160 protected String currentNumberLexeme; 161 162 private Number currentNumber; 163 164 /** Boolean value buffer for the current {@link TokenType#VALUE_BOOLEAN} token. */ 165 protected boolean currentBoolean; 166 167 /** 168 * Constructor with default {@link Settings}. 169 * 170 * @param pipe The parser input pipe to read from. Must not be <jk>null</jk>. 171 * @throws IOException If a problem occurred opening the underlying reader. 172 */ 173 public JsonTokenReader(ParserPipe pipe) throws IOException { 174 this(pipe, Settings.DEFAULT, null); 175 } 176 177 /** 178 * Constructor with explicit {@link Settings}. 179 * 180 * @param pipe The parser input pipe to read from. Must not be <jk>null</jk>. 181 * @param settings The cursor-level settings. Must not be <jk>null</jk>. 182 * @throws IOException If a problem occurred opening the underlying reader. 183 */ 184 public JsonTokenReader(ParserPipe pipe, Settings settings) throws IOException { 185 this(pipe, settings, null); 186 } 187 188 /** 189 * Constructor used by {@link JsonParser#readTokens(Object)} to plumb the calling parser's 190 * session in so that {@link #read(Class)} can delegate to the existing databind path. 191 * 192 * @param pipe The parser input pipe to read from. Must not be <jk>null</jk>. 193 * @param settings The cursor-level settings. Must not be <jk>null</jk>. 194 * @param session The {@link JsonParserSession} to use for {@link #read(Class)} delegation, 195 * or <jk>null</jk> to disable {@code read} on this reader (it will throw 196 * {@link UnsupportedOperationException}). 197 * @throws IOException If a problem occurred opening the underlying reader. 198 */ 199 public JsonTokenReader(ParserPipe pipe, Settings settings, JsonParserSession session) throws IOException { 200 this.pipe = pipe; 201 this.r = pipe.getParserReader(); 202 this.settings = settings; 203 this.session = session; 204 } 205 206 // ============================================================================================== 207 // State-machine summary. next() reads the next non-whitespace, non-comment character from the 208 // underlying ParserReader and dispatches to one of the readXxx() methods below based on the 209 // cursor's current state. Each readXxx() method is overridable by subclasses to add 210 // dialect-specific behavior. 211 // 212 // S00_expectValue - At root, after [, after :, after , inside [. 213 // Expecting a JSON value: { [ " digit t f n. 214 // Dispatches to readValueStart. 215 // S01_expectFieldName - After { or after , inside {. 216 // Expecting a quoted string field name (or }). 217 // Dispatches to readFieldNameOrEndObject. 218 // S02_expectColon - Just emitted a FIELD_NAME. Expecting :. 219 // Handled inline by readColonThenValue. 220 // S03_expectCommaOrEndObject - Just emitted a value inside {. Expecting , or }. 221 // Dispatches to readCommaOrEndObject. 222 // S04_expectCommaOrEndArray - Just emitted a value inside [. Expecting , or ]. 223 // Dispatches to readCommaOrEndArray. 224 // S05_end - Root document fully consumed. next() returns 225 // END_OF_STREAM. 226 // 227 // State transitions are performed by afterValue() (sets the post-value state based on the 228 // enclosing container) and by the dispatch methods themselves. 229 // ============================================================================================== 230 @Override /* Overridden from TokenReader */ 231 public TokenType next() throws IOException, ParseException { 232 if (state == S05_end) { 233 currentToken = TokenType.END_OF_STREAM; 234 return currentToken; 235 } 236 237 // Reset per-token scratch. 238 currentFieldName = null; 239 currentString = null; 240 currentNumberLexeme = null; 241 currentNumber = null; 242 243 int c = readSkipWsAndComments(); 244 if (c == -1) { 245 if (state != S00_expectValue || depth != 0) 246 throw parseException("Unexpected end of input"); 247 state = S05_end; 248 currentToken = TokenType.END_OF_STREAM; 249 return currentToken; 250 } 251 252 switch (state) { 253 case S00_expectValue -> readValueStart(c); 254 case S01_expectFieldName -> readFieldNameOrEndObject(c); 255 case S02_expectColon -> readColonThenValue(c); 256 case S03_expectCommaOrEndObject -> readCommaOrEndObject(c); 257 case S04_expectCommaOrEndArray -> readCommaOrEndArray(c); 258 default -> throw parseException("Internal tokenizer state error"); 259 } 260 return currentToken; 261 } 262 263 @Override /* Overridden from TokenReader */ 264 public TokenType getCurrentToken() { 265 return currentToken; 266 } 267 268 @Override /* Overridden from TokenReader */ 269 public int getDepth() { 270 return depth; 271 } 272 273 @Override /* Overridden from TokenReader */ 274 public String getFieldName() { 275 if (currentToken != TokenType.FIELD_NAME) 276 throw isex("Current token is not FIELD_NAME (was %s)", currentToken); 277 return currentFieldName; 278 } 279 280 @Override /* Overridden from TokenReader */ 281 public String getString() { 282 switch (currentToken) { 283 case VALUE_STRING: return currentString; 284 case FIELD_NAME: return currentFieldName; 285 case VALUE_NUMBER: return currentNumberLexeme; 286 case VALUE_BOOLEAN: return Boolean.toString(currentBoolean); 287 case VALUE_NULL: return null; 288 default: throw new IllegalStateException( 289 "Current token does not have a string view (was " + currentToken + ")"); 290 } 291 } 292 293 @Override /* Overridden from TokenReader */ 294 public Number getNumber() throws ParseException { 295 if (currentToken != TokenType.VALUE_NUMBER) 296 throw new IllegalStateException( 297 "Current token is not VALUE_NUMBER (was " + currentToken + ")"); 298 if (currentNumber == null) 299 currentNumber = StringUtils.parseNumber(currentNumberLexeme, null); 300 return currentNumber; 301 } 302 303 @Override /* Overridden from TokenReader */ 304 public String getNumberLexeme() { 305 if (currentToken != TokenType.VALUE_NUMBER) 306 throw new IllegalStateException( 307 "Current token is not VALUE_NUMBER (was " + currentToken + ")"); 308 return currentNumberLexeme; 309 } 310 311 @Override /* Overridden from TokenReader */ 312 public boolean getBool() { 313 if (currentToken != TokenType.VALUE_BOOLEAN) 314 throw new IllegalStateException( 315 "Current token is not VALUE_BOOLEAN (was " + currentToken + ")"); 316 return currentBoolean; 317 } 318 319 @Override /* Overridden from TokenReader */ 320 public byte[] getBinary() { 321 throw new IllegalStateException("JSON does not produce VALUE_BINARY tokens"); 322 } 323 324 @Override /* Overridden from TokenReader */ 325 public boolean canRead() throws IOException, ParseException { 326 // Auto-advance through structural separators that don't themselves emit value tokens. 327 // This lets users stream array elements with a canRead()/read() loop without manually 328 // consuming commas. 329 advanceToValueState(); 330 if (state != S00_expectValue) 331 return false; 332 // At S00_expectValue the next non-whitespace char might still be a closer (empty 333 // container). Peek to disambiguate. 334 var c = readSkipWsAndComments(); 335 if (c == -1) 336 return false; 337 r.unread(); 338 return c != ']' && c != '}'; 339 } 340 341 /** 342 * Helper for {@link #canRead()} and {@link #read(ClassMeta)}: if the cursor is 343 * sitting on a structural separator (comma between array elements, colon after a field name), 344 * consume it so the cursor lands at S00_expectValue. No-op if already at a value 345 * boundary or at end-of-container. 346 */ 347 private void advanceToValueState() throws IOException, ParseException { 348 while (true) { 349 switch (state) { 350 case S04_expectCommaOrEndArray: { 351 var c = readSkipWsAndComments(); 352 if (c == -1 || c == ']') { 353 if (c != -1) r.unread(); 354 return; 355 } 356 if (c != ',') 357 throw parseException("Expected , or ] but got '%s'", (char) c); 358 // If a trailing-comma case follows (next non-ws is ']'), keep the comma-or-end 359 // state so a subsequent next() closes the container correctly. Format-specific 360 // hook: plain JSON rejects trailing commas, JSON5 accepts. 361 var c2 = readSkipWsAndComments(); 362 if (c2 == ']') { 363 r.unread(); 364 if (!allowsTrailingComma()) 365 throw parseException("Trailing comma not allowed in this format"); 366 return; // state stays S04_expectCommaOrEndArray 367 } 368 if (c2 != -1) r.unread(); 369 state = S00_expectValue; 370 break; 371 } 372 case S03_expectCommaOrEndObject: { 373 var c = readSkipWsAndComments(); 374 if (c == -1 || c == '}') { 375 if (c != -1) r.unread(); 376 return; 377 } 378 if (c != ',') 379 throw parseException("Expected , or } but got '%s'", (char) c); 380 var c2 = readSkipWsAndComments(); 381 if (c2 == '}') { 382 r.unread(); 383 if (!allowsTrailingComma()) 384 throw parseException("Trailing comma not allowed in this format"); 385 return; // state stays S03_expectCommaOrEndObject 386 } 387 if (c2 != -1) r.unread(); 388 // After a comma inside an object, the next thing is a field name, not a value. 389 state = S01_expectFieldName; 390 return; 391 } 392 case S02_expectColon: { 393 var c = readSkipWsAndComments(); 394 if (c == -1) 395 return; 396 if (c != ':') 397 throw parseException("Expected : after field name but got '%s'", (char) c); 398 state = S00_expectValue; 399 break; 400 } 401 default: 402 return; 403 } 404 } 405 } 406 407 /** 408 * Subclass extension point: returns whether trailing commas are accepted before <c>]</c> / 409 * <c>}</c>. 410 * 411 * <p> 412 * Default returns <jk>false</jk> (strict RFC-8259 JSON). Json5TokenReader overrides this to 413 * return <jk>true</jk> for the JSON5 dialect. 414 * 415 * @return <jk>true</jk> if a trailing comma should be silently accepted; <jk>false</jk> if it 416 * should produce a parse error. 417 */ 418 protected boolean allowsTrailingComma() { 419 return false; 420 } 421 422 @Override /* Overridden from TokenReader */ 423 public <T> T read(Class<T> type) throws IOException, ParseException { 424 return read(session().getClassMeta(type)); 425 } 426 427 @Override /* Overridden from TokenReader */ 428 public <T> T read(ClassMeta<T> type) throws IOException, ParseException { 429 var s = session(); 430 advanceToValueState(); 431 assertReadValueState(); 432 try { 433 T o = s.readAnything(type, r, null, null); 434 markValueConsumed(); 435 return o; 436 } catch (Exception e) { 437 if (e instanceof IOException e2) throw e2; 438 if (e instanceof ParseException e2) throw e2; 439 throw new ParseException(s, "read failed: %s", e.getMessage()); 440 } 441 } 442 443 @Override /* Overridden from TokenReader */ 444 public <T> T read(Type type, Type... args) throws IOException, ParseException { 445 return read(session().<T>getClassMeta(type, args)); 446 } 447 448 private JsonParserSession session() { 449 if (session == null) 450 throw new UnsupportedOperationException( 451 "read is not available on this JsonTokenReader (no session attached). " + 452 "Open the cursor via JsonParser.readTokens(...) to enable read."); 453 return session; 454 } 455 456 /** 457 * Validates that the cursor is positioned at a value boundary (i.e. the next emitted token 458 * would be value-shaped). 459 */ 460 private void assertReadValueState() { 461 if (state != S00_expectValue) 462 throw new IllegalStateException(MSG_READ_NON_VALUE_STATE); 463 } 464 465 /** 466 * Resyncs the cursor's state machine after {@code parseAnything} consumed a value. The cursor's 467 * depth does not change (the value was contained at the current depth); only the post-value 468 * separator-expectation state needs to update. 469 */ 470 private void markValueConsumed() { 471 // We did not surface an explicit token to the caller, but functionally this is "a value 472 // just got emitted at the current depth," so the same post-value transition that next() 473 // would have performed applies. 474 afterValue(); 475 // The current token slot is meaningless after read (the caller has the bound POJO). 476 // Set a sentinel so accidental getStringValue() / getCurrentToken() usage doesn't return 477 // a stale prior token's value. 478 currentToken = TokenType.NOT_AVAILABLE; 479 } 480 481 @Override /* Overridden from TokenReader */ 482 public void skipChildren() throws IOException, ParseException { 483 if (currentToken != TokenType.START_OBJECT && currentToken != TokenType.START_ARRAY) 484 return; 485 var startDepth = depth - 1; 486 while (depth > startDepth) { 487 var t = next(); 488 if (t == TokenType.END_OF_STREAM) 489 throw parseException("Unexpected end of input while skipping children"); 490 } 491 } 492 493 @Override /* Overridden from TokenReader */ 494 public void close() throws IOException { 495 pipe.close(); 496 } 497 498 // State-machine dispatch 499 500 /** 501 * State-machine dispatch: handles the next character at {@link #S00_expectValue}. 502 * 503 * <p> 504 * Subclasses (e.g. Json5TokenReader) override this to add dialect-specific value shapes 505 * (single-quoted strings, bare identifiers, missing-value handling). 506 * 507 * @param c The next non-whitespace, non-comment character read from the input. 508 * @throws IOException If a problem occurred reading from the underlying stream. 509 * @throws ParseException If the input is malformed at this position. 510 */ 511 protected void readValueStart(int c) throws IOException, ParseException { 512 switch (c) { 513 case '{': 514 pushContainer(false); 515 currentToken = TokenType.START_OBJECT; 516 state = S01_expectFieldName; 517 return; 518 case '[': 519 pushContainer(true); 520 currentToken = TokenType.START_ARRAY; 521 state = S00_expectValue; 522 return; 523 case '"': 524 r.unread(); 525 currentString = maybeTrim(readString()); 526 currentToken = TokenType.VALUE_STRING; 527 afterValue(); 528 return; 529 case 't', 'f', 'n': 530 r.unread(); 531 readKeywordValue(c); 532 afterValue(); 533 return; 534 case ']': 535 if (depth > 0 && isInArray()) { 536 popContainer(); 537 currentToken = TokenType.END_ARRAY; 538 afterValue(); 539 return; 540 } 541 throw parseException("Unexpected character: '%s'", (char) c); 542 default: 543 if (isNumberStart(c)) { 544 r.unread(); 545 readNumberValue(); 546 afterValue(); 547 return; 548 } 549 throw parseException("Unexpected character: '%s'", (char) c); 550 } 551 } 552 553 /** 554 * State-machine dispatch: handles the next character at {@link #S01_expectFieldName}. 555 * 556 * <p> 557 * Subclasses (e.g. Json5TokenReader) override this to accept bare-identifier field names and 558 * single-quoted field names. 559 * 560 * @param c The next non-whitespace character read from the input. 561 * @throws IOException If a problem occurred reading from the underlying stream. 562 * @throws ParseException If the input is malformed at this position. 563 */ 564 protected void readFieldNameOrEndObject(int c) throws IOException, ParseException { 565 if (c == '}') { 566 popContainer(); 567 currentToken = TokenType.END_OBJECT; 568 afterValue(); 569 return; 570 } 571 if (c != '"') 572 throw parseException("Expected \" for field name but got '%s'", (char) c); 573 r.unread(); 574 currentFieldName = maybeTrim(readString()); 575 currentToken = TokenType.FIELD_NAME; 576 state = S02_expectColon; 577 } 578 579 private String maybeTrim(String s) { 580 if (!settings.trimStrings || s == null) 581 return s; 582 return s.trim(); 583 } 584 585 private void readColonThenValue(int c) throws IOException, ParseException { 586 if (c != ':') 587 throw parseException("Expected : after field name but got '%s'", (char) c); 588 var c2 = readSkipWsAndComments(); 589 if (c2 == -1) 590 throw parseException("Unexpected end of input after ':'"); 591 state = S00_expectValue; 592 readValueStart(c2); 593 } 594 595 /** 596 * State-machine dispatch: handles the next character at 597 * {@link #S03_expectCommaOrEndObject}. 598 * 599 * <p> 600 * Subclasses (e.g. Json5TokenReader) override this to accept trailing commas before <c>}</c>. 601 * 602 * @param c The next non-whitespace character read from the input. 603 * @throws IOException If a problem occurred reading from the underlying stream. 604 * @throws ParseException If the input is malformed at this position. 605 */ 606 protected void readCommaOrEndObject(int c) throws IOException, ParseException { 607 if (c == '}') { 608 popContainer(); 609 currentToken = TokenType.END_OBJECT; 610 afterValue(); 611 return; 612 } 613 if (c != ',') 614 throw parseException("Expected , or } but got '%s'", (char) c); 615 var c2 = readSkipWsAndComments(); 616 if (c2 == -1) 617 throw parseException("Unexpected end of input after ','"); 618 // After a comma inside an object the next thing must be a field name (or '}' is illegal here). 619 state = S01_expectFieldName; 620 readFieldNameOrEndObject(c2); 621 } 622 623 /** 624 * State-machine dispatch: handles the next character at 625 * {@link #S04_expectCommaOrEndArray}. 626 * 627 * <p> 628 * Subclasses (e.g. Json5TokenReader) override this to accept trailing commas before <c>]</c>. 629 * 630 * @param c The next non-whitespace character read from the input. 631 * @throws IOException If a problem occurred reading from the underlying stream. 632 * @throws ParseException If the input is malformed at this position. 633 */ 634 protected void readCommaOrEndArray(int c) throws IOException, ParseException { 635 if (c == ']') { 636 popContainer(); 637 currentToken = TokenType.END_ARRAY; 638 afterValue(); 639 return; 640 } 641 if (c != ',') 642 throw parseException("Expected , or ] but got '%s'", (char) c); 643 var c2 = readSkipWsAndComments(); 644 if (c2 == -1) 645 throw parseException("Unexpected end of input after ','"); 646 state = S00_expectValue; 647 readValueStart(c2); 648 } 649 650 /** 651 * Sets the post-value state based on the enclosing container. 652 * 653 * <p> 654 * Subclasses (e.g. JsonlTokenReader) override this when the format treats consecutive 655 * top-level values differently — JSONL transitions back to {@link #S00_expectValue} 656 * at depth 0 instead of {@link #S05_end} so the next line is read. 657 */ 658 protected void afterValue() { 659 if (depth == 0) 660 state = S05_end; 661 else if (isInArray()) 662 state = S04_expectCommaOrEndArray; 663 else 664 state = S03_expectCommaOrEndObject; 665 } 666 667 // Scalar readers 668 669 /** 670 * Reads a double-quoted JSON string from the current input position. 671 * 672 * <p> 673 * Handles escape sequences (<c>\\n</c>, <c>\\t</c>, <c>\\u<i>NNNN</i></c>, etc.) inline via 674 * {@link ParserReader#replace(char)}. Subclasses (e.g. Json5TokenReader) override this to 675 * also accept single-quoted strings. 676 * 677 * @return The parsed string (without surrounding quotes). 678 * @throws IOException If a problem occurred reading from the underlying stream. 679 * @throws ParseException If the input is malformed at this position. 680 */ 681 protected String readString() throws IOException, ParseException { 682 r.mark(); 683 var qc = r.read(); 684 if (qc != '"') 685 throw parseException("Did not find quote character marking beginning of string"); 686 String s = null; 687 var inEscape = false; 688 var c = 0; 689 while (c != -1) { 690 c = r.read(); 691 if (c <= 0x1F && c != -1) 692 throw parseException("Unescaped control character encountered: '0x%s'", f("%04X", c)); 693 if (inEscape) { 694 switch (c) { 695 case 'n': r.replace('\n'); break; 696 case 'r': r.replace('\r'); break; 697 case 't': r.replace('\t'); break; 698 case 'f': r.replace('\f'); break; 699 case 'b': r.replace('\b'); break; 700 case '\\': r.replace('\\'); break; 701 case '/': r.replace('/'); break; 702 case '\'': r.replace('\''); break; 703 case '"': r.replace('"'); break; 704 case 'u': { 705 var hex = r.read(4); 706 try { 707 r.replace(Integer.parseInt(hex, 16), 6); 708 } catch (@SuppressWarnings("unused") NumberFormatException e) { 709 throw parseException("Invalid Unicode escape sequence in string"); 710 } 711 break; 712 } 713 default: 714 throw parseException("Invalid escape sequence in string"); 715 } 716 inEscape = false; 717 } else { 718 if (c == '\\') { 719 inEscape = true; 720 r.delete(); 721 } else if (c == qc) { 722 s = r.getMarked(1, -1); 723 break; 724 } 725 } 726 } 727 if (s == null) 728 throw parseException("Unterminated string"); 729 return s; 730 } 731 732 /** 733 * Reads one of the JSON keyword literals (<c>true</c>, <c>false</c>, <c>null</c>) starting 734 * with the given lookahead character. Sets {@link #currentToken} and {@link #currentBoolean} 735 * accordingly. 736 * 737 * @param c The first character of the keyword (one of <c>'t'</c>, <c>'f'</c>, <c>'n'</c>). 738 * @throws IOException If a problem occurred reading from the underlying stream. 739 * @throws ParseException If the keyword is malformed. 740 */ 741 protected void readKeywordValue(int c) throws IOException, ParseException { 742 switch (c) { 743 case 't': 744 expectLiteral("true"); 745 currentBoolean = true; 746 currentToken = TokenType.VALUE_BOOLEAN; 747 return; 748 case 'f': 749 expectLiteral("false"); 750 currentBoolean = false; 751 currentToken = TokenType.VALUE_BOOLEAN; 752 return; 753 case 'n': 754 expectLiteral("null"); 755 currentToken = TokenType.VALUE_NULL; 756 return; 757 default: 758 throw parseException("Unexpected character: '%s'", (char) c); 759 } 760 } 761 762 private void expectLiteral(String literal) throws IOException, ParseException { 763 var s = r.read(literal.length()); 764 if (!literal.equals(s)) 765 throw parseException("Unrecognized syntax. Expected='%s', Actual='%s'", literal, s); 766 } 767 768 /** 769 * Reads a JSON number from the current input position. Captures the lexeme via 770 * {@link ParserReader#readNumberString()} and validates it; defers actual numeric parsing 771 * to {@link #getNumber()} (lazy). Sets {@link #currentNumberLexeme} and {@link #currentToken}. 772 * 773 * @throws IOException If a problem occurred reading from the underlying stream. 774 * @throws ParseException If the input is not a valid JSON number. 775 */ 776 protected void readNumberValue() throws IOException, ParseException { 777 // Mirror JsonParserSession#parseNumber's lexeme-collection: collect "number chars" then 778 // hand off to StringUtils.parseNumber. The lexeme is preserved as currentNumberLexeme so 779 // precision-sensitive consumers can recover the original textual form (resolved Q6). 780 var s = r.readNumberString(); 781 validateNumberLexeme(s); 782 currentNumberLexeme = s; 783 currentToken = TokenType.VALUE_NUMBER; 784 // Number itself is parsed lazily on first getNumberValue() call. 785 } 786 787 private void validateNumberLexeme(String s) throws ParseException { 788 if (s.isEmpty()) 789 throw parseException("Invalid JSON number: '%s'", s); 790 var isNegative = false; 791 var c = s.charAt(0); 792 if (c == '-') { 793 isNegative = true; 794 c = (s.length() == 1 ? 'x' : s.charAt(1)); 795 } 796 if (c == '.') 797 throw parseException("Invalid JSON number: '%s'", s); 798 if (c == '0' && s.length() > (isNegative ? 2 : 1)) { 799 var c2 = s.charAt(isNegative ? 2 : 1); 800 if (c2 != '.' && c2 != 'e' && c2 != 'E') 801 throw parseException("Invalid JSON number: '%s'", s); 802 } 803 var i = s.indexOf('.'); 804 if (i != -1 && (s.length() == (i + 1) || !isDigit(s.charAt(i + 1)))) 805 throw parseException("Invalid JSON number: '%s'", s); 806 } 807 808 // Reader helpers 809 810 /** 811 * Reads the next non-whitespace, non-comment character or -1 at end of stream. 812 * 813 * @return The next significant character, or <c>-1</c> at end of input. 814 * @throws IOException If a problem occurred reading from the underlying stream. 815 * @throws ParseException If a JSON comment is malformed. 816 */ 817 protected int readSkipWsAndComments() throws IOException, ParseException { 818 while (true) { 819 var c = r.read(); 820 if (c == -1) 821 return -1; 822 if (c == '/') 823 skipComment(); 824 else if (!Character.isWhitespace(c)) 825 return c; 826 } 827 } 828 829 @SuppressWarnings({ 830 "java:S2677" // read() return intentionally discarded: consuming the already-peeked '/' that closes a block comment. 831 }) 832 private void skipComment() throws IOException, ParseException { 833 var c = r.read(); 834 if (c == '*') { 835 while (true) { 836 c = r.read(); 837 if (c == -1) 838 throw parseException("Unterminated block comment"); 839 if (c == '*' && r.peek() == '/') { 840 r.read(); 841 return; 842 } 843 } 844 } 845 if (c == '/') { 846 while (true) { 847 c = r.read(); 848 if (c == -1 || c == '\n') 849 return; 850 } 851 } 852 throw parseException("Open ended comment"); 853 } 854 855 /** 856 * Returns whether the given character could start a JSON number lexeme (digit or minus sign). 857 * 858 * @param c The character. 859 * @return <jk>true</jk> if the character could start a number. 860 */ 861 protected static boolean isNumberStart(int c) { 862 return c == '-' || (c >= '0' && c <= '9'); 863 } 864 865 /** 866 * Returns whether the given character is an ASCII decimal digit. 867 * 868 * @param c The character. 869 * @return <jk>true</jk> if the character is between <c>'0'</c> and <c>'9'</c> inclusive. 870 */ 871 protected static boolean isDigit(int c) { 872 return c >= '0' && c <= '9'; 873 } 874 875 // Container stack 876 877 /** 878 * Pushes a new container onto the cursor's depth stack. 879 * 880 * @param isArray <jk>true</jk> if the new container is an array; <jk>false</jk> for an object. 881 */ 882 protected void pushContainer(boolean isArray) { 883 if (depth < 64) { 884 if (isArray) 885 containerBits |= (1L << depth); 886 else 887 containerBits &= ~(1L << depth); 888 } else { 889 if (containerOverflow == null || depth - 64 >= containerOverflow.length) 890 growOverflow(); 891 containerOverflow[depth - 64] = isArray; 892 } 893 depth++; 894 } 895 896 /** 897 * Pops the top container off the cursor's depth stack. 898 * 899 * @throws ParseException If the stack is already empty (defensive only; well-formed JSON 900 * cannot trigger this). 901 */ 902 protected void popContainer() throws ParseException { 903 if (depth == 0) 904 throw parseException("Container stack underflow"); 905 depth--; 906 } 907 908 /** 909 * Returns whether the cursor's current container (the one most recently pushed) is an array. 910 * 911 * @return <jk>true</jk> if currently inside an array; <jk>false</jk> if inside an object. 912 */ 913 protected boolean isInArray() { 914 var top = depth - 1; 915 if (top < 64) 916 return (containerBits & (1L << top)) != 0; 917 return containerOverflow[top - 64]; 918 } 919 920 private void growOverflow() { 921 var n = containerOverflow == null ? 16 : containerOverflow.length << 1; 922 var bigger = new boolean[n]; 923 if (containerOverflow != null) 924 System.arraycopy(containerOverflow, 0, bigger, 0, containerOverflow.length); 925 containerOverflow = bigger; 926 } 927 928 /** 929 * Builds a {@link ParseException} with the given message + {@link String#format(String, Object...) String.format}-style 930 * (<c>%s</c>) arguments. Used by all dispatch methods to surface input-malformed errors with consistent 931 * formatting. 932 * 933 * @param message The exception message format string (<c>%s</c>, <c>%d</c>, ...). 934 * @param args The format arguments. 935 * @return A new {@link ParseException} ready to throw. 936 */ 937 protected ParseException parseException(String message, Object... args) { 938 return new ParseException(message, args); 939 } 940}