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