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