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