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.*;
016import static org.apache.juneau.urlencoding.UrlEncodingSerializer.*;
017
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 boolean expandedParams;
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      expandedParams = getProperty(URLENC_expandedParams, boolean.class, ctx.expandedParams);
055   }
056
057   @Override /* Session */
058   public ObjectMap asMap() {
059      return super.asMap()
060         .append("UrlEncodingSerializerSession", new ObjectMap()
061            .append("expandedParams", expandedParams)
062         );
063   }
064
065   /*
066    * Returns <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs.
067    */
068   private boolean shouldUseExpandedParams(BeanPropertyMeta pMeta) {
069      ClassMeta<?> cm = pMeta.getClassMeta().getSerializedClassMeta(this);
070      if (cm.isCollectionOrArray()) {
071         if (expandedParams)
072            return true;
073         if (pMeta.getBeanMeta().getClassMeta().getExtendedMeta(UrlEncodingClassMeta.class).isExpandedParams())
074            return true;
075      }
076      return false;
077   }
078
079   /*
080    * Returns <jk>true</jk> if the specified value should be represented as an expanded parameter list.
081    */
082   private boolean shouldUseExpandedParams(Object value) {
083      if (value == null || ! expandedParams)
084         return false;
085      ClassMeta<?> cm = getClassMetaForObject(value).getSerializedClassMeta(this);
086      if (cm.isCollectionOrArray()) {
087         if (expandedParams)
088            return true;
089      }
090      return false;
091   }
092
093   @Override /* SerializerSession */
094   protected void doSerialize(SerializerPipe out, Object o) throws Exception {
095      serializeAnything(getUonWriter(out), o);
096   }
097
098   /*
099    * Workhorse method. Determines the type of object, and then calls the appropriate type-specific serialization method.
100    */
101   private SerializerWriter serializeAnything(UonWriter out, Object o) throws Exception {
102
103      ClassMeta<?> aType;        // The actual type
104      ClassMeta<?> sType;        // The serialized type
105
106      aType = push("root", o, object());
107      indent--;
108      if (aType == null)
109         aType = object();
110
111      sType = aType;
112      String typeName = getBeanTypeName(object(), aType, null);
113
114      // Swap if necessary
115      PojoSwap swap = aType.getPojoSwap(this);
116      if (swap != null) {
117         o = swap.swap(this, o);
118         sType = swap.getSwapClassMeta(this);
119
120         // If the getSwapClass() method returns Object, we need to figure out
121         // the actual type now.
122         if (sType.isObject())
123            sType = getClassMetaForObject(o);
124      }
125
126      if (sType.isMap()) {
127         if (o instanceof BeanMap)
128            serializeBeanMap(out, (BeanMap)o, typeName);
129         else
130            serializeMap(out, (Map)o, sType);
131      } else if (sType.isBean()) {
132         serializeBeanMap(out, toBeanMap(o), typeName);
133      } else if (sType.isCollection() || sType.isArray()) {
134         Map m = sType.isCollection() ? getCollectionMap((Collection)o) : getCollectionMap(o);
135         serializeCollectionMap(out, m, getClassMeta(Map.class, Integer.class, Object.class));
136      } else if (sType.isReader() || sType.isInputStream()) {
137         IOUtils.pipe(o, out);
138      } else {
139         // All other types can't be serialized as key/value pairs, so we create a
140         // mock key/value pair with a "_value" key.
141         out.append("_value=");
142         super.serializeAnything(out, o, null, null, null);
143      }
144
145      pop();
146      return out;
147   }
148
149   /*
150    * Converts a Collection into an integer-indexed map.
151    */
152   private static Map<Integer,Object> getCollectionMap(Collection<?> c) {
153      Map<Integer,Object> m = new TreeMap<>();
154      int i = 0;
155      for (Object o : c)
156         m.put(i++, o);
157      return m;
158   }
159
160   /*
161    * Converts an array into an integer-indexed map.
162    */
163   private static Map<Integer,Object> getCollectionMap(Object array) {
164      Map<Integer,Object> m = new TreeMap<>();
165      for (int i = 0; i < Array.getLength(array); i++)
166         m.put(i, Array.get(array, i));
167      return m;
168   }
169
170   private SerializerWriter serializeMap(UonWriter out, Map m, ClassMeta<?> type) throws Exception {
171
172      m = sort(m);
173
174      ClassMeta<?> keyType = type.getKeyType(), valueType = type.getValueType();
175
176      boolean addAmp = false;
177
178      for (Map.Entry e : (Set<Map.Entry>)m.entrySet()) {
179         Object key = generalize(e.getKey(), keyType);
180         Object value = e.getValue();
181
182         if (shouldUseExpandedParams(value)) {
183            Iterator i = value instanceof Collection ? ((Collection)value).iterator() : iterator(value);
184            while (i.hasNext()) {
185               if (addAmp)
186                  out.cr(indent).append('&');
187               out.appendObject(key, true).append('=');
188               super.serializeAnything(out, i.next(), null, (key == null ? null : key.toString()), null);
189               addAmp = true;
190            }
191         } else {
192            if (addAmp)
193               out.cr(indent).append('&');
194            out.appendObject(key, true).append('=');
195            super.serializeAnything(out, value, valueType, (key == null ? null : key.toString()), null);
196            addAmp = true;
197         }
198      }
199
200      return out;
201   }
202
203   private SerializerWriter serializeCollectionMap(UonWriter out, Map m, ClassMeta<?> type) throws Exception {
204
205      ClassMeta<?> valueType = type.getValueType();
206
207      boolean addAmp = false;
208
209      for (Map.Entry e : (Set<Map.Entry>)m.entrySet()) {
210         if (addAmp)
211            out.cr(indent).append('&');
212         out.append(e.getKey()).append('=');
213         super.serializeAnything(out, e.getValue(), valueType, null, null);
214         addAmp = true;
215      }
216
217      return out;
218   }
219
220   private SerializerWriter serializeBeanMap(UonWriter out, BeanMap<?> m, String typeName) throws Exception {
221      boolean addAmp = false;
222
223      for (BeanPropertyValue p : m.getValues(isTrimNulls(), typeName != null ? createBeanTypeNameProperty(m, typeName) : null)) {
224         BeanPropertyMeta pMeta = p.getMeta();
225         if (pMeta.canRead()) {
226            ClassMeta<?> cMeta = p.getClassMeta();
227            ClassMeta<?> sMeta = cMeta.getSerializedClassMeta(this);
228
229            String key = p.getName();
230            Object value = p.getValue();
231            Throwable t = p.getThrown();
232            if (t != null)
233               onBeanGetterException(pMeta, t);
234
235            if (canIgnoreValue(sMeta, key, value))
236               continue;
237
238            if (value != null && shouldUseExpandedParams(pMeta)) {
239               // Transformed object array bean properties may be transformed resulting in ArrayLists,
240               // so we need to check type if we think it's an array.
241               Iterator i = (sMeta.isCollection() || value instanceof Collection) ? ((Collection)value).iterator() : iterator(value);
242               while (i.hasNext()) {
243                  if (addAmp)
244                     out.cr(indent).append('&');
245
246                  out.appendObject(key, true).append('=');
247
248                  super.serializeAnything(out, i.next(), cMeta.getElementType(), key, pMeta);
249
250                  addAmp = true;
251               }
252            } else {
253               if (addAmp)
254                  out.cr(indent).append('&');
255
256               out.appendObject(key, true).append('=');
257
258               super.serializeAnything(out, value, cMeta, key, pMeta);
259
260               addAmp = true;
261            }
262            
263         }
264      }
265      return out;
266   }
267}