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