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