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