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