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