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.uon; 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.collections.FluentMap; 033import org.apache.juneau.commons.lang.*; 034import org.apache.juneau.commons.reflect.*; 035import org.apache.juneau.commons.utils.*; 036import org.apache.juneau.httppart.*; 037import org.apache.juneau.parser.*; 038import org.apache.juneau.swap.*; 039 040/** 041 * Session object that lives for the duration of a single use of {@link UonParser}. 042 * 043 * <h5 class='section'>Notes:</h5><ul> 044 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 045 * </ul> 046 * 047 * <h5 class='section'>See Also:</h5><ul> 048 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/UonBasics">UON Basics</a> 049 050 * </ul> 051 */ 052@SuppressWarnings({ "unchecked", "rawtypes", "resource" }) 053public class UonParserSession extends ReaderParserSession implements HttpPartParserSession { 054 /** 055 * Builder class. 056 */ 057 public static class Builder extends ReaderParserSession.Builder { 058 059 private boolean decoding; 060 private UonParser ctx; 061 062 /** 063 * Constructor 064 * 065 * @param ctx The context creating this session. 066 */ 067 protected Builder(UonParser ctx) { 068 super(ctx); 069 this.ctx = ctx; 070 decoding = ctx.decoding; 071 } 072 073 @Override /* Overridden from Builder */ 074 public <T> Builder apply(Class<T> type, Consumer<T> apply) { 075 super.apply(type, apply); 076 return this; 077 } 078 079 @Override 080 public UonParserSession build() { 081 return new UonParserSession(this); 082 } 083 084 @Override /* Overridden from Builder */ 085 public Builder debug(Boolean value) { 086 super.debug(value); 087 return this; 088 } 089 090 /** 091 * Overrides the decoding flag on the context for this session. 092 * 093 * @param value The new value for this setting. 094 * @return This object. 095 */ 096 public Builder decoding(boolean value) { 097 decoding = value; 098 return this; 099 } 100 101 @Override /* Overridden from Builder */ 102 public Builder fileCharset(Charset value) { 103 super.fileCharset(value); 104 return this; 105 } 106 107 @Override /* Overridden from Builder */ 108 public Builder javaMethod(Method value) { 109 super.javaMethod(value); 110 return this; 111 } 112 113 @Override /* Overridden from Builder */ 114 public Builder locale(Locale value) { 115 super.locale(value); 116 return this; 117 } 118 119 @Override /* Overridden from Builder */ 120 public Builder mediaType(MediaType value) { 121 super.mediaType(value); 122 return this; 123 } 124 125 @Override /* Overridden from Builder */ 126 public Builder mediaTypeDefault(MediaType value) { 127 super.mediaTypeDefault(value); 128 return this; 129 } 130 131 @Override /* Overridden from Builder */ 132 public Builder outer(Object value) { 133 super.outer(value); 134 return this; 135 } 136 137 @Override /* Overridden from Builder */ 138 public Builder properties(Map<String,Object> value) { 139 super.properties(value); 140 return this; 141 } 142 143 @Override /* Overridden from Builder */ 144 public Builder property(String key, Object value) { 145 super.property(key, value); 146 return this; 147 } 148 149 @Override /* Overridden from Builder */ 150 public Builder schema(HttpPartSchema value) { 151 super.schema(value); 152 return this; 153 } 154 155 @Override /* Overridden from Builder */ 156 public Builder schemaDefault(HttpPartSchema value) { 157 super.schemaDefault(value); 158 return this; 159 } 160 161 @Override /* Overridden from Builder */ 162 public Builder streamCharset(Charset value) { 163 super.streamCharset(value); 164 return this; 165 } 166 167 @Override /* Overridden from Builder */ 168 public Builder timeZone(TimeZone value) { 169 super.timeZone(value); 170 return this; 171 } 172 173 @Override /* Overridden from Builder */ 174 public Builder timeZoneDefault(TimeZone value) { 175 super.timeZoneDefault(value); 176 return this; 177 } 178 179 @Override /* Overridden from Builder */ 180 public Builder unmodifiable() { 181 super.unmodifiable(); 182 return this; 183 } 184 } 185 186 // Characters that need to be preceded with an escape character. 187 private static final AsciiSet escapedChars = AsciiSet.of("~'\u0001\u0002"); 188 189 private static final char AMP = '\u0001', EQ = '\u0002'; // Flags set in reader to denote & and = characters. 190 private static final AsciiSet endCharsParam = AsciiSet.of("" + AMP), endCharsNormal = AsciiSet.of(",)" + AMP); 191 192 /** 193 * Creates a new builder for this object. 194 * 195 * @param ctx The context creating this session. 196 * @return A new builder. 197 */ 198 public static Builder create(UonParser ctx) { 199 return new Builder(ctx); 200 } 201 202 /* 203 * Returns true if the next character in the stream is preceded by an escape '~' character. 204 */ 205 private static final boolean isInEscape(int c, ParserReader r, boolean prevIsInEscape) throws IOException { 206 if (c == '~' && ! prevIsInEscape) { 207 c = r.peek(); 208 if (escapedChars.contains(c)) { 209 r.delete(); 210 return true; 211 } 212 } 213 return false; 214 } 215 216 private static void skipSpace(ParserReader r) throws IOException { 217 int c = 0; 218 while ((c = r.read()) != -1) { 219 if (c <= 2 || ! Character.isWhitespace(c)) { 220 r.unread(); 221 return; 222 } 223 } 224 } 225 226 private final UonParser ctx; 227 228 private final boolean decoding; 229 230 /** 231 * Constructor. 232 * 233 * @param builder The builder for this object. 234 */ 235 protected UonParserSession(Builder builder) { 236 super(builder); 237 ctx = builder.ctx; 238 decoding = builder.decoding; 239 } 240 241 /** 242 * Creates a {@link UonReader} from the specified parser pipe. 243 * 244 * @param pipe The parser input. 245 * @param decodeChars Whether the reader should automatically decode URL-encoded characters. 246 * @return A new {@link UonReader} object. 247 * @throws IOException Thrown by underlying stream. 248 */ 249 public final static UonReader getUonReader(ParserPipe pipe, boolean decodeChars) throws IOException { 250 var r = pipe.getReader(); 251 if (r instanceof UonReader r2) 252 return r2; 253 return new UonReader(pipe, decodeChars); 254 } 255 256 @Override /* Overridden from HttpPartParser */ 257 public <T> T parse(HttpPartType partType, HttpPartSchema schema, String in, ClassMeta<T> toType) throws ParseException, SchemaValidationException { 258 if (in == null) 259 return null; 260 if (toType.isString() && ne(in)) { 261 // Shortcut - If we're returning a string and the value doesn't start with "'" or is "null", then 262 // just return the string since it's a plain value. 263 // This allows us to bypass the creation of a UonParserSession object. 264 char x = firstNonWhitespaceChar(in); 265 if (x != '\'' && x != 'n' && in.indexOf('~') == -1) 266 return (T)in; 267 if (x == 'n' && "null".equals(in)) 268 return null; 269 } 270 try (var pipe = createPipe(in)) { 271 try (var r = getUonReader(pipe, false)) { 272 return parseAnything(toType, r, null, true, null); 273 } 274 } catch (ParseException e) { 275 throw e; 276 } catch (Exception e) { 277 throw new ParseException(e); 278 } 279 } 280 281 /** 282 * Workhorse method. 283 * 284 * @param <T> The class type being parsed, or <jk>null</jk> if unknown. 285 * @param eType The class type being parsed, or <jk>null</jk> if unknown. 286 * @param r The reader being parsed. 287 * @param outer The outer object (for constructing nested inner classes). 288 * @param isUrlParamValue 289 * If <jk>true</jk>, then we're parsing a top-level URL-encoded value which is treated a bit different than the 290 * default case. 291 * @param pMeta The current bean property being parsed. 292 * @return The parsed object. 293 * @throws IOException Thrown by underlying stream. 294 * @throws ParseException Malformed input encountered. 295 * @throws ExecutableException Exception occurred on invoked constructor/method/field. 296 */ 297 public <T> T parseAnything(ClassMeta<?> eType, UonReader r, Object outer, boolean isUrlParamValue, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { 298 299 if (eType == null) 300 eType = object(); 301 var swap = (ObjectSwap<T,Object>)eType.getSwap(this); 302 var builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this); 303 var sType = (ClassMeta<?>)null; 304 if (nn(builder)) 305 sType = builder.getBuilderClassMeta(this); 306 else if (nn(swap)) 307 sType = swap.getSwapClassMeta(this); 308 else 309 sType = eType; 310 311 if (sType.isOptional()) 312 return (T)opt(parseAnything(eType.getElementType(), r, outer, isUrlParamValue, pMeta)); 313 314 setCurrentClass(sType); 315 316 var o = (Object)null; 317 318 int c = r.peekSkipWs(); 319 320 if (c == -1 || c == AMP) { 321 // If parameter is blank and it's an array or collection, return an empty list. 322 if (sType.isCollectionOrArray()) 323 o = sType.newInstance(); 324 else if (sType.isString() || sType.isObject()) 325 o = ""; 326 else if (sType.isPrimitive()) 327 o = sType.getPrimitiveDefault(); 328 // Otherwise, leave null. 329 } else if (sType.isVoid()) { 330 var s = parseString(r, isUrlParamValue); 331 if (nn(s)) 332 throw new ParseException(this, "Expected ''null'' for void value, but was ''{0}''.", s); 333 } else if (sType.isObject()) { 334 if (c == '(') { 335 var m = new JsonMap(this); 336 parseIntoMap(r, m, string(), object(), pMeta); 337 o = cast(m, pMeta, eType); 338 } else if (c == '@') { 339 Collection l = new JsonList(this); 340 o = parseIntoCollection(r, l, sType, isUrlParamValue, pMeta); 341 } else { 342 var s = parseString(r, isUrlParamValue); 343 if (c != '\'') { 344 if ("true".equals(s) || "false".equals(s)) 345 o = bool(s); 346 else if (! "null".equals(s)) { 347 if (isNumeric(s)) 348 o = StringUtils.parseNumber(s, Number.class); 349 else 350 o = s; 351 } 352 } else { 353 o = s; 354 } 355 } 356 } else if (sType.isBoolean()) { 357 o = parseBoolean(r); 358 } else if (sType.isCharSequence()) { 359 o = parseString(r, isUrlParamValue); 360 } else if (sType.isChar()) { 361 o = parseCharacter(parseString(r, isUrlParamValue)); 362 } else if (sType.isNumber()) { 363 o = parseNumber(r, (Class<? extends Number>)sType.inner()); 364 } else if (sType.isMap()) { 365 var m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType)); 366 o = parseIntoMap(r, m, sType.getKeyType(), sType.getValueType(), pMeta); 367 } else if (sType.isCollection()) { 368 if (c == '(') { 369 var m = new JsonMap(this); 370 parseIntoMap(r, m, string(), object(), pMeta); 371 // Handle case where it's a collection, but serialized as a map with a _type or _value key. 372 if (m.containsKey(getBeanTypePropertyName(sType))) 373 o = cast(m, pMeta, eType); 374 // Handle case where it's a collection, but only a single value was specified. 375 else { 376 var l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance(outer) : new JsonList(this)); 377 l.add(m.cast(sType.getElementType())); 378 o = l; 379 } 380 } else { 381 var l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance(outer) : new JsonList(this)); 382 o = parseIntoCollection(r, l, sType, isUrlParamValue, pMeta); 383 } 384 } else if (nn(builder)) { 385 var m = toBeanMap(builder.create(this, eType)); 386 m = parseIntoBeanMap(r, m); 387 o = m == null ? null : builder.build(this, m.getBean(), eType); 388 } else if (sType.canCreateNewBean(outer)) { 389 var m = newBeanMap(outer, sType.inner()); 390 m = parseIntoBeanMap(r, m); 391 o = m == null ? null : m.getBean(); 392 } else if (sType.canCreateNewInstanceFromString(outer)) { 393 var s = parseString(r, isUrlParamValue); 394 if (nn(s)) 395 o = sType.newInstanceFromString(outer, s); 396 } else if (sType.isArray() || sType.isArgs()) { 397 if (c == '(') { 398 var m = new JsonMap(this); 399 parseIntoMap(r, m, string(), object(), pMeta); 400 // Handle case where it's an array, but serialized as a map with a _type or _value key. 401 if (m.containsKey(getBeanTypePropertyName(sType))) 402 o = cast(m, pMeta, eType); 403 // Handle case where it's an array, but only a single value was specified. 404 else { 405 var l = listOfSize(1); 406 l.add(m.cast(sType.getElementType())); 407 o = toArray(sType, l); 408 } 409 } else { 410 var l = (ArrayList)parseIntoCollection(r, list(), sType, isUrlParamValue, pMeta); 411 o = toArray(sType, l); 412 } 413 } else if (c == '(') { 414 // It could be a non-bean with _type attribute. 415 var m = new JsonMap(this); 416 parseIntoMap(r, m, string(), object(), pMeta); 417 if (m.containsKey(getBeanTypePropertyName(sType))) 418 o = cast(m, pMeta, eType); 419 else if (nn(sType.getProxyInvocationHandler())) 420 o = newBeanMap(outer, sType.inner()).load(m).getBean(); 421 else 422 throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", cn(sType), sType.getNotABeanReason()); 423 } else if (c == 'n') { 424 r.read(); // NOSONAR - Intentional. 425 parseNull(r); 426 } else { 427 throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", cn(sType), sType.getNotABeanReason()); 428 } 429 430 if (o == null && sType.isPrimitive()) 431 o = sType.getPrimitiveDefault(); 432 if (nn(swap) && nn(o)) 433 o = unswap(swap, o, eType); 434 435 if (nn(outer)) 436 setParent(eType, o, outer); 437 438 return (T)o; 439 } 440 441 private Boolean parseBoolean(UonReader r) throws IOException, ParseException { 442 var s = parseString(r, false); 443 if (s == null || s.equals("null")) 444 return null; 445 if (eqic(s, "true")) 446 return true; 447 if (eqic(s, "false")) 448 return false; 449 throw new ParseException(this, "Unrecognized syntax for boolean. ''{0}''.", s); 450 } 451 452 private <T> BeanMap<T> parseIntoBeanMap(UonReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException { 453 454 int c = r.readSkipWs(); 455 if (c == -1 || c == AMP) 456 return null; 457 if (c == 'n') 458 return (BeanMap<T>)parseNull(r); 459 if (c != '(') 460 throw new ParseException(this, "Expected '(' at beginning of object."); 461 462 // S1: Looking for attrName start. 463 // S2: Found attrName end, looking for =. 464 // S3: Found =, looking for valStart. 465 // S4: Looking for , or } 466 boolean isInEscape = false; 467 468 var state = S1; 469 var currAttr = ""; 470 mark(); 471 try { 472 while (c != -1 && c != AMP) { 473 c = r.read(); 474 if (! isInEscape) { 475 if (state == S1) { 476 if (c == ')' || c == -1 || c == AMP) { 477 return m; 478 } 479 if (Character.isWhitespace(c)) 480 skipSpace(r); 481 else { 482 r.unread(); 483 mark(); 484 currAttr = parseAttrName(r, decoding); 485 if (currAttr == null) { // Value was '%00' 486 return null; 487 } 488 state = S2; 489 } 490 } else if (state == S2) { 491 if (c == EQ || c == '=') 492 state = S3; 493 else if (c == -1 || c == ',' || c == ')' || c == AMP) { 494 m.put(currAttr, null); 495 if (c == ')' || c == -1 || c == AMP) { 496 return m; 497 } 498 state = S1; 499 } 500 } else if (state == S3) { 501 if (c == -1 || c == ',' || c == ')' || c == AMP) { 502 if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) { 503 var pMeta = m.getPropertyMeta(currAttr); 504 if (pMeta == null) { 505 onUnknownProperty(currAttr, m, null); 506 unmark(); 507 } else { 508 unmark(); 509 var value = convertToType("", pMeta.getClassMeta()); 510 try { 511 pMeta.set(m, currAttr, value); 512 } catch (BeanRuntimeException e) { 513 onBeanSetterException(pMeta, e); 514 throw e; 515 } 516 } 517 } 518 if (c == -1 || c == ')' || c == AMP) 519 return m; 520 state = S1; 521 } else { 522 if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) { 523 var pMeta = m.getPropertyMeta(currAttr); 524 if (pMeta == null) { 525 onUnknownProperty(currAttr, m, parseAnything(object(), r.unread(), m.getBean(false), false, null)); 526 unmark(); 527 } else { 528 unmark(); 529 setCurrentProperty(pMeta); 530 var cm = pMeta.getClassMeta(); 531 var value = parseAnything(cm, r.unread(), m.getBean(false), false, pMeta); 532 setName(cm, value, currAttr); 533 try { 534 pMeta.set(m, currAttr, value); 535 } catch (BeanRuntimeException e) { 536 onBeanSetterException(pMeta, e); 537 throw e; 538 } 539 setCurrentProperty(null); 540 } 541 } 542 state = S4; 543 } 544 } else if (state == S4) { 545 if (c == ',') 546 state = S1; 547 else if (c == ')' || c == -1 || c == AMP) { 548 return m; 549 } 550 } 551 } 552 isInEscape = isInEscape(c, r, isInEscape); 553 } 554 if (state == S1) 555 throw new ParseException(this, "Could not find attribute name on object."); 556 if (state == S2) 557 throw new ParseException(this, "Could not find '=' following attribute name on object."); 558 if (state == S3) 559 throw new ParseException(this, "Could not find value following '=' on object."); 560 if (state == S4) 561 throw new ParseException(this, "Could not find ')' marking end of object."); 562 } finally { 563 unmark(); 564 } 565 566 return null; // Unreachable. 567 } 568 569 private <E> Collection<E> parseIntoCollection(UonReader r, Collection<E> l, ClassMeta<E> type, boolean isUrlParamValue, BeanPropertyMeta pMeta) 570 throws IOException, ParseException, ExecutableException { 571 572 int c = r.readSkipWs(); 573 if (c == -1 || c == AMP) 574 return null; 575 if (c == 'n') 576 return (Collection<E>)parseNull(r); 577 578 int argIndex = 0; 579 580 // If we're parsing a top-level parameter, we're allowed to have comma-delimited lists outside parenthesis (e.g. "&foo=1,2,3&bar=a,b,c") 581 // This is not allowed at lower levels since we use comma's as end delimiters. 582 boolean isInParens = (c == '@'); 583 if (! isInParens) { 584 if (isUrlParamValue) 585 r.unread(); 586 else 587 throw new ParseException(this, "Could not find '(' marking beginning of collection."); 588 } else { 589 r.read(); // NOSONAR - Intentional, we're skipping the '@' character. 590 } 591 592 if (isInParens) { 593 // S1: Looking for starting of first entry. 594 // S2: Looking for starting of subsequent entries. 595 // S3: Looking for , or ) after first entry. 596 597 var state = S1; 598 while (c != -1 && c != AMP) { 599 c = r.read(); 600 if (state == S1 || state == S2) { 601 if (c == ')') { 602 if (state == S2) { 603 l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, false, pMeta)); 604 r.read(); // NOSONAR - Intentional, we're skipping the ')' character. 605 } 606 return l; 607 } else if (Character.isWhitespace(c)) { 608 skipSpace(r); 609 } else { 610 l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, false, pMeta)); 611 state = S3; 612 } 613 } else if (state == S3) { 614 if (c == ',') { 615 state = S2; 616 } else if (c == ')') { 617 return l; 618 } 619 } 620 } 621 if (state == S1 || state == S2) 622 throw new ParseException(this, "Could not find start of entry in array."); 623 if (state == S3) 624 throw new ParseException(this, "Could not find end of entry in array."); 625 626 } else { 627 // S1: Looking for starting of entry. 628 // S2: Looking for , or & or END after first entry. 629 630 var state = S1; 631 while (c != -1 && c != AMP) { 632 c = r.read(); 633 if (state == S1) { 634 if (Character.isWhitespace(c)) { 635 skipSpace(r); 636 } else { 637 l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, false, pMeta)); 638 state = S2; 639 } 640 } else if (state == S2) { 641 if (c == ',') { 642 state = S1; 643 } else if (Character.isWhitespace(c)) { 644 skipSpace(r); 645 } else if (c == AMP || c == -1) { 646 r.unread(); 647 return l; 648 } 649 } 650 } 651 } 652 653 return null; // Unreachable. 654 } 655 656 private <K,V> Map<K,V> parseIntoMap(UonReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { 657 658 if (keyType == null) 659 keyType = (ClassMeta<K>)string(); 660 661 int c = r.read(); 662 if (c == -1 || c == AMP) 663 return null; 664 if (c == 'n') 665 return (Map<K,V>)parseNull(r); 666 if (c != '(') 667 throw new ParseException(this, "Expected '(' at beginning of object."); 668 669 // S1: Looking for attrName start. 670 // S2: Found attrName end, looking for =. 671 // S3: Found =, looking for valStart. 672 // S4: Looking for , or ) 673 674 boolean isInEscape = false; 675 676 var state = S1; 677 var currAttr = (K)null; 678 while (c != -1 && c != AMP) { 679 c = r.read(); 680 if (! isInEscape) { 681 if (state == S1) { 682 if (c == ')') 683 return m; 684 if (Character.isWhitespace(c)) 685 skipSpace(r); 686 else { 687 r.unread(); 688 var attr = parseAttr(r, decoding); 689 currAttr = attr == null ? null : convertAttrToType(m, trim(attr.toString()), keyType); 690 state = S2; 691 c = 0; // Avoid isInEscape if c was '\' 692 } 693 } else if (state == S2) { 694 if (c == EQ || c == '=') 695 state = S3; 696 else if (c == -1 || c == ',' || c == ')' || c == AMP) { 697 if (currAttr == null) { 698 // Value was '%00' 699 r.unread(); 700 return null; 701 } 702 m.put(currAttr, null); 703 if (c == ')' || c == -1 || c == AMP) 704 return m; 705 state = S1; 706 } 707 } else if (state == S3) { 708 if (c == -1 || c == ',' || c == ')' || c == AMP) { 709 V value = convertAttrToType(m, "", valueType); 710 m.put(currAttr, value); 711 if (c == -1 || c == ')' || c == AMP) 712 return m; 713 state = S1; 714 } else { 715 V value = parseAnything(valueType, r.unread(), m, false, pMeta); 716 setName(valueType, value, currAttr); 717 m.put(currAttr, value); 718 state = S4; 719 c = 0; // Avoid isInEscape if c was '\' 720 } 721 } else if (state == S4) { 722 if (c == ',') 723 state = S1; 724 else if (c == ')' || c == -1 || c == AMP) { 725 return m; 726 } 727 } 728 } 729 isInEscape = isInEscape(c, r, isInEscape); 730 } 731 if (state == S1) 732 throw new ParseException(this, "Could not find attribute name on object."); 733 if (state == S2) 734 throw new ParseException(this, "Could not find '=' following attribute name on object."); 735 if (state == S3) 736 throw new ParseException(this, "Dangling '=' found in object entry"); 737 if (state == S4) 738 throw new ParseException(this, "Could not find ')' marking end of object."); 739 740 return null; // Unreachable. 741 } 742 743 private Object parseNull(UonReader r) throws IOException, ParseException { 744 var s = parseString(r, false); 745 if ("ull".equals(s)) 746 return null; 747 throw new ParseException(this, "Unexpected character sequence: ''{0}''", s); 748 } 749 750 private Number parseNumber(UonReader r, Class<? extends Number> c) throws IOException, ParseException { 751 var s = parseString(r, false); 752 if (s == null) 753 return null; 754 return StringUtils.parseNumber(s, c); 755 } 756 757 /* 758 * Parses a string of the form "'foo'" 759 * All whitespace within parenthesis are preserved. 760 */ 761 private String parsePString(UonReader r) throws IOException, ParseException { 762 763 r.read(); // Skip first quote, NOSONAR - Intentional. 764 r.mark(); 765 int c = 0; 766 767 boolean isInEscape = false; 768 while (c != -1) { 769 c = r.read(); 770 if (! isInEscape) { 771 if (c == '\'') 772 return trim(r.getMarked(0, -1)); 773 } 774 if (c == EQ) 775 r.replace('='); 776 isInEscape = isInEscape(c, r, isInEscape); 777 } 778 throw new ParseException(this, "Unmatched parenthesis"); 779 } 780 781 /* 782 * Call this method after you've finished a parsing a string to make sure that if there's any 783 * remainder in the input, that it consists only of whitespace and comments. 784 */ 785 private void validateEnd(UonReader r) throws IOException, ParseException { 786 if (! isValidateEnd()) 787 return; 788 while (true) { 789 var c = r.read(); 790 if (c == -1) 791 return; 792 if (! Character.isWhitespace(c)) 793 throw new ParseException(this, "Remainder after parse: ''{0}''.", (char)c); 794 } 795 } 796 797 @Override /* Overridden from ParserSession */ 798 protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException { 799 try (var r = getUonReader(pipe, decoding)) { 800 T o = parseAnything(type, r, getOuter(), true, null); 801 validateEnd(r); 802 return o; 803 } 804 } 805 806 @Override /* Overridden from ReaderParserSession */ 807 protected <E> Collection<E> doParseIntoCollection(ParserPipe pipe, Collection<E> c, Type elementType) throws Exception { 808 try (var r = getUonReader(pipe, decoding)) { 809 c = parseIntoCollection(r, c, (ClassMeta<E>)getClassMeta(elementType), false, null); 810 validateEnd(r); 811 return c; 812 } 813 } 814 815 @Override /* Overridden from ReaderParserSession */ 816 protected <K,V> Map<K,V> doParseIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws Exception { 817 try (var r = getUonReader(pipe, decoding)) { 818 m = parseIntoMap(r, m, (ClassMeta<K>)getClassMeta(keyType), (ClassMeta<V>)getClassMeta(valueType), null); 819 validateEnd(r); 820 return m; 821 } 822 } 823 824 /** 825 * Decode <js>"%xx"</js> sequences. 826 * 827 * @see UonParser.Builder#decoding() 828 * @return 829 * <jk>true</jk> if URI encoded characters should be decoded, <jk>false</jk> if they've already been decoded 830 * before being passed to this parser. 831 */ 832 protected final boolean isDecoding() { return decoding; } 833 834 /** 835 * Validate end. 836 * 837 * @see UonParser.Builder#validateEnd() 838 * @return 839 * <jk>true</jk> if after parsing a POJO from the input, verifies that the remaining input in 840 * the stream consists of only comments or whitespace. 841 */ 842 protected final boolean isValidateEnd() { return ctx.isValidateEnd(); } 843 844 /** 845 * Convenience method for parsing an attribute from the specified parser. 846 * 847 * @param r The reader. 848 * @param encoded Whether the attribute is encoded. 849 * @return The parsed object 850 * @throws IOException Exception thrown by underlying stream. 851 * @throws ParseException Attribute was malformed. 852 */ 853 protected final Object parseAttr(UonReader r, boolean encoded) throws IOException, ParseException { 854 var attr = parseAttrName(r, encoded); 855 return attr; 856 } 857 858 /** 859 * Parses an attribute name from the specified reader. 860 * 861 * @param r The reader. 862 * @param encoded Whether the attribute is encoded. 863 * @return The parsed attribute name. 864 * @throws IOException Exception thrown by underlying stream. 865 * @throws ParseException Attribute name was malformed. 866 */ 867 protected final String parseAttrName(UonReader r, boolean encoded) throws IOException, ParseException { 868 869 // If string is of form 'xxx', we're looking for ' at the end. 870 // Otherwise, we're looking for '&' or '=' or WS or -1 denoting the end of this string. 871 872 int c = r.peekSkipWs(); 873 if (c == '\'') 874 return parsePString(r); 875 876 r.mark(); 877 boolean isInEscape = false; 878 if (encoded) { 879 while (c != -1) { 880 c = r.read(); 881 if (! isInEscape) { 882 if (c == AMP || c == EQ || c == -1 || Character.isWhitespace(c)) { 883 if (c != -1) 884 r.unread(); 885 var s = r.getMarked(); 886 return ("null".equals(s) ? null : s); 887 } 888 } else if (c == AMP) 889 r.replace('&'); 890 else if (c == EQ) 891 r.replace('='); 892 isInEscape = isInEscape(c, r, isInEscape); 893 } 894 } else { 895 while (c != -1) { 896 c = r.read(); 897 if (! isInEscape) { 898 if (c == '=' || c == -1 || Character.isWhitespace(c)) { 899 if (c != -1) 900 r.unread(); 901 var s = r.getMarked(); 902 return ("null".equals(s) ? null : trim(s)); 903 } 904 } 905 isInEscape = isInEscape(c, r, isInEscape); 906 } 907 } 908 909 // We should never get here. 910 throw new ParseException(this, "Unexpected condition."); 911 } 912 913 /** 914 * Parses a string value from the specified reader. 915 * 916 * @param r The input reader. 917 * @param isUrlParamValue Whether this is a URL parameter. 918 * @return The parsed string. 919 * @throws IOException Exception thrown by underlying stream. 920 * @throws ParseException Malformed input found. 921 */ 922 protected final String parseString(UonReader r, boolean isUrlParamValue) throws IOException, ParseException { 923 924 // If string is of form 'xxx', we're looking for ' at the end. 925 // Otherwise, we're looking for ',' or ')' or -1 denoting the end of this string. 926 927 int c = r.peekSkipWs(); 928 if (c == '\'') 929 return parsePString(r); 930 931 r.mark(); 932 boolean isInEscape = false; 933 var s = (String)null; 934 var endChars = (isUrlParamValue ? endCharsParam : endCharsNormal); 935 while (c != -1) { 936 c = r.read(); 937 if (! isInEscape) { 938 // If this is a URL parameter value, we're looking for: & 939 // If not, we're looking for: &,) 940 if (endChars.contains(c)) { 941 r.unread(); 942 c = -1; 943 } 944 } 945 if (c == -1) 946 s = r.getMarked(); 947 else if (c == EQ) 948 r.replace('='); 949 else if (Character.isWhitespace(c) && ! isUrlParamValue) { 950 s = r.getMarked(0, -1); 951 skipSpace(r); 952 c = -1; 953 } 954 isInEscape = isInEscape(c, r, isInEscape); 955 } 956 957 if (isUrlParamValue) 958 s = StringUtils.trim(s); 959 960 return ("null".equals(s) ? null : trim(s)); 961 } 962 963 @Override /* Overridden from ReaderParserSession */ 964 protected FluentMap<String,Object> properties() { 965 return super.properties() 966 .a("decoding", decoding); 967 } 968}