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&key=2&key=3</code>. 278 */ 279 protected final boolean isExpandedParams() { 280 return ctx.isExpandedParams(); 281 } 282}