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.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.StringUtils.*;
022import static org.apache.juneau.commons.utils.Utils.*;
023
024import java.io.*;
025import java.lang.reflect.*;
026import java.nio.charset.*;
027import java.util.*;
028import java.util.function.*;
029
030import org.apache.juneau.*;
031import org.apache.juneau.collections.*;
032import org.apache.juneau.commons.lang.*;
033import org.apache.juneau.commons.reflect.*;
034import org.apache.juneau.commons.utils.*;
035import org.apache.juneau.httppart.*;
036import org.apache.juneau.parser.*;
037import org.apache.juneau.swap.*;
038
039/**
040 * Session object that lives for the duration of a single use of {@link JsonParser}.
041 *
042 * <h5 class='section'>Notes:</h5><ul>
043 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
044 * </ul>
045 *
046 * <h5 class='section'>See Also:</h5><ul>
047 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JsonBasics">JSON Basics</a>
048
049 * </ul>
050 */
051@SuppressWarnings({ "unchecked", "rawtypes", "resource" })
052public class JsonParserSession extends ReaderParserSession {
053   /**
054    * Builder class.
055    */
056   public static class Builder extends ReaderParserSession.Builder {
057
058      private JsonParser ctx;
059
060      /**
061       * Constructor
062       *
063       * @param ctx The context creating this session.
064       */
065      protected Builder(JsonParser ctx) {
066         super(ctx);
067         this.ctx = ctx;
068      }
069
070      @Override /* Overridden from Builder */
071      public <T> Builder apply(Class<T> type, Consumer<T> apply) {
072         super.apply(type, apply);
073         return this;
074      }
075
076      @Override
077      public JsonParserSession build() {
078         return new JsonParserSession(this);
079      }
080
081      @Override /* Overridden from Builder */
082      public Builder debug(Boolean value) {
083         super.debug(value);
084         return this;
085      }
086
087      @Override /* Overridden from Builder */
088      public Builder fileCharset(Charset value) {
089         super.fileCharset(value);
090         return this;
091      }
092
093      @Override /* Overridden from Builder */
094      public Builder javaMethod(Method value) {
095         super.javaMethod(value);
096         return this;
097      }
098
099      @Override /* Overridden from Builder */
100      public Builder locale(Locale value) {
101         super.locale(value);
102         return this;
103      }
104
105      @Override /* Overridden from Builder */
106      public Builder mediaType(MediaType value) {
107         super.mediaType(value);
108         return this;
109      }
110
111      @Override /* Overridden from Builder */
112      public Builder mediaTypeDefault(MediaType value) {
113         super.mediaTypeDefault(value);
114         return this;
115      }
116
117      @Override /* Overridden from Builder */
118      public Builder outer(Object value) {
119         super.outer(value);
120         return this;
121      }
122
123      @Override /* Overridden from Builder */
124      public Builder properties(Map<String,Object> value) {
125         super.properties(value);
126         return this;
127      }
128
129      @Override /* Overridden from Builder */
130      public Builder property(String key, Object value) {
131         super.property(key, value);
132         return this;
133      }
134
135      @Override /* Overridden from Builder */
136      public Builder schema(HttpPartSchema value) {
137         super.schema(value);
138         return this;
139      }
140
141      @Override /* Overridden from Builder */
142      public Builder schemaDefault(HttpPartSchema value) {
143         super.schemaDefault(value);
144         return this;
145      }
146
147      @Override /* Overridden from Builder */
148      public Builder streamCharset(Charset value) {
149         super.streamCharset(value);
150         return this;
151      }
152
153      @Override /* Overridden from Builder */
154      public Builder timeZone(TimeZone value) {
155         super.timeZone(value);
156         return this;
157      }
158
159      @Override /* Overridden from Builder */
160      public Builder timeZoneDefault(TimeZone value) {
161         super.timeZoneDefault(value);
162         return this;
163      }
164
165      @Override /* Overridden from Builder */
166      public Builder unmodifiable() {
167         super.unmodifiable();
168         return this;
169      }
170   }
171
172   private static final AsciiSet decChars = AsciiSet.create().ranges("0-9").build();
173   private static final AsciiSet VALID_BARE_CHARS = AsciiSet.create().range('A', 'Z').range('a', 'z').range('0', '9').chars("$_-.").build();
174
175   /**
176    * Creates a new builder for this object.
177    *
178    * @param ctx The context creating this session.
179    * @return A new builder.
180    */
181   public static Builder create(JsonParser ctx) {
182      return new Builder(ctx);
183   }
184
185   private final JsonParser ctx;
186
187   /**
188    * Constructor.
189    *
190    * @param builder The builder for this object.
191    */
192   protected JsonParserSession(Builder builder) {
193      super(builder);
194      ctx = builder.ctx;
195   }
196
197   private <T> T parseAnything(ClassMeta<?> eType, ParserReader r, Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
198
199      if (eType == null)
200         eType = object();
201      var swap = (ObjectSwap<T,Object>)eType.getSwap(this);
202      var builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this);
203      var sType = (ClassMeta<?>)null;
204      if (nn(builder))
205         sType = builder.getBuilderClassMeta(this);
206      else if (nn(swap))
207         sType = swap.getSwapClassMeta(this);
208      else
209         sType = eType;
210
211      if (sType.isOptional())
212         return (T)opt(parseAnything(eType.getElementType(), r, outer, pMeta));
213
214      setCurrentClass(sType);
215      var wrapperAttr = getJsonClassMeta(sType).getWrapperAttr();
216
217      var o = (Object)null;
218
219      skipCommentsAndSpace(r);
220      if (nn(wrapperAttr))
221         skipWrapperAttrStart(r, wrapperAttr);
222      int c = r.peek();
223      if (c == -1) {
224         if (isStrict())
225            throw new ParseException(this, "Empty input.");
226         // Let o be null.
227      } else if ((c == ',' || c == '}' || c == ']')) {
228         if (isStrict())
229            throw new ParseException(this, "Missing value detected.");
230         // Handle bug in Cognos 10.2.1 that can product non-existent values.
231         // Let o be null;
232      } else if (c == 'n') {
233         parseKeyword("null", r);
234      } else if (sType.isObject()) {
235         if (c == '{') {
236            var m2 = new JsonMap(this);
237            parseIntoMap2(r, m2, string(), object(), pMeta);
238            o = cast(m2, pMeta, eType);
239         } else if (c == '[') {
240            o = parseIntoCollection2(r, new JsonList(this), object(), pMeta);
241         } else if (c == '\'' || c == '"') {
242            o = parseString(r);
243            if (sType.isChar())
244               o = parseCharacter(o);
245         } else if (c >= '0' && c <= '9' || c == '-' || c == '.') {
246            o = parseNumber(r, null);
247         } else if (c == 't') {
248            parseKeyword("true", r);
249            o = Boolean.TRUE;
250         } else {
251            parseKeyword("false", r);
252            o = Boolean.FALSE;
253         }
254      } else if (sType.isBoolean()) {
255         o = parseBoolean(r);
256      } else if (sType.isCharSequence()) {
257         o = parseString(r);
258      } else if (sType.isChar()) {
259         o = parseCharacter(parseString(r));
260      } else if (sType.isNumber()) {
261         o = parseNumber(r, (Class<? extends Number>)sType.inner());
262      } else if (sType.isMap()) {
263         Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType));
264         o = parseIntoMap2(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
265      } else if (sType.isCollection()) {
266         if (c == '{') {
267            var m = new JsonMap(this);
268            parseIntoMap2(r, m, string(), object(), pMeta);
269            o = cast(m, pMeta, eType);
270         } else {
271            Collection l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance() : new JsonList(this));
272            o = parseIntoCollection2(r, l, sType, pMeta);
273         }
274      } else if (nn(builder)) {
275         var m = toBeanMap(builder.create(this, eType));
276         o = builder.build(this, parseIntoBeanMap2(r, m).getBean(), eType);
277      } else if (sType.canCreateNewBean(outer)) {
278         var m = newBeanMap(outer, sType.inner());
279         o = parseIntoBeanMap2(r, m).getBean();
280      } else if (sType.canCreateNewInstanceFromString(outer) && (c == '\'' || c == '"')) {
281         o = sType.newInstanceFromString(outer, parseString(r));
282      } else if (sType.isArray() || sType.isArgs()) {
283         if (c == '{') {
284            var m = new JsonMap(this);
285            parseIntoMap2(r, m, string(), object(), pMeta);
286            o = cast(m, pMeta, eType);
287         } else {
288            var l = (ArrayList)parseIntoCollection2(r, list(), sType, pMeta);
289            o = toArray(sType, l);
290         }
291      } else if (c == '{') {
292         Map m = new JsonMap(this);
293         parseIntoMap2(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
294         if (m.containsKey(getBeanTypePropertyName(eType)))
295            o = cast((JsonMap)m, pMeta, eType);
296         else if (nn(sType.getProxyInvocationHandler()))
297            o = newBeanMap(outer, sType.inner()).load(m).getBean();
298         else
299            throw new ParseException(this, "Class ''{0}'' could not be instantiated.  Reason: ''{1}''", cn(sType), sType.getNotABeanReason());
300      } else if (sType.canCreateNewInstanceFromString(outer) && ! isStrict()) {
301         o = sType.newInstanceFromString(outer, parseString(r));
302      } else {
303         throw new ParseException(this, "Unrecognized syntax for class type ''{0}'', starting character ''{1}''", sType, (char)c);
304      }
305
306      if (nn(wrapperAttr))
307         skipWrapperAttrEnd(r);
308
309      if (nn(swap) && nn(o))
310         o = unswap(swap, o, eType);
311
312      if (nn(outer))
313         setParent(eType, o, outer);
314
315      return (T)o;
316   }
317
318   private Boolean parseBoolean(ParserReader r) throws IOException, ParseException {
319      int c = r.peek();
320      if (c == '\'' || c == '"')
321         return bool(parseString(r));
322      if (c == 't') {
323         parseKeyword("true", r);
324         return Boolean.TRUE;
325      } else if (c == 'f') {
326         parseKeyword("false", r);
327         return Boolean.FALSE;
328      } else {
329         throw new ParseException(this, "Unrecognized syntax.  Expected boolean value, actual=''{0}''", r.read(100));
330      }
331   }
332
333   /*
334    * Parse a JSON attribute from the character array at the specified position, then
335    * set the position marker to the last character in the field name.
336    */
337   private String parseFieldName(ParserReader r) throws IOException, ParseException {
338      int c = r.peek();
339      if (c == '\'' || c == '"')
340         return parseString(r);
341      if (isStrict())
342         throw new ParseException(this, "Unquoted attribute detected.");
343      if (! VALID_BARE_CHARS.contains(c))
344         throw new ParseException(this, "Could not find the start of the field name.");
345      r.mark();
346      // Look for whitespace.
347      while (c != -1) {
348         c = r.read();
349         if (! VALID_BARE_CHARS.contains(c)) {
350            r.unread();
351            var s = r.getMarked().intern();
352            return s.equals("null") ? null : s;
353         }
354      }
355      throw new ParseException(this, "Could not find the end of the field name.");
356   }
357
358   private <T> BeanMap<T> parseIntoBeanMap2(ParserReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException {
359
360      // S1: Looking for outer {
361      // S2: Looking for attrName start.
362      // S3: Found attrName end, looking for :.
363      // S4: Found :, looking for valStart: { [ " ' LITERAL.
364      // S5: Looking for , or }
365
366      var state = S1;
367      var currAttr = "";
368      int c = 0;
369      mark();
370      try {
371         while (c != -1) {
372            c = r.read();
373            if (state == S1) {
374               if (c == '{') {
375                  state = S2;
376               } else if (isCommentOrWhitespace(c)) {
377                  skipCommentsAndSpace(r.unread());
378               } else {
379                  break;
380               }
381            } else if (state == S2) {
382               if (c == '}') {
383                  return m;
384               } else if (isCommentOrWhitespace(c)) {
385                  skipCommentsAndSpace(r.unread());
386               } else {
387                  r.unread();
388                  mark();
389                  currAttr = parseFieldName(r);
390                  state = S3;
391               }
392            } else if (state == S3) {
393               if (c == ':')
394                  state = S4;
395            } else if (state == S4) {
396               if (isCommentOrWhitespace(c)) {
397                  skipCommentsAndSpace(r.unread());
398               } else {
399                  if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
400                     var pMeta = m.getPropertyMeta(currAttr);
401                     setCurrentProperty(pMeta);
402                     if (pMeta == null) {
403                        onUnknownProperty(currAttr, m, parseAnything(object(), r.unread(), m.getBean(false), null));
404                        unmark();
405                     } else {
406                        unmark();
407                        var cm = pMeta.getClassMeta();
408                        Object value = parseAnything(cm, r.unread(), m.getBean(false), pMeta);
409                        setName(cm, value, currAttr);
410                        try {
411                           pMeta.set(m, currAttr, value);
412                        } catch (BeanRuntimeException e) {
413                           onBeanSetterException(pMeta, e);
414                           throw e;
415                        }
416                     }
417                     setCurrentProperty(null);
418                  }
419                  state = S5;
420               }
421            } else if (state == S5) {
422               if (c == ',')
423                  state = S2;
424               else if (isCommentOrWhitespace(c))
425                  skipCommentsAndSpace(r.unread());
426               else if (c == '}') {
427                  return m;
428               }
429            }
430         }
431         if (state == S1)
432            throw new ParseException(this, "Expected '{' at beginning of JSON object.");
433         if (state == S2)
434            throw new ParseException(this, "Could not find attribute name on JSON object.");
435         if (state == S3)
436            throw new ParseException(this, "Could not find ':' following attribute name on JSON object.");
437         if (state == S4)
438            throw new ParseException(this, "Expected one of the following characters: {,[,',\",LITERAL.");
439         if (state == S5)
440            throw new ParseException(this, "Could not find '}' marking end of JSON object.");
441      } finally {
442         unmark();
443      }
444
445      return null; // Unreachable.
446   }
447
448   private <E> Collection<E> parseIntoCollection2(ParserReader r, Collection<E> l, ClassMeta<?> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
449
450      // S1: Looking for outermost [
451      // S2: Looking for starting [ or { or " or ' or LITERAL or ]
452      // S3: Looking for , or ]
453      // S4: Looking for starting [ or { or " or ' or LITERAL
454
455      int argIndex = 0;
456
457      var state = S1;
458      int c = 0;
459      while (c != -1) {
460         c = r.read();
461         if (state == S1) {
462            if (c == '[')
463               state = S2;
464            else if (isCommentOrWhitespace(c))
465               skipCommentsAndSpace(r.unread());
466            else
467               break;  // Invalid character found.
468         } else if (state == S2) {
469            if (c == ']') {
470               return l;
471            } else if (isCommentOrWhitespace(c)) {
472               skipCommentsAndSpace(r.unread());
473            } else if (c != -1) {
474               l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, pMeta));
475               state = S3;
476            }
477         } else if (state == S3) {
478            if (c == ',') {
479               state = S4;
480            } else if (isCommentOrWhitespace(c)) {
481               skipCommentsAndSpace(r.unread());
482            } else if (c == ']') {
483               return l;
484            } else {
485               break;  // Invalid character found.
486            }
487         } else if (state == S4) {
488            if (isCommentOrWhitespace(c)) {
489               skipCommentsAndSpace(r.unread());
490            } else if (c == ']') {
491               break;
492            } else if (c != -1) {
493               l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, pMeta));
494               state = S3;
495            }
496         }
497      }
498      if (state == S1)
499         throw new ParseException(this, "Expected '[' at beginning of JSON array.");
500      if (state == S2)
501         throw new ParseException(this, "Expected one of the following characters: {,[,',\",LITERAL.");
502      if (state == S3)
503         throw new ParseException(this, "Expected ',' or ']'.");
504      if (state == S4)
505         throw new ParseException(this, "Unexpected trailing comma in array.");
506
507      return null;  // Unreachable.
508   }
509
510   private <K,V> Map<K,V> parseIntoMap2(ParserReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
511
512      if (keyType == null)
513         keyType = (ClassMeta<K>)string();
514
515      // S1: Looking for outer {
516      // S2: Looking for attrName start.
517      // S3: Found attrName end, looking for :.
518      // S4: Found :, looking for valStart: { [ " ' LITERAL.
519      // S5: Looking for , or }
520      // S6: Found , looking for attr start.
521
522      skipCommentsAndSpace(r);
523      var state = S1;
524      var currAttr = (String)null;
525      int c = 0;
526      while (c != -1) {
527         c = r.read();
528         if (state == S1) {
529            if (c == '{')
530               state = S2;
531            else
532               break;
533         } else if (state == S2) {
534            if (c == '}') {
535               return m;
536            } else if (isCommentOrWhitespace(c)) {
537               skipCommentsAndSpace(r.unread());
538            } else {
539               currAttr = parseFieldName(r.unread());
540               state = S3;
541            }
542         } else if (state == S3) {
543            if (c == ':')
544               state = S4;
545         } else if (state == S4) {
546            if (isCommentOrWhitespace(c)) {
547               skipCommentsAndSpace(r.unread());
548            } else {
549               K key = convertAttrToType(m, currAttr, keyType);
550               V value = parseAnything(valueType, r.unread(), m, pMeta);
551               setName(valueType, value, key);
552               m.put(key, value);
553               state = S5;
554            }
555         } else if (state == S5) {
556            if (c == ',') {
557               state = S6;
558            } else if (isCommentOrWhitespace(c)) {
559               skipCommentsAndSpace(r.unread());
560            } else if (c == '}') {
561               return m;
562            } else {
563               break;
564            }
565         } else if (state == S6) {
566            if (c == '}') {
567               break;
568            } else if (isCommentOrWhitespace(c)) {
569               skipCommentsAndSpace(r.unread());
570            } else {
571               currAttr = parseFieldName(r.unread());
572               state = S3;
573            }
574         }
575      }
576      if (state == S1)
577         throw new ParseException(this, "Expected '{' at beginning of JSON object.");
578      if (state == S2)
579         throw new ParseException(this, "Could not find attribute name on JSON object.");
580      if (state == S3)
581         throw new ParseException(this, "Could not find ':' following attribute name on JSON object.");
582      if (state == S4)
583         throw new ParseException(this, "Expected one of the following characters: {,[,',\",LITERAL.");
584      if (state == S5)
585         throw new ParseException(this, "Could not find '}' marking end of JSON object.");
586      if (state == S6)
587         throw new ParseException(this, "Unexpected '}' found in JSON object.");
588
589      return null; // Unreachable.
590   }
591
592   /*
593    * Looks for the keywords true, false, or null.
594    * Throws an exception if any of these keywords are not found at the specified position.
595    */
596   private void parseKeyword(String keyword, ParserReader r) throws IOException, ParseException {
597      try {
598         String s = r.read(keyword.length());
599         if (s.equals(keyword))
600            return;
601         throw new ParseException(this, "Unrecognized syntax.  Expected=''{0}'', Actual=''{1}''", keyword, s);
602      } catch (@SuppressWarnings("unused") IndexOutOfBoundsException e) {
603         throw new ParseException(this, "Unrecognized syntax.  Expected=''{0}'', found end-of-file.", keyword);
604      }
605   }
606
607   private Number parseNumber(ParserReader r, Class<? extends Number> type) throws IOException, ParseException {
608      int c = r.peek();
609      if (c == '\'' || c == '"')
610         return parseNumber(r, parseString(r), type);
611      return parseNumber(r, r.parseNumberString(), type);
612   }
613
614   private Number parseNumber(ParserReader r, String s, Class<? extends Number> type) throws ParseException {
615
616      // JSON has slightly different number rules from Java.
617      // Strict mode enforces these different rules, lax does not.
618      if (isStrict()) {
619
620         // Lax allows blank strings to represent 0.
621         // Strict does not allow blank strings.
622         if (s.isEmpty())
623            throw new ParseException(this, "Invalid JSON number: ''{0}''", s);
624
625         // Need to weed out octal and hexadecimal formats:  0123,-0123,0x123,-0x123.
626         // Don't weed out 0 or -0.
627         var isNegative = false;
628         var c = s.charAt(0);
629         if (c == '-') {
630            isNegative = true;
631            c = (s.length() == 1 ? 'x' : s.charAt(1));
632         }
633
634         // JSON doesn't allow '.123' and '-.123'.
635         if (c == '.')
636            throw new ParseException(this, "Invalid JSON number: ''{0}''", s);
637
638         // '01' is not a valid number, but '0.1', '0e1', '0e+1' are valid.
639         if (c == '0' && s.length() > (isNegative ? 2 : 1)) {
640            var c2 = s.charAt((isNegative ? 2 : 1));
641            if (c2 != '.' && c2 != 'e' && c2 != 'E')
642               throw new ParseException(this, "Invalid JSON number: ''{0}''", s);
643         }
644
645         // JSON doesn't allow '1.' or '0.e1'.
646         var i = s.indexOf('.');
647         if (i != -1 && (s.length() == (i + 1) || ! decChars.contains(s.charAt(i + 1))))
648            throw new ParseException(this, "Invalid JSON number: ''{0}''", s);
649
650      }
651      return StringUtils.parseNumber(s, type);
652   }
653
654   /*
655    * Starting from the specified position in the character array, returns the
656    * position of the character " or '.
657    * If the string consists of a concatenation of strings (e.g. 'AAA' + "BBB"), this method
658    * will automatically concatenate the strings and return the result.
659    */
660   private String parseString(ParserReader r) throws IOException, ParseException {
661      r.mark();
662      int qc = r.read();      // The quote character being used (" or ')
663      if (qc != '"' && isStrict()) {
664         String msg = (qc == '\'' ? "Invalid quote character \"{0}\" being used." : "Did not find quote character marking beginning of string.  Character=\"{0}\"");
665         throw new ParseException(this, msg, (char)qc);
666      }
667      final boolean isQuoted = (qc == '\'' || qc == '"');
668      var s = (String)null;
669      boolean isInEscape = false;
670      int c = 0;
671      while (c != -1) {
672         c = r.read();
673         // Strict syntax requires that all control characters be escaped.
674         if (isStrict() && c <= 0x1F)
675            throw new ParseException(this, "Unescaped control character encountered: ''0x{0}''", String.format("%04X", c));
676         if (isInEscape) {
677            // @formatter:off
678            switch (c) {
679               case 'n': r.replace('\n'); break;
680               case 'r': r.replace('\r'); break;
681               case 't': r.replace('\t'); break;
682               case 'f': r.replace('\f'); break;
683               case 'b': r.replace('\b'); break;
684               case '\\': r.replace('\\'); break;
685               case '/': r.replace('/'); break;
686               case '\'': r.replace('\''); break;
687               case '"': r.replace('"'); break;
688               case 'u': {
689                  String n = r.read(4);
690                  try {
691                     r.replace(Integer.parseInt(n, 16), 6);
692                  } catch (@SuppressWarnings("unused") NumberFormatException e) {
693                     throw new ParseException(this, "Invalid Unicode escape sequence in string.");
694                  }
695                  break;
696               }
697               default:
698                  throw new ParseException(this, "Invalid escape sequence in string.");
699            }
700            // @formatter:on
701            isInEscape = false;
702         } else {
703            if (c == '\\') {
704               isInEscape = true;
705               r.delete();
706            } else if (isQuoted) {
707               if (c == qc) {
708                  s = r.getMarked(1, -1);
709                  break;
710               }
711            } else {
712               if (c == ',' || c == '}' || c == ']' || isWhitespace(c)) {
713                  s = r.getMarked(0, -1);
714                  r.unread();
715                  break;
716               } else if (c == -1) {
717                  s = r.getMarked(0, 0);
718                  break;
719               }
720            }
721         }
722      }
723      if (s == null)
724         throw new ParseException(this, "Could not find expected end character ''{0}''.", (char)qc);
725
726      // Look for concatenated string (i.e. whitespace followed by +).
727      skipCommentsAndSpace(r);
728      if (r.peek() == '+') {
729         if (isStrict())
730            throw new ParseException(this, "String concatenation detected.");
731         r.read();  // Skip past '+', NOSONAR - Intentional.
732         skipCommentsAndSpace(r);
733         s += parseString(r);
734      }
735      return trim(s); // End of input reached.
736   }
737
738   /*
739    * Doesn't actually parse anything, but when positioned at the beginning of comment,
740    * it will move the pointer to the last character in the comment.
741    */
742   private void skipComments(ParserReader r) throws ParseException, IOException {
743      int c = r.read();
744      //  "/* */" style comments
745      if (c == '*') {
746         while (c != -1)
747            if ((c = r.read()) == '*')
748               if ((c = r.read()) == '/')
749                  return;
750         //  "//" style comments
751      } else if (c == '/') {
752         while (c != -1) {
753            c = r.read();
754            if (c == -1 || c == '\n')
755               return;
756         }
757      }
758      throw new ParseException(this, "Open ended comment.");
759   }
760
761   /*
762    * Doesn't actually parse anything, but moves the position beyond any whitespace or comments.
763    * If positionOnNext is 'true', then the cursor will be set to the point immediately after
764    * the comments and whitespace.  Otherwise, the cursor will be set to the last position of
765    * the comments and whitespace.
766    */
767   private void skipCommentsAndSpace(ParserReader r) throws IOException, ParseException {
768      int c = 0;
769      while ((c = r.read()) != -1) {
770         if (! isWhitespace(c)) {
771            if (c == '/') {
772               if (isStrict())
773                  throw new ParseException(this, "Javascript comment detected.");
774               skipComments(r);
775            } else {
776               r.unread();
777               return;
778            }
779         }
780      }
781   }
782
783   /*
784    * Doesn't actually parse anything, but moves the position beyond the construct "}" when
785    * the @Json(wrapperAttr) annotation is used on a class.
786    */
787   private void skipWrapperAttrEnd(ParserReader r) throws ParseException, IOException {
788      int c = 0;
789      while ((c = r.read()) != -1) {
790         if (! isWhitespace(c)) {
791            if (c == '/') {
792               if (isStrict())
793                  throw new ParseException(this, "Javascript comment detected.");
794               skipComments(r);
795            } else if (c == '}') {
796               return;
797            } else {
798               throw new ParseException(this, "Could not find '}' at the end of JSON wrapper object.");
799            }
800         }
801      }
802   }
803
804   /*
805    * Doesn't actually parse anything, but moves the position beyond the construct "{wrapperAttr:" when
806    * the @Json(wrapperAttr) annotation is used on a class.
807    */
808   private void skipWrapperAttrStart(ParserReader r, String wrapperAttr) throws IOException, ParseException {
809
810      // S1: Looking for outer '{'
811      // S2: Looking for attrName start.
812      // S3: Found attrName end, looking for :.
813      // S4: Found :, looking for valStart: { [ " ' LITERAL.
814
815      var state = S1;
816      var currAttr = (String)null;
817      int c = 0;
818      while (c != -1) {
819         c = r.read();
820         if (state == S1) {
821            if (c == '{')
822               state = S2;
823         } else if (state == S2) {
824            if (isCommentOrWhitespace(c)) {
825               skipCommentsAndSpace(r.unread());
826            } else {
827               currAttr = parseFieldName(r.unread());
828               if (! currAttr.equals(wrapperAttr))
829                  throw new ParseException(this, "Expected to find wrapper attribute ''{0}'' but found attribute ''{1}''", wrapperAttr, currAttr);
830               state = S3;
831            }
832         } else if (state == S3) {
833            if (c == ':')
834               state = S4;
835         } else if (state == S4) {
836            if (isCommentOrWhitespace(c)) {
837               skipCommentsAndSpace(r.unread());
838            } else {
839               r.unread();
840               return;
841            }
842         }
843      }
844      if (state == S1)
845         throw new ParseException(this, "Expected '{' at beginning of JSON object.");
846      if (state == S2)
847         throw new ParseException(this, "Could not find attribute name on JSON object.");
848      if (state == S3)
849         throw new ParseException(this, "Could not find ':' following attribute name on JSON object.");
850      if (state == S4)
851         throw new ParseException(this, "Expected one of the following characters: {,[,',\",LITERAL.");
852   }
853
854   /*
855    * Call this method after you've finished a parsing a string to make sure that if there's any
856    * remainder in the input, that it consists only of whitespace and comments.
857    */
858   private void validateEnd(ParserReader r) throws IOException, ParseException {
859      if (! isValidateEnd())
860         return;
861      skipCommentsAndSpace(r);
862      int c = r.read();
863      if (c != -1 && c != ';')  // var x = {...}; expressions can end with a semicolon.
864         throw new ParseException(this, "Remainder after parse: ''{0}''.", (char)c);
865   }
866
867   @Override /* Overridden from ParserSession */
868   protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
869      try (var r = pipe.getParserReader()) {
870         if (r == null)
871            return null;
872         T o = parseAnything(type, r, getOuter(), null);
873         validateEnd(r);
874         return o;
875      }
876   }
877
878   @Override /* Overridden from ReaderParserSession */
879   protected <E> Collection<E> doParseIntoCollection(ParserPipe pipe, Collection<E> c, Type elementType) throws IOException, ParseException, ExecutableException {
880      try (var r = pipe.getParserReader()) {
881         c = parseIntoCollection2(r, c, getClassMeta(elementType), null);
882         validateEnd(r);
883         return c;
884      }
885   }
886
887   @Override /* Overridden from ReaderParserSession */
888   protected <K,V> Map<K,V> doParseIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws IOException, ParseException, ExecutableException {
889      try (var r = pipe.getParserReader()) {
890         m = parseIntoMap2(r, m, (ClassMeta<K>)getClassMeta(keyType), (ClassMeta<V>)getClassMeta(valueType), null);
891         validateEnd(r);
892         return m;
893      }
894   }
895
896   /**
897    * Returns the language-specific metadata on the specified class.
898    *
899    * @param cm The class to return the metadata on.
900    * @return The metadata.
901    */
902   protected JsonClassMeta getJsonClassMeta(ClassMeta<?> cm) {
903      return ctx.getJsonClassMeta(cm);
904   }
905
906   /**
907    * Returns <jk>true</jk> if the specified character is whitespace or '/'.
908    *
909    * @param cp The codepoint.
910    * @return <jk>true</jk> if the specified character is whitespace or '/'.
911    */
912   protected boolean isCommentOrWhitespace(int cp) {
913      if (cp == '/')
914         return true;
915      if (isStrict())
916         return cp <= 0x20 && (cp == 0x09 || cp == 0x0A || cp == 0x0D || cp == 0x20);
917      return Character.isWhitespace(cp);
918   }
919
920   /**
921    * Validate end.
922    *
923    * @see JsonParser.Builder#validateEnd()
924    * @return
925    *    <jk>true</jk> if after parsing a POJO from the input, verifies that the remaining input in
926    *    the stream consists of only comments or whitespace.
927    */
928   protected boolean isValidateEnd() { return ctx.isValidateEnd(); }
929
930   /**
931    * Returns <jk>true</jk> if the specified character is whitespace.
932    *
933    * <p>
934    * The definition of whitespace is different for strict vs lax mode.
935    * Strict mode only interprets 0x20 (space), 0x09 (tab), 0x0A (line feed) and 0x0D (carriage return) as whitespace.
936    * Lax mode uses {@link Character#isWhitespace(int)} to make the determination.
937    *
938    * @param cp The codepoint.
939    * @return <jk>true</jk> if the specified character is whitespace.
940    */
941   protected boolean isWhitespace(int cp) {
942      if (isStrict())
943         return cp <= 0x20 && (cp == 0x09 || cp == 0x0A || cp == 0x0D || cp == 0x20);
944      return Character.isWhitespace(cp);
945   }
946}