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 setCurrentClass(sType); 189 190 Object o = null; 191 192 int c = r.peekSkipWs(); 193 194 if (c == -1 || c == AMP) { 195 // If parameter is blank and it's an array or collection, return an empty list. 196 if (sType.isCollectionOrArray()) 197 o = sType.newInstance(); 198 else if (sType.isString() || sType.isObject()) 199 o = ""; 200 else if (sType.isPrimitive()) 201 o = sType.getPrimitiveDefault(); 202 // Otherwise, leave null. 203 } else if (sType.isVoid()) { 204 String s = parseString(r, isUrlParamValue); 205 if (s != null) 206 throw new ParseException(this, "Expected ''null'' for void value, but was ''{0}''.", s); 207 } else if (sType.isObject()) { 208 if (c == '(') { 209 ObjectMap m = new ObjectMap(this); 210 parseIntoMap(r, m, string(), object(), pMeta); 211 o = cast(m, pMeta, eType); 212 } else if (c == '@') { 213 Collection l = new ObjectList(this); 214 o = parseIntoCollection(r, l, sType, isUrlParamValue, pMeta); 215 } else { 216 String s = parseString(r, isUrlParamValue); 217 if (c != '\'') { 218 if ("true".equals(s) || "false".equals(s)) 219 o = Boolean.valueOf(s); 220 else if (! "null".equals(s)) { 221 if (isNumeric(s)) 222 o = StringUtils.parseNumber(s, Number.class); 223 else 224 o = s; 225 } 226 } else { 227 o = s; 228 } 229 } 230 } else if (sType.isBoolean()) { 231 o = parseBoolean(r); 232 } else if (sType.isCharSequence()) { 233 o = parseString(r, isUrlParamValue); 234 } else if (sType.isChar()) { 235 o = parseCharacter(parseString(r, isUrlParamValue)); 236 } else if (sType.isNumber()) { 237 o = parseNumber(r, (Class<? extends Number>)sType.getInnerClass()); 238 } else if (sType.isMap()) { 239 Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : new ObjectMap(this)); 240 o = parseIntoMap(r, m, sType.getKeyType(), sType.getValueType(), pMeta); 241 } else if (sType.isCollection()) { 242 if (c == '(') { 243 ObjectMap m = new ObjectMap(this); 244 parseIntoMap(r, m, string(), object(), pMeta); 245 // Handle case where it's a collection, but serialized as a map with a _type or _value key. 246 if (m.containsKey(getBeanTypePropertyName(sType))) 247 o = cast(m, pMeta, eType); 248 // Handle case where it's a collection, but only a single value was specified. 249 else { 250 Collection l = ( 251 sType.canCreateNewInstance(outer) 252 ? (Collection)sType.newInstance(outer) 253 : new ObjectList(this) 254 ); 255 l.add(m.cast(sType.getElementType())); 256 o = l; 257 } 258 } else { 259 Collection l = ( 260 sType.canCreateNewInstance(outer) 261 ? (Collection)sType.newInstance(outer) 262 : new ObjectList(this) 263 ); 264 o = parseIntoCollection(r, l, sType, isUrlParamValue, pMeta); 265 } 266 } else if (builder != null) { 267 BeanMap m = toBeanMap(builder.create(this, eType)); 268 m = parseIntoBeanMap(r, m); 269 o = m == null ? null : builder.build(this, m.getBean(), eType); 270 } else if (sType.canCreateNewBean(outer)) { 271 BeanMap m = newBeanMap(outer, sType.getInnerClass()); 272 m = parseIntoBeanMap(r, m); 273 o = m == null ? null : m.getBean(); 274 } else if (sType.canCreateNewInstanceFromString(outer)) { 275 String s = parseString(r, isUrlParamValue); 276 if (s != null) 277 o = sType.newInstanceFromString(outer, s); 278 } else if (sType.isArray() || sType.isArgs()) { 279 if (c == '(') { 280 ObjectMap m = new ObjectMap(this); 281 parseIntoMap(r, m, string(), object(), pMeta); 282 // Handle case where it's an array, but serialized as a map with a _type or _value key. 283 if (m.containsKey(getBeanTypePropertyName(sType))) 284 o = cast(m, pMeta, eType); 285 // Handle case where it's an array, but only a single value was specified. 286 else { 287 ArrayList l = new ArrayList(1); 288 l.add(m.cast(sType.getElementType())); 289 o = toArray(sType, l); 290 } 291 } else { 292 ArrayList l = (ArrayList)parseIntoCollection(r, new ArrayList(), sType, isUrlParamValue, pMeta); 293 o = toArray(sType, l); 294 } 295 } else if (c == '(') { 296 // It could be a non-bean with _type attribute. 297 ObjectMap m = new ObjectMap(this); 298 parseIntoMap(r, m, string(), object(), pMeta); 299 if (m.containsKey(getBeanTypePropertyName(sType))) 300 o = cast(m, pMeta, eType); 301 else 302 throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", 303 sType.getInnerClass().getName(), sType.getNotABeanReason()); 304 } else if (c == 'n') { 305 r.read(); 306 parseNull(r); 307 } else { 308 throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", 309 sType.getInnerClass().getName(), sType.getNotABeanReason()); 310 } 311 312 if (o == null && sType.isPrimitive()) 313 o = sType.getPrimitiveDefault(); 314 if (swap != null && o != null) 315 o = unswap(swap, o, eType); 316 317 if (outer != null) 318 setParent(eType, o, outer); 319 320 return (T)o; 321 } 322 323 private <K,V> Map<K,V> parseIntoMap(UonReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, 324 BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { 325 326 if (keyType == null) 327 keyType = (ClassMeta<K>)string(); 328 329 int c = r.read(); 330 if (c == -1 || c == AMP) 331 return null; 332 if (c == 'n') 333 return (Map<K,V>)parseNull(r); 334 if (c != '(') 335 throw new ParseException(this, "Expected '(' at beginning of object."); 336 337 final int S1=1; // Looking for attrName start. 338 final int S2=2; // Found attrName end, looking for =. 339 final int S3=3; // Found =, looking for valStart. 340 final int S4=4; // Looking for , or ) 341 boolean isInEscape = false; 342 343 int state = S1; 344 K currAttr = null; 345 while (c != -1 && c != AMP) { 346 c = r.read(); 347 if (! isInEscape) { 348 if (state == S1) { 349 if (c == ')') 350 return m; 351 if (Character.isWhitespace(c)) 352 skipSpace(r); 353 else { 354 r.unread(); 355 Object attr = parseAttr(r, decoding); 356 currAttr = attr == null ? null : convertAttrToType(m, trim(attr.toString()), keyType); 357 state = S2; 358 c = 0; // Avoid isInEscape if c was '\' 359 } 360 } else if (state == S2) { 361 if (c == EQ || c == '=') 362 state = S3; 363 else if (c == -1 || c == ',' || c == ')' || c == AMP) { 364 if (currAttr == null) { 365 // Value was '%00' 366 r.unread(); 367 return null; 368 } 369 m.put(currAttr, null); 370 if (c == ')' || c == -1 || c == AMP) 371 return m; 372 state = S1; 373 } 374 } else if (state == S3) { 375 if (c == -1 || c == ',' || c == ')' || c == AMP) { 376 V value = convertAttrToType(m, "", valueType); 377 m.put(currAttr, value); 378 if (c == -1 || c == ')' || c == AMP) 379 return m; 380 state = S1; 381 } else { 382 V value = parseAnything(valueType, r.unread(), m, false, pMeta); 383 setName(valueType, value, currAttr); 384 m.put(currAttr, value); 385 state = S4; 386 c = 0; // Avoid isInEscape if c was '\' 387 } 388 } else if (state == S4) { 389 if (c == ',') 390 state = S1; 391 else if (c == ')' || c == -1 || c == AMP) { 392 return m; 393 } 394 } 395 } 396 isInEscape = isInEscape(c, r, isInEscape); 397 } 398 if (state == S1) 399 throw new ParseException(this, "Could not find attribute name on object."); 400 if (state == S2) 401 throw new ParseException(this, "Could not find '=' following attribute name on object."); 402 if (state == S3) 403 throw new ParseException(this, "Dangling '=' found in object entry"); 404 if (state == S4) 405 throw new ParseException(this, "Could not find ')' marking end of object."); 406 407 return null; // Unreachable. 408 } 409 410 private <E> Collection<E> parseIntoCollection(UonReader r, Collection<E> l, ClassMeta<E> type, boolean isUrlParamValue, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { 411 412 int c = r.readSkipWs(); 413 if (c == -1 || c == AMP) 414 return null; 415 if (c == 'n') 416 return (Collection<E>)parseNull(r); 417 418 int argIndex = 0; 419 420 // 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") 421 // This is not allowed at lower levels since we use comma's as end delimiters. 422 boolean isInParens = (c == '@'); 423 if (! isInParens) { 424 if (isUrlParamValue) 425 r.unread(); 426 else 427 throw new ParseException(this, "Could not find '(' marking beginning of collection."); 428 } else { 429 r.read(); 430 } 431 432 if (isInParens) { 433 final int S1=1; // Looking for starting of first entry. 434 final int S2=2; // Looking for starting of subsequent entries. 435 final int S3=3; // Looking for , or ) after first entry. 436 437 int state = S1; 438 while (c != -1 && c != AMP) { 439 c = r.read(); 440 if (state == S1 || state == S2) { 441 if (c == ')') { 442 if (state == S2) { 443 l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), 444 r.unread(), l, false, pMeta)); 445 r.read(); 446 } 447 return l; 448 } else if (Character.isWhitespace(c)) { 449 skipSpace(r); 450 } else { 451 l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), 452 r.unread(), l, false, pMeta)); 453 state = S3; 454 } 455 } else if (state == S3) { 456 if (c == ',') { 457 state = S2; 458 } else if (c == ')') { 459 return l; 460 } 461 } 462 } 463 if (state == S1 || state == S2) 464 throw new ParseException(this, "Could not find start of entry in array."); 465 if (state == S3) 466 throw new ParseException(this, "Could not find end of entry in array."); 467 468 } else { 469 final int S1=1; // Looking for starting of entry. 470 final int S2=2; // Looking for , or & or END after first entry. 471 472 int state = S1; 473 while (c != -1 && c != AMP) { 474 c = r.read(); 475 if (state == S1) { 476 if (Character.isWhitespace(c)) { 477 skipSpace(r); 478 } else { 479 l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), 480 r.unread(), l, false, pMeta)); 481 state = S2; 482 } 483 } else if (state == S2) { 484 if (c == ',') { 485 state = S1; 486 } else if (Character.isWhitespace(c)) { 487 skipSpace(r); 488 } else if (c == AMP || c == -1) { 489 r.unread(); 490 return l; 491 } 492 } 493 } 494 } 495 496 return null; // Unreachable. 497 } 498 499 private <T> BeanMap<T> parseIntoBeanMap(UonReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException { 500 501 int c = r.readSkipWs(); 502 if (c == -1 || c == AMP) 503 return null; 504 if (c == 'n') 505 return (BeanMap<T>)parseNull(r); 506 if (c != '(') 507 throw new ParseException(this, "Expected '(' at beginning of object."); 508 509 final int S1=1; // Looking for attrName start. 510 final int S2=2; // Found attrName end, looking for =. 511 final int S3=3; // Found =, looking for valStart. 512 final int S4=4; // Looking for , or } 513 boolean isInEscape = false; 514 515 int state = S1; 516 String currAttr = ""; 517 mark(); 518 try { 519 while (c != -1 && c != AMP) { 520 c = r.read(); 521 if (! isInEscape) { 522 if (state == S1) { 523 if (c == ')' || c == -1 || c == AMP) { 524 return m; 525 } 526 if (Character.isWhitespace(c)) 527 skipSpace(r); 528 else { 529 r.unread(); 530 mark(); 531 currAttr = parseAttrName(r, decoding); 532 if (currAttr == null) { // Value was '%00' 533 return null; 534 } 535 state = S2; 536 } 537 } else if (state == S2) { 538 if (c == EQ || c == '=') 539 state = S3; 540 else if (c == -1 || c == ',' || c == ')' || c == AMP) { 541 m.put(currAttr, null); 542 if (c == ')' || c == -1 || c == AMP) { 543 return m; 544 } 545 state = S1; 546 } 547 } else if (state == S3) { 548 if (c == -1 || c == ',' || c == ')' || c == AMP) { 549 if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) { 550 BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); 551 if (pMeta == null) { 552 onUnknownProperty(currAttr, m); 553 unmark(); 554 } else { 555 unmark(); 556 Object value = convertToType("", pMeta.getClassMeta()); 557 pMeta.set(m, currAttr, value); 558 } 559 } 560 if (c == -1 || c == ')' || c == AMP) 561 return m; 562 state = S1; 563 } else { 564 if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) { 565 BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); 566 if (pMeta == null) { 567 onUnknownProperty(currAttr, m); 568 unmark(); 569 parseAnything(object(), r.unread(), m.getBean(false), false, null); // Read content anyway to ignore it 570 } else { 571 unmark(); 572 setCurrentProperty(pMeta); 573 ClassMeta<?> cm = pMeta.getClassMeta(); 574 Object value = parseAnything(cm, r.unread(), m.getBean(false), false, pMeta); 575 setName(cm, value, currAttr); 576 pMeta.set(m, currAttr, value); 577 setCurrentProperty(null); 578 } 579 } 580 state = S4; 581 } 582 } else if (state == S4) { 583 if (c == ',') 584 state = S1; 585 else if (c == ')' || c == -1 || c == AMP) { 586 return m; 587 } 588 } 589 } 590 isInEscape = isInEscape(c, r, isInEscape); 591 } 592 if (state == S1) 593 throw new ParseException(this, "Could not find attribute name on object."); 594 if (state == S2) 595 throw new ParseException(this, "Could not find '=' following attribute name on object."); 596 if (state == S3) 597 throw new ParseException(this, "Could not find value following '=' on object."); 598 if (state == S4) 599 throw new ParseException(this, "Could not find ')' marking end of object."); 600 } finally { 601 unmark(); 602 } 603 604 return null; // Unreachable. 605 } 606 607 private Object parseNull(UonReader r) throws IOException, ParseException { 608 String s = parseString(r, false); 609 if ("ull".equals(s)) 610 return null; 611 throw new ParseException(this, "Unexpected character sequence: ''{0}''", s); 612 } 613 614 /** 615 * Convenience method for parsing an attribute from the specified parser. 616 * 617 * @param r The reader. 618 * @param encoded Whether the attribute is encoded. 619 * @return The parsed object 620 * @throws IOException Exception thrown by underlying stream. 621 * @throws ParseException Attribute was malformed. 622 */ 623 protected final Object parseAttr(UonReader r, boolean encoded) throws IOException, ParseException { 624 Object attr; 625 attr = parseAttrName(r, encoded); 626 return attr; 627 } 628 629 /** 630 * Parses an attribute name from the specified reader. 631 * 632 * @param r The reader. 633 * @param encoded Whether the attribute is encoded. 634 * @return The parsed attribute name. 635 * @throws IOException Exception thrown by underlying stream. 636 * @throws ParseException Attribute name was malformed. 637 */ 638 protected final String parseAttrName(UonReader r, boolean encoded) throws IOException, ParseException { 639 640 // If string is of form 'xxx', we're looking for ' at the end. 641 // Otherwise, we're looking for '&' or '=' or WS or -1 denoting the end of this string. 642 643 int c = r.peekSkipWs(); 644 if (c == '\'') 645 return parsePString(r); 646 647 r.mark(); 648 boolean isInEscape = false; 649 if (encoded) { 650 while (c != -1) { 651 c = r.read(); 652 if (! isInEscape) { 653 if (c == AMP || c == EQ || c == -1 || Character.isWhitespace(c)) { 654 if (c != -1) 655 r.unread(); 656 String s = r.getMarked(); 657 return ("null".equals(s) ? null : s); 658 } 659 } 660 else if (c == AMP) 661 r.replace('&'); 662 else if (c == EQ) 663 r.replace('='); 664 isInEscape = isInEscape(c, r, isInEscape); 665 } 666 } else { 667 while (c != -1) { 668 c = r.read(); 669 if (! isInEscape) { 670 if (c == '=' || c == -1 || Character.isWhitespace(c)) { 671 if (c != -1) 672 r.unread(); 673 String s = r.getMarked(); 674 return ("null".equals(s) ? null : trim(s)); 675 } 676 } 677 isInEscape = isInEscape(c, r, isInEscape); 678 } 679 } 680 681 // We should never get here. 682 throw new ParseException(this, "Unexpected condition."); 683 } 684 685 686 /* 687 * Returns true if the next character in the stream is preceded by an escape '~' character. 688 */ 689 private static final boolean isInEscape(int c, ParserReader r, boolean prevIsInEscape) throws IOException { 690 if (c == '~' && ! prevIsInEscape) { 691 c = r.peek(); 692 if (escapedChars.contains(c)) { 693 r.delete(); 694 return true; 695 } 696 } 697 return false; 698 } 699 700 /** 701 * Parses a string value from the specified reader. 702 * 703 * @param r The input reader. 704 * @param isUrlParamValue Whether this is a URL parameter. 705 * @return The parsed string. 706 * @throws IOException Exception thrown by underlying stream. 707 * @throws ParseException Malformed input found. 708 */ 709 protected final String parseString(UonReader r, boolean isUrlParamValue) throws IOException, ParseException { 710 711 // If string is of form 'xxx', we're looking for ' at the end. 712 // Otherwise, we're looking for ',' or ')' or -1 denoting the end of this string. 713 714 int c = r.peekSkipWs(); 715 if (c == '\'') 716 return parsePString(r); 717 718 r.mark(); 719 boolean isInEscape = false; 720 String s = null; 721 AsciiSet endChars = (isUrlParamValue ? endCharsParam : endCharsNormal); 722 while (c != -1) { 723 c = r.read(); 724 if (! isInEscape) { 725 // If this is a URL parameter value, we're looking for: & 726 // If not, we're looking for: &,) 727 if (endChars.contains(c)) { 728 r.unread(); 729 c = -1; 730 } 731 } 732 if (c == -1) 733 s = r.getMarked(); 734 else if (c == EQ) 735 r.replace('='); 736 else if (Character.isWhitespace(c) && ! isUrlParamValue) { 737 s = r.getMarked(0, -1); 738 skipSpace(r); 739 c = -1; 740 } 741 isInEscape = isInEscape(c, r, isInEscape); 742 } 743 744 if (isUrlParamValue) 745 s = StringUtils.trim(s); 746 747 return ("null".equals(s) ? null : trim(s)); 748 } 749 750 private static final AsciiSet endCharsParam = AsciiSet.create(""+AMP), endCharsNormal = AsciiSet.create(",)"+AMP); 751 752 753 /* 754 * Parses a string of the form "'foo'" 755 * All whitespace within parenthesis are preserved. 756 */ 757 private String parsePString(UonReader r) throws IOException, ParseException { 758 759 r.read(); // Skip first quote. 760 r.mark(); 761 int c = 0; 762 763 boolean isInEscape = false; 764 while (c != -1) { 765 c = r.read(); 766 if (! isInEscape) { 767 if (c == '\'') 768 return trim(r.getMarked(0, -1)); 769 } 770 if (c == EQ) 771 r.replace('='); 772 isInEscape = isInEscape(c, r, isInEscape); 773 } 774 throw new ParseException(this, "Unmatched parenthesis"); 775 } 776 777 private Boolean parseBoolean(UonReader r) throws IOException, ParseException { 778 String s = parseString(r, false); 779 if (s == null || s.equals("null")) 780 return null; 781 if (s.equalsIgnoreCase("true")) 782 return true; 783 if (s.equalsIgnoreCase("false")) 784 return false; 785 throw new ParseException(this, "Unrecognized syntax for boolean. ''{0}''.", s); 786 } 787 788 private Number parseNumber(UonReader r, Class<? extends Number> c) throws IOException, ParseException { 789 String s = parseString(r, false); 790 if (s == null) 791 return null; 792 return StringUtils.parseNumber(s, c); 793 } 794 795 /* 796 * Call this method after you've finished a parsing a string to make sure that if there's any 797 * remainder in the input, that it consists only of whitespace and comments. 798 */ 799 private void validateEnd(UonReader r) throws IOException, ParseException { 800 if (! isValidateEnd()) 801 return; 802 while (true) { 803 int c = r.read(); 804 if (c == -1) 805 return; 806 if (! Character.isWhitespace(c)) 807 throw new ParseException(this, "Remainder after parse: ''{0}''.", (char)c); 808 } 809 } 810 811 private static void skipSpace(ParserReader r) throws IOException { 812 int c = 0; 813 while ((c = r.read()) != -1) { 814 if (c <= 2 || ! Character.isWhitespace(c)) { 815 r.unread(); 816 return; 817 } 818 } 819 } 820 821 /** 822 * Creates a {@link UonReader} from the specified parser pipe. 823 * 824 * @param pipe The parser input. 825 * @param decodeChars Whether the reader should automatically decode URL-encoded characters. 826 * @return A new {@link UonReader} object. 827 * @throws IOException Thrown by underlying stream. 828 */ 829 public final UonReader getUonReader(ParserPipe pipe, boolean decodeChars) throws IOException { 830 Reader r = pipe.getReader(); 831 if (r instanceof UonReader) 832 return (UonReader)r; 833 return new UonReader(pipe, decodeChars); 834 } 835 836 //----------------------------------------------------------------------------------------------------------------- 837 // Properties 838 //----------------------------------------------------------------------------------------------------------------- 839 840 /** 841 * Configuration property: Decode <js>"%xx"</js> sequences. 842 * 843 * @see UonParser#UON_decoding 844 * @return 845 * <jk>true</jk> if URI encoded characters should be decoded, <jk>false</jk> if they've already been decoded 846 * before being passed to this parser. 847 */ 848 protected final boolean isDecoding() { 849 return decoding; 850 } 851 852 /** 853 * Configuration property: Validate end. 854 * 855 * @see UonParser#UON_validateEnd 856 * @return 857 * <jk>true</jk> if after parsing a POJO from the input, verifies that the remaining input in 858 * the stream consists of only comments or whitespace. 859 */ 860 protected final boolean isValidateEnd() { 861 return ctx.isValidateEnd(); 862 } 863 864 //----------------------------------------------------------------------------------------------------------------- 865 // Other methods 866 //----------------------------------------------------------------------------------------------------------------- 867 868 @Override /* Session */ 869 public ObjectMap toMap() { 870 return super.toMap() 871 .append("UonParserSession", new DefaultFilteringObjectMap() 872 .append("decoding", decoding) 873 ); 874 } 875}