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