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