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