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.urlencoding.UrlEncodingParser.*;
016
017import java.lang.reflect.*;
018import java.util.*;
019
020import org.apache.juneau.*;
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 boolean expandedParams;
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      expandedParams = getProperty(URLENC_expandedParams, boolean.class, ctx.expandedParams);
050   }
051
052   @Override /* Session */
053   public ObjectMap asMap() {
054      return super.asMap()
055         .append("UrlEncodingParser", new ObjectMap()
056            .append("expandedParams", expandedParams)
057         );
058   }
059
060   /**
061    * Returns <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs.
062    * 
063    * @param pMeta The metadata on the bean property.
064    * @return <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs.
065    */
066   public final boolean shouldUseExpandedParams(BeanPropertyMeta pMeta) {
067      ClassMeta<?> cm = pMeta.getClassMeta().getSerializedClassMeta(this);
068      if (cm.isCollectionOrArray()) {
069         if (expandedParams)
070            return true;
071         if (pMeta.getBeanMeta().getClassMeta().getExtendedMeta(UrlEncodingClassMeta.class).isExpandedParams())
072            return true;
073      }
074      return false;
075   }
076
077   @Override /* ParserSession */
078   protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws Exception {
079      try (UonReader r = getUonReader(pipe, true)) {
080         return parseAnything(type, r, getOuter());
081      }
082   }
083
084   @Override /* ReaderParserSession */
085   protected <K,V> Map<K,V> doParseIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws Exception {
086      try (UonReader r = getUonReader(pipe, true)) {
087         if (r.peekSkipWs() == '?')
088            r.read();
089         m = parseIntoMap2(r, m, getClassMeta(Map.class, keyType, valueType), null);
090         return m;
091      }
092   }
093
094   private <T> T parseAnything(ClassMeta<T> eType, UonReader r, Object outer) throws Exception {
095
096      if (eType == null)
097         eType = (ClassMeta<T>)object();
098      PojoSwap<T,Object> swap = (PojoSwap<T,Object>)eType.getPojoSwap(this);
099      BuilderSwap<T,Object> builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this);
100      ClassMeta<?> sType = null;
101      if (builder != null)
102         sType = builder.getBuilderClassMeta(this);
103      else if (swap != null)
104         sType = swap.getSwapClassMeta(this);
105      else
106         sType = eType;
107
108      int c = r.peekSkipWs();
109      if (c == '?')
110         r.read();
111
112      Object o;
113
114      if (sType.isObject()) {
115         ObjectMap m = new ObjectMap(this);
116         parseIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer);
117         if (m.containsKey("_value"))
118            o = m.get("_value");
119         else
120            o = cast(m, null, eType);
121      } else if (sType.isMap()) {
122         Map m = (sType.canCreateNewInstance() ? (Map)sType.newInstance() : new ObjectMap(this));
123         o = parseIntoMap2(r, m, sType, m);
124      } else if (builder != null) {
125         BeanMap m = toBeanMap(builder.create(this, eType));
126         m = parseIntoBeanMap(r, m);
127         o = m == null ? null : builder.build(this, m.getBean(), eType);
128      } else if (sType.canCreateNewBean(outer)) {
129         BeanMap m = newBeanMap(outer, sType.getInnerClass());
130         m = parseIntoBeanMap(r, m);
131         o = m == null ? null : m.getBean();
132      } else if (sType.isCollection() || sType.isArray() || sType.isArgs()) {
133         // ?1=foo&2=bar...
134         Collection c2 = ((sType.isArray() || sType.isArgs()) || ! sType.canCreateNewInstance(outer)) ? new ObjectList(this) : (Collection)sType.newInstance();
135         Map<Integer,Object> m = new TreeMap<>();
136         parseIntoMap2(r, m, sType, c2);
137         c2.addAll(m.values());
138         if (sType.isArray())
139            o = ArrayUtils.toArray(c2, sType.getElementType().getInnerClass());
140         else if (sType.isArgs())
141            o = c2.toArray(new Object[c2.size()]);
142         else
143            o = c2;
144      } else {
145         // It could be a non-bean with _type attribute.
146         ObjectMap m = new ObjectMap(this);
147         parseIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer);
148         if (m.containsKey(getBeanTypePropertyName(eType)))
149            o = cast(m, null, eType);
150         else if (m.containsKey("_value")) {
151            o = convertToType(m.get("_value"), sType);
152         } else {
153            if (sType.getNotABeanReason() != null)
154               throw new ParseException(loc(r), "Class ''{0}'' could not be instantiated as application/x-www-form-urlencoded.  Reason: ''{1}''", sType, sType.getNotABeanReason());
155            throw new ParseException(loc(r), "Malformed application/x-www-form-urlencoded input for class ''{0}''.", sType);
156         }
157      }
158
159      if (swap != null && o != null)
160         o = swap.unswap(this, o, eType);
161
162      if (outer != null)
163         setParent(eType, o, outer);
164
165      return (T)o;
166   }
167
168   private <K,V> Map<K,V> parseIntoMap2(UonReader r, Map<K,V> m, ClassMeta<?> type, Object outer) throws Exception {
169
170      ClassMeta<K> keyType = (ClassMeta<K>)(type.isArgs() || type.isCollectionOrArray() ? getClassMeta(Integer.class) : type.getKeyType());
171
172      int c = r.peekSkipWs();
173      if (c == -1)
174         return m;
175
176      final int S1=1; // Looking for attrName start.
177      final int S2=2; // Found attrName end, looking for =.
178      final int S3=3; // Found =, looking for valStart.
179      final int S4=4; // Looking for & or end.
180      boolean isInEscape = false;
181
182      int state = S1;
183      int argIndex = 0;
184      K currAttr = null;
185      while (c != -1) {
186         c = r.read();
187         if (! isInEscape) {
188            if (state == S1) {
189               if (c == -1)
190                  return m;
191               r.unread();
192               Object attr = parseAttr(r, true);
193               currAttr = attr == null ? null : convertAttrToType(m, trim(attr.toString()), keyType);
194               state = S2;
195               c = 0; // Avoid isInEscape if c was '\'
196            } else if (state == S2) {
197               if (c == '\u0002')
198                  state = S3;
199               else if (c == -1 || c == '\u0001') {
200                  m.put(currAttr, null);
201                  if (c == -1)
202                     return m;
203                  state = S1;
204               }
205            } else if (state == S3) {
206               if (c == -1 || c == '\u0001') {
207                  ClassMeta<V> valueType = (ClassMeta<V>)(type.isArgs() ? type.getArg(argIndex++) : type.isCollectionOrArray() ? type.getElementType() : type.getValueType());
208                  V value = convertAttrToType(m, "", valueType);
209                  m.put(currAttr, value);
210                  if (c == -1)
211                     return m;
212                  state = S1;
213               } else  {
214                  // For performance, we bypass parseAnything for string values.
215                  ClassMeta<V> valueType = (ClassMeta<V>)(type.isArgs() ? type.getArg(argIndex++) : type.isCollectionOrArray() ? type.getElementType() : type.getValueType());
216                  V value = (V)(valueType.isString() ? super.parseString(r.unread(), true) : super.parseAnything(valueType, r.unread(), outer, true, null));
217
218                  // If we already encountered this parameter, turn it into a list.
219                  if (m.containsKey(currAttr) && valueType.isObject()) {
220                     Object v2 = m.get(currAttr);
221                     if (! (v2 instanceof ObjectList)) {
222                        v2 = new ObjectList(v2).setBeanSession(this);
223                        m.put(currAttr, (V)v2);
224                     }
225                     ((ObjectList)v2).add(value);
226                  } else {
227                     m.put(currAttr, value);
228                  }
229                  state = S4;
230                  c = 0; // Avoid isInEscape if c was '\'
231               }
232            } else if (state == S4) {
233               if (c == '\u0001')
234                  state = S1;
235               else if (c == -1) {
236                  return m;
237               }
238            }
239         }
240         isInEscape = (c == '\\' && ! isInEscape);
241      }
242      if (state == S1)
243         throw new ParseException(loc(r), "Could not find attribute name on object.");
244      if (state == S2)
245         throw new ParseException(loc(r), "Could not find '=' following attribute name on object.");
246      if (state == S3)
247         throw new ParseException(loc(r), "Dangling '=' found in object entry");
248      if (state == S4)
249         throw new ParseException(loc(r), "Could not find end of object.");
250
251      return null; // Unreachable.
252   }
253
254   private <T> BeanMap<T> parseIntoBeanMap(UonReader r, BeanMap<T> m) throws Exception {
255
256      int c = r.peekSkipWs();
257      if (c == -1)
258         return m;
259
260      final int S1=1; // Looking for attrName start.
261      final int S2=2; // Found attrName end, looking for =.
262      final int S3=3; // Found =, looking for valStart.
263      final int S4=4; // Looking for , or }
264      boolean isInEscape = false;
265
266      int state = S1;
267      String currAttr = "";
268      int currAttrLine = -1, currAttrCol = -1;
269      while (c != -1) {
270         c = r.read();
271         if (! isInEscape) {
272            if (state == S1) {
273               if (c == -1) {
274                  return m;
275               }
276               r.unread();
277               currAttrLine= r.getLine();
278               currAttrCol = r.getColumn();
279               currAttr = parseAttrName(r, true);
280               if (currAttr == null)  // Value was '%00'
281                  return null;
282               state = S2;
283            } else if (state == S2) {
284               if (c == '\u0002')
285                  state = S3;
286               else if (c == -1 || c == '\u0001') {
287                  m.put(currAttr, null);
288                  if (c == -1)
289                     return m;
290                  state = S1;
291               }
292            } else if (state == S3) {
293               if (c == -1 || c == '\u0001') {
294                  if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
295                     BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr);
296                     if (pMeta == null) {
297                        onUnknownProperty(r.getPipe(), currAttr, m, currAttrLine, currAttrCol);
298                     } else {
299                        setCurrentProperty(pMeta);
300                        // In cases of "&foo=", create an empty instance of the value if createable.
301                        // Otherwise, leave it null.
302                        ClassMeta<?> cm = pMeta.getClassMeta();
303                        if (cm.canCreateNewInstance())
304                           pMeta.set(m, currAttr, cm.newInstance());
305                        setCurrentProperty(null);
306                     }
307                  }
308                  if (c == -1)
309                     return m;
310                  state = S1;
311               } else {
312                  if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
313                     BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr);
314                     if (pMeta == null) {
315                        onUnknownProperty(r.getPipe(), currAttr, m, currAttrLine, currAttrCol);
316                        parseAnything(object(), r.unread(), m.getBean(false), true, null); // Read content anyway to ignore it
317                     } else {
318                        setCurrentProperty(pMeta);
319                        if (shouldUseExpandedParams(pMeta)) {
320                           ClassMeta et = pMeta.getClassMeta().getElementType();
321                           Object value = parseAnything(et, r.unread(), m.getBean(false), true, pMeta);
322                           setName(et, value, currAttr);
323                           pMeta.add(m, currAttr, value);
324                        } else {
325                           ClassMeta<?> cm = pMeta.getClassMeta();
326                           Object value = parseAnything(cm, r.unread(), m.getBean(false), true, pMeta);
327                           setName(cm, value, currAttr);
328                           pMeta.set(m, currAttr, value);
329                        }
330                        setCurrentProperty(null);
331                     }
332                  }
333                  state = S4;
334               }
335            } else if (state == S4) {
336               if (c == '\u0001')
337                  state = S1;
338               else if (c == -1) {
339                  return m;
340               }
341            }
342         }
343         isInEscape = (c == '\\' && ! isInEscape);
344      }
345      if (state == S1)
346         throw new ParseException(loc(r), "Could not find attribute name on object.");
347      if (state == S2)
348         throw new ParseException(loc(r), "Could not find '=' following attribute name on object.");
349      if (state == S3)
350         throw new ParseException(loc(r), "Could not find value following '=' on object.");
351      if (state == S4)
352         throw new ParseException(loc(r), "Could not find end of object.");
353
354      return null; // Unreachable.
355   }
356}