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 &mdash; 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 &mdash; 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 &mdash; 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 &mdash; 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}