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