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}