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