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