001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.marshall.json; 018 019import static org.apache.juneau.commons.lang.StateEnum.*; 020import static org.apache.juneau.commons.utils.CollectionUtils.*; 021import static org.apache.juneau.commons.utils.Shorts.*; 022import static org.apache.juneau.commons.utils.StringUtils.*; 023 024import java.io.*; 025import java.lang.reflect.*; 026import java.util.*; 027 028import org.apache.juneau.commons.bean.*; 029import org.apache.juneau.commons.lang.*; 030import org.apache.juneau.commons.reflect.*; 031import org.apache.juneau.commons.utils.*; 032import org.apache.juneau.marshall.*; 033import org.apache.juneau.marshall.collections.*; 034import org.apache.juneau.marshall.parser.*; 035import org.apache.juneau.marshall.stream.*; 036import org.apache.juneau.marshall.swap.spi.*; 037 038/** 039 * Session object that lives for the duration of a single use of {@link JsonParser}. 040 * 041 * <h5 class='section'>Notes:</h5><ul> 042 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 043 * </ul> 044 * 045 * <h5 class='section'>See Also:</h5><ul> 046 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JsonSupport">JSON</a> 047 048 * </ul> 049 */ 050@SuppressWarnings({ 051 "java:S110", // Deep inheritance inherent to the serializer/parser session hierarchy 052 "java:S115", // Constants use camelCase intentionally (e.g., decChars) 053 "java:S125", // State-machine and parse-path comments (S1: ..., Let o be null) are documentation, not commented-out code 054 "java:S135", // readIntoBeanMap2(), readIntoCollection2() and readIntoMap2() use several break/continue guards for invalid characters and separators; restructuring would reduce clarity 055 "java:S1168", // readFieldName() returns null for a missing name and doRead() for empty input; the unreachable fall-through returns in readIntoBeanMap2()/readIntoCollection2()/readIntoMap2() also return null 056 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 057 "java:S2583", // State variables persist across loop iterations 058 "java:S3776", // readAnything(), readIntoMap2(), readIntoCollection2() and readIntoBeanMap2() are branch-heavy JSON token state machines; splitting them would obscure the grammar 059 "java:S6541", // Brain method acceptable for JSON parser dispatch and state machines 060 "rawtypes", // Raw types necessary for generic type handling 061 "resource", // ParserReader is managed by caller 062 "unchecked" // Type erasure requires unchecked casts 063}) 064public class JsonParserSession extends ReaderParserSession implements TokenReadable, ArrayRecordReadable { 065 066 /** 067 * Builder class. 068 */ 069 @SuppressWarnings({ 070 "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name. 071 }) 072 public abstract static class Builder<SELF extends Builder<SELF>> extends ReaderParserSession.Builder<SELF> { 073 074 private boolean validateEnd; 075 private final JsonParser ctx; 076 077 /** 078 * Constructor 079 * 080 * @param ctx The context creating this session. 081 */ 082 protected Builder(JsonParser ctx) { 083 super(ctx); 084 this.ctx = ctx; 085 validateEnd = ctx.isValidateEnd(); 086 } 087 088 @Override 089 public JsonParserSession build() { 090 return new JsonParserSession(this); 091 } 092 093 /** 094 * Validate end. 095 * 096 * @param value The new value for this setting. 097 * @return This object. 098 */ 099 public SELF validateEnd(boolean value) { 100 validateEnd = value; 101 return self(); 102 } 103 104 @Override /* Overridden from Builder */ 105 public SELF property(String key, Object value) { 106 if (key == null) { super.property(key, value); return self(); } 107 switch (key) { 108 case "validateEnd", "JsonParserSession.validateEnd": 109 return validateEnd(cvt(value, Boolean.class)); 110 default: 111 super.property(key, value); 112 return self(); 113 } 114 } 115 116 } 117 118 /** 119 * Concrete default builder leaf for the non-subclassed {@code create()} path (CRTP terminal). 120 */ 121 public static final class DefaultBuilder extends Builder<DefaultBuilder> { 122 123 DefaultBuilder(JsonParser ctx) { 124 super(ctx); 125 } 126 } 127 128 private static final AsciiSet decChars = AsciiSet.create().ranges("0-9").build(); 129 130 /** 131 * Creates a new builder for this object. 132 * 133 * @param ctx The context creating this session. 134 * @return A new builder. 135 */ 136 @SuppressWarnings({ 137 "java:S1452" // Builder<?> wildcard return intentional; callers use it to construct session instances polymorphically 138 }) 139 public static Builder<?> create(JsonParser ctx) { 140 return new DefaultBuilder(ctx); 141 } 142 143 private final JsonParser ctx; 144 private final boolean validateEnd; 145 146 /** 147 * Constructor. 148 * 149 * @param builder The builder for this object. 150 */ 151 protected JsonParserSession(Builder<?> builder) { 152 super(builder); 153 ctx = builder.ctx; 154 validateEnd = builder.validateEnd; 155 } 156 157 <T> T readAnything(ClassMeta<?> eType, ParserReader r, Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { 158 159 if (eType == null) 160 eType = object(); 161 var swap = (ObjectSwap<T,Object>)eType.getSwap(this); 162 var builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this); 163 ClassMeta<?> sType = null; 164 if (nn(builder)) 165 sType = builder.getBuilderClassMeta(this); 166 else if (nn(swap)) 167 sType = swap.getSwapClassMeta(this); 168 else 169 sType = eType; 170 171 if (sType.isOptional()) 172 return (T)o(readAnything(eType.getElementType(), r, outer, pMeta)); 173 174 setCurrentClass(sType); 175 var wrapperAttr = getJsonClassMeta(sType).getWrapperAttr(); 176 177 Object o = null; 178 179 skipCommentsAndSpace(r); 180 if (nn(wrapperAttr)) 181 skipWrapperAttrStart(r, wrapperAttr); 182 int c = r.peek(); 183 if (c == -1) { 184 onEmptyInput(); 185 } else if ((c == ',' || c == '}' || c == ']')) { 186 onMissingValue(); 187 } else if (c == 'n') { 188 readKeyword("null", r); 189 } else if (sType.isObject()) { 190 if (c == '{') { 191 var m2 = newGenericMap(); 192 readIntoMap2(r, m2, string(), object(), pMeta); 193 o = cast(m2, pMeta, eType); 194 } else if (c == '[') { 195 o = readIntoCollection2(r, newGenericList(), object(), pMeta); 196 } else if (c == '\'' || c == '"') { 197 o = readString(r); 198 if (sType.isChar()) 199 o = parseCharacter(o); 200 } else if (isNumberStartChar(c)) { 201 o = readNumber(r, null); 202 } else if (c == 't') { 203 readKeyword("true", r); 204 o = Boolean.TRUE; 205 } else { 206 readKeyword("false", r); 207 o = Boolean.FALSE; 208 } 209 } else if (nn(builder)) { 210 var m = toBeanMap(builder.create(this, eType)); 211 o = builder.build(this, checkRequired(readIntoBeanMap2(r, m)).getBean(), eType); 212 } else if (sType.canCreateNewBean(outer)) { 213 var m = newBeanMap(outer, sType.inner()); 214 o = checkRequired(readIntoBeanMap2(r, m)).getBean(); 215 } else if (sType.isMap()) { 216 Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType)); 217 o = readIntoMap2(r, m, sType.getKeyType(), sType.getValueType(), pMeta); 218 } else if (sType.isCollection()) { 219 if (c == '{') { 220 var m = newGenericMap(); 221 readIntoMap2(r, m, string(), object(), pMeta); 222 o = cast(m, pMeta, eType); 223 } else { 224 Collection l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance() : newGenericList()); 225 o = readIntoCollection2(r, l, sType, pMeta); 226 } 227 } else if (sType.isArray() || sType.isArgs()) { 228 if (c == '{') { 229 var m = newGenericMap(); 230 readIntoMap2(r, m, string(), object(), pMeta); 231 o = cast(m, pMeta, eType); 232 } else { 233 var l = (ArrayList)readIntoCollection2(r, list(), sType, pMeta); 234 o = toArray(sType, l); 235 } 236 } else if (sType.isCharSequence()) { 237 o = readString(r); 238 } else if (sType.isChar()) { 239 o = parseCharacter(readString(r)); 240 } else if (sType.isNumber()) { 241 o = readNumber(r, (Class<? extends Number>)sType.inner()); 242 } else if (sType.isBoolean()) { 243 o = readBoolean(r); 244 } else if (sType.isDate()) { 245 o = readDate(readString(r), sType); 246 } else if (sType.isCalendar()) { 247 o = readCalendar(readString(r), sType); 248 } else if (sType.isTemporal()) { 249 o = readTemporal(readString(r), sType); 250 } else if (sType.isDuration()) { 251 o = readDuration(readString(r)); 252 } else if (sType.isPeriod()) { 253 o = readPeriod(readString(r)); 254 } else if (sType.canCreateNewInstanceFromString(outer) && (c == '\'' || c == '"')) { 255 o = sType.newInstanceFromString(outer, readString(r)); 256 } else if (c == '{') { 257 Map m = newGenericMap(); 258 readIntoMap2(r, m, sType.getKeyType(), sType.getValueType(), pMeta); 259 if (m.containsKey(getBeanTypePropertyName(eType))) 260 o = cast((MarshalledMap)m, pMeta, eType); 261 else if (nn(sType.getProxyInvocationHandler())) 262 o = checkRequired(newBeanMap(outer, sType.inner()).load(m)).getBean(); 263 else 264 throw new ParseException(this, "Class '%s' could not be instantiated. Reason: '%s'", cn(sType), sType.getNotABeanReason()); 265 } else if (sType.canCreateNewInstanceFromString(outer) && canCoerceNonStringToString()) { 266 o = sType.newInstanceFromString(outer, readString(r)); 267 } else { 268 throw new ParseException(this, "Unrecognized syntax for class type '%s', starting character '%s'", sType, (char)c); 269 } 270 271 if (nn(wrapperAttr)) 272 skipWrapperAttrEnd(r); 273 274 if (nn(swap) && nn(o)) 275 o = unswap(swap, o, eType); 276 277 if (nn(parentBean())) 278 setParent(eType, o, parentBean()); 279 280 return (T)o; 281 } 282 283 private Boolean readBoolean(ParserReader r) throws IOException, ParseException { 284 int c = r.peek(); 285 if (c == '\'' || c == '"') 286 return b(readString(r)); 287 if (c == 't') { 288 readKeyword("true", r); 289 return Boolean.TRUE; 290 } else if (c == 'f') { 291 readKeyword("false", r); 292 return Boolean.FALSE; 293 } else { 294 throw new ParseException(this, "Unrecognized syntax. Expected boolean value, actual='%s'", r.read(100)); 295 } 296 } 297 298 /* 299 * Parse a JSON attribute from the character array at the specified position, then 300 * set the position marker to the last character in the field name. 301 */ 302 protected String readFieldName(ParserReader r) throws IOException, ParseException { 303 int c = r.peek(); 304 if (c == '\'' || c == '"') 305 return readString(r); 306 if (c == 'n') { 307 readKeyword("null", r); 308 return null; 309 } 310 throw new ParseException(this, "Unquoted attribute detected."); 311 } 312 313 private <T> BeanMap<T> readIntoBeanMap2(ParserReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException { 314 315 // S1: Looking for outer { 316 // S2: Looking for attrName start. 317 // S3: Found attrName end, looking for :. 318 // S4: Found :, looking for valStart: { [ " ' LITERAL. 319 // S5: Looking for , or } 320 321 var state = S1; 322 var currAttr = ""; 323 int c = 0; 324 var pb = swapParentBean(m.getBean(false)); 325 mark(); 326 try { 327 while (c != -1) { 328 c = r.read(); 329 if (state == S1) { 330 if (c == '{') { 331 state = S2; 332 } else if (isCommentOrWhitespace(c)) { 333 skipCommentsAndSpace(r.unread()); 334 } else { 335 break; 336 } 337 } else if (state == S2) { 338 if (c == '}') { 339 return m; 340 } else if (isCommentOrWhitespace(c)) { 341 skipCommentsAndSpace(r.unread()); 342 } else { 343 r.unread(); 344 mark(); 345 currAttr = readFieldName(r); 346 state = S3; 347 } 348 } else if (state == S3) { // NOSONAR - State check necessary for state machine 349 if (c == ':') 350 state = S4; 351 } else if (state == S4) { 352 if (isCommentOrWhitespace(c)) { 353 skipCommentsAndSpace(r.unread()); 354 } else { 355 if (! currAttr.equals(getBeanTypePropertyName((ClassMeta<?>) m.getBeanInfo()))) { 356 var pMeta = m.getPropertyMeta(currAttr); 357 setCurrentProperty(pMeta); 358 if (pMeta == null) { 359 onUnknownProperty(currAttr, m, readAnything(object(), r.unread(), m.getBean(false), null)); 360 unmark(); 361 } else { 362 unmark(); 363 var cm = (ClassMeta<?>) pMeta.getBeanInfo(); 364 Object value = readAnything(cm, r.unread(), m.getBean(false), pMeta); 365 setName(cm, value, currAttr); 366 try { 367 pMeta.set(m, currAttr, value); 368 } catch (BeanRuntimeException e) { 369 onBeanSetterException(pMeta, e); 370 throw e; 371 } 372 } 373 setCurrentProperty(null); 374 } 375 state = S5; 376 } 377 } else if (state == S5) { 378 if (c == ',') 379 state = S2; 380 else if (isCommentOrWhitespace(c)) 381 skipCommentsAndSpace(r.unread()); 382 else if (c == '}') { 383 return m; 384 } 385 } 386 } 387 if (state == S1) 388 throw new ParseException(this, "Expected '{' at beginning of JSON object."); 389 else if (state == S3) 390 throw new ParseException(this, "Could not find ':' following attribute name on JSON object."); 391 else if (state == S5) 392 throw new ParseException(this, "Could not find '}' marking end of JSON object."); 393 } finally { 394 unmark(); 395 swapParentBean(pb); 396 } 397 398 return null; // Unreachable. 399 } 400 401 private <E> Collection<E> readIntoCollection2(ParserReader r, Collection<E> l, ClassMeta<?> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { 402 403 // S1: Looking for outermost [ 404 // S2: Looking for starting [ or { or " or ' or LITERAL or ] 405 // S3: Looking for , or ] 406 // S4: Looking for starting [ or { or " or ' or LITERAL 407 408 int argIndex = 0; 409 410 var state = S1; 411 int c = 0; 412 while (c != -1) { 413 c = r.read(); 414 if (state == S1) { 415 if (c == '[') 416 state = S2; 417 else if (isCommentOrWhitespace(c)) 418 skipCommentsAndSpace(r.unread()); 419 else 420 break; // Invalid character found. 421 } else if (state == S2) { 422 if (c == ']') { 423 return l; 424 } else if (isCommentOrWhitespace(c)) { 425 skipCommentsAndSpace(r.unread()); 426 } else if (c != -1) { 427 l.add((E)readAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, pMeta)); 428 state = S3; 429 } 430 } else if (state == S3) { // NOSONAR - State check necessary for state machine 431 if (c == ',') { 432 state = S4; 433 } else if (isCommentOrWhitespace(c)) { 434 skipCommentsAndSpace(r.unread()); 435 } else if (c == ']') { 436 return l; 437 } else { 438 break; // Invalid character found. 439 } 440 } else if (state == S4) { 441 if (isCommentOrWhitespace(c)) { 442 skipCommentsAndSpace(r.unread()); 443 } else if (c == ']') { 444 if (allowsTrailingComma()) 445 return l; 446 break; 447 } else if (c != -1) { 448 l.add((E)readAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, pMeta)); 449 state = S3; 450 } 451 } 452 } 453 if (state == S1) 454 throw new ParseException(this, "Expected '[' at beginning of JSON array."); 455 else if (state == S2) 456 throw new ParseException(this, "Expected one of the following characters: {,[,',\",LITERAL."); 457 else if (state == S3) 458 throw new ParseException(this, "Expected ',' or ']'."); 459 else if (state == S4) 460 throw new ParseException(this, "Unexpected trailing comma in array."); 461 462 return null; // Unreachable. 463 } 464 465 private <K,V> Map<K,V> readIntoMap2(ParserReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { 466 467 if (keyType == null) 468 keyType = (ClassMeta<K>)string(); 469 470 // S1: Looking for outer { 471 // S2: Looking for attrName start. 472 // S3: Found attrName end, looking for :. 473 // S4: Found :, looking for valStart: { [ " ' LITERAL. 474 // S5: Looking for , or } 475 // S6: Found , looking for attr start. 476 477 skipCommentsAndSpace(r); 478 var state = S1; 479 String currAttr = null; 480 int c = 0; 481 while (c != -1) { 482 c = r.read(); 483 if (state == S1) { 484 if (c == '{') 485 state = S2; 486 else 487 break; 488 } else if (state == S2) { 489 if (c == '}') { 490 return m; 491 } else if (isCommentOrWhitespace(c)) { 492 skipCommentsAndSpace(r.unread()); 493 } else { 494 currAttr = readFieldName(r.unread()); 495 state = S3; 496 } 497 } else if (state == S3) { // NOSONAR - State check necessary for state machine 498 if (c == ':') 499 state = S4; 500 } else if (state == S4) { 501 if (isCommentOrWhitespace(c)) { 502 skipCommentsAndSpace(r.unread()); 503 } else { 504 K key = convertAttrToType(m, currAttr, keyType); 505 V value = readAnything(valueType, r.unread(), m, pMeta); 506 setName(valueType, value, key); 507 m.put(key, value); 508 state = S5; 509 } 510 } else if (state == S5) { 511 if (c == ',') { 512 state = S6; 513 } else if (isCommentOrWhitespace(c)) { 514 skipCommentsAndSpace(r.unread()); 515 } else if (c == '}') { 516 return m; 517 } else { 518 break; 519 } 520 } else if (state == S6) { 521 if (c == '}') { 522 if (allowsTrailingComma()) 523 return m; 524 break; 525 } else if (isCommentOrWhitespace(c)) { 526 skipCommentsAndSpace(r.unread()); 527 } else { 528 currAttr = readFieldName(r.unread()); 529 state = S3; 530 } 531 } 532 } 533 if (state == S1) 534 throw new ParseException(this, "Expected '{' at beginning of JSON object."); 535 else if (state == S3) 536 throw new ParseException(this, "Could not find ':' following attribute name on JSON object."); 537 else if (state == S5) 538 throw new ParseException(this, "Could not find '}' marking end of JSON object."); 539 else if (state == S6) 540 throw new ParseException(this, "Unexpected '}' found in JSON object."); 541 542 return null; // Unreachable. 543 } 544 545 /* 546 * Looks for the keywords true, false, or null. 547 * Throws an exception if any of these keywords are not found at the specified position. 548 */ 549 protected void readKeyword(String keyword, ParserReader r) throws IOException, ParseException { 550 String s = r.read(keyword.length()); 551 if (s.equals(keyword)) 552 return; 553 throw new ParseException(this, "Unrecognized syntax. Expected='%s', Actual='%s'", keyword, s); 554 } 555 556 protected Number readNumber(ParserReader r, Class<? extends Number> type) throws IOException, ParseException { 557 int c = r.peek(); 558 if (c == '\'' || c == '"') 559 return readNumber(r, readString(r), type); 560 return readNumber(r, r.readNumberString(), type); 561 } 562 563 @SuppressWarnings({ 564 "java:S1172", // r accepted for API consistency; subclasses may use it for position tracking 565 "unused" // r accepted for API consistency; subclasses may use it for position tracking 566 }) 567 protected Number readNumber(ParserReader r, String s, Class<? extends Number> type) throws ParseException { 568 569 if (s.isEmpty()) 570 throw new ParseException(this, "Invalid JSON number: '%s'", s); 571 572 var isNegative = false; 573 var c = s.charAt(0); 574 if (c == '-') { 575 isNegative = true; 576 c = (s.length() == 1 ? 'x' : s.charAt(1)); 577 } 578 579 if (c == '.') 580 throw new ParseException(this, "Invalid JSON number: '%s'", s); 581 582 if (c == '0' && s.length() > (isNegative ? 2 : 1)) { 583 var c2 = s.charAt((isNegative ? 2 : 1)); 584 if (c2 != '.' && c2 != 'e' && c2 != 'E') 585 throw new ParseException(this, "Invalid JSON number: '%s'", s); 586 } 587 588 var i = s.indexOf('.'); 589 if (i != -1 && (s.length() == (i + 1) || ! decChars.contains(s.charAt(i + 1)))) 590 throw new ParseException(this, "Invalid JSON number: '%s'", s); 591 592 return StringUtils.parseNumber(s, type); 593 } 594 595 /* 596 * Starting from the specified position in the character array, returns the 597 * position of the character " or '. 598 * If the string consists of a concatenation of strings (e.g. 'AAA' + "BBB"), this method 599 * will automatically concatenate the strings and return the result. 600 */ 601 protected String readString(ParserReader r) throws IOException, ParseException { 602 r.mark(); 603 int qc = r.read(); 604 if (qc != '"') { 605 var msg = (qc == '\'' ? "Invalid quote character \"%s\" being used." : "Did not find quote character marking beginning of string. Character=\"%s\""); 606 throw new ParseException(this, msg, (char)qc); 607 } 608 String s = null; 609 boolean isInEscape = false; 610 int c = 0; 611 while (c != -1) { 612 c = r.read(); 613 if (c <= 0x1F) 614 throw new ParseException(this, "Unescaped control character encountered: '0x%s'", f("%04X", c)); 615 if (isInEscape) { 616 // @formatter:off 617 switch (c) { 618 case 'n': r.replace('\n'); break; 619 case 'r': r.replace('\r'); break; 620 case 't': r.replace('\t'); break; 621 case 'f': r.replace('\f'); break; 622 case 'b': r.replace('\b'); break; 623 case '\\': r.replace('\\'); break; 624 case '/': r.replace('/'); break; 625 case '\'': r.replace('\''); break; 626 case '"': r.replace('"'); break; 627 case 'u': { 628 String n = r.read(4); 629 try { 630 r.replace(Integer.parseInt(n, 16), 6); 631 } catch (@SuppressWarnings("unused") NumberFormatException e) { 632 throw new ParseException(this, "Invalid Unicode escape sequence in string."); 633 } 634 break; 635 } 636 default: 637 throw new ParseException(this, "Invalid escape sequence in string."); 638 } 639 // @formatter:on 640 isInEscape = false; 641 } else { 642 if (c == '\\') { 643 isInEscape = true; 644 r.delete(); 645 } else if (c == qc) { 646 s = r.getMarked(1, -1); 647 break; 648 } 649 } 650 } 651 skipCommentsAndSpace(r); 652 if (r.peek() == '+') 653 throw new ParseException(this, "String concatenation detected."); 654 return trim(s); 655 } 656 657 /* 658 * Doesn't actually parse anything, but when positioned at the beginning of comment, 659 * it will move the pointer to the last character in the comment. 660 */ 661 protected void skipComments(ParserReader r) throws ParseException, IOException { 662 int c = r.read(); 663 if (c == '*') { 664 while (c != -1) 665 if ((c = r.read()) == '*' && (c = r.read()) == '/') 666 return; 667 } else if (c == '/') { 668 while (c != -1) { 669 c = r.read(); 670 if (c == -1 || c == '\n') 671 return; 672 } 673 } 674 throw new ParseException(this, "Open ended comment."); 675 } 676 677 protected void skipCommentsAndSpace(ParserReader r) throws IOException, ParseException { 678 int c = 0; 679 while ((c = r.read()) != -1) { 680 if (! isWhitespace(c)) { 681 if (c == '/') 682 throw new ParseException(this, "Javascript comment detected."); 683 r.unread(); 684 return; 685 } 686 } 687 } 688 689 /* 690 * Doesn't actually parse anything, but moves the position beyond the construct "}" when 691 * the @Json(wrapperAttr) annotation is used on a class. 692 */ 693 private void skipWrapperAttrEnd(ParserReader r) throws ParseException, IOException { 694 int c = 0; 695 while ((c = r.read()) != -1) { 696 if (! isWhitespace(c)) { 697 if (c == '/') { 698 throw new ParseException(this, "Javascript comment detected."); 699 } else if (c == '}') { 700 return; 701 } else { 702 throw new ParseException(this, "Could not find '}' at the end of JSON wrapper object."); 703 } 704 } 705 } 706 } 707 708 /* 709 * Doesn't actually parse anything, but moves the position beyond the construct "{wrapperAttr:" when 710 * the @Json(wrapperAttr) annotation is used on a class. 711 */ 712 private void skipWrapperAttrStart(ParserReader r, String wrapperAttr) throws IOException, ParseException { 713 714 // S1: Looking for outer '{' 715 // S2: Looking for attrName start. 716 // S3: Found attrName end, looking for :. 717 // S4: Found :, looking for valStart: { [ " ' LITERAL. 718 719 var state = S1; 720 String currAttr = null; 721 int c = 0; 722 while (c != -1) { 723 c = r.read(); 724 if (state == S1) { 725 if (c == '{') 726 state = S2; 727 } else if (state == S2) { 728 if (isCommentOrWhitespace(c)) { 729 skipCommentsAndSpace(r.unread()); 730 } else { 731 currAttr = readFieldName(r.unread()); 732 if (! currAttr.equals(wrapperAttr)) 733 throw new ParseException(this, "Expected to find wrapper attribute '%s' but found attribute '%s'", wrapperAttr, currAttr); 734 state = S3; 735 } 736 } else if (state == S3) { // NOSONAR - State check necessary for state machine 737 if (c == ':') 738 state = S4; 739 } else if (state == S4) { 740 if (isCommentOrWhitespace(c)) { 741 skipCommentsAndSpace(r.unread()); 742 } else { 743 r.unread(); 744 return; 745 } 746 } 747 } 748 if (state == S1) 749 throw new ParseException(this, "Expected '{' at beginning of JSON object."); 750 else if (state == S2) 751 throw new ParseException(this, "Could not find attribute name on JSON object."); 752 else 753 throw new ParseException(this, "Could not find ':' following attribute name on JSON object."); 754 } 755 756 /* 757 * Call this method after you've finished a parsing a string to make sure that if there's any 758 * remainder in the input, that it consists only of whitespace and comments. 759 */ 760 private void validateEnd(ParserReader r) throws IOException, ParseException { 761 if (! isValidateEnd()) 762 return; 763 skipCommentsAndSpace(r); 764 int c = r.read(); 765 if (c != -1 && c != ';') // var x = {...}; expressions can end with a semicolon. 766 throw new ParseException(this, "Remainder after parse: '%s'.", (char)c); 767 } 768 769 @Override /* Overridden from ParserSession */ 770 protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException { 771 try (var r = pipe.getParserReader()) { 772 if (r == null) 773 return null; 774 T o = readAnything(type, r, getOuter(), null); 775 validateEnd(r); 776 return o; 777 } 778 } 779 780 /** 781 * Opens a low-level pull-parser cursor over a JSON document, bound to this session's live 782 * {@link BeanSession}/{@link MarshallingContext}. 783 * 784 * <p> 785 * The cursor is purely structural — object swaps and {@code @Schema} annotations are not 786 * applied at this layer. See {@link TokenReader} for the contract. 787 * 788 * <h5 class='section'>Builder properties honored:</h5> 789 * The structurally-applicable subset flows through to the cursor: <c>autoCloseStreams</c>, 790 * <c>unbuffered</c>, <c>trimStrings</c> (applied to {@link TokenType#VALUE_STRING} and 791 * {@link TokenType#FIELD_NAME} values), and the {@code ReaderParser} stream/file charsets. 792 * 793 * <h5 class='section'>Builder properties NOT honored:</h5> 794 * <c>validateEnd()</c> (the cursor leaves end-of-input checking to the caller), 795 * <c>listener(...)</c>/<c>debugOutputLines(...)</c>, <c>consumes(...)</c>, object swaps, and 796 * {@code @Schema} annotations (only applied on the POJO databind path). 797 * 798 * @param input The input. Accepts {@link Reader}, {@link CharSequence}, {@link InputStream}, 799 * or <code><jk>byte</jk>[]</code>. 800 * @return A new {@link JsonTokenReader}. 801 * @throws IOException If a problem occurred opening the underlying input. 802 */ 803 @SuppressWarnings({ 804 "java:S2095" // ParserPipe lifecycle is transferred to the returned JsonTokenReader, which closes it via its own close(); the caller owns the cursor via try-with-resources. 805 }) 806 @Override /* TokenReadable */ 807 public TokenReader readTokens(Object input) throws IOException { 808 var pipe = new ParserPipe( 809 input, 810 isDebug(), 811 false /* strict */, 812 isAutoCloseStreams(), 813 isUnbuffered(), 814 getStreamCharset()); 815 return new JsonTokenReader(pipe, new JsonTokenReader.Settings(isTrimStrings()), this); 816 } 817 818 /** 819 * Streaming array-element {@link RecordReader} backed by {@link JsonTokenReader}. 820 * Memory is O(1) in the array length — elements are bound on demand using this live session. 821 * 822 * @param input The input. 823 * @return A new element-streamed {@link RecordReader}. 824 * @throws IOException If a problem occurred opening the underlying input. 825 */ 826 @Override /* ArrayRecordReadable */ 827 public RecordReader readArrayRecords(Object input) throws IOException { 828 try { 829 return StreamingArrayRecord.reader(readTokens(input)); 830 } catch (ParseException e) { 831 throw new IOException(e); 832 } 833 } 834 835 @Override /* Overridden from ReaderParserSession */ 836 protected <E> Collection<E> doReadIntoCollection(ParserPipe pipe, Collection<E> c, Type elementType) throws IOException, ParseException, ExecutableException { 837 try (var r = pipe.getParserReader()) { 838 c = readIntoCollection2(r, c, getClassMeta(elementType == null ? Object.class : elementType), null); 839 validateEnd(r); 840 return c; 841 } 842 } 843 844 @Override /* Overridden from ReaderParserSession */ 845 protected <K,V> Map<K,V> doReadIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws IOException, ParseException, ExecutableException { 846 try (var r = pipe.getParserReader()) { 847 m = readIntoMap2(r, m, (ClassMeta<K>)getClassMeta(keyType == null ? Object.class : keyType), (ClassMeta<V>)getClassMeta(valueType == null ? Object.class : valueType), null); 848 validateEnd(r); 849 return m; 850 } 851 } 852 853 /** 854 * Returns the language-specific metadata on the specified class. 855 * 856 * @param cm The class to return the metadata on. 857 * @return The metadata. 858 */ 859 protected JsonClassMeta getJsonClassMeta(ClassMeta<?> cm) { 860 return ctx.getJsonClassMeta(cm); 861 } 862 863 /** 864 * Returns <jk>true</jk> if the specified character can start a number value. 865 * 866 * <p> 867 * JSON numbers start with a digit, <c>-</c>, or <c>.</c>. JSON5 subclasses also accept 868 * <c>+</c>, <c>I</c> (<c>Infinity</c>), and <c>N</c> (<c>NaN</c>). 869 * 870 * @param c The next non-whitespace character. 871 * @return <jk>true</jk> if a number lexeme should be read from this character. 872 */ 873 protected boolean isNumberStartChar(int c) { 874 return (c >= '0' && c <= '9') || c == '-' || c == '.'; 875 } 876 877 /** 878 * Returns <jk>true</jk> if the specified character is whitespace or '/'. 879 * 880 * @param cp The codepoint. 881 * @return <jk>true</jk> if the specified character is whitespace or '/'. 882 */ 883 protected boolean isCommentOrWhitespace(int cp) { 884 if (cp == '/') 885 return true; 886 return cp <= 0x20 && (cp == 0x09 || cp == 0x0A || cp == 0x0D || cp == 0x20); 887 } 888 889 /** 890 * Validate end. 891 * 892 * @see JsonParser.Builder#validateEnd() 893 * @return 894 * <jk>true</jk> if after parsing a POJO from the input, verifies that the remaining input in 895 * the stream consists of only comments or whitespace. 896 */ 897 protected boolean isValidateEnd() { return validateEnd; } 898 899 /** 900 * Returns <jk>true</jk> if the specified character is whitespace. 901 * 902 * <p> 903 * Only 0x20 (space), 0x09 (tab), 0x0A (line feed) and 0x0D (carriage return) are considered whitespace. 904 * 905 * @param cp The codepoint. 906 * @return <jk>true</jk> if the specified character is whitespace. 907 */ 908 protected boolean isWhitespace(int cp) { 909 return cp <= 0x20 && (cp == 0x09 || cp == 0x0A || cp == 0x0D || cp == 0x20); 910 } 911 912 /** 913 * Hook called when empty input is detected in {@code readAnything()}. 914 * 915 * @throws ParseException Always in strict mode. 916 */ 917 protected void onEmptyInput() throws ParseException { 918 throw new ParseException(this, "Empty input."); 919 } 920 921 /** 922 * Hook called when a missing value is detected in {@code readAnything()}. 923 * 924 * @throws ParseException Always in strict mode. 925 */ 926 protected void onMissingValue() throws ParseException { 927 throw new ParseException(this, "Missing value detected."); 928 } 929 930 /** 931 * Returns whether trailing commas are accepted before <c>]</c> / <c>}</c> in the databind path. 932 * 933 * <p> 934 * Default returns <jk>false</jk> (strict RFC-8259 JSON). {@code Json5ParserSession} overrides 935 * this to <jk>true</jk> for the JSON5 dialect. 936 * 937 * @return <jk>true</jk> if a trailing comma should be silently accepted. 938 */ 939 protected boolean allowsTrailingComma() { 940 return false; 941 } 942 943 /** 944 * Returns whether non-string types can be coerced to strings using {@code newInstanceFromString()}. 945 * 946 * @return Always {@code false} for standard JSON. 947 */ 948 protected boolean canCoerceNonStringToString() { 949 return false; 950 } 951 952 @Override /* Overridden from MarshallingSession */ 953 protected MarshalledMap newGenericMap() { 954 return new JsonMap(this); 955 } 956 957 @Override /* Overridden from MarshallingSession */ 958 protected MarshalledList newGenericList() { 959 return new JsonList(this); 960 } 961}