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 java.io.IOException;
016import java.lang.reflect.*;
017import java.util.*;
018
019import org.apache.juneau.*;
020import org.apache.juneau.collections.*;
021import org.apache.juneau.internal.*;
022import org.apache.juneau.parser.*;
023import org.apache.juneau.transform.*;
024import org.apache.juneau.uon.*;
025
026/**
027 * Session object that lives for the duration of a single use of {@link UrlEncodingParser}.
028 *
029 * <p>
030 * This class is NOT thread safe.
031 * It is typically discarded after one-time use although it can be reused against multiple inputs.
032 */
033@SuppressWarnings({ "unchecked", "rawtypes" })
034public class UrlEncodingParserSession extends UonParserSession {
035
036   private final UrlEncodingParser ctx;
037
038   /**
039    * Create a new session using properties specified in the context.
040    *
041    * @param ctx
042    *    The context creating this session object.
043    *    The context contains all the configuration settings for this object.
044    * @param args
045    *    Runtime session arguments.
046    */
047   protected UrlEncodingParserSession(UrlEncodingParser ctx, ParserSessionArgs args) {
048      super(ctx, args);
049      this.ctx = ctx;
050   }
051
052   /**
053    * Returns <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs.
054    *
055    * @param pMeta The metadata on the bean property.
056    * @return <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs.
057    */
058   public final boolean shouldUseExpandedParams(BeanPropertyMeta pMeta) {
059      ClassMeta<?> cm = pMeta.getClassMeta().getSerializedClassMeta(this);
060      if (cm.isCollectionOrArray()) {
061         if (isExpandedParams())
062            return true;
063         if (getUrlEncodingClassMeta(pMeta.getBeanMeta().getClassMeta()).isExpandedParams())
064            return true;
065      }
066      return false;
067   }
068
069   @Override /* ParserSession */
070   protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
071      try (UonReader r = getUonReader(pipe, true)) {
072         return parseAnything(type, r, getOuter());
073      }
074   }
075
076   @Override /* ReaderParserSession */
077   protected <K,V> Map<K,V> doParseIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws Exception {
078      try (UonReader r = getUonReader(pipe, true)) {
079         if (r.peekSkipWs() == '?')
080            r.read();
081         m = parseIntoMap2(r, m, getClassMeta(Map.class, keyType, valueType), null);
082         return m;
083      }
084   }
085
086   private <T> T parseAnything(ClassMeta<T> eType, UonReader r, Object outer) throws IOException, ParseException, ExecutableException {
087
088      if (eType == null)
089         eType = (ClassMeta<T>)object();
090      PojoSwap<T,Object> swap = (PojoSwap<T,Object>)eType.getSwap(this);
091      BuilderSwap<T,Object> builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this);
092      ClassMeta<?> sType = null;
093      if (builder != null)
094         sType = builder.getBuilderClassMeta(this);
095      else if (swap != null)
096         sType = swap.getSwapClassMeta(this);
097      else
098         sType = eType;
099
100      if (sType.isOptional())
101         return (T)Optional.ofNullable(parseAnything(eType.getElementType(), r, outer));
102
103      int c = r.peekSkipWs();
104      if (c == '?')
105         r.read();
106
107      Object o;
108
109      if (sType.isObject()) {
110         OMap m = new OMap(this);
111         parseIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer);
112         if (m.containsKey("_value"))
113            o = m.get("_value");
114         else
115            o = cast(m, null, eType);
116      } else if (sType.isMap()) {
117         Map m = (sType.canCreateNewInstance() ? (Map)sType.newInstance() : new OMap(this));
118         o = parseIntoMap2(r, m, sType, m);
119      } else if (builder != null) {
120         BeanMap m = toBeanMap(builder.create(this, eType));
121         m = parseIntoBeanMap(r, m);
122         o = m == null ? null : builder.build(this, m.getBean(), eType);
123      } else if (sType.canCreateNewBean(outer)) {
124         BeanMap m = newBeanMap(outer, sType.getInnerClass());
125         m = parseIntoBeanMap(r, m);
126         o = m == null ? null : m.getBean();
127      } else if (sType.isCollection() || sType.isArray() || sType.isArgs()) {
128         // ?1=foo&2=bar...
129         Collection c2 = ((sType.isArray() || sType.isArgs()) || ! sType.canCreateNewInstance(outer)) ? new OList(this) : (Collection)sType.newInstance();
130         Map<Integer,Object> m = new TreeMap<>();
131         parseIntoMap2(r, m, sType, c2);
132         c2.addAll(m.values());
133         if (sType.isArray())
134            o = ArrayUtils.toArray(c2, sType.getElementType().getInnerClass());
135         else if (sType.isArgs())
136            o = c2.toArray(new Object[c2.size()]);
137         else
138            o = c2;
139      } else {
140         // It could be a non-bean with _type attribute.
141         OMap m = new OMap(this);
142         parseIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer);
143         if (m.containsKey(getBeanTypePropertyName(eType)))
144            o = cast(m, null, eType);
145         else if (m.containsKey("_value"))
146            o = convertToType(m.get("_value"), sType);
147         else if (sType.getProxyInvocationHandler() != null) {
148            o = newBeanMap(outer, sType.getInnerClass()).load(m).getBean();
149         } else {
150            if (sType.getNotABeanReason() != null)
151               throw new ParseException(this, "Class ''{0}'' could not be instantiated as application/x-www-form-urlencoded.  Reason: ''{1}''", sType, sType.getNotABeanReason());
152            throw new ParseException(this, "Malformed application/x-www-form-urlencoded input for class ''{0}''.", sType);
153         }
154      }
155
156      if (swap != null && o != null)
157         o = unswap(swap, o, eType);
158
159      if (outer != null)
160         setParent(eType, o, outer);
161
162      return (T)o;
163   }
164
165   private <K,V> Map<K,V> parseIntoMap2(UonReader r, Map<K,V> m, ClassMeta<?> type, Object outer) throws IOException, ParseException, ExecutableException {
166
167      ClassMeta<K> keyType = (ClassMeta<K>)(type.isArgs() || type.isCollectionOrArray() ? getClassMeta(Integer.class) : type.getKeyType());
168
169      int c = r.peekSkipWs();
170      if (c == -1)
171         return m;
172
173      final int S1=1; // Looking for attrName start.
174      final int S2=2; // Found attrName end, looking for =.
175      final int S3=3; // Found =, looking for valStart.
176      final int S4=4; // Looking for & or end.
177      boolean isInEscape = false;
178
179      int state = S1;
180      int argIndex = 0;
181      K currAttr = null;
182      while (c != -1) {
183         c = r.read();
184         if (! isInEscape) {
185            if (state == S1) {
186               if (c == -1)
187                  return m;
188               r.unread();
189               Object attr = parseAttr(r, true);
190               currAttr = attr == null ? null : convertAttrToType(m, trim(attr.toString()), keyType);
191               state = S2;
192               c = 0; // Avoid isInEscape if c was '\'
193            } else if (state == S2) {
194               if (c == '\u0002')
195                  state = S3;
196               else if (c == -1 || c == '\u0001') {
197                  m.put(currAttr, null);
198                  if (c == -1)
199                     return m;
200                  state = S1;
201               }
202            } else if (state == S3) {
203               if (c == -1 || c == '\u0001') {
204                  ClassMeta<V> valueType = (ClassMeta<V>)(type.isArgs() ? type.getArg(argIndex++) : type.isCollectionOrArray() ? type.getElementType() : type.getValueType());
205                  V value = convertAttrToType(m, "", valueType);
206                  m.put(currAttr, value);
207                  if (c == -1)
208                     return m;
209                  state = S1;
210               } else  {
211                  // For performance, we bypass parseAnything for string values.
212                  ClassMeta<V> valueType = (ClassMeta<V>)(type.isArgs() ? type.getArg(argIndex++) : type.isCollectionOrArray() ? type.getElementType() : type.getValueType());
213                  V value = (V)(valueType.isString() ? super.parseString(r.unread(), true) : super.parseAnything(valueType, r.unread(), outer, true, null));
214
215                  // If we already encountered this parameter, turn it into a list.
216                  if (m.containsKey(currAttr) && valueType.isObject()) {
217                     Object v2 = m.get(currAttr);
218                     if (! (v2 instanceof OList)) {
219                        v2 = new OList(v2).setBeanSession(this);
220                        m.put(currAttr, (V)v2);
221                     }
222                     ((OList)v2).add(value);
223                  } else {
224                     m.put(currAttr, value);
225                  }
226                  state = S4;
227                  c = 0; // Avoid isInEscape if c was '\'
228               }
229            } else if (state == S4) {
230               if (c == '\u0001')
231                  state = S1;
232               else if (c == -1) {
233                  return m;
234               }
235            }
236         }
237         isInEscape = (c == '\\' && ! isInEscape);
238      }
239      if (state == S1)
240         throw new ParseException(this, "Could not find attribute name on object.");
241      if (state == S2)
242         throw new ParseException(this, "Could not find '=' following attribute name on object.");
243      if (state == S3)
244         throw new ParseException(this, "Dangling '=' found in object entry");
245      if (state == S4)
246         throw new ParseException(this, "Could not find end of object.");
247
248      return null; // Unreachable.
249   }
250
251   private <T> BeanMap<T> parseIntoBeanMap(UonReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException {
252
253      int c = r.peekSkipWs();
254      if (c == -1)
255         return m;
256
257      final int S1=1; // Looking for attrName start.
258      final int S2=2; // Found attrName end, looking for =.
259      final int S3=3; // Found =, looking for valStart.
260      final int S4=4; // Looking for , or }
261      boolean isInEscape = false;
262
263      int state = S1;
264      String currAttr = "";
265      mark();
266      try {
267         while (c != -1) {
268            c = r.read();
269            if (! isInEscape) {
270               if (state == S1) {
271                  if (c == -1) {
272                     return m;
273                  }
274                  r.unread();
275                  mark();
276                  currAttr = parseAttrName(r, true);
277                  if (currAttr == null)  // Value was '%00'
278                     return null;
279                  state = S2;
280               } else if (state == S2) {
281                  if (c == '\u0002')
282                     state = S3;
283                  else if (c == -1 || c == '\u0001') {
284                     m.put(currAttr, null);
285                     if (c == -1)
286                        return m;
287                     state = S1;
288                  }
289               } else if (state == S3) {
290                  if (c == -1 || c == '\u0001') {
291                     if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
292                        BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr);
293                        if (pMeta == null) {
294                           onUnknownProperty(currAttr, m, null);
295                           unmark();
296                        } else {
297                           unmark();
298                           setCurrentProperty(pMeta);
299                           // In cases of "&foo=", create an empty instance of the value if createable.
300                           // Otherwise, leave it null.
301                           ClassMeta<?> cm = pMeta.getClassMeta();
302                           if (cm.canCreateNewInstance()) {
303                              try {
304                                 pMeta.set(m, currAttr, cm.newInstance());
305                              } catch (BeanRuntimeException e) {
306                                 onBeanSetterException(pMeta, e);
307                                 throw e;
308                              }
309                           }
310                           setCurrentProperty(null);
311                        }
312                     }
313                     if (c == -1)
314                        return m;
315                     state = S1;
316                  } else {
317                     if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
318                        BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr);
319                        if (pMeta == null) {
320                           onUnknownProperty(currAttr, m, parseAnything(object(), r.unread(), m.getBean(false), true, null));
321                           unmark();
322                        } else {
323                           unmark();
324                           setCurrentProperty(pMeta);
325                           if (shouldUseExpandedParams(pMeta)) {
326                              ClassMeta et = pMeta.getClassMeta().getElementType();
327                              Object value = parseAnything(et, r.unread(), m.getBean(false), true, pMeta);
328                              setName(et, value, currAttr);
329                              try {
330                                 pMeta.add(m, currAttr, value);
331                              } catch (BeanRuntimeException e) {
332                                 onBeanSetterException(pMeta, e);
333                                 throw e;
334                              }
335                           } else {
336                              ClassMeta<?> cm = pMeta.getClassMeta();
337                              Object value = parseAnything(cm, r.unread(), m.getBean(false), true, pMeta);
338                              setName(cm, value, currAttr);
339                              try {
340                                 pMeta.set(m, currAttr, value);
341                              } catch (BeanRuntimeException e) {
342                                 onBeanSetterException(pMeta, e);
343                                 throw e;
344                              }
345                           }
346                           setCurrentProperty(null);
347                        }
348                     }
349                     state = S4;
350                  }
351               } else if (state == S4) {
352                  if (c == '\u0001')
353                     state = S1;
354                  else if (c == -1) {
355                     return m;
356                  }
357               }
358            }
359            isInEscape = (c == '\\' && ! isInEscape);
360         }
361         if (state == S1)
362            throw new ParseException(this, "Could not find attribute name on object.");
363         if (state == S2)
364            throw new ParseException(this, "Could not find '=' following attribute name on object.");
365         if (state == S3)
366            throw new ParseException(this, "Could not find value following '=' on object.");
367         if (state == S4)
368            throw new ParseException(this, "Could not find end of object.");
369      } finally {
370         unmark();
371      }
372
373      return null; // Unreachable.
374   }
375
376   //-----------------------------------------------------------------------------------------------------------------
377   // Properties
378   //-----------------------------------------------------------------------------------------------------------------
379
380   /**
381    * Configuration property:  Parser bean property collections/arrays as separate key/value pairs.
382    *
383    * @see UrlEncodingParser#URLENC_expandedParams
384    * @return
385    * <jk>false</jk> if serializing the array <c>[1,2,3]</c> results in <c>?key=$a(1,2,3)</c>.
386    * <br><jk>true</jk> if serializing the same array results in <c>?key=1&amp;key=2&amp;key=3</c>.
387    */
388   protected final boolean isExpandedParams() {
389      return ctx.isExpandedParams();
390   }
391
392   //-----------------------------------------------------------------------------------------------------------------
393   // Extended metadata
394   //-----------------------------------------------------------------------------------------------------------------
395
396   /**
397    * Returns the language-specific metadata on the specified class.
398    *
399    * @param cm The class to return the metadata on.
400    * @return The metadata.
401    */
402   protected UrlEncodingClassMeta getUrlEncodingClassMeta(ClassMeta<?> cm) {
403      return ctx.getUrlEncodingClassMeta(cm);
404   }
405
406   //-----------------------------------------------------------------------------------------------------------------
407   // Other methods
408   //-----------------------------------------------------------------------------------------------------------------
409
410   @Override /* Session */
411   public OMap toMap() {
412      return super.toMap()
413         .a("UrlEncodingParserSession", new DefaultFilteringOMap()
414         );
415   }
416}