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