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.internal.ArrayUtils.*;
016
017import java.lang.reflect.*;
018import java.util.*;
019
020import org.apache.juneau.*;
021import org.apache.juneau.internal.*;
022import org.apache.juneau.serializer.*;
023import org.apache.juneau.transform.*;
024import org.apache.juneau.uon.*;
025
026/**
027 * Session object that lives for the duration of a single use of {@link UrlEncodingSerializer}.
028 *
029 * <p>
030 * This class is NOT thread safe.
031 * It is typically discarded after one-time use although it can be reused within the same thread.
032 */
033@SuppressWarnings({ "rawtypes", "unchecked" })
034public class UrlEncodingSerializerSession extends UonSerializerSession {
035
036   private final UrlEncodingSerializer ctx;
037
038   /**
039    * Constructor.
040    *
041    * @param ctx
042    *    The context creating this session object.
043    *    The context contains all the configuration settings for this object.
044    * @param encode Override the {@link UonSerializer#UON_encoding} setting.
045    * @param args
046    *    Runtime arguments.
047    *    These specify session-level information such as locale and URI context.
048    *    It also include session-level properties that override the properties defined on the bean and
049    *    serializer contexts.
050    */
051   protected UrlEncodingSerializerSession(UrlEncodingSerializer ctx, Boolean encode, SerializerSessionArgs args) {
052      super(ctx, encode, args);
053      this.ctx = ctx;
054   }
055
056   @Override /* Session */
057   public ObjectMap asMap() {
058      return super.asMap()
059         .append("UrlEncodingSerializerSession", new ObjectMap()
060         );
061   }
062
063   /*
064    * Returns <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs.
065    */
066   private boolean shouldUseExpandedParams(BeanPropertyMeta pMeta) {
067      ClassMeta<?> cm = pMeta.getClassMeta().getSerializedClassMeta(this);
068      if (cm.isCollectionOrArray()) {
069         if (isExpandedParams())
070            return true;
071         if (pMeta.getBeanMeta().getClassMeta().getExtendedMeta(UrlEncodingClassMeta.class).isExpandedParams())
072            return true;
073      }
074      return false;
075   }
076
077   /*
078    * Returns <jk>true</jk> if the specified value should be represented as an expanded parameter list.
079    */
080   private boolean shouldUseExpandedParams(Object value) {
081      if (value == null || ! isExpandedParams())
082         return false;
083      ClassMeta<?> cm = getClassMetaForObject(value).getSerializedClassMeta(this);
084      if (cm.isCollectionOrArray()) {
085         if (isExpandedParams())
086            return true;
087      }
088      return false;
089   }
090
091   @Override /* SerializerSession */
092   protected void doSerialize(SerializerPipe out, Object o) throws Exception {
093      serializeAnything(getUonWriter(out), o);
094   }
095
096   /*
097    * Workhorse method. Determines the type of object, and then calls the appropriate type-specific serialization method.
098    */
099   private SerializerWriter serializeAnything(UonWriter out, Object o) throws Exception {
100
101      ClassMeta<?> aType;        // The actual type
102      ClassMeta<?> sType;        // The serialized type
103
104      ClassMeta<?> eType = getExpectedRootType(o);
105      aType = push("root", o, eType);
106      indent--;
107      if (aType == null)
108         aType = object();
109
110      sType = aType;
111      String typeName = getBeanTypeName(eType, aType, null);
112
113      // Swap if necessary
114      PojoSwap swap = aType.getPojoSwap(this);
115      if (swap != null) {
116         o = swap.swap(this, o);
117         sType = swap.getSwapClassMeta(this);
118
119         // If the getSwapClass() method returns Object, we need to figure out
120         // the actual type now.
121         if (sType.isObject())
122            sType = getClassMetaForObject(o);
123      }
124
125      if (sType.isMap()) {
126         if (o instanceof BeanMap)
127            serializeBeanMap(out, (BeanMap)o, typeName);
128         else
129            serializeMap(out, (Map)o, sType);
130      } else if (sType.isBean()) {
131         serializeBeanMap(out, toBeanMap(o), typeName);
132      } else if (sType.isCollection() || sType.isArray()) {
133         Map m = sType.isCollection() ? getCollectionMap((Collection)o) : getCollectionMap(o);
134         serializeCollectionMap(out, m, getClassMeta(Map.class, Integer.class, Object.class));
135      } else if (sType.isReader() || sType.isInputStream()) {
136         IOUtils.pipe(o, out);
137      } else {
138         // All other types can't be serialized as key/value pairs, so we create a
139         // mock key/value pair with a "_value" key.
140         out.append("_value=");
141         super.serializeAnything(out, o, null, null, null);
142      }
143
144      pop();
145      return out;
146   }
147
148   /*
149    * Converts a Collection into an integer-indexed map.
150    */
151   private static Map<Integer,Object> getCollectionMap(Collection<?> c) {
152      Map<Integer,Object> m = new TreeMap<>();
153      int i = 0;
154      for (Object o : c)
155         m.put(i++, o);
156      return m;
157   }
158
159   /*
160    * Converts an array into an integer-indexed map.
161    */
162   private static Map<Integer,Object> getCollectionMap(Object array) {
163      Map<Integer,Object> m = new TreeMap<>();
164      for (int i = 0; i < Array.getLength(array); i++)
165         m.put(i, Array.get(array, i));
166      return m;
167   }
168
169   private SerializerWriter serializeMap(UonWriter out, Map m, ClassMeta<?> type) throws Exception {
170
171      m = sort(m);
172
173      ClassMeta<?> keyType = type.getKeyType(), valueType = type.getValueType();
174
175      boolean addAmp = false;
176
177      for (Map.Entry e : (Set<Map.Entry>)m.entrySet()) {
178         Object key = generalize(e.getKey(), keyType);
179         Object value = e.getValue();
180
181         if (shouldUseExpandedParams(value)) {
182            Iterator i = value instanceof Collection ? ((Collection)value).iterator() : iterator(value);
183            while (i.hasNext()) {
184               if (addAmp)
185                  out.cr(indent).append('&');
186               out.appendObject(key, true).append('=');
187               super.serializeAnything(out, i.next(), null, (key == null ? null : key.toString()), null);
188               addAmp = true;
189            }
190         } else {
191            if (addAmp)
192               out.cr(indent).append('&');
193            out.appendObject(key, true).append('=');
194            super.serializeAnything(out, value, valueType, (key == null ? null : key.toString()), null);
195            addAmp = true;
196         }
197      }
198
199      return out;
200   }
201
202   private SerializerWriter serializeCollectionMap(UonWriter out, Map m, ClassMeta<?> type) throws Exception {
203
204      ClassMeta<?> valueType = type.getValueType();
205
206      boolean addAmp = false;
207
208      for (Map.Entry e : (Set<Map.Entry>)m.entrySet()) {
209         if (addAmp)
210            out.cr(indent).append('&');
211         out.append(e.getKey()).append('=');
212         super.serializeAnything(out, e.getValue(), valueType, null, null);
213         addAmp = true;
214      }
215
216      return out;
217   }
218
219   private SerializerWriter serializeBeanMap(UonWriter out, BeanMap<?> m, String typeName) throws Exception {
220      boolean addAmp = false;
221
222      for (BeanPropertyValue p : m.getValues(isTrimNullProperties(), typeName != null ? createBeanTypeNameProperty(m, typeName) : null)) {
223         BeanPropertyMeta pMeta = p.getMeta();
224         if (pMeta.canRead()) {
225            ClassMeta<?> cMeta = p.getClassMeta();
226            ClassMeta<?> sMeta = cMeta.getSerializedClassMeta(this);
227
228            String key = p.getName();
229            Object value = p.getValue();
230            Throwable t = p.getThrown();
231            if (t != null)
232               onBeanGetterException(pMeta, t);
233
234            if (canIgnoreValue(sMeta, key, value))
235               continue;
236
237            if (value != null && shouldUseExpandedParams(pMeta)) {
238               // Transformed object array bean properties may be transformed resulting in ArrayLists,
239               // so we need to check type if we think it's an array.
240               Iterator i = (sMeta.isCollection() || value instanceof Collection) ? ((Collection)value).iterator() : iterator(value);
241               while (i.hasNext()) {
242                  if (addAmp)
243                     out.cr(indent).append('&');
244
245                  out.appendObject(key, true).append('=');
246
247                  super.serializeAnything(out, i.next(), cMeta.getElementType(), key, pMeta);
248
249                  addAmp = true;
250               }
251            } else {
252               if (addAmp)
253                  out.cr(indent).append('&');
254
255               out.appendObject(key, true).append('=');
256
257               super.serializeAnything(out, value, cMeta, key, pMeta);
258
259               addAmp = true;
260            }
261
262         }
263      }
264      return out;
265   }
266
267   //-----------------------------------------------------------------------------------------------------------------
268   // Properties
269   //-----------------------------------------------------------------------------------------------------------------
270
271   /**
272    * Configuration property:  Serialize bean property collections/arrays as separate key/value pairs.
273    *
274    * @see UrlEncodingSerializer#URLENC_expandedParams
275    * @return
276    *    <jk>false</jk> if serializing the array <code>[1,2,3]</code> results in <code>?key=$a(1,2,3)</code>.
277    *    <br><jk>true</jk> if serializing the same array results in <code>?key=1&amp;key=2&amp;key=3</code>.
278    */
279   protected final boolean isExpandedParams() {
280      return ctx.isExpandedParams();
281   }
282}