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.utils.SystemUtils.*;
020
021import java.util.*;
022import java.util.concurrent.*;
023
024import org.apache.juneau.commons.bean.*;
025import org.apache.juneau.commons.collections.*;
026import org.apache.juneau.marshall.*;
027import org.apache.juneau.marshall.uon.*;
028
029/**
030 * Serializes POJO models to URL-encoded notation with UON-encoded values (a notation for URL-encoded query paramter values).
031 *
032 * <h5 class='section'>Media types:</h5>
033 * <p>
034 * Handles <c>Accept</c> types:  <bc>application/x-www-form-urlencoded</bc>
035 * <p>
036 * Produces <c>Content-Type</c> types:  <bc>application/x-www-form-urlencoded</bc>
037 *
038 * <h5 class='topic'>Description</h5>
039 *
040 * This serializer provides several serialization options.
041 * <br>Typically, one of the predefined DEFAULT serializers will be sufficient.
042 * <br>However, custom serializers can be constructed to fine-tune behavior.
043 *
044 * <p>
045 * The following shows a sample object defined in Javascript:
046 * <p class='bjson'>
047 *    {
048 *       id: 1,
049 *       name: <js>'John Smith'</js>,
050 *       uri: <js>'http://sample/addressBook/person/1'</js>,
051 *       addressBookUri: <js>'http://sample/addressBook'</js>,
052 *       birthDate: <js>'1946-08-12T00:00:00Z'</js>,
053 *       otherIds: <jk>null</jk>,
054 *       addresses: [
055 *          {
056 *             uri: <js>'http://sample/addressBook/address/1'</js>,
057 *             personUri: <js>'http://sample/addressBook/person/1'</js>,
058 *             id: 1,
059 *             street: <js>'100 Main Street'</js>,
060 *             city: <js>'Anywhereville'</js>,
061 *             state: <js>'NY'</js>,
062 *             zip: 12345,
063 *             isCurrent: <jk>true</jk>,
064 *          }
065 *       ]
066 *    }
067 * </p>
068 *
069 * <p>
070 * Using the "strict" syntax defined in this document, the equivalent URL-encoded notation would be as follows:
071 * <p class='burlenc'>
072 *    <ua>id</ua>=<un>1</un>
073 *    &amp;<ua>name</ua>=<us>'John+Smith'</us>,
074 *    &amp;<ua>uri</ua>=<us>http://sample/addressBook/person/1</us>,
075 *    &amp;<ua>addressBookUri</ua>=<us>http://sample/addressBook</us>,
076 *    &amp;<ua>birthDate</ua>=<us>1946-08-12T00:00:00Z</us>,
077 *    &amp;<ua>otherIds</ua>=<uk>null</uk>,
078 *    &amp;<ua>addresses</ua>=@(
079 *       (
080 *          <ua>uri</ua>=<us>http://sample/addressBook/address/1</us>,
081 *          <ua>personUri</ua>=<us>http://sample/addressBook/person/1</us>,
082 *          <ua>id</ua>=<un>1</un>,
083 *          <ua>street</ua>=<us>'100+Main+Street'</us>,
084 *          <ua>city</ua>=<us>Anywhereville</us>,
085 *          <ua>state</ua>=<us>NY</us>,
086 *          <ua>zip</ua>=<un>12345</un>,
087 *          <ua>isCurrent</ua>=<uk>true</uk>
088 *       )
089 *    )
090 * </p>
091 *
092 * <h5 class='section'>Example:</h5>
093 * <p class='bjava'>
094 *    <jc>// Serialize a Map</jc>
095 *    Map <jv>map</jv> = JsonMap.<jsm>ofText</jsm>(<js>"{a:'b',c:1,d:false,e:['f',1,false],g:{h:'i'}}"</js>);
096 *
097 *    <jc>// Serialize to value equivalent to JSON.</jc>
098 *    <jc>// Produces "a=b&amp;c=1&amp;d=false&amp;e=@(f,1,false)&amp;g=(h=i)"</jc>
099 *    String <jv>uenc</jv> = UrlEncodingSerializer.<jsf>DEFAULT</jsf>.write(<jv>map</jv>);
100 *
101 *    <jc>// Serialize a bean</jc>
102 *    <jk>public class</jk> Person {
103 *       <jk>public</jk> Person(String <jv>name</jv>);
104 *       <jk>public</jk> String getName();
105 *       <jk>public int</jk> getAge();
106 *       <jk>public</jk> Address getAddress();
107 *       <jk>public boolean</jk> deceased;
108 *    }
109 *
110 *    <jk>public class</jk> Address {
111 *       <jk>public</jk> String getStreet();
112 *       <jk>public</jk> String getCity();
113 *       <jk>public</jk> String getState();
114 *       <jk>public int</jk> getZip();
115 *    }
116 *
117 *    Person <jv>person</jv> = <jk>new</jk> Person(<js>"John Doe"</js>, 23, <js>"123 Main St"</js>, <js>"Anywhere"</js>, <js>"NY"</js>, 12345, <jk>false</jk>);
118 *
119 *    <jc>// Produces "name=John+Doe&amp;age=23&amp;address=(street='123+Main+St',city=Anywhere,state=NY,zip=12345)&amp;deceased=false"</jc>
120 *    String <jv>uenc</jv> = UrlEncodingSerializer.<jsf>DEFAULT</jsf>.write(<jv>person</jv>);
121 * </p>
122 *
123 * <h5 class='section'>Notes:</h5><ul>
124 *    <li class='note'>This class is thread safe and reusable.
125 * </ul>
126 *
127 * <h5 class='section'>See Also:</h5><ul>
128 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/UrlEncodingSupport">URL-Encoding</a>
129 * </ul>
130 */
131@SuppressWarnings({
132   "java:S110", // Inheritance depth acceptable for this class hierarchy
133   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
134   "java:S9149" // Per-format static factories intentionally shadow the parent's.
135})
136public class UrlEncodingSerializer extends UonSerializer implements UrlEncodingMetaProvider {
137
138   /**
139    * Builder class.
140    */
141   public static class Builder extends UonSerializer.Builder<Builder> {
142
143      private static final Cache<HashKey,UrlEncodingSerializer> CACHE = Cache.of(HashKey.class, UrlEncodingSerializer.class).build();
144
145      boolean expandedParams;
146
147      /**
148       * Constructor, default settings.
149       */
150      protected Builder() {
151         produces("application/x-www-form-urlencoded");
152         expandedParams = env("UrlEncoding.expandedParams", false);
153      }
154
155      /**
156       * Copy constructor.
157       *
158       * @param copyFrom The builder to copy from.
159       *    <br>Cannot be <jk>null</jk>.
160       */
161      protected Builder(Builder copyFrom) {
162         super(copyFrom);
163         expandedParams = copyFrom.expandedParams;
164      }
165
166      /**
167       * Copy constructor.
168       *
169       * @param copyFrom The bean to copy from.
170       *    <br>Cannot be <jk>null</jk>.
171       */
172      protected Builder(UrlEncodingSerializer copyFrom) {
173         super(copyFrom);
174         expandedParams = copyFrom.expandedParams;
175      }
176
177      @Override /* Overridden from Context.Builder<?> */
178      public UrlEncodingSerializer build() {
179         return cache(CACHE).build(UrlEncodingSerializer.class);
180      }
181
182      @Override /* Overridden from Context.Builder<?> */
183      public Builder copy() {
184         return new Builder(this);
185      }
186
187      /**
188       * Serialize bean property collections/arrays as separate key/value pairs.
189       *
190       * <p>
191       * By default, serializing the array <c>[1,2,3]</c> results in <c>?key=$a(1,2,3)</c>.
192       * <br>When enabled, serializing the same array results in <c>?key=1&amp;key=2&amp;key=3</c>.
193       *
194       * <p>
195       * This option only applies to beans.
196       *
197       * <h5 class='section'>Notes:</h5><ul>
198       *    <li class='note'>
199       *       If parsing multi-part parameters, it's highly recommended to use <c>Collections</c> or <c>Lists</c>
200       *       as bean property types instead of arrays since arrays have to be recreated from scratch every time a value
201       *       is added to it.
202       * </ul>
203       *
204       * <h5 class='section'>Example:</h5>
205       * <p class='bjava'>
206       *    <jc>// A sample bean.</jc>
207       *    <jk>public class</jk> A {
208       *       <jk>public</jk> String[] <jf>f1</jf> = {<js>"a"</js>,<js>"b"</js>};
209       *       <jk>public</jk> List&lt;String&gt; <jf>f2</jf> = Arrays.<jsm>asList</jsm>(<jk>new</jk> String[]{<js>"c"</js>,<js>"d"</js>});
210       *    }
211       *
212       *    <jc>// Normal serializer.</jc>
213       *    WriterSerializer <jv>serializer1</jv> = UrlEncodingSerializer.<jsf>DEFAULT</jsf>;
214       *
215       *    <jc>// Expanded-params serializer.</jc>
216       *    WriterSerializer <jv>serializer2</jv> = UrlEncodingSerializer.<jsm>create</jsm>().expandedParams().build();
217       *
218       *  <jc>// Produces "f1=(a,b)&amp;f2=(c,d)"</jc>
219       *    String <jv>out1</jv> = <jv>serializer1</jv>.write(<jk>new</jk> A());
220       *
221       *    <jc>// Produces "f1=a&amp;f1=b&amp;f2=c&amp;f2=d"</jc>
222       *    String <jv>out2</jv> = <jv>serializer2</jv>.write(<jk>new</jk> A());
223       * </p>
224       *
225       * @return This object.
226       */
227      public Builder expandedParams() {
228         return expandedParams(true);
229      }
230
231      /**
232       * Same as {@link #expandedParams()} but allows you to explicitly specify the value.
233       *
234       * @param value The value for this setting.
235       * @return This object.
236       */
237      public Builder expandedParams(boolean value) {
238         expandedParams = value;
239         return this;
240      }
241
242      @Override /* Overridden from Context.Builder<?> */
243      public HashKey hashKey() {
244         // @formatter:off
245         return HashKey.of(
246            super.hashKey(),
247            expandedParams
248         );
249         // @formatter:on
250      }
251
252
253   }
254
255   /**
256    * Equivalent to <code>UrlEncodingSerializer.<jsm>create</jsm>().expandedParams().build();</code>.
257    */
258   public static class Expanded extends UrlEncodingSerializer {
259
260      /**
261       * Constructor.
262       *
263       * @param builder The builder for this object.
264       */
265      public Expanded(Builder builder) {
266         super(builder.expandedParams());
267      }
268   }
269
270   /**
271    * Equivalent to <code>UrlEncodingSerializer.<jsm>create</jsm>().plainTextParts().build();</code>.
272    */
273   public static class PlainText extends UrlEncodingSerializer {
274
275      /**
276       * Constructor.
277       *
278       * @param builder The builder for this object.
279       */
280      public PlainText(Builder builder) {
281         super(builder.paramFormatPlain());
282      }
283   }
284
285   /**
286    * Equivalent to <code>UrlEncodingSerializer.<jsm>create</jsm>().useWhitespace().build();</code>.
287    */
288   public static class Readable extends UrlEncodingSerializer {
289
290      /**
291       * Constructor.
292       *
293       * @param builder The builder for this object.
294       */
295      public Readable(Builder builder) {
296         super(builder.useWhitespace());
297      }
298   }
299
300   /** Reusable instance of {@link UrlEncodingSerializer}, all default settings. */
301   public static final UrlEncodingSerializer DEFAULT = new UrlEncodingSerializer(create());
302   /** Reusable instance of {@link UrlEncodingSerializer.PlainText}. */
303   public static final UrlEncodingSerializer DEFAULT_PLAINTEXT = new PlainText(create());
304
305   /** Reusable instance of {@link UrlEncodingSerializer.Expanded}. */
306   public static final UrlEncodingSerializer DEFAULT_EXPANDED = new Expanded(create());
307
308   /** Reusable instance of {@link UrlEncodingSerializer.Readable}. */
309   public static final UrlEncodingSerializer DEFAULT_READABLE = new Readable(create());
310
311   /**
312    * Creates a new builder for this object.
313    *
314    * @return A new builder.
315    */
316   public static Builder create() {
317      return new Builder();
318   }
319
320   final boolean expandedParams;
321
322   private final Map<ClassMeta<?>,UrlEncodingClassMeta> urlEncodingClassMetas = new ConcurrentHashMap<>();
323   private final Map<BeanPropertyMeta,UrlEncodingBeanPropertyMeta> urlEncodingBeanPropertyMetas = new ConcurrentHashMap<>();
324
325   /**
326    * Constructor.
327    *
328    * @param builder The builder for this object.
329    */
330   public UrlEncodingSerializer(Builder builder) {
331      super(builder.encoding());
332      expandedParams = builder.expandedParams;
333   }
334
335   @Override /* Overridden from Context */
336   public Builder copy() {
337      return new Builder(this);
338   }
339
340   @Override /* Overridden from Context */
341   public UrlEncodingSerializerSession.Builder createSession() {
342      return UrlEncodingSerializerSession.create(this);
343   }
344
345   @Override /* Overridden from Context */
346   public UrlEncodingSerializerSession getSession() { return createSession().build(); }
347
348   @Override /* Overridden from UrlEncodingMetaProvider */
349   public UrlEncodingBeanPropertyMeta getUrlEncodingBeanPropertyMeta(BeanPropertyMeta bpm) {
350      if (bpm == null)
351         return UrlEncodingBeanPropertyMeta.DEFAULT;
352      return urlEncodingBeanPropertyMetas.computeIfAbsent(bpm, k -> new UrlEncodingBeanPropertyMeta(k.getDelegateFor(), this));
353   }
354
355   @Override /* Overridden from UrlEncodingMetaProvider */
356   public UrlEncodingClassMeta getUrlEncodingClassMeta(ClassMeta<?> cm) {
357      return urlEncodingClassMetas.computeIfAbsent(cm, k -> new UrlEncodingClassMeta(k, this));
358   }
359
360   /**
361    * Serialize bean property collections/arrays as separate key/value pairs.
362    *
363    * @see Builder#expandedParams()
364    * @return
365    *    <jk>false</jk> if serializing the array <c>[1,2,3]</c> results in <c>?key=$a(1,2,3)</c>.
366    *    <br><jk>true</jk> if serializing the same array results in <c>?key=1&amp;key=2&amp;key=3</c>.
367    */
368   protected final boolean isExpandedParams() { return expandedParams; }
369
370   @Override /* Overridden from UonSerializer */
371   protected FluentMap<String,Object> properties() {
372      return super.properties()
373         .a("expandedParams", expandedParams);
374   }
375}