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.common.internal.StringUtils.*; 016import static org.apache.juneau.internal.CollectionUtils.*; 017 018import java.io.*; 019import java.lang.reflect.*; 020import java.nio.charset.*; 021import java.util.*; 022import java.util.function.*; 023 024import org.apache.juneau.*; 025import org.apache.juneau.collections.*; 026import org.apache.juneau.common.internal.*; 027import org.apache.juneau.httppart.*; 028import org.apache.juneau.internal.*; 029import org.apache.juneau.parser.*; 030import org.apache.juneau.swap.*; 031 032/** 033 * Session object that lives for the duration of a single use of {@link JsonParser}. 034 * 035 * <h5 class='section'>Notes:</h5><ul> 036 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 037 * </ul> 038 * 039 * <h5 class='section'>See Also:</h5><ul> 040 * <li class='link'><a class="doclink" href="../../../../index.html#jm.JsonDetails">JSON Details</a> 041 042 * </ul> 043 */ 044@SuppressWarnings({ "unchecked", "rawtypes" }) 045public final class JsonParserSession extends ReaderParserSession { 046 047 //------------------------------------------------------------------------------------------------------------------- 048 // Static 049 //------------------------------------------------------------------------------------------------------------------- 050 051 private static final AsciiSet decChars = AsciiSet.create().ranges("0-9").build(); 052 053 /** 054 * Creates a new builder for this object. 055 * 056 * @param ctx The context creating this session. 057 * @return A new builder. 058 */ 059 public static Builder create(JsonParser ctx) { 060 return new Builder(ctx); 061 } 062 063 //------------------------------------------------------------------------------------------------------------------- 064 // Builder 065 //------------------------------------------------------------------------------------------------------------------- 066 067 /** 068 * Builder class. 069 */ 070 @FluentSetters 071 public static class Builder extends ReaderParserSession.Builder { 072 073 JsonParser ctx; 074 075 /** 076 * Constructor 077 * 078 * @param ctx The context creating this session. 079 */ 080 protected Builder(JsonParser ctx) { 081 super(ctx); 082 this.ctx = ctx; 083 } 084 085 @Override 086 public JsonParserSession build() { 087 return new JsonParserSession(this); 088 } 089 090 // <FluentSetters> 091 092 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 093 public <T> Builder apply(Class<T> type, Consumer<T> apply) { 094 super.apply(type, apply); 095 return this; 096 } 097 098 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 099 public Builder debug(Boolean value) { 100 super.debug(value); 101 return this; 102 } 103 104 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 105 public Builder properties(Map<String,Object> value) { 106 super.properties(value); 107 return this; 108 } 109 110 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 111 public Builder property(String key, Object value) { 112 super.property(key, value); 113 return this; 114 } 115 116 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 117 public Builder unmodifiable() { 118 super.unmodifiable(); 119 return this; 120 } 121 122 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 123 public Builder locale(Locale value) { 124 super.locale(value); 125 return this; 126 } 127 128 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 129 public Builder localeDefault(Locale value) { 130 super.localeDefault(value); 131 return this; 132 } 133 134 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 135 public Builder mediaType(MediaType value) { 136 super.mediaType(value); 137 return this; 138 } 139 140 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 141 public Builder mediaTypeDefault(MediaType value) { 142 super.mediaTypeDefault(value); 143 return this; 144 } 145 146 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 147 public Builder timeZone(TimeZone value) { 148 super.timeZone(value); 149 return this; 150 } 151 152 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 153 public Builder timeZoneDefault(TimeZone value) { 154 super.timeZoneDefault(value); 155 return this; 156 } 157 158 @Override /* GENERATED - org.apache.juneau.parser.ParserSession.Builder */ 159 public Builder javaMethod(Method value) { 160 super.javaMethod(value); 161 return this; 162 } 163 164 @Override /* GENERATED - org.apache.juneau.parser.ParserSession.Builder */ 165 public Builder outer(Object value) { 166 super.outer(value); 167 return this; 168 } 169 170 @Override /* GENERATED - org.apache.juneau.parser.ParserSession.Builder */ 171 public Builder schema(HttpPartSchema value) { 172 super.schema(value); 173 return this; 174 } 175 176 @Override /* GENERATED - org.apache.juneau.parser.ParserSession.Builder */ 177 public Builder schemaDefault(HttpPartSchema value) { 178 super.schemaDefault(value); 179 return this; 180 } 181 182 @Override /* GENERATED - org.apache.juneau.parser.ReaderParserSession.Builder */ 183 public Builder fileCharset(Charset value) { 184 super.fileCharset(value); 185 return this; 186 } 187 188 @Override /* GENERATED - org.apache.juneau.parser.ReaderParserSession.Builder */ 189 public Builder streamCharset(Charset value) { 190 super.streamCharset(value); 191 return this; 192 } 193 194 // </FluentSetters> 195 } 196 197 //------------------------------------------------------------------------------------------------------------------- 198 // Instance 199 //------------------------------------------------------------------------------------------------------------------- 200 201 private final JsonParser ctx; 202 203 /** 204 * Constructor. 205 * 206 * @param builder The builder for this object. 207 */ 208 protected JsonParserSession(Builder builder) { 209 super(builder); 210 ctx = builder.ctx; 211 } 212 213 /** 214 * Returns <jk>true</jk> if the specified character is whitespace. 215 * 216 * <p> 217 * The definition of whitespace is different for strict vs lax mode. 218 * Strict mode only interprets 0x20 (space), 0x09 (tab), 0x0A (line feed) and 0x0D (carriage return) as whitespace. 219 * Lax mode uses {@link Character#isWhitespace(int)} to make the determination. 220 * 221 * @param cp The codepoint. 222 * @return <jk>true</jk> if the specified character is whitespace. 223 */ 224 protected boolean isWhitespace(int cp) { 225 if (isStrict()) 226 return cp <= 0x20 && (cp == 0x09 || cp == 0x0A || cp == 0x0D || cp == 0x20); 227 return Character.isWhitespace(cp); 228 } 229 230 /** 231 * Returns <jk>true</jk> if the specified character is whitespace or '/'. 232 * 233 * @param cp The codepoint. 234 * @return <jk>true</jk> if the specified character is whitespace or '/'. 235 */ 236 protected boolean isCommentOrWhitespace(int cp) { 237 if (cp == '/') 238 return true; 239 if (isStrict()) 240 return cp <= 0x20 && (cp == 0x09 || cp == 0x0A || cp == 0x0D || cp == 0x20); 241 return Character.isWhitespace(cp); 242 } 243 244 @Override /* ParserSession */ 245 protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException { 246 try (ParserReader r = pipe.getParserReader()) { 247 if (r == null) 248 return null; 249 T o = parseAnything(type, r, getOuter(), null); 250 validateEnd(r); 251 return o; 252 } 253 } 254 255 @Override /* ReaderParserSession */ 256 protected <K,V> Map<K,V> doParseIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws IOException, ParseException, ExecutableException { 257 try (ParserReader r = pipe.getParserReader()) { 258 m = parseIntoMap2(r, m, (ClassMeta<K>)getClassMeta(keyType), (ClassMeta<V>)getClassMeta(valueType), null); 259 validateEnd(r); 260 return m; 261 } 262 } 263 264 @Override /* ReaderParserSession */ 265 protected <E> Collection<E> doParseIntoCollection(ParserPipe pipe, Collection<E> c, Type elementType) throws IOException, ParseException, ExecutableException { 266 try (ParserReader r = pipe.getParserReader()) { 267 c = parseIntoCollection2(r, c, getClassMeta(elementType), null); 268 validateEnd(r); 269 return c; 270 } 271 } 272 273 private <T> T parseAnything(ClassMeta<?> eType, ParserReader r, Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { 274 275 if (eType == null) 276 eType = object(); 277 ObjectSwap<T,Object> swap = (ObjectSwap<T,Object>)eType.getSwap(this); 278 BuilderSwap<T,Object> builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this); 279 ClassMeta<?> sType = null; 280 if (builder != null) 281 sType = builder.getBuilderClassMeta(this); 282 else if (swap != null) 283 sType = swap.getSwapClassMeta(this); 284 else 285 sType = eType; 286 287 if (sType.isOptional()) 288 return (T)optional(parseAnything(eType.getElementType(), r, outer, pMeta)); 289 290 setCurrentClass(sType); 291 String wrapperAttr = getJsonClassMeta(sType).getWrapperAttr(); 292 293 Object o = null; 294 295 skipCommentsAndSpace(r); 296 if (wrapperAttr != null) 297 skipWrapperAttrStart(r, wrapperAttr); 298 int c = r.peek(); 299 if (c == -1) { 300 if (isStrict()) 301 throw new ParseException(this, "Empty input."); 302 // Let o be null. 303 } else if ((c == ',' || c == '}' || c == ']')) { 304 if (isStrict()) 305 throw new ParseException(this, "Missing value detected."); 306 // Handle bug in Cognos 10.2.1 that can product non-existent values. 307 // Let o be null; 308 } else if (c == 'n') { 309 parseKeyword("null", r); 310 } else if (sType.isObject()) { 311 if (c == '{') { 312 JsonMap m2 = new JsonMap(this); 313 parseIntoMap2(r, m2, string(), object(), pMeta); 314 o = cast(m2, pMeta, eType); 315 } else if (c == '[') { 316 o = parseIntoCollection2(r, new JsonList(this), object(), pMeta); 317 } else if (c == '\'' || c == '"') { 318 o = parseString(r); 319 if (sType.isChar()) 320 o = parseCharacter(o); 321 } else if (c >= '0' && c <= '9' || c == '-' || c == '.') { 322 o = parseNumber(r, null); 323 } else if (c == 't') { 324 parseKeyword("true", r); 325 o = Boolean.TRUE; 326 } else { 327 parseKeyword("false", r); 328 o = Boolean.FALSE; 329 } 330 } else if (sType.isBoolean()) { 331 o = parseBoolean(r); 332 } else if (sType.isCharSequence()) { 333 o = parseString(r); 334 } else if (sType.isChar()) { 335 o = parseCharacter(parseString(r)); 336 } else if (sType.isNumber()) { 337 o = parseNumber(r, (Class<? extends Number>)sType.getInnerClass()); 338 } else if (sType.isMap()) { 339 Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType)); 340 o = parseIntoMap2(r, m, sType.getKeyType(), sType.getValueType(), pMeta); 341 } else if (sType.isCollection()) { 342 if (c == '{') { 343 JsonMap m = new JsonMap(this); 344 parseIntoMap2(r, m, string(), object(), pMeta); 345 o = cast(m, pMeta, eType); 346 } else { 347 Collection l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance() : new JsonList(this)); 348 o = parseIntoCollection2(r, l, sType, pMeta); 349 } 350 } else if (builder != null) { 351 BeanMap m = toBeanMap(builder.create(this, eType)); 352 o = builder.build(this, parseIntoBeanMap2(r, m).getBean(), eType); 353 } else if (sType.canCreateNewBean(outer)) { 354 BeanMap m = newBeanMap(outer, sType.getInnerClass()); 355 o = parseIntoBeanMap2(r, m).getBean(); 356 } else if (sType.canCreateNewInstanceFromString(outer) && (c == '\'' || c == '"')) { 357 o = sType.newInstanceFromString(outer, parseString(r)); 358 } else if (sType.isArray() || sType.isArgs()) { 359 if (c == '{') { 360 JsonMap m = new JsonMap(this); 361 parseIntoMap2(r, m, string(), object(), pMeta); 362 o = cast(m, pMeta, eType); 363 } else { 364 ArrayList l = (ArrayList)parseIntoCollection2(r, list(), sType, pMeta); 365 o = toArray(sType, l); 366 } 367 } else if (c == '{') { 368 Map m = new JsonMap(this); 369 parseIntoMap2(r, m, sType.getKeyType(), sType.getValueType(), pMeta); 370 if (m.containsKey(getBeanTypePropertyName(eType))) 371 o = cast((JsonMap)m, pMeta, eType); 372 else if (sType.getProxyInvocationHandler() != null) 373 o = newBeanMap(outer, sType.getInnerClass()).load(m).getBean(); 374 else 375 throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", 376 sType.getInnerClass().getName(), sType.getNotABeanReason()); 377 } else if (sType.canCreateNewInstanceFromString(outer) && ! isStrict()) { 378 o = sType.newInstanceFromString(outer, parseString(r)); 379 } else { 380 throw new ParseException(this, "Unrecognized syntax for class type ''{0}'', starting character ''{1}''", 381 sType, (char)c); 382 } 383 384 if (wrapperAttr != null) 385 skipWrapperAttrEnd(r); 386 387 if (swap != null && o != null) 388 o = unswap(swap, o, eType); 389 390 if (outer != null) 391 setParent(eType, o, outer); 392 393 return (T)o; 394 } 395 396 private Number parseNumber(ParserReader r, Class<? extends Number> type) throws IOException, ParseException { 397 int c = r.peek(); 398 if (c == '\'' || c == '"') 399 return parseNumber(r, parseString(r), type); 400 return parseNumber(r, r.parseNumberString(), type); 401 } 402 403 private Number parseNumber(ParserReader r, String s, Class<? extends Number> type) throws ParseException { 404 405 // JSON has slightly different number rules from Java. 406 // Strict mode enforces these different rules, lax does not. 407 if (isStrict()) { 408 409 // Lax allows blank strings to represent 0. 410 // Strict does not allow blank strings. 411 if (s.isEmpty()) 412 throw new ParseException(this, "Invalid JSON number: ''{0}''", s); 413 414 // Need to weed out octal and hexadecimal formats: 0123,-0123,0x123,-0x123. 415 // Don't weed out 0 or -0. 416 boolean isNegative = false; 417 char c = s.charAt(0); 418 if (c == '-') { 419 isNegative = true; 420 c = (s.length() == 1 ? 'x' : s.charAt(1)); 421 } 422 423 // JSON doesn't allow '.123' and '-.123'. 424 if (c == '.') 425 throw new ParseException(this, "Invalid JSON number: ''{0}''", s); 426 427 // '01' is not a valid number, but '0.1', '0e1', '0e+1' are valid. 428 if (c == '0' && s.length() > (isNegative ? 2 : 1)) { 429 char c2 = s.charAt((isNegative ? 2 : 1)); 430 if (c2 != '.' && c2 != 'e' && c2 != 'E') 431 throw new ParseException(this, "Invalid JSON number: ''{0}''", s); 432 } 433 434 // JSON doesn't allow '1.' or '0.e1'. 435 int i = s.indexOf('.'); 436 if (i != -1 && (s.length() == (i+1) || ! decChars.contains(s.charAt(i+1)))) 437 throw new ParseException(this, "Invalid JSON number: ''{0}''", s); 438 439 } 440 return StringUtils.parseNumber(s, type); 441 } 442 443 private Boolean parseBoolean(ParserReader r) throws IOException, ParseException { 444 int c = r.peek(); 445 if (c == '\'' || c == '"') 446 return Boolean.valueOf(parseString(r)); 447 if (c == 't') { 448 parseKeyword("true", r); 449 return Boolean.TRUE; 450 } else if (c == 'f') { 451 parseKeyword("false", r); 452 return Boolean.FALSE; 453 } else { 454 throw new ParseException(this, "Unrecognized syntax. Expected boolean value, actual=''{0}''", r.read(100)); 455 } 456 } 457 458 private <K,V> Map<K,V> parseIntoMap2(ParserReader r, Map<K,V> m, ClassMeta<K> keyType, 459 ClassMeta<V> valueType, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { 460 461 if (keyType == null) 462 keyType = (ClassMeta<K>)string(); 463 464 int S0=0; // Looking for outer { 465 int S1=1; // Looking for attrName start. 466 int S3=3; // Found attrName end, looking for :. 467 int S4=4; // Found :, looking for valStart: { [ " ' LITERAL. 468 int S5=5; // Looking for , or } 469 int S6=6; // Found , looking for attr start. 470 471 skipCommentsAndSpace(r); 472 int state = S0; 473 String currAttr = null; 474 int c = 0; 475 while (c != -1) { 476 c = r.read(); 477 if (state == S0) { 478 if (c == '{') 479 state = S1; 480 else 481 break; 482 } else if (state == S1) { 483 if (c == '}') { 484 return m; 485 } else if (isCommentOrWhitespace(c)) { 486 skipCommentsAndSpace(r.unread()); 487 } else { 488 currAttr = parseFieldName(r.unread()); 489 state = S3; 490 } 491 } else if (state == S3) { 492 if (c == ':') 493 state = S4; 494 } else if (state == S4) { 495 if (isCommentOrWhitespace(c)) { 496 skipCommentsAndSpace(r.unread()); 497 } else { 498 K key = convertAttrToType(m, currAttr, keyType); 499 V value = parseAnything(valueType, r.unread(), m, pMeta); 500 setName(valueType, value, key); 501 m.put(key, value); 502 state = S5; 503 } 504 } else if (state == S5) { 505 if (c == ',') { 506 state = S6; 507 } else if (isCommentOrWhitespace(c)) { 508 skipCommentsAndSpace(r.unread()); 509 } else if (c == '}') { 510 return m; 511 } else { 512 break; 513 } 514 } else if (state == S6) { 515 if (c == '}') { 516 break; 517 } else if (isCommentOrWhitespace(c)) { 518 skipCommentsAndSpace(r.unread()); 519 } else { 520 currAttr = parseFieldName(r.unread()); 521 state = S3; 522 } 523 } 524 } 525 if (state == S0) 526 throw new ParseException(this, "Expected '{' at beginning of JSON object."); 527 if (state == S1) 528 throw new ParseException(this, "Could not find attribute name on JSON object."); 529 if (state == S3) 530 throw new ParseException(this, "Could not find ':' following attribute name on JSON object."); 531 if (state == S4) 532 throw new ParseException(this, "Expected one of the following characters: {,[,',\",LITERAL."); 533 if (state == S5) 534 throw new ParseException(this, "Could not find '}' marking end of JSON object."); 535 if (state == S6) 536 throw new ParseException(this, "Unexpected '}' found in JSON object."); 537 538 return null; // Unreachable. 539 } 540 541 /* 542 * Parse a JSON attribute from the character array at the specified position, then 543 * set the position marker to the last character in the field name. 544 */ 545 private String parseFieldName(ParserReader r) throws IOException, ParseException { 546 int c = r.peek(); 547 if (c == '\'' || c == '"') 548 return parseString(r); 549 if (isStrict()) 550 throw new ParseException(this, "Unquoted attribute detected."); 551 if (! VALID_BARE_CHARS.contains(c)) 552 throw new ParseException(this, "Could not find the start of the field name."); 553 r.mark(); 554 // Look for whitespace. 555 while (c != -1) { 556 c = r.read(); 557 if (! VALID_BARE_CHARS.contains(c)) { 558 r.unread(); 559 String s = r.getMarked().intern(); 560 return s.equals("null") ? null : s; 561 } 562 } 563 throw new ParseException(this, "Could not find the end of the field name."); 564 } 565 566 private static final AsciiSet VALID_BARE_CHARS = AsciiSet.create().range('A','Z').range('a','z').range('0','9').chars("$_-.").build(); 567 568 private <E> Collection<E> parseIntoCollection2(ParserReader r, Collection<E> l, 569 ClassMeta<?> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { 570 571 int S0=0; // Looking for outermost [ 572 int S1=1; // Looking for starting [ or { or " or ' or LITERAL or ] 573 int S2=2; // Looking for , or ] 574 int S3=3; // Looking for starting [ or { or " or ' or LITERAL 575 576 int argIndex = 0; 577 578 int state = S0; 579 int c = 0; 580 while (c != -1) { 581 c = r.read(); 582 if (state == S0) { 583 if (c == '[') 584 state = S1; 585 else if (isCommentOrWhitespace(c)) 586 skipCommentsAndSpace(r.unread()); 587 else 588 break; // Invalid character found. 589 } else if (state == S1) { 590 if (c == ']') { 591 return l; 592 } else if (isCommentOrWhitespace(c)) { 593 skipCommentsAndSpace(r.unread()); 594 } else if (c != -1) { 595 l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, pMeta)); 596 state = S2; 597 } 598 } else if (state == S2) { 599 if (c == ',') { 600 state = S3; 601 } else if (isCommentOrWhitespace(c)) { 602 skipCommentsAndSpace(r.unread()); 603 } else if (c == ']') { 604 return l; 605 } else { 606 break; // Invalid character found. 607 } 608 } else if (state == S3) { 609 if (isCommentOrWhitespace(c)) { 610 skipCommentsAndSpace(r.unread()); 611 } else if (c == ']') { 612 break; 613 } else if (c != -1) { 614 l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, pMeta)); 615 state = S2; 616 } 617 } 618 } 619 if (state == S0) 620 throw new ParseException(this, "Expected '[' at beginning of JSON array."); 621 if (state == S1) 622 throw new ParseException(this, "Expected one of the following characters: {,[,',\",LITERAL."); 623 if (state == S2) 624 throw new ParseException(this, "Expected ',' or ']'."); 625 if (state == S3) 626 throw new ParseException(this, "Unexpected trailing comma in array."); 627 628 return null; // Unreachable. 629 } 630 631 private <T> BeanMap<T> parseIntoBeanMap2(ParserReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException { 632 633 int S0=0; // Looking for outer { 634 int S1=1; // Looking for attrName start. 635 int S3=3; // Found attrName end, looking for :. 636 int S4=4; // Found :, looking for valStart: { [ " ' LITERAL. 637 int S5=5; // Looking for , or } 638 639 int state = S0; 640 String currAttr = ""; 641 int c = 0; 642 mark(); 643 try { 644 while (c != -1) { 645 c = r.read(); 646 if (state == S0) { 647 if (c == '{') { 648 state = S1; 649 } else if (isCommentOrWhitespace(c)) { 650 skipCommentsAndSpace(r.unread()); 651 } else { 652 break; 653 } 654 } else if (state == S1) { 655 if (c == '}') { 656 return m; 657 } else if (isCommentOrWhitespace(c)) { 658 skipCommentsAndSpace(r.unread()); 659 } else { 660 r.unread(); 661 mark(); 662 currAttr = parseFieldName(r); 663 state = S3; 664 } 665 } else if (state == S3) { 666 if (c == ':') 667 state = S4; 668 } else if (state == S4) { 669 if (isCommentOrWhitespace(c)) { 670 skipCommentsAndSpace(r.unread()); 671 } else { 672 if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) { 673 BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); 674 setCurrentProperty(pMeta); 675 if (pMeta == null) { 676 onUnknownProperty(currAttr, m, parseAnything(object(), r.unread(), m.getBean(false), null)); 677 unmark(); 678 } else { 679 unmark(); 680 ClassMeta<?> cm = pMeta.getClassMeta(); 681 Object value = parseAnything(cm, r.unread(), m.getBean(false), pMeta); 682 setName(cm, value, currAttr); 683 try { 684 pMeta.set(m, currAttr, value); 685 } catch (BeanRuntimeException e) { 686 onBeanSetterException(pMeta, e); 687 throw e; 688 } 689 } 690 setCurrentProperty(null); 691 } 692 state = S5; 693 } 694 } else if (state == S5) { 695 if (c == ',') 696 state = S1; 697 else if (isCommentOrWhitespace(c)) 698 skipCommentsAndSpace(r.unread()); 699 else if (c == '}') { 700 return m; 701 } 702 } 703 } 704 if (state == S0) 705 throw new ParseException(this, "Expected '{' at beginning of JSON object."); 706 if (state == S1) 707 throw new ParseException(this, "Could not find attribute name on JSON object."); 708 if (state == S3) 709 throw new ParseException(this, "Could not find ':' following attribute name on JSON object."); 710 if (state == S4) 711 throw new ParseException(this, "Expected one of the following characters: {,[,',\",LITERAL."); 712 if (state == S5) 713 throw new ParseException(this, "Could not find '}' marking end of JSON object."); 714 } finally { 715 unmark(); 716 } 717 718 return null; // Unreachable. 719 } 720 721 /* 722 * Starting from the specified position in the character array, returns the 723 * position of the character " or '. 724 * If the string consists of a concatenation of strings (e.g. 'AAA' + "BBB"), this method 725 * will automatically concatenate the strings and return the result. 726 */ 727 private String parseString(ParserReader r) throws IOException, ParseException { 728 r.mark(); 729 int qc = r.read(); // The quote character being used (" or ') 730 if (qc != '"' && isStrict()) { 731 String msg = ( 732 qc == '\'' 733 ? "Invalid quote character \"{0}\" being used." 734 : "Did not find quote character marking beginning of string. Character=\"{0}\"" 735 ); 736 throw new ParseException(this, msg, (char)qc); 737 } 738 final boolean isQuoted = (qc == '\'' || qc == '"'); 739 String s = null; 740 boolean isInEscape = false; 741 int c = 0; 742 while (c != -1) { 743 c = r.read(); 744 // Strict syntax requires that all control characters be escaped. 745 if (isStrict() && c <= 0x1F) 746 throw new ParseException(this, "Unescaped control character encountered: ''0x{0}''", String.format("%04X", c)); 747 if (isInEscape) { 748 switch (c) { 749 case 'n': r.replace('\n'); break; 750 case 'r': r.replace('\r'); break; 751 case 't': r.replace('\t'); break; 752 case 'f': r.replace('\f'); break; 753 case 'b': r.replace('\b'); break; 754 case '\\': r.replace('\\'); break; 755 case '/': r.replace('/'); break; 756 case '\'': r.replace('\''); break; 757 case '"': r.replace('"'); break; 758 case 'u': { 759 String n = r.read(4); 760 try { 761 r.replace(Integer.parseInt(n, 16), 6); 762 } catch (NumberFormatException e) { 763 throw new ParseException(this, "Invalid Unicode escape sequence in string."); 764 } 765 break; 766 } 767 default: 768 throw new ParseException(this, "Invalid escape sequence in string."); 769 } 770 isInEscape = false; 771 } else { 772 if (c == '\\') { 773 isInEscape = true; 774 r.delete(); 775 } else if (isQuoted) { 776 if (c == qc) { 777 s = r.getMarked(1, -1); 778 break; 779 } 780 } else { 781 if (c == ',' || c == '}' || c == ']' || isWhitespace(c)) { 782 s = r.getMarked(0, -1); 783 r.unread(); 784 break; 785 } else if (c == -1) { 786 s = r.getMarked(0, 0); 787 break; 788 } 789 } 790 } 791 } 792 if (s == null) 793 throw new ParseException(this, "Could not find expected end character ''{0}''.", (char)qc); 794 795 // Look for concatenated string (i.e. whitespace followed by +). 796 skipCommentsAndSpace(r); 797 if (r.peek() == '+') { 798 if (isStrict()) 799 throw new ParseException(this, "String concatenation detected."); 800 r.read(); // Skip past '+' 801 skipCommentsAndSpace(r); 802 s += parseString(r); 803 } 804 return trim(s); // End of input reached. 805 } 806 807 /* 808 * Looks for the keywords true, false, or null. 809 * Throws an exception if any of these keywords are not found at the specified position. 810 */ 811 private void parseKeyword(String keyword, ParserReader r) throws IOException, ParseException { 812 try { 813 String s = r.read(keyword.length()); 814 if (s.equals(keyword)) 815 return; 816 throw new ParseException(this, "Unrecognized syntax. Expected=''{0}'', Actual=''{1}''", keyword, s); 817 } catch (IndexOutOfBoundsException e) { 818 throw new ParseException(this, "Unrecognized syntax. Expected=''{0}'', found end-of-file.", keyword); 819 } 820 } 821 822 /* 823 * Doesn't actually parse anything, but moves the position beyond any whitespace or comments. 824 * If positionOnNext is 'true', then the cursor will be set to the point immediately after 825 * the comments and whitespace. Otherwise, the cursor will be set to the last position of 826 * the comments and whitespace. 827 */ 828 private void skipCommentsAndSpace(ParserReader r) throws IOException, ParseException { 829 int c = 0; 830 while ((c = r.read()) != -1) { 831 if (! isWhitespace(c)) { 832 if (c == '/') { 833 if (isStrict()) 834 throw new ParseException(this, "Javascript comment detected."); 835 skipComments(r); 836 } else { 837 r.unread(); 838 return; 839 } 840 } 841 } 842 } 843 844 /* 845 * Doesn't actually parse anything, but moves the position beyond the construct "{wrapperAttr:" when 846 * the @Json(wrapperAttr) annotation is used on a class. 847 */ 848 private void skipWrapperAttrStart(ParserReader r, String wrapperAttr) throws IOException, ParseException { 849 850 int S0=0; // Looking for outer { 851 int S1=1; // Looking for attrName start. 852 int S3=3; // Found attrName end, looking for :. 853 int S4=4; // Found :, looking for valStart: { [ " ' LITERAL. 854 855 int state = S0; 856 String currAttr = null; 857 int c = 0; 858 while (c != -1) { 859 c = r.read(); 860 if (state == S0) { 861 if (c == '{') 862 state = S1; 863 } else if (state == S1) { 864 if (isCommentOrWhitespace(c)) { 865 skipCommentsAndSpace(r.unread()); 866 } else { 867 currAttr = parseFieldName(r.unread()); 868 if (! currAttr.equals(wrapperAttr)) 869 throw new ParseException(this, 870 "Expected to find wrapper attribute ''{0}'' but found attribute ''{1}''", wrapperAttr, currAttr); 871 state = S3; 872 } 873 } else if (state == S3) { 874 if (c == ':') 875 state = S4; 876 } else if (state == S4) { 877 if (isCommentOrWhitespace(c)) { 878 skipCommentsAndSpace(r.unread()); 879 } else { 880 r.unread(); 881 return; 882 } 883 } 884 } 885 if (state == S0) 886 throw new ParseException(this, "Expected '{' at beginning of JSON object."); 887 if (state == S1) 888 throw new ParseException(this, "Could not find attribute name on JSON object."); 889 if (state == S3) 890 throw new ParseException(this, "Could not find ':' following attribute name on JSON object."); 891 if (state == S4) 892 throw new ParseException(this, "Expected one of the following characters: {,[,',\",LITERAL."); 893 } 894 895 /* 896 * Doesn't actually parse anything, but moves the position beyond the construct "}" when 897 * the @Json(wrapperAttr) annotation is used on a class. 898 */ 899 private void skipWrapperAttrEnd(ParserReader r) throws ParseException, IOException { 900 int c = 0; 901 while ((c = r.read()) != -1) { 902 if (! isWhitespace(c)) { 903 if (c == '/') { 904 if (isStrict()) 905 throw new ParseException(this, "Javascript comment detected."); 906 skipComments(r); 907 } else if (c == '}') { 908 return; 909 } else { 910 throw new ParseException(this, "Could not find '}' at the end of JSON wrapper object."); 911 } 912 } 913 } 914 } 915 916 /* 917 * Doesn't actually parse anything, but when positioned at the beginning of comment, 918 * it will move the pointer to the last character in the comment. 919 */ 920 private void skipComments(ParserReader r) throws ParseException, IOException { 921 int c = r.read(); 922 // "/* */" style comments 923 if (c == '*') { 924 while (c != -1) 925 if ((c = r.read()) == '*') 926 if ((c = r.read()) == '/') 927 return; 928 // "//" style comments 929 } else if (c == '/') { 930 while (c != -1) { 931 c = r.read(); 932 if (c == -1 || c == '\n') 933 return; 934 } 935 } 936 throw new ParseException(this, "Open ended comment."); 937 } 938 939 /* 940 * Call this method after you've finished a parsing a string to make sure that if there's any 941 * remainder in the input, that it consists only of whitespace and comments. 942 */ 943 private void validateEnd(ParserReader r) throws IOException, ParseException { 944 if (! isValidateEnd()) 945 return; 946 skipCommentsAndSpace(r); 947 int c = r.read(); 948 if (c != -1 && c != ';') // var x = {...}; expressions can end with a semicolon. 949 throw new ParseException(this, "Remainder after parse: ''{0}''.", (char)c); 950 } 951 952 //----------------------------------------------------------------------------------------------------------------- 953 // Properties 954 //----------------------------------------------------------------------------------------------------------------- 955 956 /** 957 * Validate end. 958 * 959 * @see JsonParser.Builder#validateEnd() 960 * @return 961 * <jk>true</jk> if after parsing a POJO from the input, verifies that the remaining input in 962 * the stream consists of only comments or whitespace. 963 */ 964 protected boolean isValidateEnd() { 965 return ctx.isValidateEnd(); 966 } 967 968 //----------------------------------------------------------------------------------------------------------------- 969 // Extended metadata 970 //----------------------------------------------------------------------------------------------------------------- 971 972 /** 973 * Returns the language-specific metadata on the specified class. 974 * 975 * @param cm The class to return the metadata on. 976 * @return The metadata. 977 */ 978 protected JsonClassMeta getJsonClassMeta(ClassMeta<?> cm) { 979 return ctx.getJsonClassMeta(cm); 980 } 981}