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.marshall.urlencoding; 018 019import static org.apache.juneau.commons.utils.IoUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import java.io.*; 023import java.lang.reflect.*; 024import java.util.*; 025import java.util.function.*; 026 027import org.apache.juneau.commons.bean.*; 028import org.apache.juneau.commons.lang.*; 029import org.apache.juneau.marshall.*; 030import org.apache.juneau.marshall.serializer.*; 031import org.apache.juneau.marshall.uon.*; 032 033/** 034 * Session object that lives for the duration of a single use of {@link UrlEncodingSerializer}. 035 * 036 * <h5 class='section'>Notes:</h5><ul> 037 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 038 * </ul> 039 * 040 * <h5 class='section'>See Also:</h5><ul> 041 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/UrlEncodingSupport">URL-Encoding</a> 042 * </ul> 043 */ 044@SuppressWarnings({ 045 "java:S110", // Inheritance depth acceptable for this class hierarchy 046 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 047 "java:S2177", // Private writeAnything()/writeBeanMap()/writeMap() intentionally share names (with different signatures) with the inherited UON writers but emit URL-encoding-specific output 048 "java:S3776", // writeAnything() and writeBeanMap() branch over every value kind and expanded-param mode in one pass 049 "rawtypes", // Raw types necessary for generic type handling 050 "resource", // Resource management handled externally 051 "unchecked" // Type erasure requires unchecked casts 052}) 053public class UrlEncodingSerializerSession extends UonSerializerSession { 054 055 /** 056 * Builder class. 057 */ 058 public static class Builder extends UonSerializerSession.Builder<Builder> { 059 060 private boolean expandedParams; 061 private final UrlEncodingSerializer ctx; 062 063 /** 064 * Constructor 065 * 066 * @param ctx The context creating this session. 067 * <br>Cannot be <jk>null</jk>. 068 */ 069 protected Builder(UrlEncodingSerializer ctx) { 070 super(ctx); 071 this.ctx = ctx; 072 expandedParams = ctx.isExpandedParams(); 073 } 074 075 @Override 076 public UrlEncodingSerializerSession build() { 077 return new UrlEncodingSerializerSession(this); 078 } 079 080 /** 081 * Serialize bean property collections/arrays as separate key/value pairs. 082 * 083 * @param value The new value for this setting. 084 * @return This object. 085 */ 086 public Builder expandedParams(boolean value) { 087 expandedParams = value; 088 return this; 089 } 090 091 @Override /* Overridden from Builder */ 092 public Builder property(String key, Object value) { 093 if (key == null) { super.property(key, value); return this; } 094 switch (key) { 095 case "expandedParams", "UrlEncodingSerializerSession.expandedParams": 096 return expandedParams(cvt(value, Boolean.class)); 097 default: 098 super.property(key, value); 099 return this; 100 } 101 } 102 103 } 104 105 /** 106 * Creates a new builder for this object. 107 * 108 * @param ctx The context creating this session. 109 * <br>Cannot be <jk>null</jk>. 110 * @return A new builder. 111 */ 112 public static Builder create(UrlEncodingSerializer ctx) { 113 return new Builder(ctx); 114 } 115 116 /* 117 * Converts a Collection into an integer-indexed map. 118 */ 119 private static Map<Integer,Object> getCollectionMap(Collection<?> c) { 120 var m = new TreeMap<Integer,Object>(); 121 var i = IntegerHolder.create(); 122 c.forEach(o -> m.put(i.getAndIncrement(), o)); 123 return m; 124 } 125 126 /* 127 * Converts an array into an integer-indexed map. 128 */ 129 private static Map<Integer,Object> getCollectionMap(Object array) { 130 var m = new TreeMap<Integer,Object>(); 131 for (var i = 0; i < Array.getLength(array); i++) 132 m.put(i, Array.get(array, i)); 133 return m; 134 } 135 136 private final UrlEncodingSerializer ctx; 137 private final boolean expandedParams; 138 139 /** 140 * Constructor. 141 * 142 * @param builder The builder for this object. 143 */ 144 protected UrlEncodingSerializerSession(Builder builder) { 145 super(builder); 146 ctx = builder.ctx; 147 expandedParams = builder.expandedParams; 148 } 149 150 /* 151 * Workhorse method. Determines the type of object, and then calls the appropriate type-specific serialization method. 152 */ 153 private SerializerWriter<?> writeAnything(UonWriter out, Object o) throws IOException, SerializeException { 154 155 ClassMeta<?> aType = null; // The actual type 156 ClassMeta<?> sType = null; // The serialized type 157 158 var eType = getExpectedRootType(o); 159 aType = push2("root", o, eType); 160 indent--; 161 if (aType == null) 162 aType = object(); 163 164 sType = aType; 165 var typeName = getBeanTypeName(this, eType, aType, null); 166 167 // Swap if necessary 168 var swap = aType.getSwap(this); 169 if (nn(swap)) { 170 o = swap(swap, o); 171 sType = swap.getSwapClassMeta(this); 172 173 // If the getSwapClass() method returns Object, we need to figure out 174 // the actual type now. 175 if (sType.isObject()) 176 sType = getClassMetaForObject(o); 177 } 178 179 if (sType.isBean()) { 180 writeBeanMap(out, toBeanMap(o), typeName); 181 } else if (sType.isMap()) { 182 if (sType.isBeanMap()) 183 writeBeanMap(out, (BeanMap)o, typeName); 184 else 185 writeMap(out, (Map)o, sType); 186 } else if (sType.isCollection() || sType.isArray()) { 187 var m = sType.isCollection() ? getCollectionMap((Collection)o) : getCollectionMap(o); 188 writeCollectionMap(out, m, getClassMeta(Map.class, Integer.class, Object.class)); 189 } else if (sType.isStreamable()) { 190 writeStreamableAsCollectionMap(out, o, sType); 191 } else if (sType.isReader()) { 192 pipe((Reader)o, out); 193 } else if (sType.isInputStream()) { 194 pipe((InputStream)o, out); 195 } else { 196 // All other types can't be serialized as key/value pairs, so we create a 197 // mock key/value pair with a "_value" key. 198 out.append("_value="); 199 pop(); 200 super.writeAnything(out, o, null, null, null); 201 return out; 202 } 203 204 pop(); 205 return out; 206 } 207 208 209 private SerializerWriter<?> writeBeanMap(UonWriter out, BeanMap<?> m, String typeName) throws SerializeException { 210 var addAmp = Flag.create(); 211 212 if (nn(typeName)) { 213 var pm = m.getMeta().getTypeProperty(); 214 out.appendObject(pm.getName(), true).append('=').appendObject(typeName, false); 215 addAmp.set(); 216 } 217 218 Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x); 219 m.forEachValue(checkNull, (pMeta, key, value, thrown) -> { 220 var cMeta = (ClassMeta<?>) pMeta.getBeanInfo(); 221 var sMeta = cMeta.getSerializedClassMeta(this); 222 223 if (nn(thrown)) 224 onBeanGetterException(pMeta, thrown); 225 226 if (canIgnoreValue(pMeta, key, value)) 227 return; 228 229 if (nn(value) && shouldUseExpandedParams(pMeta)) { 230 // Transformed object array bean properties may be transformed resulting in ArrayLists, 231 // so we need to check type if we think it's an array. 232 if (sMeta.isCollection() || value instanceof Collection) { 233 var c = (Collection<?>) value; 234 if (c.isEmpty()) { 235 // Empty expanded-params collection: emit a single "key=" segment so the 236 // parser can distinguish "non-null empty collection" from "null property" 237 // (both would otherwise produce no wire output, defeating round-trip). 238 addAmp.ifSet(() -> out.cr(indent).append('&')).set(); 239 out.appendObject(key, true).append('='); 240 } else { 241 c.forEach(x -> { 242 addAmp.ifSet(() -> out.cr(indent).append('&')).set(); 243 out.appendObject(key, true).append('='); 244 super.writeAnything(out, x, cMeta.getElementType(), key, pMeta); 245 }); 246 } 247 } else /* array */ { 248 var len = Array.getLength(value); 249 if (len == 0) { 250 // Empty expanded-params array: emit a single "key=" segment so the parser 251 // can distinguish "non-null empty array" from "null property" (both would 252 // otherwise produce no wire output, defeating round-trip). 253 addAmp.ifSet(() -> out.cr(indent).append('&')).set(); 254 out.appendObject(key, true).append('='); 255 } else { 256 for (var i = 0; i < len; i++) { 257 addAmp.ifSet(() -> out.cr(indent).append('&')).set(); 258 out.appendObject(key, true).append('='); 259 super.writeAnything(out, Array.get(value, i), cMeta.getElementType(), key, pMeta); 260 } 261 } 262 } 263 } else { 264 addAmp.ifSet(() -> out.cr(indent).append('&')).set(); 265 out.appendObject(key, true).append('='); 266 super.writeAnything(out, value, cMeta, key, pMeta); 267 } 268 }); 269 270 return out; 271 } 272 273 private SerializerWriter<?> writeCollectionMap(UonWriter out, Map<?,?> m, ClassMeta<?> type) throws SerializeException { 274 275 var valueType = type.getValueType(); 276 277 var addAmp = Flag.create(); 278 279 m.forEach((k, v) -> { 280 addAmp.ifSet(() -> out.cr(indent).append('&')).set(); 281 out.append(k).append('='); 282 super.writeAnything(out, v, valueType, null, null); 283 }); 284 285 return out; 286 } 287 288 private SerializerWriter<?> writeStreamableAsCollectionMap(UonWriter out, Object o, ClassMeta<?> sType) throws SerializeException { 289 var addAmp = Flag.create(); 290 var i = IntegerHolder.create(); 291 forEachStreamableEntry(o, sType, v -> { 292 addAmp.ifSet(() -> out.cr(indent).append('&')).set(); 293 out.append(i.getAndIncrement()).append('='); 294 super.writeAnything(out, v, null, null, null); 295 }); 296 return out; 297 } 298 299 private SerializerWriter<?> writeMap(UonWriter out, Map m, ClassMeta<?> type) throws SerializeException { 300 301 var keyType = type.getKeyType(); 302 var valueType = type.getValueType(); 303 304 var addAmp = Flag.create(); 305 306 forEachEntry(m, e -> { 307 var key = generalize(e.getKey(), keyType); 308 var value = e.getValue(); 309 310 if (shouldUseExpandedParams(value)) { 311 if (value instanceof Collection value2) { 312 value2.forEach(x -> { 313 addAmp.ifSet(() -> out.cr(indent).append('&')).set(); 314 out.appendObject(key, true).append('='); 315 super.writeAnything(out, x, null, s(key), null); 316 }); 317 } else /* array */ { 318 for (var i = 0; i < Array.getLength(value); i++) { 319 addAmp.ifSet(() -> out.cr(indent).append('&')).set(); 320 out.appendObject(key, true).append('='); 321 super.writeAnything(out, Array.get(value, i), null, s(key), null); 322 } 323 } 324 } else { 325 addAmp.ifSet(() -> out.cr(indent).append('&')).set(); 326 out.appendObject(key, true).append('='); 327 super.writeAnything(out, value, valueType, (key == null ? null : key.toString()), null); 328 } 329 }); 330 331 return out; 332 } 333 334 /* 335 * Returns <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs. 336 */ 337 private boolean shouldUseExpandedParams(BeanPropertyMeta pMeta) { 338 var cm = ((ClassMeta<?>) pMeta.getBeanInfo()).getSerializedClassMeta(this); 339 return cm.isCollectionOrArray() && (isExpandedParams() || getUrlEncodingClassMeta((ClassMeta<?>) pMeta.getBeanMeta().getBeanInfo()).isExpandedParams()); 340 } 341 342 /* 343 * Returns <jk>true</jk> if the specified value should be represented as an expanded parameter list. 344 */ 345 private boolean shouldUseExpandedParams(Object value) { 346 if (value == null || ! isExpandedParams()) 347 return false; 348 var cm = getClassMetaForObject(value).getSerializedClassMeta(this); 349 return cm.isCollectionOrArray() && isExpandedParams(); 350 } 351 352 @Override /* Overridden from SerializerSession */ 353 protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException { 354 writeAnything(getUonWriter(out).i(getInitialDepth()), o); 355 } 356 357 /** 358 * Returns the language-specific metadata on the specified class. 359 * 360 * @param cm The class to return the metadata on. 361 * @return The metadata. 362 */ 363 protected UrlEncodingClassMeta getUrlEncodingClassMeta(ClassMeta<?> cm) { 364 return ctx.getUrlEncodingClassMeta(cm); 365 } 366 367 /** 368 * Serialize bean property collections/arrays as separate key/value pairs. 369 * 370 * @see UrlEncodingSerializer.Builder#expandedParams() 371 * @return 372 * <jk>false</jk> if serializing the array <c>[1,2,3]</c> results in <c>?key=$a(1,2,3)</c>. 373 * <br><jk>true</jk> if serializing the same array results in <c>?key=1&key=2&key=3</c>. 374 */ 375 protected final boolean isExpandedParams() { return expandedParams; } 376}