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