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