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