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