001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.urlencoding; 018 019import static org.apache.juneau.commons.utils.IoUtils.*; 020import static org.apache.juneau.commons.utils.Utils.*; 021 022import java.io.*; 023import java.lang.reflect.*; 024import java.nio.charset.*; 025import java.util.*; 026import java.util.function.*; 027 028import org.apache.juneau.*; 029import org.apache.juneau.commons.lang.*; 030import org.apache.juneau.httppart.*; 031import org.apache.juneau.serializer.*; 032import org.apache.juneau.svl.*; 033import org.apache.juneau.uon.*; 034 035/** 036 * Session object that lives for the duration of a single use of {@link UrlEncodingSerializer}. 037 * 038 * <h5 class='section'>Notes:</h5><ul> 039 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 040 * </ul> 041 * 042 * <h5 class='section'>See Also:</h5><ul> 043 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/UrlEncodingBasics">URL-Encoding Basics</a> 044 * </ul> 045 */ 046@SuppressWarnings({ "rawtypes", "unchecked", "resource" }) 047public class UrlEncodingSerializerSession extends UonSerializerSession { 048 /** 049 * Builder class. 050 */ 051 public static class Builder extends UonSerializerSession.Builder { 052 053 private UrlEncodingSerializer ctx; 054 055 /** 056 * Constructor 057 * 058 * @param ctx The context creating this session. 059 */ 060 protected Builder(UrlEncodingSerializer ctx) { 061 super(ctx); 062 this.ctx = ctx; 063 } 064 065 @Override /* Overridden from Builder */ 066 public <T> Builder apply(Class<T> type, Consumer<T> apply) { 067 super.apply(type, apply); 068 return this; 069 } 070 071 @Override 072 public UrlEncodingSerializerSession build() { 073 return new UrlEncodingSerializerSession(this); 074 } 075 076 @Override /* Overridden from Builder */ 077 public Builder debug(Boolean value) { 078 super.debug(value); 079 return this; 080 } 081 082 @Override /* Overridden from Builder */ 083 public Builder fileCharset(Charset value) { 084 super.fileCharset(value); 085 return this; 086 } 087 088 @Override /* Overridden from Builder */ 089 public Builder javaMethod(Method value) { 090 super.javaMethod(value); 091 return this; 092 } 093 094 @Override /* Overridden from Builder */ 095 public Builder locale(Locale value) { 096 super.locale(value); 097 return this; 098 } 099 100 @Override /* Overridden from Builder */ 101 public Builder mediaType(MediaType value) { 102 super.mediaType(value); 103 return this; 104 } 105 106 @Override /* Overridden from Builder */ 107 public Builder mediaTypeDefault(MediaType value) { 108 super.mediaTypeDefault(value); 109 return this; 110 } 111 112 @Override /* Overridden from Builder */ 113 public Builder properties(Map<String,Object> value) { 114 super.properties(value); 115 return this; 116 } 117 118 @Override /* Overridden from Builder */ 119 public Builder property(String key, Object value) { 120 super.property(key, value); 121 return this; 122 } 123 124 @Override /* Overridden from Builder */ 125 public Builder resolver(VarResolverSession value) { 126 super.resolver(value); 127 return this; 128 } 129 130 @Override /* Overridden from Builder */ 131 public Builder schema(HttpPartSchema value) { 132 super.schema(value); 133 return this; 134 } 135 136 @Override /* Overridden from Builder */ 137 public Builder schemaDefault(HttpPartSchema value) { 138 super.schemaDefault(value); 139 return this; 140 } 141 142 @Override /* Overridden from Builder */ 143 public Builder streamCharset(Charset value) { 144 super.streamCharset(value); 145 return this; 146 } 147 148 @Override /* Overridden from Builder */ 149 public Builder timeZone(TimeZone value) { 150 super.timeZone(value); 151 return this; 152 } 153 154 @Override /* Overridden from Builder */ 155 public Builder timeZoneDefault(TimeZone value) { 156 super.timeZoneDefault(value); 157 return this; 158 } 159 160 @Override /* Overridden from Builder */ 161 public Builder unmodifiable() { 162 super.unmodifiable(); 163 return this; 164 } 165 166 @Override /* Overridden from Builder */ 167 public Builder uriContext(UriContext value) { 168 super.uriContext(value); 169 return this; 170 } 171 172 @Override /* Overridden from Builder */ 173 public Builder useWhitespace(Boolean value) { 174 super.useWhitespace(value); 175 return this; 176 } 177 } 178 179 /** 180 * Creates a new builder for this object. 181 * 182 * @param ctx The context creating this session. 183 * @return A new builder. 184 */ 185 public static Builder create(UrlEncodingSerializer ctx) { 186 return new Builder(ctx); 187 } 188 189 /* 190 * Converts a Collection into an integer-indexed map. 191 */ 192 private static Map<Integer,Object> getCollectionMap(Collection<?> c) { 193 var m = new TreeMap<Integer,Object>(); 194 var i = IntegerValue.create(); 195 c.forEach(o -> m.put(i.getAndIncrement(), o)); 196 return m; 197 } 198 199 /* 200 * Converts an array into an integer-indexed map. 201 */ 202 private static Map<Integer,Object> getCollectionMap(Object array) { 203 var m = new TreeMap<Integer,Object>(); 204 for (var i = 0; i < Array.getLength(array); i++) 205 m.put(i, Array.get(array, i)); 206 return m; 207 } 208 209 private final UrlEncodingSerializer ctx; 210 211 /** 212 * Constructor. 213 * 214 * @param builder The builder for this object. 215 */ 216 protected UrlEncodingSerializerSession(Builder builder) { 217 super(builder); 218 ctx = builder.ctx; 219 } 220 221 /* 222 * Workhorse method. Determines the type of object, and then calls the appropriate type-specific serialization method. 223 */ 224 private SerializerWriter serializeAnything(UonWriter out, Object o) throws IOException, SerializeException { 225 226 var aType = (ClassMeta<?>)null; // The actual type 227 var sType = (ClassMeta<?>)null; // The serialized type 228 229 var eType = getExpectedRootType(o); 230 aType = push2("root", o, eType); 231 indent--; 232 if (aType == null) 233 aType = object(); 234 235 sType = aType; 236 var typeName = getBeanTypeName(this, eType, aType, null); 237 238 // Swap if necessary 239 var swap = aType.getSwap(this); 240 if (nn(swap)) { 241 o = swap(swap, o); 242 sType = swap.getSwapClassMeta(this); 243 244 // If the getSwapClass() method returns Object, we need to figure out 245 // the actual type now. 246 if (sType.isObject()) 247 sType = getClassMetaForObject(o); 248 } 249 250 if (sType.isMap()) { 251 if (o instanceof BeanMap o2) 252 serializeBeanMap(out, o2, typeName); 253 else 254 serializeMap(out, (Map)o, sType); 255 } else if (sType.isBean()) { 256 serializeBeanMap(out, toBeanMap(o), typeName); 257 } else if (sType.isCollection() || sType.isArray()) { 258 var m = sType.isCollection() ? getCollectionMap((Collection)o) : getCollectionMap(o); 259 serializeCollectionMap(out, m, getClassMeta(Map.class, Integer.class, Object.class)); 260 } else if (sType.isReader()) { 261 pipe((Reader)o, out); 262 } else if (sType.isInputStream()) { 263 pipe((InputStream)o, out); 264 } else { 265 // All other types can't be serialized as key/value pairs, so we create a 266 // mock key/value pair with a "_value" key. 267 out.append("_value="); 268 pop(); 269 super.serializeAnything(out, o, null, null, null); 270 return out; 271 } 272 273 pop(); 274 return out; 275 } 276 277 private SerializerWriter serializeBeanMap(UonWriter out, BeanMap<?> m, String typeName) throws SerializeException { 278 var addAmp = Flag.create(); 279 280 if (nn(typeName)) { 281 var pm = m.getMeta().getTypeProperty(); 282 out.appendObject(pm.getName(), true).append('=').appendObject(typeName, false); 283 addAmp.set(); 284 } 285 286 Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x); 287 m.forEachValue(checkNull, (pMeta, key, value, thrown) -> { 288 var cMeta = pMeta.getClassMeta(); 289 var sMeta = cMeta.getSerializedClassMeta(this); 290 291 if (nn(thrown)) 292 onBeanGetterException(pMeta, thrown); 293 294 if (canIgnoreValue(sMeta, key, value)) 295 return; 296 297 if (nn(value) && shouldUseExpandedParams(pMeta)) { 298 // Transformed object array bean properties may be transformed resulting in ArrayLists, 299 // so we need to check type if we think it's an array. 300 if (sMeta.isCollection() || value instanceof Collection) { 301 ((Collection<?>)value).forEach(x -> { 302 addAmp.ifSet(() -> out.cr(indent).append('&')).set(); 303 out.appendObject(key, true).append('='); 304 super.serializeAnything(out, x, cMeta.getElementType(), key, pMeta); 305 }); 306 } else /* array */ { 307 for (var i = 0; i < Array.getLength(value); i++) { 308 addAmp.ifSet(() -> out.cr(indent).append('&')).set(); 309 out.appendObject(key, true).append('='); 310 super.serializeAnything(out, Array.get(value, i), cMeta.getElementType(), key, pMeta); 311 } 312 } 313 } else { 314 addAmp.ifSet(() -> out.cr(indent).append('&')).set(); 315 out.appendObject(key, true).append('='); 316 super.serializeAnything(out, value, cMeta, key, pMeta); 317 } 318 }); 319 320 return out; 321 } 322 323 private SerializerWriter serializeCollectionMap(UonWriter out, Map<?,?> m, ClassMeta<?> type) throws SerializeException { 324 325 var valueType = type.getValueType(); 326 327 var addAmp = Flag.create(); 328 329 m.forEach((k, v) -> { 330 addAmp.ifSet(() -> out.cr(indent).append('&')).set(); 331 out.append(k).append('='); 332 super.serializeAnything(out, v, valueType, null, null); 333 }); 334 335 return out; 336 } 337 338 private SerializerWriter serializeMap(UonWriter out, Map m, ClassMeta<?> type) throws SerializeException { 339 340 var keyType = type.getKeyType(); 341 var valueType = type.getValueType(); 342 343 var addAmp = Flag.create(); 344 345 forEachEntry(m, e -> { 346 var key = generalize(e.getKey(), keyType); 347 var value = e.getValue(); 348 349 if (shouldUseExpandedParams(value)) { 350 if (value instanceof Collection value2) { 351 value2.forEach(x -> { 352 addAmp.ifSet(() -> out.cr(indent).append('&')).set(); 353 out.appendObject(key, true).append('='); 354 super.serializeAnything(out, x, null, s(key), null); 355 }); 356 } else /* array */ { 357 for (var i = 0; i < Array.getLength(value); i++) { 358 addAmp.ifSet(() -> out.cr(indent).append('&')).set(); 359 out.appendObject(key, true).append('='); 360 super.serializeAnything(out, Array.get(value, i), null, s(key), null); 361 } 362 } 363 } else { 364 addAmp.ifSet(() -> out.cr(indent).append('&')).set(); 365 out.appendObject(key, true).append('='); 366 super.serializeAnything(out, value, valueType, (key == null ? null : key.toString()), null); 367 } 368 }); 369 370 return out; 371 } 372 373 /* 374 * Returns <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs. 375 */ 376 private boolean shouldUseExpandedParams(BeanPropertyMeta pMeta) { 377 var cm = pMeta.getClassMeta().getSerializedClassMeta(this); 378 if (cm.isCollectionOrArray()) { 379 if (isExpandedParams() || getUrlEncodingClassMeta(pMeta.getBeanMeta().getClassMeta()).isExpandedParams()) 380 return true; 381 } 382 return false; 383 } 384 385 /* 386 * Returns <jk>true</jk> if the specified value should be represented as an expanded parameter list. 387 */ 388 private boolean shouldUseExpandedParams(Object value) { 389 if (value == null || ! isExpandedParams()) 390 return false; 391 var cm = getClassMetaForObject(value).getSerializedClassMeta(this); 392 if (cm.isCollectionOrArray()) { 393 if (isExpandedParams()) 394 return true; 395 } 396 return false; 397 } 398 399 @Override /* Overridden from SerializerSession */ 400 protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException { 401 serializeAnything(getUonWriter(out).i(getInitialDepth()), o); 402 } 403 404 /** 405 * Returns the language-specific metadata on the specified class. 406 * 407 * @param cm The class to return the metadata on. 408 * @return The metadata. 409 */ 410 protected UrlEncodingClassMeta getUrlEncodingClassMeta(ClassMeta<?> cm) { 411 return ctx.getUrlEncodingClassMeta(cm); 412 } 413 414 /** 415 * Serialize bean property collections/arrays as separate key/value pairs. 416 * 417 * @see UrlEncodingSerializer.Builder#expandedParams() 418 * @return 419 * <jk>false</jk> if serializing the array <c>[1,2,3]</c> results in <c>?key=$a(1,2,3)</c>. 420 * <br><jk>true</jk> if serializing the same array results in <c>?key=1&key=2&key=3</c>. 421 */ 422 protected final boolean isExpandedParams() { return ctx.isExpandedParams(); } 423}