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