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      
189      if (sType.isOptional()) 
190         return (T)Optional.ofNullable(parseAnything(eType.getElementType(), r, outer, isUrlParamValue, pMeta));
191      
192      setCurrentClass(sType);
193
194      Object o = null;
195
196      int c = r.peekSkipWs();
197
198      if (c == -1 || c == AMP) {
199         // If parameter is blank and it's an array or collection, return an empty list.
200         if (sType.isCollectionOrArray())
201            o = sType.newInstance();
202         else if (sType.isString() || sType.isObject())
203            o = "";
204         else if (sType.isPrimitive())
205            o = sType.getPrimitiveDefault();
206         // Otherwise, leave null.
207      } else if (sType.isVoid()) {
208         String s = parseString(r, isUrlParamValue);
209         if (s != null)
210            throw new ParseException(this, "Expected ''null'' for void value, but was ''{0}''.", s);
211      } else if (sType.isObject()) {
212         if (c == '(') {
213            ObjectMap m = new ObjectMap(this);
214            parseIntoMap(r, m, string(), object(), pMeta);
215            o = cast(m, pMeta, eType);
216         } else if (c == '@') {
217            Collection l = new ObjectList(this);
218            o = parseIntoCollection(r, l, sType, isUrlParamValue, pMeta);
219         } else {
220            String s = parseString(r, isUrlParamValue);
221            if (c != '\'') {
222               if ("true".equals(s) || "false".equals(s))
223                  o = Boolean.valueOf(s);
224               else if (! "null".equals(s)) {
225                  if (isNumeric(s))
226                     o = StringUtils.parseNumber(s, Number.class);
227                  else
228                     o = s;
229               }
230            } else {
231               o = s;
232            }
233         }
234      } else if (sType.isBoolean()) {
235         o = parseBoolean(r);
236      } else if (sType.isCharSequence()) {
237         o = parseString(r, isUrlParamValue);
238      } else if (sType.isChar()) {
239         o = parseCharacter(parseString(r, isUrlParamValue));
240      } else if (sType.isNumber()) {
241         o = parseNumber(r, (Class<? extends Number>)sType.getInnerClass());
242      } else if (sType.isMap()) {
243         Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : new ObjectMap(this));
244         o = parseIntoMap(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
245      } else if (sType.isCollection()) {
246         if (c == '(') {
247            ObjectMap m = new ObjectMap(this);
248            parseIntoMap(r, m, string(), object(), pMeta);
249            // Handle case where it's a collection, but serialized as a map with a _type or _value key.
250            if (m.containsKey(getBeanTypePropertyName(sType)))
251               o = cast(m, pMeta, eType);
252            // Handle case where it's a collection, but only a single value was specified.
253            else {
254               Collection l = (
255                  sType.canCreateNewInstance(outer)
256                  ? (Collection)sType.newInstance(outer)
257                  : new ObjectList(this)
258               );
259               l.add(m.cast(sType.getElementType()));
260               o = l;
261            }
262         } else {
263            Collection l = (
264               sType.canCreateNewInstance(outer)
265               ? (Collection)sType.newInstance(outer)
266               : new ObjectList(this)
267            );
268            o = parseIntoCollection(r, l, sType, isUrlParamValue, pMeta);
269         }
270      } else if (builder != null) {
271         BeanMap m = toBeanMap(builder.create(this, eType));
272         m = parseIntoBeanMap(r, m);
273         o = m == null ? null : builder.build(this, m.getBean(), eType);
274      } else if (sType.canCreateNewBean(outer)) {
275         BeanMap m = newBeanMap(outer, sType.getInnerClass());
276         m = parseIntoBeanMap(r, m);
277         o = m == null ? null : m.getBean();
278      } else if (sType.canCreateNewInstanceFromString(outer)) {
279         String s = parseString(r, isUrlParamValue);
280         if (s != null)
281            o = sType.newInstanceFromString(outer, s);
282      } else if (sType.isArray() || sType.isArgs()) {
283         if (c == '(') {
284            ObjectMap m = new ObjectMap(this);
285            parseIntoMap(r, m, string(), object(), pMeta);
286            // Handle case where it's an array, but serialized as a map with a _type or _value key.
287            if (m.containsKey(getBeanTypePropertyName(sType)))
288               o = cast(m, pMeta, eType);
289            // Handle case where it's an array, but only a single value was specified.
290            else {
291               ArrayList l = new ArrayList(1);
292               l.add(m.cast(sType.getElementType()));
293               o = toArray(sType, l);
294            }
295         } else {
296            ArrayList l = (ArrayList)parseIntoCollection(r, new ArrayList(), sType, isUrlParamValue, pMeta);
297            o = toArray(sType, l);
298         }
299      } else if (c == '(') {
300         // It could be a non-bean with _type attribute.
301         ObjectMap m = new ObjectMap(this);
302         parseIntoMap(r, m, string(), object(), pMeta);
303         if (m.containsKey(getBeanTypePropertyName(sType)))
304            o = cast(m, pMeta, eType);
305         else
306            throw new ParseException(this, "Class ''{0}'' could not be instantiated.  Reason: ''{1}''",
307               sType.getInnerClass().getName(), sType.getNotABeanReason());
308      } else if (c == 'n') {
309         r.read();
310         parseNull(r);
311      } else {
312         throw new ParseException(this, "Class ''{0}'' could not be instantiated.  Reason: ''{1}''",
313            sType.getInnerClass().getName(), sType.getNotABeanReason());
314      }
315
316      if (o == null && sType.isPrimitive())
317         o = sType.getPrimitiveDefault();
318      if (swap != null && o != null)
319         o = unswap(swap, o, eType);
320
321      if (outer != null)
322         setParent(eType, o, outer);
323
324      return (T)o;
325   }
326
327   private <K,V> Map<K,V> parseIntoMap(UonReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType,
328         BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
329
330      if (keyType == null)
331         keyType = (ClassMeta<K>)string();
332
333      int c = r.read();
334      if (c == -1 || c == AMP)
335         return null;
336      if (c == 'n')
337         return (Map<K,V>)parseNull(r);
338      if (c != '(')
339         throw new ParseException(this, "Expected '(' at beginning of object.");
340
341      final int S1=1; // Looking for attrName start.
342      final int S2=2; // Found attrName end, looking for =.
343      final int S3=3; // Found =, looking for valStart.
344      final int S4=4; // Looking for , or )
345      boolean isInEscape = false;
346
347      int state = S1;
348      K currAttr = null;
349      while (c != -1 && c != AMP) {
350         c = r.read();
351         if (! isInEscape) {
352            if (state == S1) {
353               if (c == ')')
354                  return m;
355               if (Character.isWhitespace(c))
356                  skipSpace(r);
357               else {
358                  r.unread();
359                  Object attr = parseAttr(r, decoding);
360                  currAttr = attr == null ? null : convertAttrToType(m, trim(attr.toString()), keyType);
361                  state = S2;
362                  c = 0; // Avoid isInEscape if c was '\'
363               }
364            } else if (state == S2) {
365               if (c == EQ || c == '=')
366                  state = S3;
367               else if (c == -1 || c == ',' || c == ')' || c == AMP) {
368                  if (currAttr == null) {
369                     // Value was '%00'
370                     r.unread();
371                     return null;
372                  }
373                  m.put(currAttr, null);
374                  if (c == ')' || c == -1 || c == AMP)
375                     return m;
376                  state = S1;
377               }
378            } else if (state == S3) {
379               if (c == -1 || c == ',' || c == ')' || c == AMP) {
380                  V value = convertAttrToType(m, "", valueType);
381                  m.put(currAttr, value);
382                  if (c == -1 || c == ')' || c == AMP)
383                     return m;
384                  state = S1;
385               } else  {
386                  V value = parseAnything(valueType, r.unread(), m, false, pMeta);
387                  setName(valueType, value, currAttr);
388                  m.put(currAttr, value);
389                  state = S4;
390                  c = 0; // Avoid isInEscape if c was '\'
391               }
392            } else if (state == S4) {
393               if (c == ',')
394                  state = S1;
395               else if (c == ')' || c == -1 || c == AMP) {
396                  return m;
397               }
398            }
399         }
400         isInEscape = isInEscape(c, r, isInEscape);
401      }
402      if (state == S1)
403         throw new ParseException(this, "Could not find attribute name on object.");
404      if (state == S2)
405         throw new ParseException(this, "Could not find '=' following attribute name on object.");
406      if (state == S3)
407         throw new ParseException(this, "Dangling '=' found in object entry");
408      if (state == S4)
409         throw new ParseException(this, "Could not find ')' marking end of object.");
410
411      return null; // Unreachable.
412   }
413
414   private <E> Collection<E> parseIntoCollection(UonReader r, Collection<E> l, ClassMeta<E> type, boolean isUrlParamValue, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
415
416      int c = r.readSkipWs();
417      if (c == -1 || c == AMP)
418         return null;
419      if (c == 'n')
420         return (Collection<E>)parseNull(r);
421
422      int argIndex = 0;
423
424      // 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")
425      // This is not allowed at lower levels since we use comma's as end delimiters.
426      boolean isInParens = (c == '@');
427      if (! isInParens) {
428         if (isUrlParamValue)
429            r.unread();
430         else
431            throw new ParseException(this, "Could not find '(' marking beginning of collection.");
432      } else {
433         r.read();
434      }
435
436      if (isInParens) {
437         final int S1=1; // Looking for starting of first entry.
438         final int S2=2; // Looking for starting of subsequent entries.
439         final int S3=3; // Looking for , or ) after first entry.
440
441         int state = S1;
442         while (c != -1 && c != AMP) {
443            c = r.read();
444            if (state == S1 || state == S2) {
445               if (c == ')') {
446                  if (state == S2) {
447                     l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(),
448                           r.unread(), l, false, pMeta));
449                     r.read();
450                  }
451                  return l;
452               } else if (Character.isWhitespace(c)) {
453                  skipSpace(r);
454               } else {
455                  l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(),
456                        r.unread(), l, false, pMeta));
457                  state = S3;
458               }
459            } else if (state == S3) {
460               if (c == ',') {
461                  state = S2;
462               } else if (c == ')') {
463                  return l;
464               }
465            }
466         }
467         if (state == S1 || state == S2)
468            throw new ParseException(this, "Could not find start of entry in array.");
469         if (state == S3)
470            throw new ParseException(this, "Could not find end of entry in array.");
471
472      } else {
473         final int S1=1; // Looking for starting of entry.
474         final int S2=2; // Looking for , or & or END after first entry.
475
476         int state = S1;
477         while (c != -1 && c != AMP) {
478            c = r.read();
479            if (state == S1) {
480               if (Character.isWhitespace(c)) {
481                  skipSpace(r);
482               } else {
483                  l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(),
484                        r.unread(), l, false, pMeta));
485                  state = S2;
486               }
487            } else if (state == S2) {
488               if (c == ',') {
489                  state = S1;
490               } else if (Character.isWhitespace(c)) {
491                  skipSpace(r);
492               } else if (c == AMP || c == -1) {
493                  r.unread();
494                  return l;
495               }
496            }
497         }
498      }
499
500      return null;  // Unreachable.
501   }
502
503   private <T> BeanMap<T> parseIntoBeanMap(UonReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException {
504
505      int c = r.readSkipWs();
506      if (c == -1 || c == AMP)
507         return null;
508      if (c == 'n')
509         return (BeanMap<T>)parseNull(r);
510      if (c != '(')
511         throw new ParseException(this, "Expected '(' at beginning of object.");
512
513      final int S1=1; // Looking for attrName start.
514      final int S2=2; // Found attrName end, looking for =.
515      final int S3=3; // Found =, looking for valStart.
516      final int S4=4; // Looking for , or }
517      boolean isInEscape = false;
518
519      int state = S1;
520      String currAttr = "";
521      mark();
522      try {
523         while (c != -1 && c != AMP) {
524            c = r.read();
525            if (! isInEscape) {
526               if (state == S1) {
527                  if (c == ')' || c == -1 || c == AMP) {
528                     return m;
529                  }
530                  if (Character.isWhitespace(c))
531                     skipSpace(r);
532                  else {
533                     r.unread();
534                     mark();
535                     currAttr = parseAttrName(r, decoding);
536                     if (currAttr == null) { // Value was '%00'
537                        return null;
538                     }
539                     state = S2;
540                  }
541               } else if (state == S2) {
542                  if (c == EQ || c == '=')
543                     state = S3;
544                  else if (c == -1 || c == ',' || c == ')' || c == AMP) {
545                     m.put(currAttr, null);
546                     if (c == ')' || c == -1 || c == AMP) {
547                        return m;
548                     }
549                     state = S1;
550                  }
551               } else if (state == S3) {
552                  if (c == -1 || c == ',' || c == ')' || c == AMP) {
553                     if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
554                        BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr);
555                        if (pMeta == null) {
556                           onUnknownProperty(currAttr, m);
557                           unmark();
558                        } else {
559                           unmark();
560                           Object value = convertToType("", pMeta.getClassMeta());
561                           pMeta.set(m, currAttr, value);
562                        }
563                     }
564                     if (c == -1 || c == ')' || c == AMP)
565                        return m;
566                     state = S1;
567                  } else {
568                     if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
569                        BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr);
570                        if (pMeta == null) {
571                           onUnknownProperty(currAttr, m);
572                           unmark();
573                           parseAnything(object(), r.unread(), m.getBean(false), false, null); // Read content anyway to ignore it
574                        } else {
575                           unmark();
576                           setCurrentProperty(pMeta);
577                           ClassMeta<?> cm = pMeta.getClassMeta();
578                           Object value = parseAnything(cm, r.unread(), m.getBean(false), false, pMeta);
579                           setName(cm, value, currAttr);
580                           pMeta.set(m, currAttr, value);
581                           setCurrentProperty(null);
582                        }
583                     }
584                     state = S4;
585                  }
586               } else if (state == S4) {
587                  if (c == ',')
588                     state = S1;
589                  else if (c == ')' || c == -1 || c == AMP) {
590                     return m;
591                  }
592               }
593            }
594            isInEscape = isInEscape(c, r, isInEscape);
595         }
596         if (state == S1)
597            throw new ParseException(this, "Could not find attribute name on object.");
598         if (state == S2)
599            throw new ParseException(this, "Could not find '=' following attribute name on object.");
600         if (state == S3)
601            throw new ParseException(this, "Could not find value following '=' on object.");
602         if (state == S4)
603            throw new ParseException(this, "Could not find ')' marking end of object.");
604      } finally {
605         unmark();
606      }
607
608      return null; // Unreachable.
609   }
610
611   private Object parseNull(UonReader r) throws IOException, ParseException {
612      String s = parseString(r, false);
613      if ("ull".equals(s))
614         return null;
615      throw new ParseException(this, "Unexpected character sequence: ''{0}''", s);
616   }
617
618   /**
619    * Convenience method for parsing an attribute from the specified parser.
620    *
621    * @param r The reader.
622    * @param encoded Whether the attribute is encoded.
623    * @return The parsed object
624    * @throws IOException Exception thrown by underlying stream.
625    * @throws ParseException Attribute was malformed.
626    */
627   protected final Object parseAttr(UonReader r, boolean encoded) throws IOException, ParseException {
628      Object attr;
629      attr = parseAttrName(r, encoded);
630      return attr;
631   }
632
633   /**
634    * Parses an attribute name from the specified reader.
635    *
636    * @param r The reader.
637    * @param encoded Whether the attribute is encoded.
638    * @return The parsed attribute name.
639    * @throws IOException Exception thrown by underlying stream.
640    * @throws ParseException Attribute name was malformed.
641    */
642   protected final String parseAttrName(UonReader r, boolean encoded) throws IOException, ParseException {
643
644      // If string is of form 'xxx', we're looking for ' at the end.
645      // Otherwise, we're looking for '&' or '=' or WS or -1 denoting the end of this string.
646
647      int c = r.peekSkipWs();
648      if (c == '\'')
649         return parsePString(r);
650
651      r.mark();
652      boolean isInEscape = false;
653      if (encoded) {
654         while (c != -1) {
655            c = r.read();
656            if (! isInEscape) {
657               if (c == AMP || c == EQ || c == -1 || Character.isWhitespace(c)) {
658                  if (c != -1)
659                     r.unread();
660                  String s = r.getMarked();
661                  return ("null".equals(s) ? null : s);
662               }
663            }
664            else if (c == AMP)
665               r.replace('&');
666            else if (c == EQ)
667               r.replace('=');
668            isInEscape = isInEscape(c, r, isInEscape);
669         }
670      } else {
671         while (c != -1) {
672            c = r.read();
673            if (! isInEscape) {
674               if (c == '=' || c == -1 || Character.isWhitespace(c)) {
675                  if (c != -1)
676                     r.unread();
677                  String s = r.getMarked();
678                  return ("null".equals(s) ? null : trim(s));
679               }
680            }
681            isInEscape = isInEscape(c, r, isInEscape);
682         }
683      }
684
685      // We should never get here.
686      throw new ParseException(this, "Unexpected condition.");
687   }
688
689
690   /*
691    * Returns true if the next character in the stream is preceded by an escape '~' character.
692    */
693   private static final boolean isInEscape(int c, ParserReader r, boolean prevIsInEscape) throws IOException {
694      if (c == '~' && ! prevIsInEscape) {
695         c = r.peek();
696         if (escapedChars.contains(c)) {
697            r.delete();
698            return true;
699         }
700      }
701      return false;
702   }
703
704   /**
705    * Parses a string value from the specified reader.
706    *
707    * @param r The input reader.
708    * @param isUrlParamValue Whether this is a URL parameter.
709    * @return The parsed string.
710    * @throws IOException Exception thrown by underlying stream.
711    * @throws ParseException Malformed input found.
712    */
713   protected final String parseString(UonReader r, boolean isUrlParamValue) throws IOException, ParseException {
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 IOException, ParseException {
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 IOException, ParseException {
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 IOException, ParseException {
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 IOException, ParseException {
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 IOException {
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 IOException Thrown by underlying stream.
832    */
833   public final UonReader getUonReader(ParserPipe pipe, boolean decodeChars) throws IOException {
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 isDecoding() {
853      return decoding;
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
868   //-----------------------------------------------------------------------------------------------------------------
869   // Other methods
870   //-----------------------------------------------------------------------------------------------------------------
871
872   @Override /* Session */
873   public ObjectMap toMap() {
874      return super.toMap()
875         .append("UonParserSession", new DefaultFilteringObjectMap()
876            .append("decoding", decoding)
877         );
878   }
879}