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