001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.marshall.urlencoding;
018
019import static org.apache.juneau.commons.lang.StateEnum.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021
022import java.io.*;
023import java.lang.reflect.*;
024import java.util.*;
025
026import org.apache.juneau.commons.bean.*;
027import org.apache.juneau.commons.reflect.*;
028import org.apache.juneau.commons.utils.*;
029import org.apache.juneau.marshall.*;
030import org.apache.juneau.marshall.collections.*;
031import org.apache.juneau.marshall.parser.*;
032import org.apache.juneau.marshall.swap.spi.*;
033import org.apache.juneau.marshall.uon.*;
034
035/**
036 * Session object that lives for the duration of a single use of {@link UrlEncodingParser}.
037 *
038 * <h5 class='section'>Notes:</h5><ul>
039 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
040 * </ul>
041 *
042 * <h5 class='section'>See Also:</h5><ul>
043 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/UrlEncodingSupport">URL-Encoding</a>
044 * </ul>
045 */
046@SuppressWarnings({
047   "java:S110", // Inheritance depth acceptable for session hierarchy
048   "java:S115", // Constants use UPPER_snakeCase convention (e.g., CONST_value)
049   "java:S1168", // readIntoBeanMap()/readIntoMap2() return null for blank or literal null input, which callers treat as 'no value'.
050   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
051   "java:S3776", // readAnything(), readIntoBeanMap() and readIntoMap2() are state machines over the URL-encoded grammar; splitting them would obscure the parse flow.
052   "rawtypes", // Raw types necessary for generic type handling
053   "resource", // UonReader is managed by caller
054   "unchecked", // Type erasure requires unchecked casts
055   "unused" // Intentionally unused; variable/parameter is required by the interface contract
056})
057public class UrlEncodingParserSession extends UonParserSession {
058
059   /**
060    * Builder class.
061    */
062   public static class Builder extends UonParserSession.Builder<Builder> {
063
064      private boolean expandedParams;
065      private final UrlEncodingParser ctx;
066
067      /**
068       * Constructor
069       *
070       * @param ctx The context creating this session.
071       *    <br>Cannot be <jk>null</jk>.
072       */
073      protected Builder(UrlEncodingParser ctx) {
074         super(ctx);
075         this.ctx = ctx;
076         expandedParams = ctx.isExpandedParams();
077      }
078
079      @Override
080      public UrlEncodingParserSession build() {
081         return new UrlEncodingParserSession(this);
082      }
083
084      /**
085       * Serialize bean property collections/arrays as separate key/value pairs.
086       *
087       * @param value The new value for this setting.
088       * @return This object.
089       */
090      public Builder expandedParams(boolean value) {
091         expandedParams = value;
092         return this;
093      }
094
095      @Override /* Overridden from Builder */
096      public Builder property(String key, Object value) {
097         if (key == null) { super.property(key, value); return this; }
098         switch (key) {
099            case "expandedParams", "UrlEncodingParserSession.expandedParams":
100               return expandedParams(cvt(value, Boolean.class));
101            default:
102               super.property(key, value);
103               return this;
104         }
105      }
106
107   }
108
109   /**
110    * Creates a new builder for this object.
111    *
112    * @param ctx The context creating this session.
113    *    <br>Cannot be <jk>null</jk>.
114    * @return A new builder.
115    */
116   public static Builder create(UrlEncodingParser ctx) {
117      return new Builder(ctx);
118   }
119
120   private final UrlEncodingParser ctx;
121   private final boolean expandedParams;
122
123   /**
124    * Constructor.
125    *
126    * @param builder The builder for this object.
127    */
128   public UrlEncodingParserSession(Builder builder) {
129      super(builder);
130      ctx = builder.ctx;
131      expandedParams = builder.expandedParams;
132   }
133
134   /**
135    * Returns <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs.
136    *
137    * @param pMeta The metadata on the bean property.
138    * @return <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs.
139    */
140   public final boolean shouldUseExpandedParams(BeanPropertyMeta pMeta) {
141      var cm = ((ClassMeta<?>) pMeta.getBeanInfo()).getSerializedClassMeta(this);
142      return cm.isCollectionOrArray() && (isExpandedParams() || getUrlEncodingClassMeta((ClassMeta<?>) pMeta.getBeanMeta().getBeanInfo()).isExpandedParams());
143   }
144
145   private <T> T readAnything(ClassMeta<T> eType, UonReader r, Object outer) throws IOException, ParseException, ExecutableException {
146
147      if (eType == null)
148         eType = (ClassMeta<T>)object();
149      var swap = (ObjectSwap<T,Object>)eType.getSwap(this);
150      var builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this);
151      ClassMeta<?> sType = null;
152      if (nn(builder))
153         sType = builder.getBuilderClassMeta(this);
154      else if (nn(swap))
155         sType = swap.getSwapClassMeta(this);
156      else
157         sType = eType;
158
159      if (sType.isOptional())
160         return (T)o(readAnything(eType.getElementType(), r, outer));
161
162      int c = r.peekSkipWs();
163      if (c == '?') {
164         int ignored = r.read();
165      }
166
167      Object o;
168
169      if (sType.isObject()) {
170         var m = newGenericMap();
171         readIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer);
172         if (m.containsKey("_value"))
173            o = m.get("_value");
174         else
175            o = cast(m, null, eType);
176      } else if (nn(builder)) {
177         var m = toBeanMap(builder.create(this, eType));
178         m = readIntoBeanMap(r, m);
179         o = m == null ? null : builder.build(this, checkRequired(m).getBean(), eType);
180      } else if (sType.canCreateNewBean(outer)) {
181         var m = newBeanMap(outer, sType.inner());
182         m = readIntoBeanMap(r, m);
183         o = m == null ? null : checkRequired(m).getBean();
184      } else if (sType.isMap()) {
185         var m = (sType.canCreateNewInstance() ? (Map)sType.newInstance() : newGenericMap(sType));
186         o = readIntoMap2(r, m, sType, m);
187      } else if (sType.isCollection() || sType.isArray() || sType.isArgs()) {
188         // ?1=foo&2=bar...
189         var c2 = ((sType.isArray() || sType.isArgs()) || ! sType.canCreateNewInstance(outer)) ? newGenericList() : (Collection)sType.newInstance();
190         var m = new TreeMap<Integer,Object>();
191         readIntoMap2(r, m, sType, c2);
192         c2.addAll(m.values());
193         if (sType.isArgs())
194            o = c2.toArray(new Object[c2.size()]);
195         else if (sType.isArray())
196            o = CollectionUtils.toArray(c2, sType.getElementType().inner());
197         else
198            o = c2;
199      } else {
200         // It could be a non-bean with _type attribute.
201         var m = newGenericMap();
202         readIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer);
203         if (m.containsKey(getBeanTypePropertyName(eType)))
204            o = cast(m, null, eType);
205         else if (m.containsKey("_value"))
206            o = unwrapValueAs(m.get("_value"), sType);
207         else if (nn(sType.getProxyInvocationHandler())) {
208            o = checkRequired(newBeanMap(outer, sType.inner()).load(m)).getBean();
209         } else {
210            if (nn(sType.getNotABeanReason()))
211               throw new ParseException(this, "Class '%s' could not be instantiated as application/x-www-form-urlencoded.  Reason: '%s'", sType, sType.getNotABeanReason());
212            throw new ParseException(this, "Malformed application/x-www-form-urlencoded input for class '%s'.", sType);
213         }
214      }
215
216      if (nn(swap) && nn(o))
217         o = unswap(swap, o, eType);
218
219      if (nn(parentBean()))
220         setParent(eType, o, parentBean());
221
222      return (T)o;
223   }
224
225   @SuppressWarnings({
226      "java:S125", // State-machine comments (S1: ..., S2: ...)
227      "java:S2177", // Intentional: UrlEncodingParserSession provides its own private readIntoBeanMap() with expanded-params logic
228      "java:S2583", // State variables persist across loop iterations
229      "java:S2589", // Final if (state==S4) is always true given prior checks; exhaustive state error-reporting pattern
230      "java:S6541" // Single-threaded session contexts do not require synchronization
231   })
232   private <T> BeanMap<T> readIntoBeanMap(UonReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException {
233
234      int c = r.peekSkipWs();
235      if (c == -1)
236         return m;
237
238      // S1: Looking for attrName start.
239      // S2: Found attrName end, looking for =.
240      // S3: Found =, looking for valStart.
241      // S4: Looking for , or }
242
243      boolean isInEscape = false;
244
245      var state = S1;
246      var currAttr = "";
247      var pb = swapParentBean(m.getBean(false));
248      mark();
249      try {
250         while (c != -1) {
251            c = r.read();
252            if (! isInEscape) {
253               if (state == S1) {
254                  if (c == -1) {
255                     return m;
256                  }
257                  r.unread();
258                  mark();
259                  currAttr = readAttrName(r, true);
260                  if (currAttr == null)  // Value was '%00'
261                     return null;
262                  state = S2;
263               } else if (state == S2) {
264                  if (c == '\u0002')
265                     state = S3;
266                  else if (c == -1 || c == '\u0001') {
267                     m.put(currAttr, null);
268                     if (c == -1)
269                        return m;
270                     state = S1;
271                  }
272               } else if (state == S3) {  // NOSONAR - State check necessary for state machine
273                  if (c == -1 || c == '\u0001') {
274                     if (! currAttr.equals(getBeanTypePropertyName((ClassMeta<?>) m.getBeanInfo()))) {
275                        var pMeta = m.getPropertyMeta(currAttr);
276                        if (pMeta == null) {
277                           onUnknownProperty(currAttr, m, null);
278                           unmark();
279                        } else {
280                           unmark();
281                           setCurrentProperty(pMeta);
282                           // In cases of "&foo=", create an empty instance of the value if createable.
283                           // Otherwise, leave it null.  Arrays (especially primitive arrays
284                           // such as byte[]) report canCreateNewInstance()=false because their
285                           // element type has no no-arg constructor, but ClassMeta.newInstance()
286                           // already handles arrays via Array.newInstance(...,0); fall through
287                           // to that path so empty expanded-params arrays round-trip from the
288                           // "key=" sentinel emitted by the serializer.
289                           var cm = (ClassMeta<?>) pMeta.getBeanInfo();
290                           if (cm.canCreateNewInstance() || cm.isArray()) {
291                              try {
292                                 pMeta.set(m, currAttr, cm.newInstance());
293                              } catch (BeanRuntimeException e) {
294                                 onBeanSetterException(pMeta, e);
295                                 throw e;
296                              }
297                           }
298                           setCurrentProperty(null);
299                        }
300                     }
301                     if (c == -1)
302                        return m;
303                     state = S1;
304                  } else {
305                     if (! currAttr.equals(getBeanTypePropertyName((ClassMeta<?>) m.getBeanInfo()))) {
306                        var pMeta = m.getPropertyMeta(currAttr);
307                        if (pMeta == null) {
308                           onUnknownProperty(currAttr, m, readAnything(object(), r.unread(), m.getBean(false), true, null));
309                           unmark();
310                        } else {
311                           unmark();
312                           setCurrentProperty(pMeta);
313                           if (shouldUseExpandedParams(pMeta)) {
314                              var et = ((ClassMeta<?>) pMeta.getBeanInfo()).getElementType();
315                              var value = readAnything(et, r.unread(), m.getBean(false), true, pMeta);
316                              setName(et, value, currAttr);
317                              try {
318                                 pMeta.add(m, currAttr, value);
319                              } catch (BeanRuntimeException e) {
320                                 onBeanSetterException(pMeta, e);
321                                 throw e;
322                              }
323                           } else {
324                              var cm = (ClassMeta<?>) pMeta.getBeanInfo();
325                              var value = readAnything(cm, r.unread(), m.getBean(false), true, pMeta);
326                              setName(cm, value, currAttr);
327                              try {
328                                 pMeta.set(m, currAttr, value);
329                              } catch (BeanRuntimeException e) {
330                                 onBeanSetterException(pMeta, e);
331                                 throw e;
332                              }
333                           }
334                           setCurrentProperty(null);
335                        }
336                     }
337                     state = S4;
338                  }
339               } else if (state == S4) {
340                  if (c == '\u0001')
341                     state = S1;
342                  else if (c == -1) {
343                     return m;
344                  }
345               }
346            }
347            isInEscape = (c == '\\' && ! isInEscape);
348         }
349         if (state == S1)
350            throw new ParseException(this, "Could not find attribute name on object.");
351         if (state == S2)
352            throw new ParseException(this, "Could not find '=' following attribute name on object.");
353         if (state == S3)
354            throw new ParseException(this, "Could not find value following '=' on object.");
355         if (state == S4)
356            throw new ParseException(this, "Could not find end of object.");
357      } finally {
358         unmark();
359         swapParentBean(pb);
360      }
361
362      return null; // Unreachable.
363   }
364
365   private <K,V> Map<K,V> readIntoMap2(UonReader r, Map<K,V> m, ClassMeta<?> type, Object outer) throws IOException, ParseException, ExecutableException {
366
367      var keyType = (ClassMeta<K>)(type.isArgs() || type.isCollectionOrArray() ? getClassMeta(Integer.class) : type.getKeyType());
368
369      int c = r.peekSkipWs();
370      if (c == -1)
371         return m;
372
373      // S1: Looking for attrName start.
374      // S2: Found attrName end, looking for =.
375      // S3: Found =, looking for valStart.
376      // S4: Looking for & or end.
377
378      boolean isInEscape = false;
379
380      var state = S1;
381      int argIndex = 0;
382      K currAttr = null;
383      while (c != -1) {
384         c = r.read();
385         if (! isInEscape) {
386            if (state == S1) {
387               if (c == -1)
388                  return m;
389               r.unread();
390               var attr = readAttr(r, true);
391               currAttr = attr == null ? null : convertAttrToType(m, trim(attr.toString()), keyType);
392               state = S2;
393               c = 0; // Avoid isInEscape if c was '\'
394            } else if (state == S2) {
395               if (c == '\u0002')
396                  state = S3;
397               else if (c == -1 || c == '\u0001') {
398                  m.put(currAttr, null);
399                  if (c == -1)
400                     return m;
401                  state = S1;
402               }
403            } else if (state == S3) {
404               if (c == -1 || c == '\u0001') {
405                  ClassMeta<V> valueType;
406                  if (type.isArgs()) {
407                     valueType = (ClassMeta<V>)type.getArg(argIndex++);
408                  } else if (type.isCollectionOrArray()) {
409                     valueType = (ClassMeta<V>)type.getElementType();
410                  } else {
411                     valueType = (ClassMeta<V>)type.getValueType();
412                  }
413                  V value = convertAttrToType(m, "", valueType);
414                  m.put(currAttr, value);
415                  if (c == -1)
416                     return m;
417                  state = S1;
418               } else {
419                  // For performance, we bypass readAnything for string values.
420                  ClassMeta<V> valueType;
421                  if (type.isArgs()) {
422                     valueType = (ClassMeta<V>)type.getArg(argIndex++);
423                  } else if (type.isCollectionOrArray()) {
424                     valueType = (ClassMeta<V>)type.getElementType();
425                  } else {
426                     valueType = (ClassMeta<V>)type.getValueType();
427                  }
428                  V value = (V)(valueType.isString() ? super.readString(r.unread(), true) : super.readAnything(valueType, r.unread(), outer, true, null));
429
430                  // If we already encountered this parameter, turn it into a list.
431                  if (m.containsKey(currAttr) && valueType.isObject()) {
432                     Object v2 = m.get(currAttr);
433                     if (! (v2 instanceof MarshalledList)) {
434                        v2 = new JsonList(v2).setBeanSession(this);
435                        m.put(currAttr, (V)v2);
436                     }
437                     ((MarshalledList)v2).add(value);
438                  } else {
439                     m.put(currAttr, value);
440                  }
441                  state = S4;
442                  c = 0; // Avoid isInEscape if c was '\'
443               }
444            } else if (state == S4) {
445               if (c == '\u0001')
446                  state = S1;
447               else if (c == -1) {
448                  return m;
449               }
450            }
451         }
452         isInEscape = (c == '\\' && ! isInEscape);
453      }
454      if (state == S1)
455         throw new ParseException(this, "Could not find attribute name on object.");
456      if (state == S2)
457         throw new ParseException(this, "Could not find '=' following attribute name on object.");
458      if (state == S3)
459         throw new ParseException(this, "Dangling '=' found in object entry");
460      if (state == S4)
461         throw new ParseException(this, "Could not find end of object.");
462
463      return null; // Unreachable.
464   }
465
466   @Override /* Overridden from ParserSession */
467   protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
468      try (var r = getUonReader(pipe, true)) {
469         return readAnything(type, r, getOuter());
470      }
471   }
472
473   @Override /* Overridden from ReaderParserSession */
474   protected <K,V> Map<K,V> doReadIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws Exception {
475      try (var r = getUonReader(pipe, true)) {
476         if (r.peekSkipWs() == '?') {
477            int ignored = r.read();
478         }
479         m = readIntoMap2(r, m, getClassMeta(Map.class, keyType, valueType), null);
480         return m;
481      }
482   }
483
484   /**
485    * Returns the language-specific metadata on the specified class.
486    *
487    * @param cm The class to return the metadata on.
488    * @return The metadata.
489    */
490   protected UrlEncodingClassMeta getUrlEncodingClassMeta(ClassMeta<?> cm) {
491      return ctx.getUrlEncodingClassMeta(cm);
492   }
493
494   /**
495    * Routes the {@code _value=...} top-level unwrap through the format-aware
496    * Duration/Period/Date/Calendar/Temporal parsers when the target type is one of those
497    * date/time/duration cluster types.  Otherwise falls through to the generic
498    * {@link #convertToType(Object, ClassMeta)} path.
499    *
500    * <p>
501    * Without this routing the inner value (typically a {@link Number} parsed from a bare wire
502    * literal) is converted via the generic {@code Number → T} coercion which does not consult
503    * {@link MarshallingContext#getDurationFormat()} / {@code getTemporalFormat()} etc., so the
504    * configured wire format hint (NANOS, ISO_YEAR, MILLIS, …) is silently dropped.
505    */
506   private <T> Object unwrapValueAs(Object rawValue, ClassMeta<T> sType) {
507      if (rawValue != null) {
508         if (sType.isDuration())
509            return readDuration(rawValue.toString());
510         if (sType.isPeriod())
511            return readPeriod(rawValue.toString());
512         if (sType.isDate())
513            return readDate(rawValue.toString(), sType);
514         if (sType.isCalendar())
515            return readCalendar(rawValue.toString(), sType);
516         if (sType.isTemporal())
517            return readTemporal(rawValue.toString(), sType);
518      }
519      return convertToType(rawValue, sType);
520   }
521
522   /**
523    * Parse bean property collections/arrays as separate key/value pairs.
524    *
525    * @see UrlEncodingParser.Builder#expandedParams()
526    * @return
527    * <jk>false</jk> if serializing the array <c>[1,2,3]</c> results in <c>?key=$a(1,2,3)</c>.
528    * <br><jk>true</jk> if serializing the same array results in <c>?key=1&amp;key=2&amp;key=3</c>.
529    */
530   protected final boolean isExpandedParams() { return expandedParams; }
531}