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.collections.JsonMap.*;
016import static org.apache.juneau.common.internal.StringUtils.*;
017import static org.apache.juneau.internal.CollectionUtils.*;
018
019import java.io.*;
020import java.lang.reflect.*;
021import java.nio.charset.*;
022import java.util.*;
023import java.util.function.*;
024
025import org.apache.juneau.*;
026import org.apache.juneau.collections.*;
027import org.apache.juneau.common.internal.*;
028import org.apache.juneau.httppart.*;
029import org.apache.juneau.internal.*;
030import org.apache.juneau.parser.*;
031import org.apache.juneau.swap.*;
032
033/**
034 * Session object that lives for the duration of a single use of {@link UonParser}.
035 *
036 * <h5 class='section'>Notes:</h5><ul>
037 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
038 * </ul>
039 *
040 * <h5 class='section'>See Also:</h5><ul>
041 *    <li class='link'><a class="doclink" href="../../../../index.html#jm.UonDetails">UON Details</a>
042
043 * </ul>
044 */
045@SuppressWarnings({ "unchecked", "rawtypes" })
046public class UonParserSession extends ReaderParserSession implements HttpPartParserSession {
047
048   //-------------------------------------------------------------------------------------------------------------------
049   // Static
050   //-------------------------------------------------------------------------------------------------------------------
051
052   // Characters that need to be preceded with an escape character.
053   private static final AsciiSet escapedChars = AsciiSet.create("~'\u0001\u0002");
054
055   private static final char AMP='\u0001', EQ='\u0002';  // Flags set in reader to denote & and = characters.
056
057   /**
058    * Creates a new builder for this object.
059    *
060    * @param ctx The context creating this session.
061    * @return A new builder.
062    */
063   public static Builder create(UonParser ctx) {
064      return new Builder(ctx);
065   }
066
067   //-------------------------------------------------------------------------------------------------------------------
068   // Builder
069   //-------------------------------------------------------------------------------------------------------------------
070
071   /**
072    * Builder class.
073    */
074   @FluentSetters
075   public static class Builder extends ReaderParserSession.Builder {
076
077      UonParser ctx;
078      boolean decoding;
079
080      /**
081       * Constructor
082       *
083       * @param ctx The context creating this session.
084       */
085      protected Builder(UonParser ctx) {
086         super(ctx);
087         this.ctx = ctx;
088         decoding = ctx.decoding;
089      }
090
091      @Override
092      public UonParserSession build() {
093         return new UonParserSession(this);
094      }
095
096      /**
097       * Overrides the decoding flag on the context for this session.
098       *
099       * @param value The new value for this setting.
100       * @return This object.
101       */
102      @FluentSetter
103      public Builder decoding(boolean value) {
104         decoding = value;
105         return this;
106      }
107
108      // <FluentSetters>
109
110      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
111      public <T> Builder apply(Class<T> type, Consumer<T> apply) {
112         super.apply(type, apply);
113         return this;
114      }
115
116      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
117      public Builder debug(Boolean value) {
118         super.debug(value);
119         return this;
120      }
121
122      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
123      public Builder properties(Map<String,Object> value) {
124         super.properties(value);
125         return this;
126      }
127
128      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
129      public Builder property(String key, Object value) {
130         super.property(key, value);
131         return this;
132      }
133
134      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
135      public Builder unmodifiable() {
136         super.unmodifiable();
137         return this;
138      }
139
140      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
141      public Builder locale(Locale value) {
142         super.locale(value);
143         return this;
144      }
145
146      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
147      public Builder localeDefault(Locale value) {
148         super.localeDefault(value);
149         return this;
150      }
151
152      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
153      public Builder mediaType(MediaType value) {
154         super.mediaType(value);
155         return this;
156      }
157
158      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
159      public Builder mediaTypeDefault(MediaType value) {
160         super.mediaTypeDefault(value);
161         return this;
162      }
163
164      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
165      public Builder timeZone(TimeZone value) {
166         super.timeZone(value);
167         return this;
168      }
169
170      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
171      public Builder timeZoneDefault(TimeZone value) {
172         super.timeZoneDefault(value);
173         return this;
174      }
175
176      @Override /* GENERATED - org.apache.juneau.parser.ParserSession.Builder */
177      public Builder javaMethod(Method value) {
178         super.javaMethod(value);
179         return this;
180      }
181
182      @Override /* GENERATED - org.apache.juneau.parser.ParserSession.Builder */
183      public Builder outer(Object value) {
184         super.outer(value);
185         return this;
186      }
187
188      @Override /* GENERATED - org.apache.juneau.parser.ParserSession.Builder */
189      public Builder schema(HttpPartSchema value) {
190         super.schema(value);
191         return this;
192      }
193
194      @Override /* GENERATED - org.apache.juneau.parser.ParserSession.Builder */
195      public Builder schemaDefault(HttpPartSchema value) {
196         super.schemaDefault(value);
197         return this;
198      }
199
200      @Override /* GENERATED - org.apache.juneau.parser.ReaderParserSession.Builder */
201      public Builder fileCharset(Charset value) {
202         super.fileCharset(value);
203         return this;
204      }
205
206      @Override /* GENERATED - org.apache.juneau.parser.ReaderParserSession.Builder */
207      public Builder streamCharset(Charset value) {
208         super.streamCharset(value);
209         return this;
210      }
211
212      // </FluentSetters>
213   }
214
215   //-------------------------------------------------------------------------------------------------------------------
216   // Instance
217   //-------------------------------------------------------------------------------------------------------------------
218
219   private final UonParser ctx;
220   private final boolean decoding;
221
222   /**
223    * Constructor.
224    *
225    * @param builder The builder for this object.
226    */
227   protected UonParserSession(Builder builder) {
228      super(builder);
229      ctx = builder.ctx;
230      decoding = builder.decoding;
231   }
232
233   @Override /* ParserSession */
234   protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
235      try (UonReader r = getUonReader(pipe, decoding)) {
236         T o = parseAnything(type, r, getOuter(), true, null);
237         validateEnd(r);
238         return o;
239      }
240   }
241
242   @Override /* ReaderParserSession */
243   protected <K,V> Map<K,V> doParseIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws Exception {
244      try (UonReader r = getUonReader(pipe, decoding)) {
245         m = parseIntoMap(r, m, (ClassMeta<K>)getClassMeta(keyType), (ClassMeta<V>)getClassMeta(valueType), null);
246         validateEnd(r);
247         return m;
248      }
249   }
250
251   @Override /* ReaderParserSession */
252   protected <E> Collection<E> doParseIntoCollection(ParserPipe pipe, Collection<E> c, Type elementType) throws Exception {
253      try (UonReader r = getUonReader(pipe, decoding)) {
254         c = parseIntoCollection(r, c, (ClassMeta<E>)getClassMeta(elementType), false, null);
255         validateEnd(r);
256         return c;
257      }
258   }
259
260   @Override /* HttpPartParser */
261   public <T> T parse(HttpPartType partType, HttpPartSchema schema, String in, ClassMeta<T> toType) throws ParseException, SchemaValidationException {
262      if (in == null)
263         return null;
264      if (toType.isString() && in.length() > 0) {
265         // Shortcut - If we're returning a string and the value doesn't start with "'" or is "null", then
266         // just return the string since it's a plain value.
267         // This allows us to bypass the creation of a UonParserSession object.
268         char x = firstNonWhitespaceChar(in);
269         if (x != '\'' && x != 'n' && in.indexOf('~') == -1)
270            return (T)in;
271         if (x == 'n' && "null".equals(in))
272            return null;
273      }
274      try (ParserPipe pipe = createPipe(in)) {
275         try (UonReader r = getUonReader(pipe, false)) {
276            return parseAnything(toType, r, null, true, null);
277         }
278      } catch (ParseException e) {
279         throw e;
280      } catch (Exception e) {
281         throw new ParseException(e);
282      }
283   }
284
285   /**
286    * Workhorse method.
287    *
288    * @param <T> The class type being parsed, or <jk>null</jk> if unknown.
289    * @param eType The class type being parsed, or <jk>null</jk> if unknown.
290    * @param r The reader being parsed.
291    * @param outer The outer object (for constructing nested inner classes).
292    * @param isUrlParamValue
293    *    If <jk>true</jk>, then we're parsing a top-level URL-encoded value which is treated a bit different than the
294    *    default case.
295    * @param pMeta The current bean property being parsed.
296    * @return The parsed object.
297    * @throws IOException Thrown by underlying stream.
298    * @throws ParseException Malformed input encountered.
299    * @throws ExecutableException Exception occurred on invoked constructor/method/field.
300    */
301   public <T> T parseAnything(ClassMeta<?> eType, UonReader r, Object outer, boolean isUrlParamValue, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
302
303      if (eType == null)
304         eType = object();
305      ObjectSwap<T,Object> swap = (ObjectSwap<T,Object>)eType.getSwap(this);
306      BuilderSwap<T,Object> builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this);
307      ClassMeta<?> sType = null;
308      if (builder != null)
309         sType = builder.getBuilderClassMeta(this);
310      else if (swap != null)
311         sType = swap.getSwapClassMeta(this);
312      else
313         sType = eType;
314
315      if (sType.isOptional())
316         return (T)optional(parseAnything(eType.getElementType(), r, outer, isUrlParamValue, pMeta));
317
318      setCurrentClass(sType);
319
320      Object o = null;
321
322      int c = r.peekSkipWs();
323
324      if (c == -1 || c == AMP) {
325         // If parameter is blank and it's an array or collection, return an empty list.
326         if (sType.isCollectionOrArray())
327            o = sType.newInstance();
328         else if (sType.isString() || sType.isObject())
329            o = "";
330         else if (sType.isPrimitive())
331            o = sType.getPrimitiveDefault();
332         // Otherwise, leave null.
333      } else if (sType.isVoid()) {
334         String s = parseString(r, isUrlParamValue);
335         if (s != null)
336            throw new ParseException(this, "Expected ''null'' for void value, but was ''{0}''.", s);
337      } else if (sType.isObject()) {
338         if (c == '(') {
339            JsonMap m = new JsonMap(this);
340            parseIntoMap(r, m, string(), object(), pMeta);
341            o = cast(m, pMeta, eType);
342         } else if (c == '@') {
343            Collection l = new JsonList(this);
344            o = parseIntoCollection(r, l, sType, isUrlParamValue, pMeta);
345         } else {
346            String s = parseString(r, isUrlParamValue);
347            if (c != '\'') {
348               if ("true".equals(s) || "false".equals(s))
349                  o = Boolean.valueOf(s);
350               else if (! "null".equals(s)) {
351                  if (isNumeric(s))
352                     o = StringUtils.parseNumber(s, Number.class);
353                  else
354                     o = s;
355               }
356            } else {
357               o = s;
358            }
359         }
360      } else if (sType.isBoolean()) {
361         o = parseBoolean(r);
362      } else if (sType.isCharSequence()) {
363         o = parseString(r, isUrlParamValue);
364      } else if (sType.isChar()) {
365         o = parseCharacter(parseString(r, isUrlParamValue));
366      } else if (sType.isNumber()) {
367         o = parseNumber(r, (Class<? extends Number>)sType.getInnerClass());
368      } else if (sType.isMap()) {
369         Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType));
370         o = parseIntoMap(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
371      } else if (sType.isCollection()) {
372         if (c == '(') {
373            JsonMap m = new JsonMap(this);
374            parseIntoMap(r, m, string(), object(), pMeta);
375            // Handle case where it's a collection, but serialized as a map with a _type or _value key.
376            if (m.containsKey(getBeanTypePropertyName(sType)))
377               o = cast(m, pMeta, eType);
378            // Handle case where it's a collection, but only a single value was specified.
379            else {
380               Collection l = (
381                  sType.canCreateNewInstance(outer)
382                  ? (Collection)sType.newInstance(outer)
383                  : new JsonList(this)
384               );
385               l.add(m.cast(sType.getElementType()));
386               o = l;
387            }
388         } else {
389            Collection l = (
390               sType.canCreateNewInstance(outer)
391               ? (Collection)sType.newInstance(outer)
392               : new JsonList(this)
393            );
394            o = parseIntoCollection(r, l, sType, isUrlParamValue, pMeta);
395         }
396      } else if (builder != null) {
397         BeanMap m = toBeanMap(builder.create(this, eType));
398         m = parseIntoBeanMap(r, m);
399         o = m == null ? null : builder.build(this, m.getBean(), eType);
400      } else if (sType.canCreateNewBean(outer)) {
401         BeanMap m = newBeanMap(outer, sType.getInnerClass());
402         m = parseIntoBeanMap(r, m);
403         o = m == null ? null : m.getBean();
404      } else if (sType.canCreateNewInstanceFromString(outer)) {
405         String s = parseString(r, isUrlParamValue);
406         if (s != null)
407            o = sType.newInstanceFromString(outer, s);
408      } else if (sType.isArray() || sType.isArgs()) {
409         if (c == '(') {
410            JsonMap m = new JsonMap(this);
411            parseIntoMap(r, m, string(), object(), pMeta);
412            // Handle case where it's an array, but serialized as a map with a _type or _value key.
413            if (m.containsKey(getBeanTypePropertyName(sType)))
414               o = cast(m, pMeta, eType);
415            // Handle case where it's an array, but only a single value was specified.
416            else {
417               ArrayList l = list(1);
418               l.add(m.cast(sType.getElementType()));
419               o = toArray(sType, l);
420            }
421         } else {
422            ArrayList l = (ArrayList)parseIntoCollection(r, list(), sType, isUrlParamValue, pMeta);
423            o = toArray(sType, l);
424         }
425      } else if (c == '(') {
426         // It could be a non-bean with _type attribute.
427         JsonMap m = new JsonMap(this);
428         parseIntoMap(r, m, string(), object(), pMeta);
429         if (m.containsKey(getBeanTypePropertyName(sType)))
430            o = cast(m, pMeta, eType);
431         else if (sType.getProxyInvocationHandler() != null)
432            o = newBeanMap(outer, sType.getInnerClass()).load(m).getBean();
433         else
434            throw new ParseException(this, "Class ''{0}'' could not be instantiated.  Reason: ''{1}''",
435               sType.getInnerClass().getName(), sType.getNotABeanReason());
436      } else if (c == 'n') {
437         r.read();
438         parseNull(r);
439      } else {
440         throw new ParseException(this, "Class ''{0}'' could not be instantiated.  Reason: ''{1}''",
441            sType.getInnerClass().getName(), sType.getNotABeanReason());
442      }
443
444      if (o == null && sType.isPrimitive())
445         o = sType.getPrimitiveDefault();
446      if (swap != null && o != null)
447         o = unswap(swap, o, eType);
448
449      if (outer != null)
450         setParent(eType, o, outer);
451
452      return (T)o;
453   }
454
455   private <K,V> Map<K,V> parseIntoMap(UonReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType,
456         BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
457
458      if (keyType == null)
459         keyType = (ClassMeta<K>)string();
460
461      int c = r.read();
462      if (c == -1 || c == AMP)
463         return null;
464      if (c == 'n')
465         return (Map<K,V>)parseNull(r);
466      if (c != '(')
467         throw new ParseException(this, "Expected '(' at beginning of object.");
468
469      final int S1=1; // Looking for attrName start.
470      final int S2=2; // Found attrName end, looking for =.
471      final int S3=3; // Found =, looking for valStart.
472      final int S4=4; // Looking for , or )
473      boolean isInEscape = false;
474
475      int state = S1;
476      K currAttr = null;
477      while (c != -1 && c != AMP) {
478         c = r.read();
479         if (! isInEscape) {
480            if (state == S1) {
481               if (c == ')')
482                  return m;
483               if (Character.isWhitespace(c))
484                  skipSpace(r);
485               else {
486                  r.unread();
487                  Object attr = parseAttr(r, decoding);
488                  currAttr = attr == null ? null : convertAttrToType(m, trim(attr.toString()), keyType);
489                  state = S2;
490                  c = 0; // Avoid isInEscape if c was '\'
491               }
492            } else if (state == S2) {
493               if (c == EQ || c == '=')
494                  state = S3;
495               else if (c == -1 || c == ',' || c == ')' || c == AMP) {
496                  if (currAttr == null) {
497                     // Value was '%00'
498                     r.unread();
499                     return null;
500                  }
501                  m.put(currAttr, null);
502                  if (c == ')' || c == -1 || c == AMP)
503                     return m;
504                  state = S1;
505               }
506            } else if (state == S3) {
507               if (c == -1 || c == ',' || c == ')' || c == AMP) {
508                  V value = convertAttrToType(m, "", valueType);
509                  m.put(currAttr, value);
510                  if (c == -1 || c == ')' || c == AMP)
511                     return m;
512                  state = S1;
513               } else  {
514                  V value = parseAnything(valueType, r.unread(), m, false, pMeta);
515                  setName(valueType, value, currAttr);
516                  m.put(currAttr, value);
517                  state = S4;
518                  c = 0; // Avoid isInEscape if c was '\'
519               }
520            } else if (state == S4) {
521               if (c == ',')
522                  state = S1;
523               else if (c == ')' || c == -1 || c == AMP) {
524                  return m;
525               }
526            }
527         }
528         isInEscape = isInEscape(c, r, isInEscape);
529      }
530      if (state == S1)
531         throw new ParseException(this, "Could not find attribute name on object.");
532      if (state == S2)
533         throw new ParseException(this, "Could not find '=' following attribute name on object.");
534      if (state == S3)
535         throw new ParseException(this, "Dangling '=' found in object entry");
536      if (state == S4)
537         throw new ParseException(this, "Could not find ')' marking end of object.");
538
539      return null; // Unreachable.
540   }
541
542   private <E> Collection<E> parseIntoCollection(UonReader r, Collection<E> l, ClassMeta<E> type, boolean isUrlParamValue, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
543
544      int c = r.readSkipWs();
545      if (c == -1 || c == AMP)
546         return null;
547      if (c == 'n')
548         return (Collection<E>)parseNull(r);
549
550      int argIndex = 0;
551
552      // 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")
553      // This is not allowed at lower levels since we use comma's as end delimiters.
554      boolean isInParens = (c == '@');
555      if (! isInParens) {
556         if (isUrlParamValue)
557            r.unread();
558         else
559            throw new ParseException(this, "Could not find '(' marking beginning of collection.");
560      } else {
561         r.read();
562      }
563
564      if (isInParens) {
565         final int S1=1; // Looking for starting of first entry.
566         final int S2=2; // Looking for starting of subsequent entries.
567         final int S3=3; // Looking for , or ) after first entry.
568
569         int state = S1;
570         while (c != -1 && c != AMP) {
571            c = r.read();
572            if (state == S1 || state == S2) {
573               if (c == ')') {
574                  if (state == S2) {
575                     l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(),
576                           r.unread(), l, false, pMeta));
577                     r.read();
578                  }
579                  return l;
580               } else if (Character.isWhitespace(c)) {
581                  skipSpace(r);
582               } else {
583                  l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(),
584                        r.unread(), l, false, pMeta));
585                  state = S3;
586               }
587            } else if (state == S3) {
588               if (c == ',') {
589                  state = S2;
590               } else if (c == ')') {
591                  return l;
592               }
593            }
594         }
595         if (state == S1 || state == S2)
596            throw new ParseException(this, "Could not find start of entry in array.");
597         if (state == S3)
598            throw new ParseException(this, "Could not find end of entry in array.");
599
600      } else {
601         final int S1=1; // Looking for starting of entry.
602         final int S2=2; // Looking for , or & or END after first entry.
603
604         int state = S1;
605         while (c != -1 && c != AMP) {
606            c = r.read();
607            if (state == S1) {
608               if (Character.isWhitespace(c)) {
609                  skipSpace(r);
610               } else {
611                  l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(),
612                        r.unread(), l, false, pMeta));
613                  state = S2;
614               }
615            } else if (state == S2) {
616               if (c == ',') {
617                  state = S1;
618               } else if (Character.isWhitespace(c)) {
619                  skipSpace(r);
620               } else if (c == AMP || c == -1) {
621                  r.unread();
622                  return l;
623               }
624            }
625         }
626      }
627
628      return null;  // Unreachable.
629   }
630
631   private <T> BeanMap<T> parseIntoBeanMap(UonReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException {
632
633      int c = r.readSkipWs();
634      if (c == -1 || c == AMP)
635         return null;
636      if (c == 'n')
637         return (BeanMap<T>)parseNull(r);
638      if (c != '(')
639         throw new ParseException(this, "Expected '(' at beginning of object.");
640
641      final int S1=1; // Looking for attrName start.
642      final int S2=2; // Found attrName end, looking for =.
643      final int S3=3; // Found =, looking for valStart.
644      final int S4=4; // Looking for , or }
645      boolean isInEscape = false;
646
647      int state = S1;
648      String currAttr = "";
649      mark();
650      try {
651         while (c != -1 && c != AMP) {
652            c = r.read();
653            if (! isInEscape) {
654               if (state == S1) {
655                  if (c == ')' || c == -1 || c == AMP) {
656                     return m;
657                  }
658                  if (Character.isWhitespace(c))
659                     skipSpace(r);
660                  else {
661                     r.unread();
662                     mark();
663                     currAttr = parseAttrName(r, decoding);
664                     if (currAttr == null) { // Value was '%00'
665                        return null;
666                     }
667                     state = S2;
668                  }
669               } else if (state == S2) {
670                  if (c == EQ || c == '=')
671                     state = S3;
672                  else if (c == -1 || c == ',' || c == ')' || c == AMP) {
673                     m.put(currAttr, null);
674                     if (c == ')' || c == -1 || c == AMP) {
675                        return m;
676                     }
677                     state = S1;
678                  }
679               } else if (state == S3) {
680                  if (c == -1 || c == ',' || c == ')' || c == AMP) {
681                     if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
682                        BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr);
683                        if (pMeta == null) {
684                           onUnknownProperty(currAttr, m, null);
685                           unmark();
686                        } else {
687                           unmark();
688                           Object value = convertToType("", pMeta.getClassMeta());
689                           try {
690                              pMeta.set(m, currAttr, value);
691                           } catch (BeanRuntimeException e) {
692                              onBeanSetterException(pMeta, e);
693                              throw e;
694                           }
695                        }
696                     }
697                     if (c == -1 || c == ')' || c == AMP)
698                        return m;
699                     state = S1;
700                  } else {
701                     if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
702                        BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr);
703                        if (pMeta == null) {
704                           onUnknownProperty(currAttr, m, parseAnything(object(), r.unread(), m.getBean(false), false, null));
705                           unmark();
706                        } else {
707                           unmark();
708                           setCurrentProperty(pMeta);
709                           ClassMeta<?> cm = pMeta.getClassMeta();
710                           Object value = parseAnything(cm, r.unread(), m.getBean(false), false, pMeta);
711                           setName(cm, value, currAttr);
712                           try {
713                              pMeta.set(m, currAttr, value);
714                           } catch (BeanRuntimeException e) {
715                              onBeanSetterException(pMeta, e);
716                              throw e;
717                           }
718                           setCurrentProperty(null);
719                        }
720                     }
721                     state = S4;
722                  }
723               } else if (state == S4) {
724                  if (c == ',')
725                     state = S1;
726                  else if (c == ')' || c == -1 || c == AMP) {
727                     return m;
728                  }
729               }
730            }
731            isInEscape = isInEscape(c, r, isInEscape);
732         }
733         if (state == S1)
734            throw new ParseException(this, "Could not find attribute name on object.");
735         if (state == S2)
736            throw new ParseException(this, "Could not find '=' following attribute name on object.");
737         if (state == S3)
738            throw new ParseException(this, "Could not find value following '=' on object.");
739         if (state == S4)
740            throw new ParseException(this, "Could not find ')' marking end of object.");
741      } finally {
742         unmark();
743      }
744
745      return null; // Unreachable.
746   }
747
748   private Object parseNull(UonReader r) throws IOException, ParseException {
749      String s = parseString(r, false);
750      if ("ull".equals(s))
751         return null;
752      throw new ParseException(this, "Unexpected character sequence: ''{0}''", s);
753   }
754
755   /**
756    * Convenience method for parsing an attribute from the specified parser.
757    *
758    * @param r The reader.
759    * @param encoded Whether the attribute is encoded.
760    * @return The parsed object
761    * @throws IOException Exception thrown by underlying stream.
762    * @throws ParseException Attribute was malformed.
763    */
764   protected final Object parseAttr(UonReader r, boolean encoded) throws IOException, ParseException {
765      Object attr;
766      attr = parseAttrName(r, encoded);
767      return attr;
768   }
769
770   /**
771    * Parses an attribute name from the specified reader.
772    *
773    * @param r The reader.
774    * @param encoded Whether the attribute is encoded.
775    * @return The parsed attribute name.
776    * @throws IOException Exception thrown by underlying stream.
777    * @throws ParseException Attribute name was malformed.
778    */
779   protected final String parseAttrName(UonReader r, boolean encoded) throws IOException, ParseException {
780
781      // If string is of form 'xxx', we're looking for ' at the end.
782      // Otherwise, we're looking for '&' or '=' or WS or -1 denoting the end of this string.
783
784      int c = r.peekSkipWs();
785      if (c == '\'')
786         return parsePString(r);
787
788      r.mark();
789      boolean isInEscape = false;
790      if (encoded) {
791         while (c != -1) {
792            c = r.read();
793            if (! isInEscape) {
794               if (c == AMP || c == EQ || c == -1 || Character.isWhitespace(c)) {
795                  if (c != -1)
796                     r.unread();
797                  String s = r.getMarked();
798                  return ("null".equals(s) ? null : s);
799               }
800            }
801            else if (c == AMP)
802               r.replace('&');
803            else if (c == EQ)
804               r.replace('=');
805            isInEscape = isInEscape(c, r, isInEscape);
806         }
807      } else {
808         while (c != -1) {
809            c = r.read();
810            if (! isInEscape) {
811               if (c == '=' || c == -1 || Character.isWhitespace(c)) {
812                  if (c != -1)
813                     r.unread();
814                  String s = r.getMarked();
815                  return ("null".equals(s) ? null : trim(s));
816               }
817            }
818            isInEscape = isInEscape(c, r, isInEscape);
819         }
820      }
821
822      // We should never get here.
823      throw new ParseException(this, "Unexpected condition.");
824   }
825
826
827   /*
828    * Returns true if the next character in the stream is preceded by an escape '~' character.
829    */
830   private static final boolean isInEscape(int c, ParserReader r, boolean prevIsInEscape) throws IOException {
831      if (c == '~' && ! prevIsInEscape) {
832         c = r.peek();
833         if (escapedChars.contains(c)) {
834            r.delete();
835            return true;
836         }
837      }
838      return false;
839   }
840
841   /**
842    * Parses a string value from the specified reader.
843    *
844    * @param r The input reader.
845    * @param isUrlParamValue Whether this is a URL parameter.
846    * @return The parsed string.
847    * @throws IOException Exception thrown by underlying stream.
848    * @throws ParseException Malformed input found.
849    */
850   protected final String parseString(UonReader r, boolean isUrlParamValue) throws IOException, ParseException {
851
852      // If string is of form 'xxx', we're looking for ' at the end.
853      // Otherwise, we're looking for ',' or ')' or -1 denoting the end of this string.
854
855      int c = r.peekSkipWs();
856      if (c == '\'')
857         return parsePString(r);
858
859      r.mark();
860      boolean isInEscape = false;
861      String s = null;
862      AsciiSet endChars = (isUrlParamValue ? endCharsParam : endCharsNormal);
863      while (c != -1) {
864         c = r.read();
865         if (! isInEscape) {
866            // If this is a URL parameter value, we're looking for:  &
867            // If not, we're looking for:  &,)
868            if (endChars.contains(c)) {
869               r.unread();
870               c = -1;
871            }
872         }
873         if (c == -1)
874            s = r.getMarked();
875         else if (c == EQ)
876            r.replace('=');
877         else if (Character.isWhitespace(c) && ! isUrlParamValue) {
878            s = r.getMarked(0, -1);
879            skipSpace(r);
880            c = -1;
881         }
882         isInEscape = isInEscape(c, r, isInEscape);
883      }
884
885      if (isUrlParamValue)
886         s = StringUtils.trim(s);
887
888      return ("null".equals(s) ? null : trim(s));
889   }
890
891   private static final AsciiSet endCharsParam = AsciiSet.create(""+AMP), endCharsNormal = AsciiSet.create(",)"+AMP);
892
893
894   /*
895    * Parses a string of the form "'foo'"
896    * All whitespace within parenthesis are preserved.
897    */
898   private String parsePString(UonReader r) throws IOException, ParseException {
899
900      r.read(); // Skip first quote.
901      r.mark();
902      int c = 0;
903
904      boolean isInEscape = false;
905      while (c != -1) {
906         c = r.read();
907         if (! isInEscape) {
908            if (c == '\'')
909               return trim(r.getMarked(0, -1));
910         }
911         if (c == EQ)
912            r.replace('=');
913         isInEscape = isInEscape(c, r, isInEscape);
914      }
915      throw new ParseException(this, "Unmatched parenthesis");
916   }
917
918   private Boolean parseBoolean(UonReader r) throws IOException, ParseException {
919      String s = parseString(r, false);
920      if (s == null || s.equals("null"))
921         return null;
922      if (s.equalsIgnoreCase("true"))
923         return true;
924      if (s.equalsIgnoreCase("false"))
925         return false;
926      throw new ParseException(this, "Unrecognized syntax for boolean.  ''{0}''.", s);
927   }
928
929   private Number parseNumber(UonReader r, Class<? extends Number> c) throws IOException, ParseException {
930      String s = parseString(r, false);
931      if (s == null)
932         return null;
933      return StringUtils.parseNumber(s, c);
934   }
935
936   /*
937    * Call this method after you've finished a parsing a string to make sure that if there's any
938    * remainder in the input, that it consists only of whitespace and comments.
939    */
940   private void validateEnd(UonReader r) throws IOException, ParseException {
941      if (! isValidateEnd())
942         return;
943      while (true) {
944         int c = r.read();
945         if (c == -1)
946            return;
947         if (! Character.isWhitespace(c))
948            throw new ParseException(this, "Remainder after parse: ''{0}''.", (char)c);
949      }
950   }
951
952   private static void skipSpace(ParserReader r) throws IOException {
953      int c = 0;
954      while ((c = r.read()) != -1) {
955         if (c <= 2 || ! Character.isWhitespace(c)) {
956            r.unread();
957            return;
958         }
959      }
960   }
961
962   /**
963    * Creates a {@link UonReader} from the specified parser pipe.
964    *
965    * @param pipe The parser input.
966    * @param decodeChars Whether the reader should automatically decode URL-encoded characters.
967    * @return A new {@link UonReader} object.
968    * @throws IOException Thrown by underlying stream.
969    */
970   public final UonReader getUonReader(ParserPipe pipe, boolean decodeChars) throws IOException {
971      Reader r = pipe.getReader();
972      if (r instanceof UonReader)
973         return (UonReader)r;
974      return new UonReader(pipe, decodeChars);
975   }
976
977   //-----------------------------------------------------------------------------------------------------------------
978   // Properties
979   //-----------------------------------------------------------------------------------------------------------------
980
981   /**
982    * Decode <js>"%xx"</js> sequences.
983    *
984    * @see UonParser.Builder#decoding()
985    * @return
986    *    <jk>true</jk> if URI encoded characters should be decoded, <jk>false</jk> if they've already been decoded
987    *    before being passed to this parser.
988    */
989   protected final boolean isDecoding() {
990      return decoding;
991   }
992
993   /**
994    * Validate end.
995    *
996    * @see UonParser.Builder#validateEnd()
997    * @return
998    *    <jk>true</jk> if after parsing a POJO from the input, verifies that the remaining input in
999    *    the stream consists of only comments or whitespace.
1000    */
1001   protected final boolean isValidateEnd() {
1002      return ctx.isValidateEnd();
1003   }
1004
1005   //-----------------------------------------------------------------------------------------------------------------
1006   // Other methods
1007   //-----------------------------------------------------------------------------------------------------------------
1008
1009   @Override /* ContextSession */
1010   protected JsonMap properties() {
1011      return filteredMap("decoding", decoding);
1012   }
1013}