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.lang.StateEnum.*;
020import static org.apache.juneau.commons.utils.CollectionUtils.*;
021import static org.apache.juneau.commons.utils.Shorts.*;
022import static org.apache.juneau.commons.utils.StringUtils.*;
023
024import java.io.*;
025import java.lang.reflect.*;
026import java.util.*;
027
028import org.apache.juneau.commons.bean.*;
029import org.apache.juneau.commons.lang.*;
030import org.apache.juneau.commons.reflect.*;
031import org.apache.juneau.commons.utils.*;
032import org.apache.juneau.marshall.*;
033import org.apache.juneau.marshall.collections.*;
034import org.apache.juneau.marshall.parser.*;
035import org.apache.juneau.marshall.stream.*;
036import org.apache.juneau.marshall.swap.spi.*;
037
038/**
039 * Session object that lives for the duration of a single use of {@link JsonParser}.
040 *
041 * <h5 class='section'>Notes:</h5><ul>
042 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
043 * </ul>
044 *
045 * <h5 class='section'>See Also:</h5><ul>
046 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JsonSupport">JSON</a>
047
048 * </ul>
049 */
050@SuppressWarnings({
051   "java:S110", // Deep inheritance inherent to the serializer/parser session hierarchy
052   "java:S115", // Constants use camelCase intentionally (e.g., decChars)
053   "java:S125", // State-machine and parse-path comments (S1: ..., Let o be null) are documentation, not commented-out code
054   "java:S135", // readIntoBeanMap2(), readIntoCollection2() and readIntoMap2() use several break/continue guards for invalid characters and separators; restructuring would reduce clarity
055   "java:S1168", // readFieldName() returns null for a missing name and doRead() for empty input; the unreachable fall-through returns in readIntoBeanMap2()/readIntoCollection2()/readIntoMap2() also return null
056   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
057   "java:S2583", // State variables persist across loop iterations
058   "java:S3776", // readAnything(), readIntoMap2(), readIntoCollection2() and readIntoBeanMap2() are branch-heavy JSON token state machines; splitting them would obscure the grammar
059   "java:S6541", // Brain method acceptable for JSON parser dispatch and state machines
060   "rawtypes", // Raw types necessary for generic type handling
061   "resource", // ParserReader is managed by caller
062   "unchecked" // Type erasure requires unchecked casts
063})
064public class JsonParserSession extends ReaderParserSession implements TokenReadable, ArrayRecordReadable {
065
066   /**
067    * Builder class.
068    */
069   @SuppressWarnings({
070      "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name.
071   })
072   public abstract static class Builder<SELF extends Builder<SELF>> extends ReaderParserSession.Builder<SELF> {
073
074      private boolean validateEnd;
075      private final JsonParser ctx;
076
077      /**
078       * Constructor
079       *
080       * @param ctx The context creating this session.
081       */
082      protected Builder(JsonParser ctx) {
083         super(ctx);
084         this.ctx = ctx;
085         validateEnd = ctx.isValidateEnd();
086      }
087
088      @Override
089      public JsonParserSession build() {
090         return new JsonParserSession(this);
091      }
092
093      /**
094       * Validate end.
095       *
096       * @param value The new value for this setting.
097       * @return This object.
098       */
099      public SELF validateEnd(boolean value) {
100         validateEnd = value;
101         return self();
102      }
103
104      @Override /* Overridden from Builder */
105      public SELF property(String key, Object value) {
106         if (key == null) { super.property(key, value); return self(); }
107         switch (key) {
108            case "validateEnd", "JsonParserSession.validateEnd":
109               return validateEnd(cvt(value, Boolean.class));
110            default:
111               super.property(key, value);
112               return self();
113         }
114      }
115
116   }
117
118   /**
119    * Concrete default builder leaf for the non-subclassed {@code create()} path (CRTP terminal).
120    */
121   public static final class DefaultBuilder extends Builder<DefaultBuilder> {
122
123      DefaultBuilder(JsonParser ctx) {
124         super(ctx);
125      }
126   }
127
128   private static final AsciiSet decChars = AsciiSet.create().ranges("0-9").build();
129
130   /**
131    * Creates a new builder for this object.
132    *
133    * @param ctx The context creating this session.
134    * @return A new builder.
135    */
136   @SuppressWarnings({
137      "java:S1452" // Builder<?> wildcard return intentional; callers use it to construct session instances polymorphically
138   })
139   public static Builder<?> create(JsonParser ctx) {
140      return new DefaultBuilder(ctx);
141   }
142
143   private final JsonParser ctx;
144   private final boolean validateEnd;
145
146   /**
147    * Constructor.
148    *
149    * @param builder The builder for this object.
150    */
151   protected JsonParserSession(Builder<?> builder) {
152      super(builder);
153      ctx = builder.ctx;
154      validateEnd = builder.validateEnd;
155   }
156
157   <T> T readAnything(ClassMeta<?> eType, ParserReader r, Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
158
159      if (eType == null)
160         eType = object();
161      var swap = (ObjectSwap<T,Object>)eType.getSwap(this);
162      var builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this);
163      ClassMeta<?> sType = null;
164      if (nn(builder))
165         sType = builder.getBuilderClassMeta(this);
166      else if (nn(swap))
167         sType = swap.getSwapClassMeta(this);
168      else
169         sType = eType;
170
171      if (sType.isOptional())
172         return (T)o(readAnything(eType.getElementType(), r, outer, pMeta));
173
174      setCurrentClass(sType);
175      var wrapperAttr = getJsonClassMeta(sType).getWrapperAttr();
176
177      Object o = null;
178
179      skipCommentsAndSpace(r);
180      if (nn(wrapperAttr))
181         skipWrapperAttrStart(r, wrapperAttr);
182      int c = r.peek();
183      if (c == -1) {
184         onEmptyInput();
185      } else if ((c == ',' || c == '}' || c == ']')) {
186         onMissingValue();
187      } else if (c == 'n') {
188         readKeyword("null", r);
189      } else if (sType.isObject()) {
190         if (c == '{') {
191            var m2 = newGenericMap();
192            readIntoMap2(r, m2, string(), object(), pMeta);
193            o = cast(m2, pMeta, eType);
194         } else if (c == '[') {
195            o = readIntoCollection2(r, newGenericList(), object(), pMeta);
196         } else if (c == '\'' || c == '"') {
197            o = readString(r);
198            if (sType.isChar())
199               o = parseCharacter(o);
200         } else if (isNumberStartChar(c)) {
201            o = readNumber(r, null);
202         } else if (c == 't') {
203            readKeyword("true", r);
204            o = Boolean.TRUE;
205         } else {
206            readKeyword("false", r);
207            o = Boolean.FALSE;
208         }
209      } else if (nn(builder)) {
210         var m = toBeanMap(builder.create(this, eType));
211         o = builder.build(this, checkRequired(readIntoBeanMap2(r, m)).getBean(), eType);
212      } else if (sType.canCreateNewBean(outer)) {
213         var m = newBeanMap(outer, sType.inner());
214         o = checkRequired(readIntoBeanMap2(r, m)).getBean();
215      } else if (sType.isMap()) {
216         Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType));
217         o = readIntoMap2(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
218      } else if (sType.isCollection()) {
219         if (c == '{') {
220            var m = newGenericMap();
221            readIntoMap2(r, m, string(), object(), pMeta);
222            o = cast(m, pMeta, eType);
223         } else {
224            Collection l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance() : newGenericList());
225            o = readIntoCollection2(r, l, sType, pMeta);
226         }
227      } else if (sType.isArray() || sType.isArgs()) {
228         if (c == '{') {
229            var m = newGenericMap();
230            readIntoMap2(r, m, string(), object(), pMeta);
231            o = cast(m, pMeta, eType);
232         } else {
233            var l = (ArrayList)readIntoCollection2(r, list(), sType, pMeta);
234            o = toArray(sType, l);
235         }
236      } else if (sType.isCharSequence()) {
237         o = readString(r);
238      } else if (sType.isChar()) {
239         o = parseCharacter(readString(r));
240      } else if (sType.isNumber()) {
241         o = readNumber(r, (Class<? extends Number>)sType.inner());
242      } else if (sType.isBoolean()) {
243         o = readBoolean(r);
244      } else if (sType.isDate()) {
245         o = readDate(readString(r), sType);
246      } else if (sType.isCalendar()) {
247         o = readCalendar(readString(r), sType);
248      } else if (sType.isTemporal()) {
249         o = readTemporal(readString(r), sType);
250      } else if (sType.isDuration()) {
251         o = readDuration(readString(r));
252      } else if (sType.isPeriod()) {
253         o = readPeriod(readString(r));
254      } else if (sType.canCreateNewInstanceFromString(outer) && (c == '\'' || c == '"')) {
255         o = sType.newInstanceFromString(outer, readString(r));
256      } else if (c == '{') {
257         Map m = newGenericMap();
258         readIntoMap2(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
259         if (m.containsKey(getBeanTypePropertyName(eType)))
260            o = cast((MarshalledMap)m, pMeta, eType);
261         else if (nn(sType.getProxyInvocationHandler()))
262            o = checkRequired(newBeanMap(outer, sType.inner()).load(m)).getBean();
263         else
264            throw new ParseException(this, "Class '%s' could not be instantiated.  Reason: '%s'", cn(sType), sType.getNotABeanReason());
265      } else if (sType.canCreateNewInstanceFromString(outer) && canCoerceNonStringToString()) {
266         o = sType.newInstanceFromString(outer, readString(r));
267      } else {
268         throw new ParseException(this, "Unrecognized syntax for class type '%s', starting character '%s'", sType, (char)c);
269      }
270
271      if (nn(wrapperAttr))
272         skipWrapperAttrEnd(r);
273
274      if (nn(swap) && nn(o))
275         o = unswap(swap, o, eType);
276
277      if (nn(parentBean()))
278         setParent(eType, o, parentBean());
279
280      return (T)o;
281   }
282
283   private Boolean readBoolean(ParserReader r) throws IOException, ParseException {
284      int c = r.peek();
285      if (c == '\'' || c == '"')
286         return b(readString(r));
287      if (c == 't') {
288         readKeyword("true", r);
289         return Boolean.TRUE;
290      } else if (c == 'f') {
291         readKeyword("false", r);
292         return Boolean.FALSE;
293      } else {
294         throw new ParseException(this, "Unrecognized syntax.  Expected boolean value, actual='%s'", r.read(100));
295      }
296   }
297
298   /*
299    * Parse a JSON attribute from the character array at the specified position, then
300    * set the position marker to the last character in the field name.
301    */
302   protected String readFieldName(ParserReader r) throws IOException, ParseException {
303      int c = r.peek();
304      if (c == '\'' || c == '"')
305         return readString(r);
306      if (c == 'n') {
307         readKeyword("null", r);
308         return null;
309      }
310      throw new ParseException(this, "Unquoted attribute detected.");
311   }
312
313   private <T> BeanMap<T> readIntoBeanMap2(ParserReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException {
314
315      // S1: Looking for outer {
316      // S2: Looking for attrName start.
317      // S3: Found attrName end, looking for :.
318      // S4: Found :, looking for valStart: { [ " ' LITERAL.
319      // S5: Looking for , or }
320
321      var state = S1;
322      var currAttr = "";
323      int c = 0;
324      var pb = swapParentBean(m.getBean(false));
325      mark();
326      try {
327         while (c != -1) {
328            c = r.read();
329            if (state == S1) {
330               if (c == '{') {
331                  state = S2;
332               } else if (isCommentOrWhitespace(c)) {
333                  skipCommentsAndSpace(r.unread());
334               } else {
335                  break;
336               }
337            } else if (state == S2) {
338               if (c == '}') {
339                  return m;
340               } else if (isCommentOrWhitespace(c)) {
341                  skipCommentsAndSpace(r.unread());
342               } else {
343                  r.unread();
344                  mark();
345                  currAttr = readFieldName(r);
346                  state = S3;
347               }
348            } else if (state == S3) {  // NOSONAR - State check necessary for state machine
349               if (c == ':')
350                  state = S4;
351            } else if (state == S4) {
352               if (isCommentOrWhitespace(c)) {
353                  skipCommentsAndSpace(r.unread());
354               } else {
355               if (! currAttr.equals(getBeanTypePropertyName((ClassMeta<?>) m.getBeanInfo()))) {
356                  var pMeta = m.getPropertyMeta(currAttr);
357                  setCurrentProperty(pMeta);
358                  if (pMeta == null) {
359                     onUnknownProperty(currAttr, m, readAnything(object(), r.unread(), m.getBean(false), null));
360                     unmark();
361                  } else {
362                     unmark();
363                     var cm = (ClassMeta<?>) pMeta.getBeanInfo();
364                     Object value = readAnything(cm, r.unread(), m.getBean(false), pMeta);
365                     setName(cm, value, currAttr);
366                        try {
367                           pMeta.set(m, currAttr, value);
368                        } catch (BeanRuntimeException e) {
369                           onBeanSetterException(pMeta, e);
370                           throw e;
371                        }
372                     }
373                     setCurrentProperty(null);
374                  }
375                  state = S5;
376               }
377            } else if (state == S5) {
378               if (c == ',')
379                  state = S2;
380               else if (isCommentOrWhitespace(c))
381                  skipCommentsAndSpace(r.unread());
382               else if (c == '}') {
383                  return m;
384               }
385            }
386         }
387         if (state == S1)
388            throw new ParseException(this, "Expected '{' at beginning of JSON object.");
389         else if (state == S3)
390            throw new ParseException(this, "Could not find ':' following attribute name on JSON object.");
391         else if (state == S5)
392            throw new ParseException(this, "Could not find '}' marking end of JSON object.");
393      } finally {
394         unmark();
395         swapParentBean(pb);
396      }
397
398      return null; // Unreachable.
399   }
400
401   private <E> Collection<E> readIntoCollection2(ParserReader r, Collection<E> l, ClassMeta<?> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
402
403      // S1: Looking for outermost [
404      // S2: Looking for starting [ or { or " or ' or LITERAL or ]
405      // S3: Looking for , or ]
406      // S4: Looking for starting [ or { or " or ' or LITERAL
407
408      int argIndex = 0;
409
410      var state = S1;
411      int c = 0;
412      while (c != -1) {
413         c = r.read();
414         if (state == S1) {
415            if (c == '[')
416               state = S2;
417            else if (isCommentOrWhitespace(c))
418               skipCommentsAndSpace(r.unread());
419            else
420               break;  // Invalid character found.
421         } else if (state == S2) {
422            if (c == ']') {
423               return l;
424            } else if (isCommentOrWhitespace(c)) {
425               skipCommentsAndSpace(r.unread());
426            } else if (c != -1) {
427               l.add((E)readAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, pMeta));
428               state = S3;
429            }
430         } else if (state == S3) {  // NOSONAR - State check necessary for state machine
431            if (c == ',') {
432               state = S4;
433            } else if (isCommentOrWhitespace(c)) {
434               skipCommentsAndSpace(r.unread());
435            } else if (c == ']') {
436               return l;
437            } else {
438               break;  // Invalid character found.
439            }
440         } else if (state == S4) {
441            if (isCommentOrWhitespace(c)) {
442               skipCommentsAndSpace(r.unread());
443            } else if (c == ']') {
444               if (allowsTrailingComma())
445                  return l;
446               break;
447            } else if (c != -1) {
448               l.add((E)readAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, pMeta));
449               state = S3;
450            }
451         }
452      }
453      if (state == S1)
454         throw new ParseException(this, "Expected '[' at beginning of JSON array.");
455      else if (state == S2)
456         throw new ParseException(this, "Expected one of the following characters: {,[,',\",LITERAL.");
457      else if (state == S3)
458         throw new ParseException(this, "Expected ',' or ']'.");
459      else if (state == S4)
460         throw new ParseException(this, "Unexpected trailing comma in array.");
461
462      return null;  // Unreachable.
463   }
464
465   private <K,V> Map<K,V> readIntoMap2(ParserReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
466
467      if (keyType == null)
468         keyType = (ClassMeta<K>)string();
469
470      // S1: Looking for outer {
471      // S2: Looking for attrName start.
472      // S3: Found attrName end, looking for :.
473      // S4: Found :, looking for valStart: { [ " ' LITERAL.
474      // S5: Looking for , or }
475      // S6: Found , looking for attr start.
476
477      skipCommentsAndSpace(r);
478      var state = S1;
479      String currAttr = null;
480      int c = 0;
481      while (c != -1) {
482         c = r.read();
483         if (state == S1) {
484            if (c == '{')
485               state = S2;
486            else
487               break;
488         } else if (state == S2) {
489            if (c == '}') {
490               return m;
491            } else if (isCommentOrWhitespace(c)) {
492               skipCommentsAndSpace(r.unread());
493            } else {
494               currAttr = readFieldName(r.unread());
495               state = S3;
496            }
497         } else if (state == S3) {  // NOSONAR - State check necessary for state machine
498            if (c == ':')
499               state = S4;
500         } else if (state == S4) {
501            if (isCommentOrWhitespace(c)) {
502               skipCommentsAndSpace(r.unread());
503            } else {
504               K key = convertAttrToType(m, currAttr, keyType);
505               V value = readAnything(valueType, r.unread(), m, pMeta);
506               setName(valueType, value, key);
507               m.put(key, value);
508               state = S5;
509            }
510         } else if (state == S5) {
511            if (c == ',') {
512               state = S6;
513            } else if (isCommentOrWhitespace(c)) {
514               skipCommentsAndSpace(r.unread());
515            } else if (c == '}') {
516               return m;
517            } else {
518               break;
519            }
520         } else if (state == S6) {
521            if (c == '}') {
522               if (allowsTrailingComma())
523                  return m;
524               break;
525            } else if (isCommentOrWhitespace(c)) {
526               skipCommentsAndSpace(r.unread());
527            } else {
528               currAttr = readFieldName(r.unread());
529               state = S3;
530            }
531         }
532      }
533      if (state == S1)
534         throw new ParseException(this, "Expected '{' at beginning of JSON object.");
535      else if (state == S3)
536         throw new ParseException(this, "Could not find ':' following attribute name on JSON object.");
537      else if (state == S5)
538         throw new ParseException(this, "Could not find '}' marking end of JSON object.");
539      else if (state == S6)
540         throw new ParseException(this, "Unexpected '}' found in JSON object.");
541
542      return null; // Unreachable.
543   }
544
545   /*
546    * Looks for the keywords true, false, or null.
547    * Throws an exception if any of these keywords are not found at the specified position.
548    */
549   protected void readKeyword(String keyword, ParserReader r) throws IOException, ParseException {
550      String s = r.read(keyword.length());
551      if (s.equals(keyword))
552         return;
553      throw new ParseException(this, "Unrecognized syntax.  Expected='%s', Actual='%s'", keyword, s);
554   }
555
556   protected Number readNumber(ParserReader r, Class<? extends Number> type) throws IOException, ParseException {
557      int c = r.peek();
558      if (c == '\'' || c == '"')
559         return readNumber(r, readString(r), type);
560      return readNumber(r, r.readNumberString(), type);
561   }
562
563   @SuppressWarnings({
564      "java:S1172", // r accepted for API consistency; subclasses may use it for position tracking
565      "unused" // r accepted for API consistency; subclasses may use it for position tracking
566   })
567   protected Number readNumber(ParserReader r, String s, Class<? extends Number> type) throws ParseException {
568
569      if (s.isEmpty())
570         throw new ParseException(this, "Invalid JSON number: '%s'", s);
571
572      var isNegative = false;
573      var c = s.charAt(0);
574      if (c == '-') {
575         isNegative = true;
576         c = (s.length() == 1 ? 'x' : s.charAt(1));
577      }
578
579      if (c == '.')
580         throw new ParseException(this, "Invalid JSON number: '%s'", s);
581
582      if (c == '0' && s.length() > (isNegative ? 2 : 1)) {
583         var c2 = s.charAt((isNegative ? 2 : 1));
584         if (c2 != '.' && c2 != 'e' && c2 != 'E')
585            throw new ParseException(this, "Invalid JSON number: '%s'", s);
586      }
587
588      var i = s.indexOf('.');
589      if (i != -1 && (s.length() == (i + 1) || ! decChars.contains(s.charAt(i + 1))))
590         throw new ParseException(this, "Invalid JSON number: '%s'", s);
591
592      return StringUtils.parseNumber(s, type);
593   }
594
595   /*
596    * Starting from the specified position in the character array, returns the
597    * position of the character " or '.
598    * If the string consists of a concatenation of strings (e.g. 'AAA' + "BBB"), this method
599    * will automatically concatenate the strings and return the result.
600    */
601   protected String readString(ParserReader r) throws IOException, ParseException {
602      r.mark();
603      int qc = r.read();
604      if (qc != '"') {
605         var msg = (qc == '\'' ? "Invalid quote character \"%s\" being used." : "Did not find quote character marking beginning of string.  Character=\"%s\"");
606         throw new ParseException(this, msg, (char)qc);
607      }
608      String s = null;
609      boolean isInEscape = false;
610      int c = 0;
611      while (c != -1) {
612         c = r.read();
613         if (c <= 0x1F)
614            throw new ParseException(this, "Unescaped control character encountered: '0x%s'", f("%04X", c));
615         if (isInEscape) {
616            // @formatter:off
617            switch (c) {
618               case 'n': r.replace('\n'); break;
619               case 'r': r.replace('\r'); break;
620               case 't': r.replace('\t'); break;
621               case 'f': r.replace('\f'); break;
622               case 'b': r.replace('\b'); break;
623               case '\\': r.replace('\\'); break;
624               case '/': r.replace('/'); break;
625               case '\'': r.replace('\''); break;
626               case '"': r.replace('"'); break;
627               case 'u': {
628                  String n = r.read(4);
629                  try {
630                     r.replace(Integer.parseInt(n, 16), 6);
631                  } catch (@SuppressWarnings("unused") NumberFormatException e) {
632                     throw new ParseException(this, "Invalid Unicode escape sequence in string.");
633                  }
634                  break;
635               }
636               default:
637                  throw new ParseException(this, "Invalid escape sequence in string.");
638            }
639            // @formatter:on
640            isInEscape = false;
641         } else {
642            if (c == '\\') {
643               isInEscape = true;
644               r.delete();
645            } else if (c == qc) {
646               s = r.getMarked(1, -1);
647               break;
648            }
649         }
650      }
651      skipCommentsAndSpace(r);
652      if (r.peek() == '+')
653         throw new ParseException(this, "String concatenation detected.");
654      return trim(s);
655   }
656
657   /*
658    * Doesn't actually parse anything, but when positioned at the beginning of comment,
659    * it will move the pointer to the last character in the comment.
660    */
661   protected void skipComments(ParserReader r) throws ParseException, IOException {
662      int c = r.read();
663      if (c == '*') {
664         while (c != -1)
665            if ((c = r.read()) == '*' && (c = r.read()) == '/')
666               return;
667      } else if (c == '/') {
668         while (c != -1) {
669            c = r.read();
670            if (c == -1 || c == '\n')
671               return;
672         }
673      }
674      throw new ParseException(this, "Open ended comment.");
675   }
676
677   protected void skipCommentsAndSpace(ParserReader r) throws IOException, ParseException {
678      int c = 0;
679      while ((c = r.read()) != -1) {
680         if (! isWhitespace(c)) {
681            if (c == '/')
682               throw new ParseException(this, "Javascript comment detected.");
683            r.unread();
684            return;
685         }
686      }
687   }
688
689   /*
690    * Doesn't actually parse anything, but moves the position beyond the construct "}" when
691    * the @Json(wrapperAttr) annotation is used on a class.
692    */
693   private void skipWrapperAttrEnd(ParserReader r) throws ParseException, IOException {
694      int c = 0;
695      while ((c = r.read()) != -1) {
696         if (! isWhitespace(c)) {
697            if (c == '/') {
698               throw new ParseException(this, "Javascript comment detected.");
699            } else if (c == '}') {
700               return;
701            } else {
702               throw new ParseException(this, "Could not find '}' at the end of JSON wrapper object.");
703            }
704         }
705      }
706   }
707
708   /*
709    * Doesn't actually parse anything, but moves the position beyond the construct "{wrapperAttr:" when
710    * the @Json(wrapperAttr) annotation is used on a class.
711    */
712   private void skipWrapperAttrStart(ParserReader r, String wrapperAttr) throws IOException, ParseException {
713
714      // S1: Looking for outer '{'
715      // S2: Looking for attrName start.
716      // S3: Found attrName end, looking for :.
717      // S4: Found :, looking for valStart: { [ " ' LITERAL.
718
719      var state = S1;
720      String currAttr = null;
721      int c = 0;
722      while (c != -1) {
723         c = r.read();
724         if (state == S1) {
725            if (c == '{')
726               state = S2;
727         } else if (state == S2) {
728            if (isCommentOrWhitespace(c)) {
729               skipCommentsAndSpace(r.unread());
730            } else {
731               currAttr = readFieldName(r.unread());
732               if (! currAttr.equals(wrapperAttr))
733                  throw new ParseException(this, "Expected to find wrapper attribute '%s' but found attribute '%s'", wrapperAttr, currAttr);
734               state = S3;
735            }
736         } else if (state == S3) {  // NOSONAR - State check necessary for state machine
737            if (c == ':')
738               state = S4;
739         } else if (state == S4) {
740            if (isCommentOrWhitespace(c)) {
741               skipCommentsAndSpace(r.unread());
742            } else {
743               r.unread();
744               return;
745            }
746         }
747      }
748      if (state == S1)
749         throw new ParseException(this, "Expected '{' at beginning of JSON object.");
750      else if (state == S2)
751         throw new ParseException(this, "Could not find attribute name on JSON object.");
752      else
753         throw new ParseException(this, "Could not find ':' following attribute name on JSON object.");
754   }
755
756   /*
757    * Call this method after you've finished a parsing a string to make sure that if there's any
758    * remainder in the input, that it consists only of whitespace and comments.
759    */
760   private void validateEnd(ParserReader r) throws IOException, ParseException {
761      if (! isValidateEnd())
762         return;
763      skipCommentsAndSpace(r);
764      int c = r.read();
765      if (c != -1 && c != ';')  // var x = {...}; expressions can end with a semicolon.
766         throw new ParseException(this, "Remainder after parse: '%s'.", (char)c);
767   }
768
769   @Override /* Overridden from ParserSession */
770   protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
771      try (var r = pipe.getParserReader()) {
772         if (r == null)
773            return null;
774         T o = readAnything(type, r, getOuter(), null);
775         validateEnd(r);
776         return o;
777      }
778   }
779
780   /**
781    * Opens a low-level pull-parser cursor over a JSON document, bound to this session's live
782    * {@link BeanSession}/{@link MarshallingContext}.
783    *
784    * <p>
785    * The cursor is purely structural &mdash; object swaps and {@code @Schema} annotations are not
786    * applied at this layer.  See {@link TokenReader} for the contract.
787    *
788    * <h5 class='section'>Builder properties honored:</h5>
789    * The structurally-applicable subset flows through to the cursor: <c>autoCloseStreams</c>,
790    * <c>unbuffered</c>, <c>trimStrings</c> (applied to {@link TokenType#VALUE_STRING} and
791    * {@link TokenType#FIELD_NAME} values), and the {@code ReaderParser} stream/file charsets.
792    *
793    * <h5 class='section'>Builder properties NOT honored:</h5>
794    * <c>validateEnd()</c> (the cursor leaves end-of-input checking to the caller),
795    * <c>listener(...)</c>/<c>debugOutputLines(...)</c>, <c>consumes(...)</c>, object swaps, and
796    * {@code @Schema} annotations (only applied on the POJO databind path).
797    *
798    * @param input The input.  Accepts {@link Reader}, {@link CharSequence}, {@link InputStream},
799    *    or <code><jk>byte</jk>[]</code>.
800    * @return A new {@link JsonTokenReader}.
801    * @throws IOException If a problem occurred opening the underlying input.
802    */
803   @SuppressWarnings({
804      "java:S2095" // ParserPipe lifecycle is transferred to the returned JsonTokenReader, which closes it via its own close(); the caller owns the cursor via try-with-resources.
805   })
806   @Override /* TokenReadable */
807   public TokenReader readTokens(Object input) throws IOException {
808      var pipe = new ParserPipe(
809         input,
810         isDebug(),
811         false /* strict */,
812         isAutoCloseStreams(),
813         isUnbuffered(),
814         getStreamCharset());
815      return new JsonTokenReader(pipe, new JsonTokenReader.Settings(isTrimStrings()), this);
816   }
817
818   /**
819    * Streaming array-element {@link RecordReader} backed by {@link JsonTokenReader}.
820    * Memory is O(1) in the array length &mdash; elements are bound on demand using this live session.
821    *
822    * @param input The input.
823    * @return A new element-streamed {@link RecordReader}.
824    * @throws IOException If a problem occurred opening the underlying input.
825    */
826   @Override /* ArrayRecordReadable */
827   public RecordReader readArrayRecords(Object input) throws IOException {
828      try {
829         return StreamingArrayRecord.reader(readTokens(input));
830      } catch (ParseException e) {
831         throw new IOException(e);
832      }
833   }
834
835   @Override /* Overridden from ReaderParserSession */
836   protected <E> Collection<E> doReadIntoCollection(ParserPipe pipe, Collection<E> c, Type elementType) throws IOException, ParseException, ExecutableException {
837      try (var r = pipe.getParserReader()) {
838         c = readIntoCollection2(r, c, getClassMeta(elementType == null ? Object.class : elementType), null);
839         validateEnd(r);
840         return c;
841      }
842   }
843
844   @Override /* Overridden from ReaderParserSession */
845   protected <K,V> Map<K,V> doReadIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws IOException, ParseException, ExecutableException {
846      try (var r = pipe.getParserReader()) {
847         m = readIntoMap2(r, m, (ClassMeta<K>)getClassMeta(keyType == null ? Object.class : keyType), (ClassMeta<V>)getClassMeta(valueType == null ? Object.class : valueType), null);
848         validateEnd(r);
849         return m;
850      }
851   }
852
853   /**
854    * Returns the language-specific metadata on the specified class.
855    *
856    * @param cm The class to return the metadata on.
857    * @return The metadata.
858    */
859   protected JsonClassMeta getJsonClassMeta(ClassMeta<?> cm) {
860      return ctx.getJsonClassMeta(cm);
861   }
862
863   /**
864    * Returns <jk>true</jk> if the specified character can start a number value.
865    *
866    * <p>
867    * JSON numbers start with a digit, <c>-</c>, or <c>.</c>.  JSON5 subclasses also accept
868    * <c>+</c>, <c>I</c> (<c>Infinity</c>), and <c>N</c> (<c>NaN</c>).
869    *
870    * @param c The next non-whitespace character.
871    * @return <jk>true</jk> if a number lexeme should be read from this character.
872    */
873   protected boolean isNumberStartChar(int c) {
874      return (c >= '0' && c <= '9') || c == '-' || c == '.';
875   }
876
877   /**
878    * Returns <jk>true</jk> if the specified character is whitespace or '/'.
879    *
880    * @param cp The codepoint.
881    * @return <jk>true</jk> if the specified character is whitespace or '/'.
882    */
883   protected boolean isCommentOrWhitespace(int cp) {
884      if (cp == '/')
885         return true;
886      return cp <= 0x20 && (cp == 0x09 || cp == 0x0A || cp == 0x0D || cp == 0x20);
887   }
888
889   /**
890    * Validate end.
891    *
892    * @see JsonParser.Builder#validateEnd()
893    * @return
894    *    <jk>true</jk> if after parsing a POJO from the input, verifies that the remaining input in
895    *    the stream consists of only comments or whitespace.
896    */
897   protected boolean isValidateEnd() { return validateEnd; }
898
899   /**
900    * Returns <jk>true</jk> if the specified character is whitespace.
901    *
902    * <p>
903    * Only 0x20 (space), 0x09 (tab), 0x0A (line feed) and 0x0D (carriage return) are considered whitespace.
904    *
905    * @param cp The codepoint.
906    * @return <jk>true</jk> if the specified character is whitespace.
907    */
908   protected boolean isWhitespace(int cp) {
909      return cp <= 0x20 && (cp == 0x09 || cp == 0x0A || cp == 0x0D || cp == 0x20);
910   }
911
912   /**
913    * Hook called when empty input is detected in {@code readAnything()}.
914    *
915    * @throws ParseException Always in strict mode.
916    */
917   protected void onEmptyInput() throws ParseException {
918      throw new ParseException(this, "Empty input.");
919   }
920
921   /**
922    * Hook called when a missing value is detected in {@code readAnything()}.
923    *
924    * @throws ParseException Always in strict mode.
925    */
926   protected void onMissingValue() throws ParseException {
927      throw new ParseException(this, "Missing value detected.");
928   }
929
930   /**
931    * Returns whether trailing commas are accepted before <c>]</c> / <c>}</c> in the databind path.
932    *
933    * <p>
934    * Default returns <jk>false</jk> (strict RFC-8259 JSON).  {@code Json5ParserSession} overrides
935    * this to <jk>true</jk> for the JSON5 dialect.
936    *
937    * @return <jk>true</jk> if a trailing comma should be silently accepted.
938    */
939   protected boolean allowsTrailingComma() {
940      return false;
941   }
942
943   /**
944    * Returns whether non-string types can be coerced to strings using {@code newInstanceFromString()}.
945    *
946    * @return Always {@code false} for standard JSON.
947    */
948   protected boolean canCoerceNonStringToString() {
949      return false;
950   }
951
952   @Override /* Overridden from MarshallingSession */
953   protected MarshalledMap newGenericMap() {
954      return new JsonMap(this);
955   }
956
957   @Override /* Overridden from MarshallingSession */
958   protected MarshalledList newGenericList() {
959      return new JsonList(this);
960   }
961}