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}