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 (getUrlEncodingClassMeta(pMeta.getBeanMeta().getClassMeta()).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&key=2&key=3</c>. 273 */ 274 protected final boolean isExpandedParams() { 275 return ctx.isExpandedParams(); 276 } 277 278 //----------------------------------------------------------------------------------------------------------------- 279 // Extended metadata 280 //----------------------------------------------------------------------------------------------------------------- 281 282 /** 283 * Returns the language-specific metadata on the specified class. 284 * 285 * @param cm The class to return the metadata on. 286 * @return The metadata. 287 */ 288 protected UrlEncodingClassMeta getUrlEncodingClassMeta(ClassMeta<?> cm) { 289 return ctx.getUrlEncodingClassMeta(cm); 290 } 291 292 //----------------------------------------------------------------------------------------------------------------- 293 // Other methods 294 //----------------------------------------------------------------------------------------------------------------- 295 296 @Override /* Session */ 297 public ObjectMap toMap() { 298 return super.toMap() 299 .append("UrlEncodingSerializerSession", new DefaultFilteringObjectMap() 300 ); 301 } 302}