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.lang.reflect.*;
016import java.util.*;
017
018import org.apache.juneau.*;
019import org.apache.juneau.internal.*;
020import org.apache.juneau.parser.*;
021import org.apache.juneau.transform.*;
022import org.apache.juneau.uon.*;
023
024/**
025 * Session object that lives for the duration of a single use of {@link UrlEncodingParser}.
026 *
027 * <p>
028 * This class is NOT thread safe.
029 * It is typically discarded after one-time use although it can be reused against multiple inputs.
030 */
031@SuppressWarnings({ "unchecked", "rawtypes" })
032public class UrlEncodingParserSession extends UonParserSession {
033
034   private final UrlEncodingParser ctx;
035
036   /**
037    * Create a new session using properties specified in the context.
038    *
039    * @param ctx
040    *    The context creating this session object.
041    *    The context contains all the configuration settings for this object.
042    * @param args
043    *    Runtime session arguments.
044    */
045   protected UrlEncodingParserSession(UrlEncodingParser ctx, ParserSessionArgs args) {
046      super(ctx, args);
047      this.ctx = ctx;
048   }
049
050   @Override /* Session */
051   public ObjectMap asMap() {
052      return super.asMap()
053         .append("UrlEncodingParser", new ObjectMap()
054         );
055   }
056
057   /**
058    * Returns <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs.
059    *
060    * @param pMeta The metadata on the bean property.
061    * @return <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs.
062    */
063   public final boolean shouldUseExpandedParams(BeanPropertyMeta pMeta) {
064      ClassMeta<?> cm = pMeta.getClassMeta().getSerializedClassMeta(this);
065      if (cm.isCollectionOrArray()) {
066         if (isExpandedParams())
067            return true;
068         if (pMeta.getBeanMeta().getClassMeta().getExtendedMeta(UrlEncodingClassMeta.class).isExpandedParams())
069            return true;
070      }
071      return false;
072   }
073
074   @Override /* ParserSession */
075   protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws Exception {
076      try (UonReader r = getUonReader(pipe, true)) {
077         return parseAnything(type, r, getOuter());
078      }
079   }
080
081   @Override /* ReaderParserSession */
082   protected <K,V> Map<K,V> doParseIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws Exception {
083      try (UonReader r = getUonReader(pipe, true)) {
084         if (r.peekSkipWs() == '?')
085            r.read();
086         m = parseIntoMap2(r, m, getClassMeta(Map.class, keyType, valueType), null);
087         return m;
088      }
089   }
090
091   private <T> T parseAnything(ClassMeta<T> eType, UonReader r, Object outer) throws Exception {
092
093      if (eType == null)
094         eType = (ClassMeta<T>)object();
095      PojoSwap<T,Object> swap = (PojoSwap<T,Object>)eType.getPojoSwap(this);
096      BuilderSwap<T,Object> builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this);
097      ClassMeta<?> sType = null;
098      if (builder != null)
099         sType = builder.getBuilderClassMeta(this);
100      else if (swap != null)
101         sType = swap.getSwapClassMeta(this);
102      else
103         sType = eType;
104
105      int c = r.peekSkipWs();
106      if (c == '?')
107         r.read();
108
109      Object o;
110
111      if (sType.isObject()) {
112         ObjectMap m = new ObjectMap(this);
113         parseIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer);
114         if (m.containsKey("_value"))
115            o = m.get("_value");
116         else
117            o = cast(m, null, eType);
118      } else if (sType.isMap()) {
119         Map m = (sType.canCreateNewInstance() ? (Map)sType.newInstance() : new ObjectMap(this));
120         o = parseIntoMap2(r, m, sType, m);
121      } else if (builder != null) {
122         BeanMap m = toBeanMap(builder.create(this, eType));
123         m = parseIntoBeanMap(r, m);
124         o = m == null ? null : builder.build(this, m.getBean(), eType);
125      } else if (sType.canCreateNewBean(outer)) {
126         BeanMap m = newBeanMap(outer, sType.getInnerClass());
127         m = parseIntoBeanMap(r, m);
128         o = m == null ? null : m.getBean();
129      } else if (sType.isCollection() || sType.isArray() || sType.isArgs()) {
130         // ?1=foo&2=bar...
131         Collection c2 = ((sType.isArray() || sType.isArgs()) || ! sType.canCreateNewInstance(outer)) ? new ObjectList(this) : (Collection)sType.newInstance();
132         Map<Integer,Object> m = new TreeMap<>();
133         parseIntoMap2(r, m, sType, c2);
134         c2.addAll(m.values());
135         if (sType.isArray())
136            o = ArrayUtils.toArray(c2, sType.getElementType().getInnerClass());
137         else if (sType.isArgs())
138            o = c2.toArray(new Object[c2.size()]);
139         else
140            o = c2;
141      } else {
142         // It could be a non-bean with _type attribute.
143         ObjectMap m = new ObjectMap(this);
144         parseIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer);
145         if (m.containsKey(getBeanTypePropertyName(eType)))
146            o = cast(m, null, eType);
147         else if (m.containsKey("_value")) {
148            o = convertToType(m.get("_value"), sType);
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 = swap.unswap(this, 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 Exception {
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 ObjectList)) {
219                        v2 = new ObjectList(v2).setBeanSession(this);
220                        m.put(currAttr, (V)v2);
221                     }
222                     ((ObjectList)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 Exception {
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);
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                              pMeta.set(m, currAttr, cm.newInstance());
304                           setCurrentProperty(null);
305                        }
306                     }
307                     if (c == -1)
308                        return m;
309                     state = S1;
310                  } else {
311                     if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
312                        BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr);
313                        if (pMeta == null) {
314                           onUnknownProperty(currAttr, m);
315                           unmark();
316                           parseAnything(object(), r.unread(), m.getBean(false), true, null); // Read content anyway to ignore it
317                        } else {
318                           unmark();
319                           setCurrentProperty(pMeta);
320                           if (shouldUseExpandedParams(pMeta)) {
321                              ClassMeta et = pMeta.getClassMeta().getElementType();
322                              Object value = parseAnything(et, r.unread(), m.getBean(false), true, pMeta);
323                              setName(et, value, currAttr);
324                              pMeta.add(m, currAttr, value);
325                           } else {
326                              ClassMeta<?> cm = pMeta.getClassMeta();
327                              Object value = parseAnything(cm, r.unread(), m.getBean(false), true, pMeta);
328                              setName(cm, value, currAttr);
329                              pMeta.set(m, currAttr, value);
330                           }
331                           setCurrentProperty(null);
332                        }
333                     }
334                     state = S4;
335                  }
336               } else if (state == S4) {
337                  if (c == '\u0001')
338                     state = S1;
339                  else if (c == -1) {
340                     return m;
341                  }
342               }
343            }
344            isInEscape = (c == '\\' && ! isInEscape);
345         }
346         if (state == S1)
347            throw new ParseException(this, "Could not find attribute name on object.");
348         if (state == S2)
349            throw new ParseException(this, "Could not find '=' following attribute name on object.");
350         if (state == S3)
351            throw new ParseException(this, "Could not find value following '=' on object.");
352         if (state == S4)
353            throw new ParseException(this, "Could not find end of object.");
354      } finally {
355         unmark();
356      }
357
358      return null; // Unreachable.
359   }
360
361   //-----------------------------------------------------------------------------------------------------------------
362   // Properties
363   //-----------------------------------------------------------------------------------------------------------------
364
365   /**
366    * Configuration property:  Parser bean property collections/arrays as separate key/value pairs.
367    *
368    * @see UrlEncodingParser#URLENC_expandedParams
369    * @return
370    * <jk>false</jk> if serializing the array <code>[1,2,3]</code> results in <code>?key=$a(1,2,3)</code>.
371    * <br><jk>true</jk> if serializing the same array results in <code>?key=1&amp;key=2&amp;key=3</code>.
372    */
373   protected final boolean isExpandedParams() {
374      return ctx.isExpandedParams();
375   }
376}