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