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