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