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