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      int c = r.peekSkipWs();
100      if (c == '?')
101         r.read();
102
103      Object o;
104
105      if (sType.isObject()) {
106         ObjectMap m = new ObjectMap(this);
107         parseIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer);
108         if (m.containsKey("_value"))
109            o = m.get("_value");
110         else
111            o = cast(m, null, eType);
112      } else if (sType.isMap()) {
113         Map m = (sType.canCreateNewInstance() ? (Map)sType.newInstance() : new ObjectMap(this));
114         o = parseIntoMap2(r, m, sType, m);
115      } else if (builder != null) {
116         BeanMap m = toBeanMap(builder.create(this, eType));
117         m = parseIntoBeanMap(r, m);
118         o = m == null ? null : builder.build(this, m.getBean(), eType);
119      } else if (sType.canCreateNewBean(outer)) {
120         BeanMap m = newBeanMap(outer, sType.getInnerClass());
121         m = parseIntoBeanMap(r, m);
122         o = m == null ? null : m.getBean();
123      } else if (sType.isCollection() || sType.isArray() || sType.isArgs()) {
124         // ?1=foo&2=bar...
125         Collection c2 = ((sType.isArray() || sType.isArgs()) || ! sType.canCreateNewInstance(outer)) ? new ObjectList(this) : (Collection)sType.newInstance();
126         Map<Integer,Object> m = new TreeMap<>();
127         parseIntoMap2(r, m, sType, c2);
128         c2.addAll(m.values());
129         if (sType.isArray())
130            o = ArrayUtils.toArray(c2, sType.getElementType().getInnerClass());
131         else if (sType.isArgs())
132            o = c2.toArray(new Object[c2.size()]);
133         else
134            o = c2;
135      } else {
136         // It could be a non-bean with _type attribute.
137         ObjectMap m = new ObjectMap(this);
138         parseIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer);
139         if (m.containsKey(getBeanTypePropertyName(eType)))
140            o = cast(m, null, eType);
141         else if (m.containsKey("_value")) {
142            o = convertToType(m.get("_value"), sType);
143         } else {
144            if (sType.getNotABeanReason() != null)
145               throw new ParseException(this, "Class ''{0}'' could not be instantiated as application/x-www-form-urlencoded.  Reason: ''{1}''", sType, sType.getNotABeanReason());
146            throw new ParseException(this, "Malformed application/x-www-form-urlencoded input for class ''{0}''.", sType);
147         }
148      }
149
150      if (swap != null && o != null)
151         o = unswap(swap, o, eType);
152
153      if (outer != null)
154         setParent(eType, o, outer);
155
156      return (T)o;
157   }
158
159   private <K,V> Map<K,V> parseIntoMap2(UonReader r, Map<K,V> m, ClassMeta<?> type, Object outer) throws IOException, ParseException, ExecutableException {
160
161      ClassMeta<K> keyType = (ClassMeta<K>)(type.isArgs() || type.isCollectionOrArray() ? getClassMeta(Integer.class) : type.getKeyType());
162
163      int c = r.peekSkipWs();
164      if (c == -1)
165         return m;
166
167      final int S1=1; // Looking for attrName start.
168      final int S2=2; // Found attrName end, looking for =.
169      final int S3=3; // Found =, looking for valStart.
170      final int S4=4; // Looking for & or end.
171      boolean isInEscape = false;
172
173      int state = S1;
174      int argIndex = 0;
175      K currAttr = null;
176      while (c != -1) {
177         c = r.read();
178         if (! isInEscape) {
179            if (state == S1) {
180               if (c == -1)
181                  return m;
182               r.unread();
183               Object attr = parseAttr(r, true);
184               currAttr = attr == null ? null : convertAttrToType(m, trim(attr.toString()), keyType);
185               state = S2;
186               c = 0; // Avoid isInEscape if c was '\'
187            } else if (state == S2) {
188               if (c == '\u0002')
189                  state = S3;
190               else if (c == -1 || c == '\u0001') {
191                  m.put(currAttr, null);
192                  if (c == -1)
193                     return m;
194                  state = S1;
195               }
196            } else if (state == S3) {
197               if (c == -1 || c == '\u0001') {
198                  ClassMeta<V> valueType = (ClassMeta<V>)(type.isArgs() ? type.getArg(argIndex++) : type.isCollectionOrArray() ? type.getElementType() : type.getValueType());
199                  V value = convertAttrToType(m, "", valueType);
200                  m.put(currAttr, value);
201                  if (c == -1)
202                     return m;
203                  state = S1;
204               } else  {
205                  // For performance, we bypass parseAnything for string values.
206                  ClassMeta<V> valueType = (ClassMeta<V>)(type.isArgs() ? type.getArg(argIndex++) : type.isCollectionOrArray() ? type.getElementType() : type.getValueType());
207                  V value = (V)(valueType.isString() ? super.parseString(r.unread(), true) : super.parseAnything(valueType, r.unread(), outer, true, null));
208
209                  // If we already encountered this parameter, turn it into a list.
210                  if (m.containsKey(currAttr) && valueType.isObject()) {
211                     Object v2 = m.get(currAttr);
212                     if (! (v2 instanceof ObjectList)) {
213                        v2 = new ObjectList(v2).setBeanSession(this);
214                        m.put(currAttr, (V)v2);
215                     }
216                     ((ObjectList)v2).add(value);
217                  } else {
218                     m.put(currAttr, value);
219                  }
220                  state = S4;
221                  c = 0; // Avoid isInEscape if c was '\'
222               }
223            } else if (state == S4) {
224               if (c == '\u0001')
225                  state = S1;
226               else if (c == -1) {
227                  return m;
228               }
229            }
230         }
231         isInEscape = (c == '\\' && ! isInEscape);
232      }
233      if (state == S1)
234         throw new ParseException(this, "Could not find attribute name on object.");
235      if (state == S2)
236         throw new ParseException(this, "Could not find '=' following attribute name on object.");
237      if (state == S3)
238         throw new ParseException(this, "Dangling '=' found in object entry");
239      if (state == S4)
240         throw new ParseException(this, "Could not find end of object.");
241
242      return null; // Unreachable.
243   }
244
245   private <T> BeanMap<T> parseIntoBeanMap(UonReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException {
246
247      int c = r.peekSkipWs();
248      if (c == -1)
249         return m;
250
251      final int S1=1; // Looking for attrName start.
252      final int S2=2; // Found attrName end, looking for =.
253      final int S3=3; // Found =, looking for valStart.
254      final int S4=4; // Looking for , or }
255      boolean isInEscape = false;
256
257      int state = S1;
258      String currAttr = "";
259      mark();
260      try {
261         while (c != -1) {
262            c = r.read();
263            if (! isInEscape) {
264               if (state == S1) {
265                  if (c == -1) {
266                     return m;
267                  }
268                  r.unread();
269                  mark();
270                  currAttr = parseAttrName(r, true);
271                  if (currAttr == null)  // Value was '%00'
272                     return null;
273                  state = S2;
274               } else if (state == S2) {
275                  if (c == '\u0002')
276                     state = S3;
277                  else if (c == -1 || c == '\u0001') {
278                     m.put(currAttr, null);
279                     if (c == -1)
280                        return m;
281                     state = S1;
282                  }
283               } else if (state == S3) {
284                  if (c == -1 || c == '\u0001') {
285                     if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
286                        BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr);
287                        if (pMeta == null) {
288                           onUnknownProperty(currAttr, m);
289                           unmark();
290                        } else {
291                           unmark();
292                           setCurrentProperty(pMeta);
293                           // In cases of "&foo=", create an empty instance of the value if createable.
294                           // Otherwise, leave it null.
295                           ClassMeta<?> cm = pMeta.getClassMeta();
296                           if (cm.canCreateNewInstance())
297                              pMeta.set(m, currAttr, cm.newInstance());
298                           setCurrentProperty(null);
299                        }
300                     }
301                     if (c == -1)
302                        return m;
303                     state = S1;
304                  } else {
305                     if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
306                        BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr);
307                        if (pMeta == null) {
308                           onUnknownProperty(currAttr, m);
309                           unmark();
310                           parseAnything(object(), r.unread(), m.getBean(false), true, null); // Read content anyway to ignore it
311                        } else {
312                           unmark();
313                           setCurrentProperty(pMeta);
314                           if (shouldUseExpandedParams(pMeta)) {
315                              ClassMeta et = pMeta.getClassMeta().getElementType();
316                              Object value = parseAnything(et, r.unread(), m.getBean(false), true, pMeta);
317                              setName(et, value, currAttr);
318                              pMeta.add(m, currAttr, value);
319                           } else {
320                              ClassMeta<?> cm = pMeta.getClassMeta();
321                              Object value = parseAnything(cm, r.unread(), m.getBean(false), true, pMeta);
322                              setName(cm, value, currAttr);
323                              pMeta.set(m, currAttr, value);
324                           }
325                           setCurrentProperty(null);
326                        }
327                     }
328                     state = S4;
329                  }
330               } else if (state == S4) {
331                  if (c == '\u0001')
332                     state = S1;
333                  else if (c == -1) {
334                     return m;
335                  }
336               }
337            }
338            isInEscape = (c == '\\' && ! isInEscape);
339         }
340         if (state == S1)
341            throw new ParseException(this, "Could not find attribute name on object.");
342         if (state == S2)
343            throw new ParseException(this, "Could not find '=' following attribute name on object.");
344         if (state == S3)
345            throw new ParseException(this, "Could not find value following '=' on object.");
346         if (state == S4)
347            throw new ParseException(this, "Could not find end of object.");
348      } finally {
349         unmark();
350      }
351
352      return null; // Unreachable.
353   }
354
355   //-----------------------------------------------------------------------------------------------------------------
356   // Properties
357   //-----------------------------------------------------------------------------------------------------------------
358
359   /**
360    * Configuration property:  Parser bean property collections/arrays as separate key/value pairs.
361    *
362    * @see UrlEncodingParser#URLENC_expandedParams
363    * @return
364    * <jk>false</jk> if serializing the array <c>[1,2,3]</c> results in <c>?key=$a(1,2,3)</c>.
365    * <br><jk>true</jk> if serializing the same array results in <c>?key=1&amp;key=2&amp;key=3</c>.
366    */
367   protected final boolean isExpandedParams() {
368      return ctx.isExpandedParams();
369   }
370
371   //-----------------------------------------------------------------------------------------------------------------
372   // Other methods
373   //-----------------------------------------------------------------------------------------------------------------
374
375   @Override /* Session */
376   public ObjectMap toMap() {
377      return super.toMap()
378         .append("UrlEncodingParserSession", new DefaultFilteringObjectMap()
379         );
380   }
381}