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((Throwable)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}