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