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.marshall.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.Shorts.*;
022import static org.apache.juneau.commons.utils.StringUtils.*;
023
024import java.io.*;
025import java.lang.reflect.*;
026import java.util.*;
027
028import org.apache.juneau.commons.bean.*;
029import org.apache.juneau.commons.collections.*;
030import org.apache.juneau.commons.httppart.*;
031import org.apache.juneau.commons.lang.*;
032import org.apache.juneau.commons.reflect.*;
033import org.apache.juneau.commons.utils.*;
034import org.apache.juneau.marshall.*;
035import org.apache.juneau.marshall.httppart.*;
036import org.apache.juneau.marshall.parser.*;
037import org.apache.juneau.marshall.stream.*;
038import org.apache.juneau.marshall.swap.spi.*;
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/UonSupport">UON</a>
049
050 * </ul>
051 */
052@SuppressWarnings({
053   "java:S110", // Deep inheritance inherent to the serializer/parser session hierarchy
054   "java:S115", // Constants use UPPER_snakeCase convention (e.g., CONST_value)
055   "java:S125", // State-machine comments are documentation, not commented-out code
056   "java:S1168", // readIntoMap()/readIntoCollection()/readIntoBeanMap() return null for blank, literal null or '%00' input, which callers treat as 'no value'.
057   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
058   "java:S2583", // The post-loop state checks in readIntoCollection() look constant to static analysis but are reachable when input ends mid-entry.
059   "java:S3776", // readAnything() and the readInto*() methods are state machines over the UON grammar; splitting them would obscure the parse flow.
060   "java:S6541", // readAnything() is the single type-dispatch point of the parser and is long by design.
061   "rawtypes", // Raw types necessary for generic type handling
062   "resource", // UonReader is managed by caller
063   "unchecked", // Type erasure requires unchecked casts
064   "unused" // Intentionally unused; variable/parameter is required by the interface contract
065})
066public class UonParserSession extends ReaderParserSession implements HttpPartParserSession, RecordReadable {
067
068   /**
069    * Builder class.
070    */
071   @SuppressWarnings({
072      "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name.
073   })
074   public abstract static class Builder<SELF extends Builder<SELF>> extends ReaderParserSession.Builder<SELF> {
075
076      private boolean decoding;
077      private boolean validateEnd;
078
079      /**
080       * Constructor
081       *
082       * @param ctx The context creating this session.
083       */
084      protected Builder(UonParser ctx) {
085         super(ctx);
086         decoding = ctx.decoding;
087         validateEnd = ctx.isValidateEnd();
088      }
089
090      @Override
091      public UonParserSession build() {
092         return new UonParserSession(this);
093      }
094
095      /**
096       * Overrides the decoding flag on the context for this session.
097       *
098       * @param value The new value for this setting.
099       * @return This object.
100       */
101      public SELF decoding(boolean value) {
102         decoding = value;
103         return self();
104      }
105
106      @Override /* Overridden from Builder */
107      public SELF property(String key, Object value) {
108         if (key == null) { super.property(key, value); return self(); }
109         switch (key) {
110            case "decoding":
111               return decoding(cvt(value, Boolean.class));
112            case "validateEnd", "UonParserSession.validateEnd":
113               return validateEnd(cvt(value, Boolean.class));
114            default:
115               super.property(key, value);
116               return self();
117         }
118      }
119
120      /**
121       * Validate end.
122       *
123       * @param value The new value for this setting.
124       * @return This object.
125       */
126      public SELF validateEnd(boolean value) {
127         validateEnd = value;
128         return self();
129      }
130
131   }
132
133   /**
134    * Concrete default builder leaf for the non-subclassed {@code create()} path (CRTP terminal).
135    */
136   public static final class DefaultBuilder extends Builder<DefaultBuilder> {
137
138      DefaultBuilder(UonParser ctx) {
139         super(ctx);
140      }
141   }
142
143   // Characters that need to be preceded with an escape character.
144   private static final AsciiSet escapedChars = AsciiSet.of("~'\u0001\u0002");
145
146   private static final char AMP = '\u0001';  // Flags set in reader to denote & and = characters.
147   private static final char EQ = '\u0002';
148   private static final AsciiSet endCharsParam = AsciiSet.of("" + AMP);
149   private static final AsciiSet endCharsNormal = AsciiSet.of(",)" + AMP);
150
151   /**
152    * Creates a new builder for this object.
153    *
154    * @param ctx The context creating this session.
155    * @return A new builder.
156    */
157   @SuppressWarnings({
158      "java:S1452" // Builder<?> wildcard return intentional; callers use it to construct session instances polymorphically
159   })
160   public static Builder<?> create(UonParser ctx) {
161      return new DefaultBuilder(ctx);
162   }
163
164   /*
165    * Returns true if the next character in the stream is preceded by an escape '~' character.
166    */
167   private static final boolean isInEscape(int c, ParserReader r, boolean prevIsInEscape) throws IOException {
168      if (c == '~' && ! prevIsInEscape) {
169         c = r.peek();
170         if (escapedChars.contains(c)) {
171            r.delete();
172            return true;
173         }
174      }
175      return false;
176   }
177
178   private static void skipSpace(ParserReader r) throws IOException {
179      int c = 0;
180      while ((c = r.read()) != -1) {
181         if (c <= 2 || ! Character.isWhitespace(c)) {
182            r.unread();
183            return;
184         }
185      }
186   }
187
188   private final boolean decoding;
189   private final boolean validateEnd;
190
191   /**
192    * Constructor.
193    *
194    * @param builder The builder for this object.
195    */
196   protected UonParserSession(Builder<?> builder) {
197      super(builder);
198      decoding = builder.decoding;
199      validateEnd = builder.validateEnd;
200   }
201
202   @Override /* RecordReadable */
203   public RecordReader readRecords(Object input) throws IOException {
204      return RecordAdapter.reader(this, input);
205   }
206
207   @Override /* RecordReadable */
208   public boolean isRecordStreaming() {
209      return false;
210   }
211
212   /**
213    * Creates a {@link UonReader} from the specified parser pipe.
214    *
215    * @param pipe The parser input.
216    * @param decodeChars Whether the reader should automatically decode URL-encoded characters.
217    * @return A new {@link UonReader} object.
218    * @throws IOException Thrown by underlying stream.
219    */
220   public static final UonReader getUonReader(ParserPipe pipe, boolean decodeChars) throws IOException {
221      var r = pipe.getReader();
222      if (r instanceof UonReader r2)
223         return r2;
224      return new UonReader(pipe, decodeChars);
225   }
226
227   @Override /* Overridden from HttpPartParser */
228   public <T> T read(HttpPartType partType, HttpPartSchema schema, String in, ClassMeta<T> toType) throws ParseException, SchemaValidationException {
229      if (in == null)
230         return null;
231      if (toType.isString() && ine(in)) {
232         // Shortcut - If we're returning a string and the value doesn't start with "'" or is "null", then
233         // just return the string since it's a plain value.
234         // This allows us to bypass the creation of a UonParserSession object.
235         char x = firstNonWhitespaceChar(in);
236         if (x != '\'' && x != 'n' && in.indexOf('~') == -1)
237            return (T)in;
238         if (x == 'n' && "null".equals(in))
239            return null;
240      }
241      try (var pipe = createPipe(in)) {
242         try (var r = getUonReader(pipe, false)) {
243            return readAnything(toType, r, null, true, null);
244         }
245      } catch (ParseException e) {
246         throw e;
247      } catch (Exception e) {
248         throw new ParseException(e);
249      }
250   }
251
252   /**
253    * Workhorse method.
254    *
255    * @param <T> The class type being parsed, or <jk>null</jk> if unknown.
256    * @param eType The class type being parsed, or <jk>null</jk> if unknown.
257    * @param r The reader being parsed.
258    *    <br>Must not be <jk>null</jk>.
259    * @param outer The outer object (for constructing nested inner classes).
260    *    <br>Can be <jk>null</jk> if there is no enclosing (outer) object; the parsed object is then not linked to a parent.
261    * @param isUrlParamValue
262    *    If <jk>true</jk>, then we're parsing a top-level URL-encoded value which is treated a bit different than the
263    *    default case.
264    * @param pMeta The current bean property being parsed.
265    *    <br>Can be <jk>null</jk> if no bean property metadata applies (e.g. the root value or an unknown property).
266    * @return The parsed object.
267    * @throws IOException Thrown by underlying stream.
268    * @throws ParseException Malformed input encountered.
269    * @throws ExecutableException Exception occurred on invoked constructor/method/field.
270    */
271   public <T> T readAnything(ClassMeta<?> eType, UonReader r, Object outer, boolean isUrlParamValue, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
272
273      if (eType == null)
274         eType = object();
275      var swap = (ObjectSwap<T,Object>)eType.getSwap(this);
276      var builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this);
277      ClassMeta<?> sType = null;
278      if (nn(builder))
279         sType = builder.getBuilderClassMeta(this);
280      else if (nn(swap))
281         sType = swap.getSwapClassMeta(this);
282      else
283         sType = eType;
284
285      if (sType.isOptional())
286         return (T)o(readAnything(eType.getElementType(), r, outer, isUrlParamValue, pMeta));
287
288      setCurrentClass(sType);
289
290      Object o = null;
291
292      int c = r.peekSkipWs();
293
294      if (c == -1 || c == AMP) {
295         // If parameter is blank and it's an array or collection, return an empty list.
296         if (sType.isCollectionOrArray())
297            o = sType.newInstance();
298         else if (sType.isString() || sType.isObject())
299            o = "";
300         else if (sType.isPrimitive())
301            o = sType.getPrimitiveDefault();
302         // Otherwise, leave null.
303      } else if (sType.isVoid()) {
304         var s = readString(r, isUrlParamValue);
305         if (nn(s))
306            throw new ParseException(this, "Expected 'null' for void value, but was '%s'.", s);
307      } else if (sType.isObject()) {
308         if (c == '(') {
309            var m = newGenericMap();
310            readIntoMap(r, m, string(), object(), pMeta);
311            o = cast(m, pMeta, eType);
312         } else if (c == '@') {
313            Collection l = newGenericList();
314            o = readIntoCollection(r, l, sType, isUrlParamValue, pMeta);
315         } else {
316            var s = readString(r, isUrlParamValue);
317            if (c != '\'') {
318               if ("true".equals(s) || "false".equals(s))
319                  o = b(s);
320               else {
321                  if (isNumeric(s))
322                     o = StringUtils.parseNumber(s, Number.class);
323                  else
324                     o = s;
325               }
326            } else {
327               o = s;
328            }
329         }
330      } else if (nn(builder)) {
331         var m = toBeanMap(builder.create(this, eType));
332         m = readIntoBeanMap(r, m);
333         o = m == null ? null : builder.build(this, checkRequired(m).getBean(), eType);
334      } else if (sType.canCreateNewBean(outer)) {
335         var m = newBeanMap(outer, sType.inner());
336         m = readIntoBeanMap(r, m);
337         o = m == null ? null : checkRequired(m).getBean();
338      } else if (sType.isMap()) {
339         var m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType));
340         o = readIntoMap(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
341      } else if (sType.isCollection()) {
342         if (c == '(') {
343            var m = newGenericMap();
344            readIntoMap(r, m, string(), object(), pMeta);
345            // Handle case where it's a collection, but serialized as a map with a _type or _value key.
346            if (m.containsKey(getBeanTypePropertyName(sType)))
347               o = cast(m, pMeta, eType);
348            // Handle case where it's a collection, but only a single value was specified.
349            else {
350               var l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance(outer) : newGenericList());
351               l.add(m.cast(sType.getElementType()));
352               o = l;
353            }
354         } else {
355            var l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance(outer) : newGenericList());
356            o = readIntoCollection(r, l, sType, isUrlParamValue, pMeta);
357         }
358      } else if (sType.isArray() || sType.isArgs()) {
359         if (c == '(') {
360            var m = newGenericMap();
361            readIntoMap(r, m, string(), object(), pMeta);
362            // Handle case where it's an array, but serialized as a map with a _type or _value key.
363            if (m.containsKey(getBeanTypePropertyName(sType)))
364               o = cast(m, pMeta, eType);
365            // Handle case where it's an array, but only a single value was specified.
366            else {
367               var l = listOfSize(1);
368               l.add(m.cast(sType.getElementType()));
369               o = toArray(sType, l);
370            }
371         } else {
372            var l = (ArrayList)readIntoCollection(r, list(), sType, isUrlParamValue, pMeta);
373            o = toArray(sType, l);
374         }
375      } else if (sType.isCharSequence()) {
376         o = readString(r, isUrlParamValue);
377      } else if (sType.isChar()) {
378         o = parseCharacter(readString(r, isUrlParamValue));
379      } else if (sType.isNumber()) {
380         o = readNumber(r, (Class<? extends Number>)sType.inner());
381      } else if (sType.isBoolean()) {
382         o = readBoolean(r);
383      } else if (sType.isDate()) {
384         o = readDate(readString(r, isUrlParamValue), sType);
385      } else if (sType.isCalendar()) {
386         o = readCalendar(readString(r, isUrlParamValue), sType);
387      } else if (sType.isTemporal()) {
388         o = readTemporal(readString(r, isUrlParamValue), sType);
389      } else if (sType.isDuration()) {
390         o = readDuration(readString(r, isUrlParamValue));
391      } else if (sType.isPeriod()) {
392         o = readPeriod(readString(r, isUrlParamValue));
393      } else if (sType.canCreateNewInstanceFromString(outer)) {
394         var s = readString(r, isUrlParamValue);
395         if (nn(s))
396            o = sType.newInstanceFromString(outer, s);
397      } else if (c == '(') {
398         // It could be a non-bean with _type attribute.
399         var m = newGenericMap();
400         readIntoMap(r, m, string(), object(), pMeta);
401         if (m.containsKey(getBeanTypePropertyName(sType)))
402            o = cast(m, pMeta, eType);
403         else if (nn(sType.getProxyInvocationHandler()))
404            o = checkRequired(newBeanMap(outer, sType.inner()).load(m)).getBean();
405         else
406            throw new ParseException(this, "Class '%s' could not be instantiated.  Reason: '%s'", cn(sType), sType.getNotABeanReason());
407      } else if (c == 'n') {
408         int ignored = r.read();
409         readNull(r);
410      } else {
411         throw new ParseException(this, "Class '%s' could not be instantiated.  Reason: '%s'", cn(sType), sType.getNotABeanReason());
412      }
413
414      if (o == null && sType.isPrimitive())
415         o = sType.getPrimitiveDefault();
416      if (nn(swap) && nn(o))
417         o = unswap(swap, o, eType);
418
419      if (nn(parentBean()))
420         setParent(eType, o, parentBean());
421
422      return (T)o;
423   }
424
425   @SuppressWarnings({
426      "java:S2447" // null is a valid UON value distinct from true/false; Boolean (boxed) is deliberately used for this tristate
427   })
428   private Boolean readBoolean(UonReader r) throws IOException, ParseException {
429      var s = readString(r, false);
430      if (s == null || s.equals("null"))
431         return null;
432      if (eqic(s, "true"))
433         return true;
434      if (eqic(s, "false"))
435         return false;
436      throw new ParseException(this, "Unrecognized syntax for boolean.  '%s'.", s);
437   }
438
439   private <T> BeanMap<T> readIntoBeanMap(UonReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException {
440
441      int c = r.readSkipWs();
442      if (c == -1 || c == AMP)
443         return null;
444      if (c == 'n')
445         return (BeanMap<T>)readNull(r);
446      if (c != '(')
447         throw new ParseException(this, "Expected '(' at beginning of object.");
448
449      // S1: Looking for attrName start.
450      // S2: Found attrName end, looking for =.
451      // S3: Found =, looking for valStart.
452      // S4: Looking for , or }
453
454      var state = S1;
455      var currAttr = "";
456      var pb = swapParentBean(m.getBean(false));
457      mark();
458      try {
459         while (true) {
460            c = r.read();
461            if (state == S1) {
462               if (c == ')' || c == -1 || c == AMP) {
463                  return m;
464               }
465               if (Character.isWhitespace(c))
466                  skipSpace(r);
467               else {
468                  r.unread();
469                  mark();
470                  currAttr = readAttrName(r, decoding);
471                  if (currAttr == null) { // Value was '%00'
472                     return null;
473                  }
474                  state = S2;
475               }
476            } else if (state == S2) {
477               if (c == EQ || c == '=')
478                  state = S3;
479               else if (c == -1 || c == ',' || c == ')' || c == AMP) {
480                  m.put(currAttr, null);
481                  if (c == ')' || c == -1 || c == AMP) {
482                     return m;
483                  }
484                  state = S1;
485               }
486            } else if (state == S3) {
487               if (c == -1 || c == ',' || c == ')' || c == AMP) {
488                  if (! currAttr.equals(getBeanTypePropertyName((ClassMeta<?>) m.getBeanInfo()))) {
489                     var pMeta = m.getPropertyMeta(currAttr);
490                     if (pMeta == null) {
491                        onUnknownProperty(currAttr, m, null);
492                        unmark();
493                     } else {
494                        unmark();
495                        var value = convertToType("", (ClassMeta<?>) pMeta.getBeanInfo());
496                        try {
497                           pMeta.set(m, currAttr, value);
498                        } catch (BeanRuntimeException e) {
499                           onBeanSetterException(pMeta, e);
500                           throw e;
501                        }
502                     }
503                  }
504                  if (c == -1 || c == ')' || c == AMP)
505                     return m;
506                  state = S1;
507               } else {
508                  if (! currAttr.equals(getBeanTypePropertyName((ClassMeta<?>) m.getBeanInfo()))) {
509                     var pMeta = m.getPropertyMeta(currAttr);
510                     if (pMeta == null) {
511                        onUnknownProperty(currAttr, m, readAnything(object(), r.unread(), m.getBean(false), false, null));
512                        unmark();
513                     } else {
514                        unmark();
515                        setCurrentProperty(pMeta);
516                        var cm = (ClassMeta<?>) pMeta.getBeanInfo();
517                        var value = readAnything(cm, r.unread(), m.getBean(false), false, pMeta);
518                        setName(cm, value, currAttr);
519                        try {
520                           pMeta.set(m, currAttr, value);
521                        } catch (BeanRuntimeException e) {
522                           onBeanSetterException(pMeta, e);
523                           throw e;
524                        }
525                        setCurrentProperty(null);
526                     }
527                  }
528                  state = S4;
529               }
530            } else if (state == S4) {
531               if (c == ',')
532                  state = S1;
533               else if (c == ')' || c == -1 || c == AMP) {
534                  return m;
535               }
536            }
537         }
538      } finally {
539         unmark();
540         swapParentBean(pb);
541      }
542   }
543
544   @SuppressWarnings({
545      "java:S1854", // argIndex=0 is the valid starting arg index; Sonar FP on early-return paths that never read it
546      "java:S2589" // Final if (state==S3) is always true given prior check; exhaustive state error-reporting pattern
547   })
548   private <E> Collection<E> readIntoCollection(UonReader r, Collection<E> l, ClassMeta<E> type, boolean isUrlParamValue, BeanPropertyMeta pMeta)
549      throws IOException, ParseException, ExecutableException {
550
551      int c = r.readSkipWs();
552      if (c == -1 || c == AMP)
553         return null;
554      if (c == 'n')
555         return (Collection<E>)readNull(r);
556
557      int argIndex = 0;
558
559      // 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")
560      // This is not allowed at lower levels since we use comma's as end delimiters.
561      boolean isInParens = (c == '@');
562      if (! isInParens) {
563         if (isUrlParamValue)
564            r.unread();
565         else
566            throw new ParseException(this, "Could not find '(' marking beginning of collection.");
567      } else {
568         int ignored = r.read();
569      }
570
571      if (isInParens) {
572         // S1: Looking for starting of first entry.
573         // S2: Looking for starting of subsequent entries.
574         // S3: Looking for , or ) after first entry.
575
576         var state = S1;
577         while (c != -1 && c != AMP) {
578            c = r.read();
579            if (state == S1 || state == S2) {
580               if (c == ')') {
581                  if (state == S2) {
582                     l.add((E)readAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, false, pMeta));
583                     int ignored = r.read();
584                  }
585                  return l;
586               } else if (Character.isWhitespace(c)) {
587                  skipSpace(r);
588               } else {
589                  l.add((E)readAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, false, pMeta));
590                  state = S3;
591               }
592            } else if (state == S3) {
593               if (c == ',') {
594                  state = S2;
595               } else if (c == ')') {
596                  return l;
597               }
598            }
599         }
600         if (state == S1 || state == S2)
601            throw new ParseException(this, "Could not find start of entry in array.");
602         if (state == S3)
603            throw new ParseException(this, "Could not find end of entry in array.");
604
605      } else {
606         // S1: Looking for starting of entry.
607         // S2: Looking for , or & or END after first entry.
608
609         var state = S1;
610         while (c != -1 && c != AMP) {
611            c = r.read();
612            if (state == S1) {
613               if (Character.isWhitespace(c)) {
614                  skipSpace(r);
615               } else {
616                  l.add((E)readAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, false, pMeta));
617                  state = S2;
618               }
619            } else if (state == S2) {
620               if (c == ',') {
621                  state = S1;
622               } else if (Character.isWhitespace(c)) {
623                  skipSpace(r);
624               } else if (c == AMP || c == -1) {
625                  r.unread();
626                  return l;
627               }
628            }
629         }
630      }
631
632      return null;  // Unreachable.
633   }
634
635   private <K,V> Map<K,V> readIntoMap(UonReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
636
637      if (keyType == null)
638         keyType = (ClassMeta<K>)string();
639
640      int c = r.read();
641      if (c == -1 || c == AMP)
642         return null;
643      if (c == 'n')
644         return (Map<K,V>)readNull(r);
645      if (c != '(')
646         throw new ParseException(this, "Expected '(' at beginning of object.");
647
648      // S1: Looking for attrName start.
649      // S2: Found attrName end, looking for =.
650      // S3: Found =, looking for valStart.
651      // S4: Looking for , or )
652
653      var state = S1;
654      K currAttr = null;
655      while (c != -1 && c != AMP) {
656         c = r.read();
657         if (state == S1) {
658            if (c == ')')
659               return m;
660            if (Character.isWhitespace(c))
661               skipSpace(r);
662            else {
663               r.unread();
664               var attr = readAttr(r, decoding);
665               currAttr = attr == null ? null : convertAttrToType(m, trim(attr.toString()), keyType);
666               state = S2;
667            }
668         } else if (state == S2) {
669            if (c == EQ || c == '=')
670               state = S3;
671            else if (c == -1 || c == ',' || c == ')' || c == AMP) {
672               if (currAttr == null) {
673                  // Value was '%00'
674                  r.unread();
675                  return null;
676               }
677               m.put(currAttr, null);
678               if (c == ')' || c == -1 || c == AMP)
679                  return m;
680               state = S1;
681            }
682         } else if (state == S3) {
683            if (c == -1 || c == ',' || c == ')' || c == AMP) {
684               V value = convertAttrToType(m, "", valueType);
685               m.put(currAttr, value);
686               if (c == -1 || c == ')' || c == AMP)
687                  return m;
688               state = S1;
689            } else {
690               V value = readAnything(valueType, r.unread(), m, false, pMeta);
691               setName(valueType, value, currAttr);
692               m.put(currAttr, value);
693               state = S4;
694            }
695         } else if (state == S4) {
696            if (c == ',')
697               state = S1;
698            else if (c == ')' || c == -1 || c == AMP) {
699               return m;
700            }
701         }
702      }
703
704      return null; // Reachable via trailing comma at EOF (lenient parse of e.g. "(a=1,").
705   }
706
707   private Object readNull(UonReader r) throws IOException, ParseException {
708      var s = readString(r, false);
709      if ("ull".equals(s))
710         return null;
711      throw new ParseException(this, "Unexpected character sequence: '%s'", s);
712   }
713
714   private Number readNumber(UonReader r, Class<? extends Number> c) throws IOException, ParseException {
715      var s = readString(r, false);
716      if (s == null)
717         return null;
718      return StringUtils.parseNumber(s, c);
719   }
720
721   /*
722    * Parses a string of the form "'foo'"
723    * All whitespace within parenthesis are preserved.
724    */
725   private String readPString(UonReader r) throws IOException, ParseException {
726
727      int ignored = r.read();
728      r.mark();
729      int c = 0;
730
731      boolean isInEscape = false;
732      while (c != -1) {
733         c = r.read();
734         if (! isInEscape && c == '\'')
735            return trim(r.getMarked(0, -1));
736         if (c == EQ)
737            r.replace('=');
738         isInEscape = isInEscape(c, r, isInEscape);
739      }
740      throw new ParseException(this, "Unmatched parenthesis");
741   }
742
743   /*
744    * Call this method after you've finished a parsing a string to make sure that if there's any
745    * remainder in the input, that it consists only of whitespace and comments.
746    */
747   private void validateEnd(UonReader r) throws IOException, ParseException {
748      if (! isValidateEnd())
749         return;
750      while (true) {
751         var c = r.read();
752         if (c == -1)
753            return;
754         if (! Character.isWhitespace(c))
755            throw new ParseException(this, "Remainder after parse: '%s'.", (char)c);
756      }
757   }
758
759   @Override /* Overridden from ParserSession */
760   protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
761      try (var r = getUonReader(pipe, decoding)) {
762         T o = readAnything(type, r, getOuter(), true, null);
763         validateEnd(r);
764         return o;
765      }
766   }
767
768   @Override /* Overridden from ReaderParserSession */
769   protected <E> Collection<E> doReadIntoCollection(ParserPipe pipe, Collection<E> c, Type elementType) throws Exception {
770      try (var r = getUonReader(pipe, decoding)) {
771         c = readIntoCollection(r, c, (ClassMeta<E>)getClassMeta(elementType), false, null);
772         validateEnd(r);
773         return c;
774      }
775   }
776
777   @Override /* Overridden from ReaderParserSession */
778   protected <K,V> Map<K,V> doReadIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws Exception {
779      try (var r = getUonReader(pipe, decoding)) {
780         m = readIntoMap(r, m, (ClassMeta<K>)getClassMeta(keyType), (ClassMeta<V>)getClassMeta(valueType), null);
781         validateEnd(r);
782         return m;
783      }
784   }
785
786   /**
787    * Decode <js>"%xx"</js> sequences.
788    *
789    * @see UonParser.Builder#decoding()
790    * @return
791    *    <jk>true</jk> if URI encoded characters should be decoded, <jk>false</jk> if they've already been decoded
792    *    before being passed to this parser.
793    */
794   protected final boolean isDecoding() { return decoding; }
795
796   /**
797    * Validate end.
798    *
799    * @see UonParser.Builder#validateEnd()
800    * @return
801    *    <jk>true</jk> if after parsing a POJO from the input, verifies that the remaining input in
802    *    the stream consists of only comments or whitespace.
803    */
804   protected final boolean isValidateEnd() { return validateEnd; }
805
806   /**
807    * Convenience method for parsing an attribute from the specified parser.
808    *
809    * @param r The reader.
810    *    <br>Must not be <jk>null</jk>.
811    * @param encoded Whether the attribute is encoded.
812    * @return The parsed object, or <jk>null</jk> if the attribute was the literal <js>"null"</js>.
813    * @throws IOException Exception thrown by underlying stream.
814    * @throws ParseException Attribute was malformed.
815    */
816   protected final Object readAttr(UonReader r, boolean encoded) throws IOException, ParseException {
817      return readAttrName(r, encoded);
818   }
819
820   /**
821    * Parses an attribute name from the specified reader.
822    *
823    * @param r The reader.
824    *    <br>Must not be <jk>null</jk>.
825    * @param encoded Whether the attribute is encoded.
826    * @return The parsed attribute name, or <jk>null</jk> if the attribute name was the literal <js>"null"</js>.
827    * @throws IOException Exception thrown by underlying stream.
828    * @throws ParseException Attribute name was malformed.
829    */
830   protected final String readAttrName(UonReader r, boolean encoded) throws IOException, ParseException {
831
832      // If string is of form 'xxx', we're looking for ' at the end.
833      // Otherwise, we're looking for '&' or '=' or WS or -1 denoting the end of this string.
834
835      int c = r.peekSkipWs();
836      if (c == '\'')
837         return readPString(r);
838
839      r.mark();
840      boolean isInEscape = false;
841      if (encoded) {
842         while (true) {
843            c = r.read();
844            if (! isInEscape) {
845               if (c == AMP || c == EQ || c == -1 || Character.isWhitespace(c)) {
846                  if (c != -1)
847                     r.unread();
848                  var s = r.getMarked();
849                  return ("null".equals(s) ? null : s);
850               }
851            } else if (c == AMP)
852               r.replace('&');
853            else if (c == EQ)
854               r.replace('=');
855            isInEscape = isInEscape(c, r, isInEscape);
856         }
857      } else {
858         while (true) {
859            c = r.read();
860            if (! isInEscape && (c == '=' || c == -1 || Character.isWhitespace(c))) {
861               if (c != -1)
862                  r.unread();
863               var s = r.getMarked();
864               return ("null".equals(s) ? null : trim(s));
865            }
866            isInEscape = isInEscape(c, r, isInEscape);
867         }
868      }
869   }
870
871   /**
872    * Parses a string value from the specified reader.
873    *
874    * @param r The input reader.
875    *    <br>Must not be <jk>null</jk>.
876    * @param isUrlParamValue Whether this is a URL parameter.
877    * @return The parsed string, or <jk>null</jk> if the value was the literal <js>"null"</js>.
878    * @throws IOException Exception thrown by underlying stream.
879    * @throws ParseException Malformed input found.
880    */
881   protected final String readString(UonReader r, boolean isUrlParamValue) throws IOException, ParseException {
882
883      // If string is of form 'xxx', we're looking for ' at the end.
884      // Otherwise, we're looking for ',' or ')' or -1 denoting the end of this string.
885
886      int c = r.peekSkipWs();
887      if (c == '\'')
888         return readPString(r);
889
890      r.mark();
891      boolean isInEscape = false;
892      String s = null;
893      var endChars = (isUrlParamValue ? endCharsParam : endCharsNormal);
894      while (c != -1) {
895         c = r.read();
896         if (! isInEscape && endChars.contains(c)) {
897            // If this is a URL parameter value, we're looking for:  &
898            // If not, we're looking for:  &,)
899            r.unread();
900            c = -1;
901         }
902         if (c == -1)
903            s = r.getMarked();
904         else if (c == EQ)
905            r.replace('=');
906         else if (Character.isWhitespace(c) && ! isUrlParamValue) {
907            s = r.getMarked(0, -1);
908            skipSpace(r);
909            c = -1;
910         }
911         isInEscape = isInEscape(c, r, isInEscape);
912      }
913
914      if (isUrlParamValue)
915         s = StringUtils.trim(s);
916
917      return ("null".equals(s) ? null : trim(s));
918   }
919
920   @Override /* Overridden from ReaderParserSession */
921   protected FluentMap<String,Object> properties() {
922      return super.properties()
923         .a("decoding", decoding)
924         .a("validateEnd", validateEnd);
925   }
926}