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