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