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