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.IoUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021
022import java.io.*;
023import java.lang.reflect.*;
024import java.util.*;
025import java.util.function.*;
026
027import org.apache.juneau.commons.bean.*;
028import org.apache.juneau.commons.lang.*;
029import org.apache.juneau.marshall.*;
030import org.apache.juneau.marshall.serializer.*;
031import org.apache.juneau.marshall.uon.*;
032
033/**
034 * Session object that lives for the duration of a single use of {@link UrlEncodingSerializer}.
035 *
036 * <h5 class='section'>Notes:</h5><ul>
037 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
038 * </ul>
039 *
040 * <h5 class='section'>See Also:</h5><ul>
041 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/UrlEncodingSupport">URL-Encoding</a>
042 * </ul>
043 */
044@SuppressWarnings({
045   "java:S110", // Inheritance depth acceptable for this class hierarchy
046   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
047   "java:S2177", // Private writeAnything()/writeBeanMap()/writeMap() intentionally share names (with different signatures) with the inherited UON writers but emit URL-encoding-specific output
048   "java:S3776", // writeAnything() and writeBeanMap() branch over every value kind and expanded-param mode in one pass
049   "rawtypes", // Raw types necessary for generic type handling
050   "resource", // Resource management handled externally
051   "unchecked" // Type erasure requires unchecked casts
052})
053public class UrlEncodingSerializerSession extends UonSerializerSession {
054
055   /**
056    * Builder class.
057    */
058   public static class Builder extends UonSerializerSession.Builder<Builder> {
059
060      private boolean expandedParams;
061      private final UrlEncodingSerializer ctx;
062
063      /**
064       * Constructor
065       *
066       * @param ctx The context creating this session.
067       *    <br>Cannot be <jk>null</jk>.
068       */
069      protected Builder(UrlEncodingSerializer ctx) {
070         super(ctx);
071         this.ctx = ctx;
072         expandedParams = ctx.isExpandedParams();
073      }
074
075      @Override
076      public UrlEncodingSerializerSession build() {
077         return new UrlEncodingSerializerSession(this);
078      }
079
080      /**
081       * Serialize bean property collections/arrays as separate key/value pairs.
082       *
083       * @param value The new value for this setting.
084       * @return This object.
085       */
086      public Builder expandedParams(boolean value) {
087         expandedParams = value;
088         return this;
089      }
090
091      @Override /* Overridden from Builder */
092      public Builder property(String key, Object value) {
093         if (key == null) { super.property(key, value); return this; }
094         switch (key) {
095            case "expandedParams", "UrlEncodingSerializerSession.expandedParams":
096               return expandedParams(cvt(value, Boolean.class));
097            default:
098               super.property(key, value);
099               return this;
100         }
101      }
102
103   }
104
105   /**
106    * Creates a new builder for this object.
107    *
108    * @param ctx The context creating this session.
109    *    <br>Cannot be <jk>null</jk>.
110    * @return A new builder.
111    */
112   public static Builder create(UrlEncodingSerializer ctx) {
113      return new Builder(ctx);
114   }
115
116   /*
117    * Converts a Collection into an integer-indexed map.
118    */
119   private static Map<Integer,Object> getCollectionMap(Collection<?> c) {
120      var m = new TreeMap<Integer,Object>();
121      var i = IntegerHolder.create();
122      c.forEach(o -> m.put(i.getAndIncrement(), o));
123      return m;
124   }
125
126   /*
127    * Converts an array into an integer-indexed map.
128    */
129   private static Map<Integer,Object> getCollectionMap(Object array) {
130      var m = new TreeMap<Integer,Object>();
131      for (var i = 0; i < Array.getLength(array); i++)
132         m.put(i, Array.get(array, i));
133      return m;
134   }
135
136   private final UrlEncodingSerializer ctx;
137   private final boolean expandedParams;
138
139   /**
140    * Constructor.
141    *
142    * @param builder The builder for this object.
143    */
144   protected UrlEncodingSerializerSession(Builder builder) {
145      super(builder);
146      ctx = builder.ctx;
147      expandedParams = builder.expandedParams;
148   }
149
150   /*
151    * Workhorse method. Determines the type of object, and then calls the appropriate type-specific serialization method.
152    */
153   private SerializerWriter<?> writeAnything(UonWriter out, Object o) throws IOException, SerializeException {
154
155      ClassMeta<?> aType = null;       // The actual type
156      ClassMeta<?> sType = null;       // The serialized type
157
158      var eType = getExpectedRootType(o);
159      aType = push2("root", o, eType);
160      indent--;
161      if (aType == null)
162         aType = object();
163
164      sType = aType;
165      var typeName = getBeanTypeName(this, eType, aType, null);
166
167      // Swap if necessary
168      var swap = aType.getSwap(this);
169      if (nn(swap)) {
170         o = swap(swap, o);
171         sType = swap.getSwapClassMeta(this);
172
173         // If the getSwapClass() method returns Object, we need to figure out
174         // the actual type now.
175         if (sType.isObject())
176            sType = getClassMetaForObject(o);
177      }
178
179      if (sType.isBean()) {
180         writeBeanMap(out, toBeanMap(o), typeName);
181      } else if (sType.isMap()) {
182         if (sType.isBeanMap())
183            writeBeanMap(out, (BeanMap)o, typeName);
184         else
185            writeMap(out, (Map)o, sType);
186      } else if (sType.isCollection() || sType.isArray()) {
187         var m = sType.isCollection() ? getCollectionMap((Collection)o) : getCollectionMap(o);
188         writeCollectionMap(out, m, getClassMeta(Map.class, Integer.class, Object.class));
189      } else if (sType.isStreamable()) {
190         writeStreamableAsCollectionMap(out, o, sType);
191      } else if (sType.isReader()) {
192         pipe((Reader)o, out);
193      } else if (sType.isInputStream()) {
194         pipe((InputStream)o, out);
195      } else {
196         // All other types can't be serialized as key/value pairs, so we create a
197         // mock key/value pair with a "_value" key.
198         out.append("_value=");
199         pop();
200         super.writeAnything(out, o, null, null, null);
201         return out;
202      }
203
204      pop();
205      return out;
206   }
207
208   
209   private SerializerWriter<?> writeBeanMap(UonWriter out, BeanMap<?> m, String typeName) throws SerializeException {
210      var addAmp = Flag.create();
211
212      if (nn(typeName)) {
213         var pm = m.getMeta().getTypeProperty();
214         out.appendObject(pm.getName(), true).append('=').appendObject(typeName, false);
215         addAmp.set();
216      }
217
218      Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x);
219      m.forEachValue(checkNull, (pMeta, key, value, thrown) -> {
220         var cMeta = (ClassMeta<?>) pMeta.getBeanInfo();
221         var sMeta = cMeta.getSerializedClassMeta(this);
222
223         if (nn(thrown))
224            onBeanGetterException(pMeta, thrown);
225
226         if (canIgnoreValue(pMeta, key, value))
227            return;
228
229         if (nn(value) && shouldUseExpandedParams(pMeta)) {
230            // Transformed object array bean properties may be transformed resulting in ArrayLists,
231            // so we need to check type if we think it's an array.
232            if (sMeta.isCollection() || value instanceof Collection) {
233               var c = (Collection<?>) value;
234               if (c.isEmpty()) {
235                  // Empty expanded-params collection: emit a single "key=" segment so the
236                  // parser can distinguish "non-null empty collection" from "null property"
237                  // (both would otherwise produce no wire output, defeating round-trip).
238                  addAmp.ifSet(() -> out.cr(indent).append('&')).set();
239                  out.appendObject(key, true).append('=');
240               } else {
241                  c.forEach(x -> {
242                     addAmp.ifSet(() -> out.cr(indent).append('&')).set();
243                     out.appendObject(key, true).append('=');
244                     super.writeAnything(out, x, cMeta.getElementType(), key, pMeta);
245                  });
246               }
247            } else /* array */ {
248               var len = Array.getLength(value);
249               if (len == 0) {
250                  // Empty expanded-params array: emit a single "key=" segment so the parser
251                  // can distinguish "non-null empty array" from "null property" (both would
252                  // otherwise produce no wire output, defeating round-trip).
253                  addAmp.ifSet(() -> out.cr(indent).append('&')).set();
254                  out.appendObject(key, true).append('=');
255               } else {
256                  for (var i = 0; i < len; i++) {
257                     addAmp.ifSet(() -> out.cr(indent).append('&')).set();
258                     out.appendObject(key, true).append('=');
259                     super.writeAnything(out, Array.get(value, i), cMeta.getElementType(), key, pMeta);
260                  }
261               }
262            }
263         } else {
264            addAmp.ifSet(() -> out.cr(indent).append('&')).set();
265            out.appendObject(key, true).append('=');
266            super.writeAnything(out, value, cMeta, key, pMeta);
267         }
268      });
269
270      return out;
271   }
272
273   private SerializerWriter<?> writeCollectionMap(UonWriter out, Map<?,?> m, ClassMeta<?> type) throws SerializeException {
274
275      var valueType = type.getValueType();
276
277      var addAmp = Flag.create();
278
279      m.forEach((k, v) -> {
280         addAmp.ifSet(() -> out.cr(indent).append('&')).set();
281         out.append(k).append('=');
282         super.writeAnything(out, v, valueType, null, null);
283      });
284
285      return out;
286   }
287
288   private SerializerWriter<?> writeStreamableAsCollectionMap(UonWriter out, Object o, ClassMeta<?> sType) throws SerializeException {
289      var addAmp = Flag.create();
290      var i = IntegerHolder.create();
291      forEachStreamableEntry(o, sType, v -> {
292         addAmp.ifSet(() -> out.cr(indent).append('&')).set();
293         out.append(i.getAndIncrement()).append('=');
294         super.writeAnything(out, v, null, null, null);
295      });
296      return out;
297   }
298
299   private SerializerWriter<?> writeMap(UonWriter out, Map m, ClassMeta<?> type) throws SerializeException {
300
301      var keyType = type.getKeyType();
302      var valueType = type.getValueType();
303
304      var addAmp = Flag.create();
305
306      forEachEntry(m, e -> {
307         var key = generalize(e.getKey(), keyType);
308         var value = e.getValue();
309
310         if (shouldUseExpandedParams(value)) {
311            if (value instanceof Collection value2) {
312               value2.forEach(x -> {
313                  addAmp.ifSet(() -> out.cr(indent).append('&')).set();
314                  out.appendObject(key, true).append('=');
315                  super.writeAnything(out, x, null, s(key), null);
316               });
317            } else /* array */ {
318               for (var i = 0; i < Array.getLength(value); i++) {
319                  addAmp.ifSet(() -> out.cr(indent).append('&')).set();
320                  out.appendObject(key, true).append('=');
321                  super.writeAnything(out, Array.get(value, i), null, s(key), null);
322               }
323            }
324         } else {
325            addAmp.ifSet(() -> out.cr(indent).append('&')).set();
326            out.appendObject(key, true).append('=');
327            super.writeAnything(out, value, valueType, (key == null ? null : key.toString()), null);
328         }
329      });
330
331      return out;
332   }
333
334   /*
335    * Returns <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs.
336    */
337   private boolean shouldUseExpandedParams(BeanPropertyMeta pMeta) {
338      var cm = ((ClassMeta<?>) pMeta.getBeanInfo()).getSerializedClassMeta(this);
339      return cm.isCollectionOrArray() && (isExpandedParams() || getUrlEncodingClassMeta((ClassMeta<?>) pMeta.getBeanMeta().getBeanInfo()).isExpandedParams());
340   }
341
342   /*
343    * Returns <jk>true</jk> if the specified value should be represented as an expanded parameter list.
344    */
345   private boolean shouldUseExpandedParams(Object value) {
346      if (value == null || ! isExpandedParams())
347         return false;
348      var cm = getClassMetaForObject(value).getSerializedClassMeta(this);
349      return cm.isCollectionOrArray() && isExpandedParams();
350   }
351
352   @Override /* Overridden from SerializerSession */
353   protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException {
354      writeAnything(getUonWriter(out).i(getInitialDepth()), o);
355   }
356
357   /**
358    * Returns the language-specific metadata on the specified class.
359    *
360    * @param cm The class to return the metadata on.
361    * @return The metadata.
362    */
363   protected UrlEncodingClassMeta getUrlEncodingClassMeta(ClassMeta<?> cm) {
364      return ctx.getUrlEncodingClassMeta(cm);
365   }
366
367   /**
368    * Serialize bean property collections/arrays as separate key/value pairs.
369    *
370    * @see UrlEncodingSerializer.Builder#expandedParams()
371    * @return
372    *    <jk>false</jk> if serializing the array <c>[1,2,3]</c> results in <c>?key=$a(1,2,3)</c>.
373    *    <br><jk>true</jk> if serializing the same array results in <c>?key=1&amp;key=2&amp;key=3</c>.
374    */
375   protected final boolean isExpandedParams() { return expandedParams; }
376}