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.urlencoding;
018
019import static org.apache.juneau.commons.lang.StateEnum.*;
020import static org.apache.juneau.commons.utils.Utils.*;
021
022import java.io.*;
023import java.lang.reflect.*;
024import java.nio.charset.*;
025import java.util.*;
026import java.util.function.*;
027
028import org.apache.juneau.*;
029import org.apache.juneau.collections.*;
030import org.apache.juneau.commons.reflect.*;
031import org.apache.juneau.commons.utils.*;
032import org.apache.juneau.httppart.*;
033import org.apache.juneau.parser.*;
034import org.apache.juneau.swap.*;
035import org.apache.juneau.uon.*;
036
037/**
038 * Session object that lives for the duration of a single use of {@link UrlEncodingParser}.
039 *
040 * <h5 class='section'>Notes:</h5><ul>
041 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
042 * </ul>
043 *
044 * <h5 class='section'>See Also:</h5><ul>
045 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/UrlEncodingBasics">URL-Encoding Basics</a>
046 * </ul>
047 */
048@SuppressWarnings({ "unchecked", "rawtypes", "resource" })
049public class UrlEncodingParserSession extends UonParserSession {
050   /**
051    * Builder class.
052    */
053   public static class Builder extends UonParserSession.Builder {
054
055      private UrlEncodingParser ctx;
056
057      /**
058       * Constructor
059       *
060       * @param ctx The context creating this session.
061       */
062      protected Builder(UrlEncodingParser ctx) {
063         super(ctx);
064         this.ctx = ctx;
065      }
066
067      @Override /* Overridden from Builder */
068      public <T> Builder apply(Class<T> type, Consumer<T> apply) {
069         super.apply(type, apply);
070         return this;
071      }
072
073      @Override
074      public UrlEncodingParserSession build() {
075         return new UrlEncodingParserSession(this);
076      }
077
078      @Override /* Overridden from Builder */
079      public Builder debug(Boolean value) {
080         super.debug(value);
081         return this;
082      }
083
084      @Override /* Overridden from Builder */
085      public Builder decoding(boolean value) {
086         super.decoding(value);
087         return this;
088      }
089
090      @Override /* Overridden from Builder */
091      public Builder fileCharset(Charset value) {
092         super.fileCharset(value);
093         return this;
094      }
095
096      @Override /* Overridden from Builder */
097      public Builder javaMethod(Method value) {
098         super.javaMethod(value);
099         return this;
100      }
101
102      @Override /* Overridden from Builder */
103      public Builder locale(Locale value) {
104         super.locale(value);
105         return this;
106      }
107
108      @Override /* Overridden from Builder */
109      public Builder mediaType(MediaType value) {
110         super.mediaType(value);
111         return this;
112      }
113
114      @Override /* Overridden from Builder */
115      public Builder mediaTypeDefault(MediaType value) {
116         super.mediaTypeDefault(value);
117         return this;
118      }
119
120      @Override /* Overridden from Builder */
121      public Builder outer(Object value) {
122         super.outer(value);
123         return this;
124      }
125
126      @Override /* Overridden from Builder */
127      public Builder properties(Map<String,Object> value) {
128         super.properties(value);
129         return this;
130      }
131
132      @Override /* Overridden from Builder */
133      public Builder property(String key, Object value) {
134         super.property(key, value);
135         return this;
136      }
137
138      @Override /* Overridden from Builder */
139      public Builder schema(HttpPartSchema value) {
140         super.schema(value);
141         return this;
142      }
143
144      @Override /* Overridden from Builder */
145      public Builder schemaDefault(HttpPartSchema value) {
146         super.schemaDefault(value);
147         return this;
148      }
149
150      @Override /* Overridden from Builder */
151      public Builder streamCharset(Charset value) {
152         super.streamCharset(value);
153         return this;
154      }
155
156      @Override /* Overridden from Builder */
157      public Builder timeZone(TimeZone value) {
158         super.timeZone(value);
159         return this;
160      }
161
162      @Override /* Overridden from Builder */
163      public Builder timeZoneDefault(TimeZone value) {
164         super.timeZoneDefault(value);
165         return this;
166      }
167
168      @Override /* Overridden from Builder */
169      public Builder unmodifiable() {
170         super.unmodifiable();
171         return this;
172      }
173   }
174
175   /**
176    * Creates a new builder for this object.
177    *
178    * @param ctx The context creating this session.
179    * @return A new builder.
180    */
181   public static Builder create(UrlEncodingParser ctx) {
182      return new Builder(ctx);
183   }
184
185   private final UrlEncodingParser ctx;
186
187   /**
188    * Constructor.
189    *
190    * @param builder The builder for this object.
191    */
192   public UrlEncodingParserSession(Builder builder) {
193      super(builder);
194      ctx = builder.ctx;
195   }
196
197   /**
198    * Returns <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs.
199    *
200    * @param pMeta The metadata on the bean property.
201    * @return <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs.
202    */
203   public final boolean shouldUseExpandedParams(BeanPropertyMeta pMeta) {
204      var cm = pMeta.getClassMeta().getSerializedClassMeta(this);
205      if (cm.isCollectionOrArray()) {
206         if (isExpandedParams() || getUrlEncodingClassMeta(pMeta.getBeanMeta().getClassMeta()).isExpandedParams())
207            return true;
208      }
209      return false;
210   }
211
212   private <T> T parseAnything(ClassMeta<T> eType, UonReader r, Object outer) throws IOException, ParseException, ExecutableException {
213
214      if (eType == null)
215         eType = (ClassMeta<T>)object();
216      var swap = (ObjectSwap<T,Object>)eType.getSwap(this);
217      var builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this);
218      var sType = (ClassMeta<?>)null;
219      if (nn(builder))
220         sType = builder.getBuilderClassMeta(this);
221      else if (nn(swap))
222         sType = swap.getSwapClassMeta(this);
223      else
224         sType = eType;
225
226      if (sType.isOptional())
227         return (T)opt(parseAnything(eType.getElementType(), r, outer));
228
229      int c = r.peekSkipWs();
230      if (c == '?')
231         r.read();  // NOSONAR - skip leading '?'.
232
233      Object o;
234
235      if (sType.isObject()) {
236         var m = new JsonMap(this);
237         parseIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer);
238         if (m.containsKey("_value"))
239            o = m.get("_value");
240         else
241            o = cast(m, null, eType);
242      } else if (sType.isMap()) {
243         var m = (sType.canCreateNewInstance() ? (Map)sType.newInstance() : newGenericMap(sType));
244         o = parseIntoMap2(r, m, sType, m);
245      } else if (nn(builder)) {
246         var m = toBeanMap(builder.create(this, eType));
247         m = parseIntoBeanMap(r, m);
248         o = m == null ? null : builder.build(this, m.getBean(), eType);
249      } else if (sType.canCreateNewBean(outer)) {
250         var m = newBeanMap(outer, sType.inner());
251         m = parseIntoBeanMap(r, m);
252         o = m == null ? null : m.getBean();
253      } else if (sType.isCollection() || sType.isArray() || sType.isArgs()) {
254         // ?1=foo&2=bar...
255         var c2 = ((sType.isArray() || sType.isArgs()) || ! sType.canCreateNewInstance(outer)) ? new JsonList(this) : (Collection)sType.newInstance();
256         var m = new TreeMap<Integer,Object>();
257         parseIntoMap2(r, m, sType, c2);
258         c2.addAll(m.values());
259         if (sType.isArgs())
260            o = c2.toArray(new Object[c2.size()]);
261         else if (sType.isArray())
262            o = CollectionUtils.toArray(c2, sType.getElementType().inner());
263         else
264            o = c2;
265      } else {
266         // It could be a non-bean with _type attribute.
267         var m = new JsonMap(this);
268         parseIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer);
269         if (m.containsKey(getBeanTypePropertyName(eType)))
270            o = cast(m, null, eType);
271         else if (m.containsKey("_value"))
272            o = convertToType(m.get("_value"), sType);
273         else if (nn(sType.getProxyInvocationHandler())) {
274            o = newBeanMap(outer, sType.inner()).load(m).getBean();
275         } else {
276            if (nn(sType.getNotABeanReason()))
277               throw new ParseException(this, "Class ''{0}'' could not be instantiated as application/x-www-form-urlencoded.  Reason: ''{1}''", sType, sType.getNotABeanReason());
278            throw new ParseException(this, "Malformed application/x-www-form-urlencoded input for class ''{0}''.", sType);
279         }
280      }
281
282      if (nn(swap) && nn(o))
283         o = unswap(swap, o, eType);
284
285      if (nn(outer))
286         setParent(eType, o, outer);
287
288      return (T)o;
289   }
290
291   private <T> BeanMap<T> parseIntoBeanMap(UonReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException {
292
293      int c = r.peekSkipWs();
294      if (c == -1)
295         return m;
296
297      // S1: Looking for attrName start.
298      // S2: Found attrName end, looking for =.
299      // S3: Found =, looking for valStart.
300      // S4: Looking for , or }
301
302      boolean isInEscape = false;
303
304      var state = S1;
305      var currAttr = "";
306      mark();
307      try {
308         while (c != -1) {
309            c = r.read();
310            if (! isInEscape) {
311               if (state == S1) {
312                  if (c == -1) {
313                     return m;
314                  }
315                  r.unread();
316                  mark();
317                  currAttr = parseAttrName(r, true);
318                  if (currAttr == null)  // Value was '%00'
319                     return null;
320                  state = S2;
321               } else if (state == S2) {
322                  if (c == '\u0002')
323                     state = S3;
324                  else if (c == -1 || c == '\u0001') {
325                     m.put(currAttr, null);
326                     if (c == -1)
327                        return m;
328                     state = S1;
329                  }
330               } else if (state == S3) {
331                  if (c == -1 || c == '\u0001') {
332                     if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
333                        var pMeta = m.getPropertyMeta(currAttr);
334                        if (pMeta == null) {
335                           onUnknownProperty(currAttr, m, null);
336                           unmark();
337                        } else {
338                           unmark();
339                           setCurrentProperty(pMeta);
340                           // In cases of "&foo=", create an empty instance of the value if createable.
341                           // Otherwise, leave it null.
342                           var cm = pMeta.getClassMeta();
343                           if (cm.canCreateNewInstance()) {
344                              try {
345                                 pMeta.set(m, currAttr, cm.newInstance());
346                              } catch (BeanRuntimeException e) {
347                                 onBeanSetterException(pMeta, e);
348                                 throw e;
349                              }
350                           }
351                           setCurrentProperty(null);
352                        }
353                     }
354                     if (c == -1)
355                        return m;
356                     state = S1;
357                  } else {
358                     if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
359                        var pMeta = m.getPropertyMeta(currAttr);
360                        if (pMeta == null) {
361                           onUnknownProperty(currAttr, m, parseAnything(object(), r.unread(), m.getBean(false), true, null));
362                           unmark();
363                        } else {
364                           unmark();
365                           setCurrentProperty(pMeta);
366                           if (shouldUseExpandedParams(pMeta)) {
367                              var et = pMeta.getClassMeta().getElementType();
368                              var value = parseAnything(et, r.unread(), m.getBean(false), true, pMeta);
369                              setName(et, value, currAttr);
370                              try {
371                                 pMeta.add(m, currAttr, value);
372                              } catch (BeanRuntimeException e) {
373                                 onBeanSetterException(pMeta, e);
374                                 throw e;
375                              }
376                           } else {
377                              var cm = pMeta.getClassMeta();
378                              var value = parseAnything(cm, r.unread(), m.getBean(false), true, pMeta);
379                              setName(cm, value, currAttr);
380                              try {
381                                 pMeta.set(m, currAttr, value);
382                              } catch (BeanRuntimeException e) {
383                                 onBeanSetterException(pMeta, e);
384                                 throw e;
385                              }
386                           }
387                           setCurrentProperty(null);
388                        }
389                     }
390                     state = S4;
391                  }
392               } else if (state == S4) {
393                  if (c == '\u0001')
394                     state = S1;
395                  else if (c == -1) {
396                     return m;
397                  }
398               }
399            }
400            isInEscape = (c == '\\' && ! isInEscape);
401         }
402         if (state == S1)
403            throw new ParseException(this, "Could not find attribute name on object.");
404         if (state == S2)
405            throw new ParseException(this, "Could not find '=' following attribute name on object.");
406         if (state == S3)
407            throw new ParseException(this, "Could not find value following '=' on object.");
408         if (state == S4)
409            throw new ParseException(this, "Could not find end of object.");
410      } finally {
411         unmark();
412      }
413
414      return null; // Unreachable.
415   }
416
417   private <K,V> Map<K,V> parseIntoMap2(UonReader r, Map<K,V> m, ClassMeta<?> type, Object outer) throws IOException, ParseException, ExecutableException {
418
419      var keyType = (ClassMeta<K>)(type.isArgs() || type.isCollectionOrArray() ? getClassMeta(Integer.class) : type.getKeyType());
420
421      int c = r.peekSkipWs();
422      if (c == -1)
423         return m;
424
425      // S1: Looking for attrName start.
426      // S2: Found attrName end, looking for =.
427      // S3: Found =, looking for valStart.
428      // S4: Looking for & or end.
429
430      boolean isInEscape = false;
431
432      var state = S1;
433      int argIndex = 0;
434      var currAttr = (K)null;
435      while (c != -1) {
436         c = r.read();
437         if (! isInEscape) {
438            if (state == S1) {
439               if (c == -1)
440                  return m;
441               r.unread();
442               var attr = parseAttr(r, true);
443               currAttr = attr == null ? null : convertAttrToType(m, trim(attr.toString()), keyType);
444               state = S2;
445               c = 0; // Avoid isInEscape if c was '\'
446            } else if (state == S2) {
447               if (c == '\u0002')
448                  state = S3;
449               else if (c == -1 || c == '\u0001') {
450                  m.put(currAttr, null);
451                  if (c == -1)
452                     return m;
453                  state = S1;
454               }
455            } else if (state == S3) {
456               if (c == -1 || c == '\u0001') {
457                  var valueType = (ClassMeta<V>)(type.isArgs() ? type.getArg(argIndex++) : type.isCollectionOrArray() ? type.getElementType() : type.getValueType());
458                  V value = convertAttrToType(m, "", valueType);
459                  m.put(currAttr, value);
460                  if (c == -1)
461                     return m;
462                  state = S1;
463               } else {
464                  // For performance, we bypass parseAnything for string values.
465                  var valueType = (ClassMeta<V>)(type.isArgs() ? type.getArg(argIndex++) : type.isCollectionOrArray() ? type.getElementType() : type.getValueType());
466                  V value = (V)(valueType.isString() ? super.parseString(r.unread(), true) : super.parseAnything(valueType, r.unread(), outer, true, null));
467
468                  // If we already encountered this parameter, turn it into a list.
469                  if (m.containsKey(currAttr) && valueType.isObject()) {
470                     Object v2 = m.get(currAttr);
471                     if (! (v2 instanceof JsonList)) {
472                        v2 = new JsonList(v2).setBeanSession(this);
473                        m.put(currAttr, (V)v2);
474                     }
475                     ((JsonList)v2).add(value);
476                  } else {
477                     m.put(currAttr, value);
478                  }
479                  state = S4;
480                  c = 0; // Avoid isInEscape if c was '\'
481               }
482            } else if (state == S4) {
483               if (c == '\u0001')
484                  state = S1;
485               else if (c == -1) {
486                  return m;
487               }
488            }
489         }
490         isInEscape = (c == '\\' && ! isInEscape);
491      }
492      if (state == S1)
493         throw new ParseException(this, "Could not find attribute name on object.");
494      if (state == S2)
495         throw new ParseException(this, "Could not find '=' following attribute name on object.");
496      if (state == S3)
497         throw new ParseException(this, "Dangling '=' found in object entry");
498      if (state == S4)
499         throw new ParseException(this, "Could not find end of object.");
500
501      return null; // Unreachable.
502   }
503
504   @Override /* Overridden from ParserSession */
505   protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
506      try (var r = getUonReader(pipe, true)) {
507         return parseAnything(type, r, getOuter());
508      }
509   }
510
511   @Override /* Overridden from ReaderParserSession */
512   protected <K,V> Map<K,V> doParseIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws Exception {
513      try (var r = getUonReader(pipe, true)) {
514         if (r.peekSkipWs() == '?')
515            r.read();  // NOSONAR - skip leading '?'.
516         m = parseIntoMap2(r, m, getClassMeta(Map.class, keyType, valueType), null);
517         return m;
518      }
519   }
520
521   /**
522    * Returns the language-specific metadata on the specified class.
523    *
524    * @param cm The class to return the metadata on.
525    * @return The metadata.
526    */
527   protected UrlEncodingClassMeta getUrlEncodingClassMeta(ClassMeta<?> cm) {
528      return ctx.getUrlEncodingClassMeta(cm);
529   }
530
531   /**
532    * Parser bean property collections/arrays as separate key/value pairs.
533    *
534    * @see UrlEncodingParser.Builder#expandedParams()
535    * @return
536    * <jk>false</jk> if serializing the array <c>[1,2,3]</c> results in <c>?key=$a(1,2,3)</c>.
537    * <br><jk>true</jk> if serializing the same array results in <c>?key=1&amp;key=2&amp;key=3</c>.
538    */
539   protected final boolean isExpandedParams() { return ctx.isExpandedParams(); }
540}