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.urlencoding;
018
019import static org.apache.juneau.commons.utils.IoUtils.*;
020import static org.apache.juneau.commons.utils.Utils.*;
021
022import java.io.*;
023import java.lang.reflect.*;
024import java.nio.charset.*;
025import java.util.*;
026import java.util.function.*;
027
028import org.apache.juneau.*;
029import org.apache.juneau.commons.lang.*;
030import org.apache.juneau.httppart.*;
031import org.apache.juneau.serializer.*;
032import org.apache.juneau.svl.*;
033import org.apache.juneau.uon.*;
034
035/**
036 * Session object that lives for the duration of a single use of {@link UrlEncodingSerializer}.
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/UrlEncodingBasics">URL-Encoding Basics</a>
044 * </ul>
045 */
046@SuppressWarnings({ "rawtypes", "unchecked", "resource" })
047public class UrlEncodingSerializerSession extends UonSerializerSession {
048   /**
049    * Builder class.
050    */
051   public static class Builder extends UonSerializerSession.Builder {
052
053      private UrlEncodingSerializer ctx;
054
055      /**
056       * Constructor
057       *
058       * @param ctx The context creating this session.
059       */
060      protected Builder(UrlEncodingSerializer ctx) {
061         super(ctx);
062         this.ctx = ctx;
063      }
064
065      @Override /* Overridden from Builder */
066      public <T> Builder apply(Class<T> type, Consumer<T> apply) {
067         super.apply(type, apply);
068         return this;
069      }
070
071      @Override
072      public UrlEncodingSerializerSession build() {
073         return new UrlEncodingSerializerSession(this);
074      }
075
076      @Override /* Overridden from Builder */
077      public Builder debug(Boolean value) {
078         super.debug(value);
079         return this;
080      }
081
082      @Override /* Overridden from Builder */
083      public Builder fileCharset(Charset value) {
084         super.fileCharset(value);
085         return this;
086      }
087
088      @Override /* Overridden from Builder */
089      public Builder javaMethod(Method value) {
090         super.javaMethod(value);
091         return this;
092      }
093
094      @Override /* Overridden from Builder */
095      public Builder locale(Locale value) {
096         super.locale(value);
097         return this;
098      }
099
100      @Override /* Overridden from Builder */
101      public Builder mediaType(MediaType value) {
102         super.mediaType(value);
103         return this;
104      }
105
106      @Override /* Overridden from Builder */
107      public Builder mediaTypeDefault(MediaType value) {
108         super.mediaTypeDefault(value);
109         return this;
110      }
111
112      @Override /* Overridden from Builder */
113      public Builder properties(Map<String,Object> value) {
114         super.properties(value);
115         return this;
116      }
117
118      @Override /* Overridden from Builder */
119      public Builder property(String key, Object value) {
120         super.property(key, value);
121         return this;
122      }
123
124      @Override /* Overridden from Builder */
125      public Builder resolver(VarResolverSession value) {
126         super.resolver(value);
127         return this;
128      }
129
130      @Override /* Overridden from Builder */
131      public Builder schema(HttpPartSchema value) {
132         super.schema(value);
133         return this;
134      }
135
136      @Override /* Overridden from Builder */
137      public Builder schemaDefault(HttpPartSchema value) {
138         super.schemaDefault(value);
139         return this;
140      }
141
142      @Override /* Overridden from Builder */
143      public Builder streamCharset(Charset value) {
144         super.streamCharset(value);
145         return this;
146      }
147
148      @Override /* Overridden from Builder */
149      public Builder timeZone(TimeZone value) {
150         super.timeZone(value);
151         return this;
152      }
153
154      @Override /* Overridden from Builder */
155      public Builder timeZoneDefault(TimeZone value) {
156         super.timeZoneDefault(value);
157         return this;
158      }
159
160      @Override /* Overridden from Builder */
161      public Builder unmodifiable() {
162         super.unmodifiable();
163         return this;
164      }
165
166      @Override /* Overridden from Builder */
167      public Builder uriContext(UriContext value) {
168         super.uriContext(value);
169         return this;
170      }
171
172      @Override /* Overridden from Builder */
173      public Builder useWhitespace(Boolean value) {
174         super.useWhitespace(value);
175         return this;
176      }
177   }
178
179   /**
180    * Creates a new builder for this object.
181    *
182    * @param ctx The context creating this session.
183    * @return A new builder.
184    */
185   public static Builder create(UrlEncodingSerializer ctx) {
186      return new Builder(ctx);
187   }
188
189   /*
190    * Converts a Collection into an integer-indexed map.
191    */
192   private static Map<Integer,Object> getCollectionMap(Collection<?> c) {
193      var m = new TreeMap<Integer,Object>();
194      var i = IntegerValue.create();
195      c.forEach(o -> m.put(i.getAndIncrement(), o));
196      return m;
197   }
198
199   /*
200    * Converts an array into an integer-indexed map.
201    */
202   private static Map<Integer,Object> getCollectionMap(Object array) {
203      var m = new TreeMap<Integer,Object>();
204      for (var i = 0; i < Array.getLength(array); i++)
205         m.put(i, Array.get(array, i));
206      return m;
207   }
208
209   private final UrlEncodingSerializer ctx;
210
211   /**
212    * Constructor.
213    *
214    * @param builder The builder for this object.
215    */
216   protected UrlEncodingSerializerSession(Builder builder) {
217      super(builder);
218      ctx = builder.ctx;
219   }
220
221   /*
222    * Workhorse method. Determines the type of object, and then calls the appropriate type-specific serialization method.
223    */
224   private SerializerWriter serializeAnything(UonWriter out, Object o) throws IOException, SerializeException {
225
226      var aType = (ClassMeta<?>)null;        // The actual type
227      var sType = (ClassMeta<?>)null;        // The serialized type
228
229      var eType = getExpectedRootType(o);
230      aType = push2("root", o, eType);
231      indent--;
232      if (aType == null)
233         aType = object();
234
235      sType = aType;
236      var typeName = getBeanTypeName(this, eType, aType, null);
237
238      // Swap if necessary
239      var swap = aType.getSwap(this);
240      if (nn(swap)) {
241         o = swap(swap, o);
242         sType = swap.getSwapClassMeta(this);
243
244         // If the getSwapClass() method returns Object, we need to figure out
245         // the actual type now.
246         if (sType.isObject())
247            sType = getClassMetaForObject(o);
248      }
249
250      if (sType.isMap()) {
251         if (o instanceof BeanMap o2)
252            serializeBeanMap(out, o2, typeName);
253         else
254            serializeMap(out, (Map)o, sType);
255      } else if (sType.isBean()) {
256         serializeBeanMap(out, toBeanMap(o), typeName);
257      } else if (sType.isCollection() || sType.isArray()) {
258         var m = sType.isCollection() ? getCollectionMap((Collection)o) : getCollectionMap(o);
259         serializeCollectionMap(out, m, getClassMeta(Map.class, Integer.class, Object.class));
260      } else if (sType.isReader()) {
261         pipe((Reader)o, out);
262      } else if (sType.isInputStream()) {
263         pipe((InputStream)o, out);
264      } else {
265         // All other types can't be serialized as key/value pairs, so we create a
266         // mock key/value pair with a "_value" key.
267         out.append("_value=");
268         pop();
269         super.serializeAnything(out, o, null, null, null);
270         return out;
271      }
272
273      pop();
274      return out;
275   }
276
277   private SerializerWriter serializeBeanMap(UonWriter out, BeanMap<?> m, String typeName) throws SerializeException {
278      var addAmp = Flag.create();
279
280      if (nn(typeName)) {
281         var pm = m.getMeta().getTypeProperty();
282         out.appendObject(pm.getName(), true).append('=').appendObject(typeName, false);
283         addAmp.set();
284      }
285
286      Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x);
287      m.forEachValue(checkNull, (pMeta, key, value, thrown) -> {
288         var cMeta = pMeta.getClassMeta();
289         var sMeta = cMeta.getSerializedClassMeta(this);
290
291         if (nn(thrown))
292            onBeanGetterException(pMeta, thrown);
293
294         if (canIgnoreValue(sMeta, key, value))
295            return;
296
297         if (nn(value) && shouldUseExpandedParams(pMeta)) {
298            // Transformed object array bean properties may be transformed resulting in ArrayLists,
299            // so we need to check type if we think it's an array.
300            if (sMeta.isCollection() || value instanceof Collection) {
301               ((Collection<?>)value).forEach(x -> {
302                  addAmp.ifSet(() -> out.cr(indent).append('&')).set();
303                  out.appendObject(key, true).append('=');
304                  super.serializeAnything(out, x, cMeta.getElementType(), key, pMeta);
305               });
306            } else /* array */ {
307               for (var i = 0; i < Array.getLength(value); i++) {
308                  addAmp.ifSet(() -> out.cr(indent).append('&')).set();
309                  out.appendObject(key, true).append('=');
310                  super.serializeAnything(out, Array.get(value, i), cMeta.getElementType(), key, pMeta);
311               }
312            }
313         } else {
314            addAmp.ifSet(() -> out.cr(indent).append('&')).set();
315            out.appendObject(key, true).append('=');
316            super.serializeAnything(out, value, cMeta, key, pMeta);
317         }
318      });
319
320      return out;
321   }
322
323   private SerializerWriter serializeCollectionMap(UonWriter out, Map<?,?> m, ClassMeta<?> type) throws SerializeException {
324
325      var valueType = type.getValueType();
326
327      var addAmp = Flag.create();
328
329      m.forEach((k, v) -> {
330         addAmp.ifSet(() -> out.cr(indent).append('&')).set();
331         out.append(k).append('=');
332         super.serializeAnything(out, v, valueType, null, null);
333      });
334
335      return out;
336   }
337
338   private SerializerWriter serializeMap(UonWriter out, Map m, ClassMeta<?> type) throws SerializeException {
339
340      var keyType = type.getKeyType();
341      var valueType = type.getValueType();
342
343      var addAmp = Flag.create();
344
345      forEachEntry(m, e -> {
346         var key = generalize(e.getKey(), keyType);
347         var value = e.getValue();
348
349         if (shouldUseExpandedParams(value)) {
350            if (value instanceof Collection value2) {
351               value2.forEach(x -> {
352                  addAmp.ifSet(() -> out.cr(indent).append('&')).set();
353                  out.appendObject(key, true).append('=');
354                  super.serializeAnything(out, x, null, s(key), null);
355               });
356            } else /* array */ {
357               for (var i = 0; i < Array.getLength(value); i++) {
358                  addAmp.ifSet(() -> out.cr(indent).append('&')).set();
359                  out.appendObject(key, true).append('=');
360                  super.serializeAnything(out, Array.get(value, i), null, s(key), null);
361               }
362            }
363         } else {
364            addAmp.ifSet(() -> out.cr(indent).append('&')).set();
365            out.appendObject(key, true).append('=');
366            super.serializeAnything(out, value, valueType, (key == null ? null : key.toString()), null);
367         }
368      });
369
370      return out;
371   }
372
373   /*
374    * Returns <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs.
375    */
376   private boolean shouldUseExpandedParams(BeanPropertyMeta pMeta) {
377      var cm = pMeta.getClassMeta().getSerializedClassMeta(this);
378      if (cm.isCollectionOrArray()) {
379         if (isExpandedParams() || getUrlEncodingClassMeta(pMeta.getBeanMeta().getClassMeta()).isExpandedParams())
380            return true;
381      }
382      return false;
383   }
384
385   /*
386    * Returns <jk>true</jk> if the specified value should be represented as an expanded parameter list.
387    */
388   private boolean shouldUseExpandedParams(Object value) {
389      if (value == null || ! isExpandedParams())
390         return false;
391      var cm = getClassMetaForObject(value).getSerializedClassMeta(this);
392      if (cm.isCollectionOrArray()) {
393         if (isExpandedParams())
394            return true;
395      }
396      return false;
397   }
398
399   @Override /* Overridden from SerializerSession */
400   protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException {
401      serializeAnything(getUonWriter(out).i(getInitialDepth()), o);
402   }
403
404   /**
405    * Returns the language-specific metadata on the specified class.
406    *
407    * @param cm The class to return the metadata on.
408    * @return The metadata.
409    */
410   protected UrlEncodingClassMeta getUrlEncodingClassMeta(ClassMeta<?> cm) {
411      return ctx.getUrlEncodingClassMeta(cm);
412   }
413
414   /**
415    * Serialize bean property collections/arrays as separate key/value pairs.
416    *
417    * @see UrlEncodingSerializer.Builder#expandedParams()
418    * @return
419    *    <jk>false</jk> if serializing the array <c>[1,2,3]</c> results in <c>?key=$a(1,2,3)</c>.
420    *    <br><jk>true</jk> if serializing the same array results in <c>?key=1&amp;key=2&amp;key=3</c>.
421    */
422   protected final boolean isExpandedParams() { return ctx.isExpandedParams(); }
423}