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 decodeChars; 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 decodeChars = getProperty(UON_decoding, boolean.class, ctx.isDecodeChars()); 059 } 060 061 @Override /* Session */ 062 public ObjectMap asMap() { 063 return super.asMap() 064 .append("UonParser", new ObjectMap() 065 .append("decodeChars", decodeChars) 066 ); 067 } 068 069 /** 070 * Create a specialized parser session for parsing URL parameters. 071 * 072 * <p> 073 * The main difference is that characters are never decoded, and the {@link UonParser#UON_decoding} 074 * property is always ignored. 075 * 076 * @param ctx 077 * The context creating this session object. 078 * The context contains all the configuration settings for this object. 079 * @param args 080 * Runtime session arguments. 081 * @param decodeChars 082 * Whether to decode characters. 083 */ 084 protected UonParserSession(UonParser ctx, ParserSessionArgs args, boolean decodeChars) { 085 super(ctx, args); 086 this.ctx = ctx; 087 this.decodeChars = decodeChars; 088 } 089 090 @Override /* ParserSession */ 091 protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws Exception { 092 try (UonReader r = getUonReader(pipe, decodeChars)) { 093 T o = parseAnything(type, r, getOuter(), true, null); 094 validateEnd(r); 095 return o; 096 } 097 } 098 099 @Override /* ReaderParserSession */ 100 protected <K,V> Map<K,V> doParseIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws Exception { 101 try (UonReader r = getUonReader(pipe, decodeChars)) { 102 m = parseIntoMap(r, m, (ClassMeta<K>)getClassMeta(keyType), (ClassMeta<V>)getClassMeta(valueType), null); 103 validateEnd(r); 104 return m; 105 } 106 } 107 108 @Override /* ReaderParserSession */ 109 protected <E> Collection<E> doParseIntoCollection(ParserPipe pipe, Collection<E> c, Type elementType) throws Exception { 110 try (UonReader r = getUonReader(pipe, decodeChars)) { 111 c = parseIntoCollection(r, c, (ClassMeta<E>)getClassMeta(elementType), false, null); 112 validateEnd(r); 113 return c; 114 } 115 } 116 117 @Override /* HttpPartParser */ 118 public <T> T parse(HttpPartType partType, HttpPartSchema schema, String in, ClassMeta<T> toType) throws ParseException, SchemaValidationException { 119 if (in == null) 120 return null; 121 if (toType.isString() && in.length() > 0) { 122 // Shortcut - If we're returning a string and the value doesn't start with "'" or is "null", then 123 // just return the string since it's a plain value. 124 // This allows us to bypass the creation of a UonParserSession object. 125 char x = firstNonWhitespaceChar(in); 126 if (x != '\'' && x != 'n' && in.indexOf('~') == -1) 127 return (T)in; 128 if (x == 'n' && "null".equals(in)) 129 return null; 130 } 131 try (ParserPipe pipe = createPipe(in)) { 132 try (UonReader r = getUonReader(pipe, false)) { 133 return parseAnything(toType, r, null, true, null); 134 } 135 } catch (ParseException e) { 136 throw e; 137 } catch (Exception e) { 138 throw new ParseException(e); 139 } 140 } 141 142 @Override /* HttpPartParserSession */ 143 public <T> T parse(HttpPartType partType, HttpPartSchema schema, String in, Class<T> toType) throws ParseException, SchemaValidationException { 144 return parse(null, schema, in, getClassMeta(toType)); 145 } 146 147 @Override /* HttpPartParserSession */ 148 public <T> T parse(HttpPartType partType, HttpPartSchema schema, String in, Type toType, Type...toTypeArgs) throws ParseException, SchemaValidationException { 149 return (T)parse(null, schema, in, getClassMeta(toType, toTypeArgs)); 150 } 151 152 @Override /* HttpPartParserSession */ 153 public <T> T parse(HttpPartSchema schema, String in, Class<T> toType) throws ParseException, SchemaValidationException { 154 return parse(null, schema, in, getClassMeta(toType)); 155 } 156 157 @Override /* HttpPartParserSession */ 158 public <T> T parse(HttpPartSchema schema, String in, ClassMeta<T> toType) throws ParseException, SchemaValidationException { 159 return parse(null, schema, in, toType); 160 } 161 162 @Override /* HttpPartParserSession */ 163 public <T> T parse(HttpPartSchema schema, String in, Type toType, Type...toTypeArgs) throws ParseException, SchemaValidationException { 164 return (T)parse(null, schema, in, getClassMeta(toType, toTypeArgs)); 165 } 166 167 /** 168 * Workhorse method. 169 * 170 * @param eType The class type being parsed, or <jk>null</jk> if unknown. 171 * @param r The reader being parsed. 172 * @param outer The outer object (for constructing nested inner classes). 173 * @param isUrlParamValue 174 * If <jk>true</jk>, then we're parsing a top-level URL-encoded value which is treated a bit different than the 175 * default case. 176 * @param pMeta The current bean property being parsed. 177 * @return The parsed object. 178 * @throws Exception 179 */ 180 public <T> T parseAnything(ClassMeta<?> eType, UonReader r, Object outer, boolean isUrlParamValue, BeanPropertyMeta pMeta) throws Exception { 181 182 if (eType == null) 183 eType = object(); 184 PojoSwap<T,Object> swap = (PojoSwap<T,Object>)eType.getPojoSwap(this); 185 BuilderSwap<T,Object> builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this); 186 ClassMeta<?> sType = null; 187 if (builder != null) 188 sType = builder.getBuilderClassMeta(this); 189 else if (swap != null) 190 sType = swap.getSwapClassMeta(this); 191 else 192 sType = eType; 193 setCurrentClass(sType); 194 195 Object o = null; 196 197 int c = r.peekSkipWs(); 198 199 if (c == -1 || c == AMP) { 200 // If parameter is blank and it's an array or collection, return an empty list. 201 if (sType.isCollectionOrArray()) 202 o = sType.newInstance(); 203 else if (sType.isString() || sType.isObject()) 204 o = ""; 205 else if (sType.isPrimitive()) 206 o = sType.getPrimitiveDefault(); 207 // Otherwise, leave null. 208 } else if (sType.isVoid()) { 209 String s = parseString(r, isUrlParamValue); 210 if (s != null) 211 throw new ParseException(this, "Expected ''null'' for void value, but was ''{0}''.", s); 212 } else if (sType.isObject()) { 213 if (c == '(') { 214 ObjectMap m = new ObjectMap(this); 215 parseIntoMap(r, m, string(), object(), pMeta); 216 o = cast(m, pMeta, eType); 217 } else if (c == '@') { 218 Collection l = new ObjectList(this); 219 o = parseIntoCollection(r, l, sType, isUrlParamValue, pMeta); 220 } else { 221 String s = parseString(r, isUrlParamValue); 222 if (c != '\'') { 223 if ("true".equals(s) || "false".equals(s)) 224 o = Boolean.valueOf(s); 225 else if (! "null".equals(s)) { 226 if (isNumeric(s)) 227 o = StringUtils.parseNumber(s, Number.class); 228 else 229 o = s; 230 } 231 } else { 232 o = s; 233 } 234 } 235 } else if (sType.isBoolean()) { 236 o = parseBoolean(r); 237 } else if (sType.isCharSequence()) { 238 o = parseString(r, isUrlParamValue); 239 } else if (sType.isChar()) { 240 o = parseCharacter(parseString(r, isUrlParamValue)); 241 } else if (sType.isNumber()) { 242 o = parseNumber(r, (Class<? extends Number>)sType.getInnerClass()); 243 } else if (sType.isMap()) { 244 Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : new ObjectMap(this)); 245 o = parseIntoMap(r, m, sType.getKeyType(), sType.getValueType(), pMeta); 246 } else if (sType.isCollection()) { 247 if (c == '(') { 248 ObjectMap m = new ObjectMap(this); 249 parseIntoMap(r, m, string(), object(), pMeta); 250 // Handle case where it's a collection, but serialized as a map with a _type or _value key. 251 if (m.containsKey(getBeanTypePropertyName(sType))) 252 o = cast(m, pMeta, eType); 253 // Handle case where it's a collection, but only a single value was specified. 254 else { 255 Collection l = ( 256 sType.canCreateNewInstance(outer) 257 ? (Collection)sType.newInstance(outer) 258 : new ObjectList(this) 259 ); 260 l.add(m.cast(sType.getElementType())); 261 o = l; 262 } 263 } else { 264 Collection l = ( 265 sType.canCreateNewInstance(outer) 266 ? (Collection)sType.newInstance(outer) 267 : new ObjectList(this) 268 ); 269 o = parseIntoCollection(r, l, sType, isUrlParamValue, pMeta); 270 } 271 } else if (builder != null) { 272 BeanMap m = toBeanMap(builder.create(this, eType)); 273 m = parseIntoBeanMap(r, m); 274 o = m == null ? null : builder.build(this, m.getBean(), eType); 275 } else if (sType.canCreateNewBean(outer)) { 276 BeanMap m = newBeanMap(outer, sType.getInnerClass()); 277 m = parseIntoBeanMap(r, m); 278 o = m == null ? null : m.getBean(); 279 } else if (sType.canCreateNewInstanceFromString(outer)) { 280 String s = parseString(r, isUrlParamValue); 281 if (s != null) 282 o = sType.newInstanceFromString(outer, s); 283 } else if (sType.canCreateNewInstanceFromNumber(outer)) { 284 o = sType.newInstanceFromNumber(this, outer, parseNumber(r, sType.getNewInstanceFromNumberClass())); 285 } else if (sType.isArray() || sType.isArgs()) { 286 if (c == '(') { 287 ObjectMap m = new ObjectMap(this); 288 parseIntoMap(r, m, string(), object(), pMeta); 289 // Handle case where it's an array, but serialized as a map with a _type or _value key. 290 if (m.containsKey(getBeanTypePropertyName(sType))) 291 o = cast(m, pMeta, eType); 292 // Handle case where it's an array, but only a single value was specified. 293 else { 294 ArrayList l = new ArrayList(1); 295 l.add(m.cast(sType.getElementType())); 296 o = toArray(sType, l); 297 } 298 } else { 299 ArrayList l = (ArrayList)parseIntoCollection(r, new ArrayList(), sType, isUrlParamValue, pMeta); 300 o = toArray(sType, l); 301 } 302 } else if (c == '(') { 303 // It could be a non-bean with _type attribute. 304 ObjectMap m = new ObjectMap(this); 305 parseIntoMap(r, m, string(), object(), pMeta); 306 if (m.containsKey(getBeanTypePropertyName(sType))) 307 o = cast(m, pMeta, eType); 308 else 309 throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", 310 sType.getInnerClass().getName(), sType.getNotABeanReason()); 311 } else if (c == 'n') { 312 r.read(); 313 parseNull(r); 314 } else { 315 throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", 316 sType.getInnerClass().getName(), sType.getNotABeanReason()); 317 } 318 319 if (o == null && sType.isPrimitive()) 320 o = sType.getPrimitiveDefault(); 321 if (swap != null && o != null) 322 o = swap.unswap(this, o, eType); 323 324 if (outer != null) 325 setParent(eType, o, outer); 326 327 return (T)o; 328 } 329 330 private <K,V> Map<K,V> parseIntoMap(UonReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, 331 BeanPropertyMeta pMeta) throws Exception { 332 333 if (keyType == null) 334 keyType = (ClassMeta<K>)string(); 335 336 int c = r.read(); 337 if (c == -1 || c == AMP) 338 return null; 339 if (c == 'n') 340 return (Map<K,V>)parseNull(r); 341 if (c != '(') 342 throw new ParseException(this, "Expected '(' at beginning of object."); 343 344 final int S1=1; // Looking for attrName start. 345 final int S2=2; // Found attrName end, looking for =. 346 final int S3=3; // Found =, looking for valStart. 347 final int S4=4; // Looking for , or ) 348 boolean isInEscape = false; 349 350 int state = S1; 351 K currAttr = null; 352 while (c != -1 && c != AMP) { 353 c = r.read(); 354 if (! isInEscape) { 355 if (state == S1) { 356 if (c == ')') 357 return m; 358 if (Character.isWhitespace(c)) 359 skipSpace(r); 360 else { 361 r.unread(); 362 Object attr = parseAttr(r, decodeChars); 363 currAttr = attr == null ? null : convertAttrToType(m, trim(attr.toString()), keyType); 364 state = S2; 365 c = 0; // Avoid isInEscape if c was '\' 366 } 367 } else if (state == S2) { 368 if (c == EQ || c == '=') 369 state = S3; 370 else if (c == -1 || c == ',' || c == ')' || c == AMP) { 371 if (currAttr == null) { 372 // Value was '%00' 373 r.unread(); 374 return null; 375 } 376 m.put(currAttr, null); 377 if (c == ')' || c == -1 || c == AMP) 378 return m; 379 state = S1; 380 } 381 } else if (state == S3) { 382 if (c == -1 || c == ',' || c == ')' || c == AMP) { 383 V value = convertAttrToType(m, "", valueType); 384 m.put(currAttr, value); 385 if (c == -1 || c == ')' || c == AMP) 386 return m; 387 state = S1; 388 } else { 389 V value = parseAnything(valueType, r.unread(), m, false, pMeta); 390 setName(valueType, value, currAttr); 391 m.put(currAttr, value); 392 state = S4; 393 c = 0; // Avoid isInEscape if c was '\' 394 } 395 } else if (state == S4) { 396 if (c == ',') 397 state = S1; 398 else if (c == ')' || c == -1 || c == AMP) { 399 return m; 400 } 401 } 402 } 403 isInEscape = isInEscape(c, r, isInEscape); 404 } 405 if (state == S1) 406 throw new ParseException(this, "Could not find attribute name on object."); 407 if (state == S2) 408 throw new ParseException(this, "Could not find '=' following attribute name on object."); 409 if (state == S3) 410 throw new ParseException(this, "Dangling '=' found in object entry"); 411 if (state == S4) 412 throw new ParseException(this, "Could not find ')' marking end of object."); 413 414 return null; // Unreachable. 415 } 416 417 private <E> Collection<E> parseIntoCollection(UonReader r, Collection<E> l, ClassMeta<E> type, boolean isUrlParamValue, BeanPropertyMeta pMeta) throws Exception { 418 419 int c = r.readSkipWs(); 420 if (c == -1 || c == AMP) 421 return null; 422 if (c == 'n') 423 return (Collection<E>)parseNull(r); 424 425 int argIndex = 0; 426 427 // 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") 428 // This is not allowed at lower levels since we use comma's as end delimiters. 429 boolean isInParens = (c == '@'); 430 if (! isInParens) { 431 if (isUrlParamValue) 432 r.unread(); 433 else 434 throw new ParseException(this, "Could not find '(' marking beginning of collection."); 435 } else { 436 r.read(); 437 } 438 439 if (isInParens) { 440 final int S1=1; // Looking for starting of first entry. 441 final int S2=2; // Looking for starting of subsequent entries. 442 final int S3=3; // Looking for , or ) after first entry. 443 444 int state = S1; 445 while (c != -1 && c != AMP) { 446 c = r.read(); 447 if (state == S1 || state == S2) { 448 if (c == ')') { 449 if (state == S2) { 450 l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), 451 r.unread(), l, false, pMeta)); 452 r.read(); 453 } 454 return l; 455 } else if (Character.isWhitespace(c)) { 456 skipSpace(r); 457 } else { 458 l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), 459 r.unread(), l, false, pMeta)); 460 state = S3; 461 } 462 } else if (state == S3) { 463 if (c == ',') { 464 state = S2; 465 } else if (c == ')') { 466 return l; 467 } 468 } 469 } 470 if (state == S1 || state == S2) 471 throw new ParseException(this, "Could not find start of entry in array."); 472 if (state == S3) 473 throw new ParseException(this, "Could not find end of entry in array."); 474 475 } else { 476 final int S1=1; // Looking for starting of entry. 477 final int S2=2; // Looking for , or & or END after first entry. 478 479 int state = S1; 480 while (c != -1 && c != AMP) { 481 c = r.read(); 482 if (state == S1) { 483 if (Character.isWhitespace(c)) { 484 skipSpace(r); 485 } else { 486 l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), 487 r.unread(), l, false, pMeta)); 488 state = S2; 489 } 490 } else if (state == S2) { 491 if (c == ',') { 492 state = S1; 493 } else if (Character.isWhitespace(c)) { 494 skipSpace(r); 495 } else if (c == AMP || c == -1) { 496 r.unread(); 497 return l; 498 } 499 } 500 } 501 } 502 503 return null; // Unreachable. 504 } 505 506 private <T> BeanMap<T> parseIntoBeanMap(UonReader r, BeanMap<T> m) throws Exception { 507 508 int c = r.readSkipWs(); 509 if (c == -1 || c == AMP) 510 return null; 511 if (c == 'n') 512 return (BeanMap<T>)parseNull(r); 513 if (c != '(') 514 throw new ParseException(this, "Expected '(' at beginning of object."); 515 516 final int S1=1; // Looking for attrName start. 517 final int S2=2; // Found attrName end, looking for =. 518 final int S3=3; // Found =, looking for valStart. 519 final int S4=4; // Looking for , or } 520 boolean isInEscape = false; 521 522 int state = S1; 523 String currAttr = ""; 524 mark(); 525 try { 526 while (c != -1 && c != AMP) { 527 c = r.read(); 528 if (! isInEscape) { 529 if (state == S1) { 530 if (c == ')' || c == -1 || c == AMP) { 531 return m; 532 } 533 if (Character.isWhitespace(c)) 534 skipSpace(r); 535 else { 536 r.unread(); 537 mark(); 538 currAttr = parseAttrName(r, decodeChars); 539 if (currAttr == null) { // Value was '%00' 540 return null; 541 } 542 state = S2; 543 } 544 } else if (state == S2) { 545 if (c == EQ || c == '=') 546 state = S3; 547 else if (c == -1 || c == ',' || c == ')' || c == AMP) { 548 m.put(currAttr, null); 549 if (c == ')' || c == -1 || c == AMP) { 550 return m; 551 } 552 state = S1; 553 } 554 } else if (state == S3) { 555 if (c == -1 || c == ',' || c == ')' || c == AMP) { 556 if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) { 557 BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); 558 if (pMeta == null) { 559 onUnknownProperty(currAttr, m); 560 unmark(); 561 } else { 562 unmark(); 563 Object value = convertToType("", pMeta.getClassMeta()); 564 pMeta.set(m, currAttr, value); 565 } 566 } 567 if (c == -1 || c == ')' || c == AMP) 568 return m; 569 state = S1; 570 } else { 571 if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) { 572 BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); 573 if (pMeta == null) { 574 onUnknownProperty(currAttr, m); 575 unmark(); 576 parseAnything(object(), r.unread(), m.getBean(false), false, null); // Read content anyway to ignore it 577 } else { 578 unmark(); 579 setCurrentProperty(pMeta); 580 ClassMeta<?> cm = pMeta.getClassMeta(); 581 Object value = parseAnything(cm, r.unread(), m.getBean(false), false, pMeta); 582 setName(cm, value, currAttr); 583 pMeta.set(m, currAttr, value); 584 setCurrentProperty(null); 585 } 586 } 587 state = S4; 588 } 589 } else if (state == S4) { 590 if (c == ',') 591 state = S1; 592 else if (c == ')' || c == -1 || c == AMP) { 593 return m; 594 } 595 } 596 } 597 isInEscape = isInEscape(c, r, isInEscape); 598 } 599 if (state == S1) 600 throw new ParseException(this, "Could not find attribute name on object."); 601 if (state == S2) 602 throw new ParseException(this, "Could not find '=' following attribute name on object."); 603 if (state == S3) 604 throw new ParseException(this, "Could not find value following '=' on object."); 605 if (state == S4) 606 throw new ParseException(this, "Could not find ')' marking end of object."); 607 } finally { 608 unmark(); 609 } 610 611 return null; // Unreachable. 612 } 613 614 private Object parseNull(UonReader r) throws Exception { 615 String s = parseString(r, false); 616 if ("ull".equals(s)) 617 return null; 618 throw new ParseException(this, "Unexpected character sequence: ''{0}''", s); 619 } 620 621 /** 622 * Convenience method for parsing an attribute from the specified parser. 623 * 624 * @param r 625 * @param encoded 626 * @return The parsed object 627 * @throws Exception 628 */ 629 protected final Object parseAttr(UonReader r, boolean encoded) throws Exception { 630 Object attr; 631 attr = parseAttrName(r, encoded); 632 return attr; 633 } 634 635 /** 636 * Parses an attribute name from the specified reader. 637 * 638 * @param r 639 * @param encoded 640 * @return The parsed attribute name. 641 * @throws Exception 642 */ 643 protected final String parseAttrName(UonReader r, boolean encoded) throws Exception { 644 645 // If string is of form 'xxx', we're looking for ' at the end. 646 // Otherwise, we're looking for '&' or '=' or WS or -1 denoting the end of this string. 647 648 int c = r.peekSkipWs(); 649 if (c == '\'') 650 return parsePString(r); 651 652 r.mark(); 653 boolean isInEscape = false; 654 if (encoded) { 655 while (c != -1) { 656 c = r.read(); 657 if (! isInEscape) { 658 if (c == AMP || c == EQ || c == -1 || Character.isWhitespace(c)) { 659 if (c != -1) 660 r.unread(); 661 String s = r.getMarked(); 662 return ("null".equals(s) ? null : s); 663 } 664 } 665 else if (c == AMP) 666 r.replace('&'); 667 else if (c == EQ) 668 r.replace('='); 669 isInEscape = isInEscape(c, r, isInEscape); 670 } 671 } else { 672 while (c != -1) { 673 c = r.read(); 674 if (! isInEscape) { 675 if (c == '=' || c == -1 || Character.isWhitespace(c)) { 676 if (c != -1) 677 r.unread(); 678 String s = r.getMarked(); 679 return ("null".equals(s) ? null : trim(s)); 680 } 681 } 682 isInEscape = isInEscape(c, r, isInEscape); 683 } 684 } 685 686 // We should never get here. 687 throw new ParseException(this, "Unexpected condition."); 688 } 689 690 691 /* 692 * Returns true if the next character in the stream is preceded by an escape '~' character. 693 */ 694 private static final boolean isInEscape(int c, ParserReader r, boolean prevIsInEscape) throws Exception { 695 if (c == '~' && ! prevIsInEscape) { 696 c = r.peek(); 697 if (escapedChars.contains(c)) { 698 r.delete(); 699 return true; 700 } 701 } 702 return false; 703 } 704 705 /** 706 * Parses a string value from the specified reader. 707 * 708 * @param r 709 * @param isUrlParamValue 710 * @return The parsed string. 711 * @throws Exception 712 */ 713 protected final String parseString(UonReader r, boolean isUrlParamValue) throws Exception { 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 Exception { 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 Exception { 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 Exception { 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 Exception { 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 Exception { 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 Exception 832 */ 833 public final UonReader getUonReader(ParserPipe pipe, boolean decodeChars) throws Exception { 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 isDecodeChars() { 853 return decodeChars; 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}