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}