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&key=2&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}