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.SystemUtils.*; 020 021import java.util.*; 022import java.util.concurrent.*; 023 024import org.apache.juneau.commons.bean.*; 025import org.apache.juneau.commons.collections.*; 026import org.apache.juneau.marshall.*; 027import org.apache.juneau.marshall.uon.*; 028 029/** 030 * Serializes POJO models to URL-encoded notation with UON-encoded values (a notation for URL-encoded query paramter values). 031 * 032 * <h5 class='section'>Media types:</h5> 033 * <p> 034 * Handles <c>Accept</c> types: <bc>application/x-www-form-urlencoded</bc> 035 * <p> 036 * Produces <c>Content-Type</c> types: <bc>application/x-www-form-urlencoded</bc> 037 * 038 * <h5 class='topic'>Description</h5> 039 * 040 * This serializer provides several serialization options. 041 * <br>Typically, one of the predefined DEFAULT serializers will be sufficient. 042 * <br>However, custom serializers can be constructed to fine-tune behavior. 043 * 044 * <p> 045 * The following shows a sample object defined in Javascript: 046 * <p class='bjson'> 047 * { 048 * id: 1, 049 * name: <js>'John Smith'</js>, 050 * uri: <js>'http://sample/addressBook/person/1'</js>, 051 * addressBookUri: <js>'http://sample/addressBook'</js>, 052 * birthDate: <js>'1946-08-12T00:00:00Z'</js>, 053 * otherIds: <jk>null</jk>, 054 * addresses: [ 055 * { 056 * uri: <js>'http://sample/addressBook/address/1'</js>, 057 * personUri: <js>'http://sample/addressBook/person/1'</js>, 058 * id: 1, 059 * street: <js>'100 Main Street'</js>, 060 * city: <js>'Anywhereville'</js>, 061 * state: <js>'NY'</js>, 062 * zip: 12345, 063 * isCurrent: <jk>true</jk>, 064 * } 065 * ] 066 * } 067 * </p> 068 * 069 * <p> 070 * Using the "strict" syntax defined in this document, the equivalent URL-encoded notation would be as follows: 071 * <p class='burlenc'> 072 * <ua>id</ua>=<un>1</un> 073 * &<ua>name</ua>=<us>'John+Smith'</us>, 074 * &<ua>uri</ua>=<us>http://sample/addressBook/person/1</us>, 075 * &<ua>addressBookUri</ua>=<us>http://sample/addressBook</us>, 076 * &<ua>birthDate</ua>=<us>1946-08-12T00:00:00Z</us>, 077 * &<ua>otherIds</ua>=<uk>null</uk>, 078 * &<ua>addresses</ua>=@( 079 * ( 080 * <ua>uri</ua>=<us>http://sample/addressBook/address/1</us>, 081 * <ua>personUri</ua>=<us>http://sample/addressBook/person/1</us>, 082 * <ua>id</ua>=<un>1</un>, 083 * <ua>street</ua>=<us>'100+Main+Street'</us>, 084 * <ua>city</ua>=<us>Anywhereville</us>, 085 * <ua>state</ua>=<us>NY</us>, 086 * <ua>zip</ua>=<un>12345</un>, 087 * <ua>isCurrent</ua>=<uk>true</uk> 088 * ) 089 * ) 090 * </p> 091 * 092 * <h5 class='section'>Example:</h5> 093 * <p class='bjava'> 094 * <jc>// Serialize a Map</jc> 095 * Map <jv>map</jv> = JsonMap.<jsm>ofText</jsm>(<js>"{a:'b',c:1,d:false,e:['f',1,false],g:{h:'i'}}"</js>); 096 * 097 * <jc>// Serialize to value equivalent to JSON.</jc> 098 * <jc>// Produces "a=b&c=1&d=false&e=@(f,1,false)&g=(h=i)"</jc> 099 * String <jv>uenc</jv> = UrlEncodingSerializer.<jsf>DEFAULT</jsf>.write(<jv>map</jv>); 100 * 101 * <jc>// Serialize a bean</jc> 102 * <jk>public class</jk> Person { 103 * <jk>public</jk> Person(String <jv>name</jv>); 104 * <jk>public</jk> String getName(); 105 * <jk>public int</jk> getAge(); 106 * <jk>public</jk> Address getAddress(); 107 * <jk>public boolean</jk> deceased; 108 * } 109 * 110 * <jk>public class</jk> Address { 111 * <jk>public</jk> String getStreet(); 112 * <jk>public</jk> String getCity(); 113 * <jk>public</jk> String getState(); 114 * <jk>public int</jk> getZip(); 115 * } 116 * 117 * Person <jv>person</jv> = <jk>new</jk> Person(<js>"John Doe"</js>, 23, <js>"123 Main St"</js>, <js>"Anywhere"</js>, <js>"NY"</js>, 12345, <jk>false</jk>); 118 * 119 * <jc>// Produces "name=John+Doe&age=23&address=(street='123+Main+St',city=Anywhere,state=NY,zip=12345)&deceased=false"</jc> 120 * String <jv>uenc</jv> = UrlEncodingSerializer.<jsf>DEFAULT</jsf>.write(<jv>person</jv>); 121 * </p> 122 * 123 * <h5 class='section'>Notes:</h5><ul> 124 * <li class='note'>This class is thread safe and reusable. 125 * </ul> 126 * 127 * <h5 class='section'>See Also:</h5><ul> 128 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/UrlEncodingSupport">URL-Encoding</a> 129 * </ul> 130 */ 131@SuppressWarnings({ 132 "java:S110", // Inheritance depth acceptable for this class hierarchy 133 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 134 "java:S9149" // Per-format static factories intentionally shadow the parent's. 135}) 136public class UrlEncodingSerializer extends UonSerializer implements UrlEncodingMetaProvider { 137 138 /** 139 * Builder class. 140 */ 141 public static class Builder extends UonSerializer.Builder<Builder> { 142 143 private static final Cache<HashKey,UrlEncodingSerializer> CACHE = Cache.of(HashKey.class, UrlEncodingSerializer.class).build(); 144 145 boolean expandedParams; 146 147 /** 148 * Constructor, default settings. 149 */ 150 protected Builder() { 151 produces("application/x-www-form-urlencoded"); 152 expandedParams = env("UrlEncoding.expandedParams", false); 153 } 154 155 /** 156 * Copy constructor. 157 * 158 * @param copyFrom The builder to copy from. 159 * <br>Cannot be <jk>null</jk>. 160 */ 161 protected Builder(Builder copyFrom) { 162 super(copyFrom); 163 expandedParams = copyFrom.expandedParams; 164 } 165 166 /** 167 * Copy constructor. 168 * 169 * @param copyFrom The bean to copy from. 170 * <br>Cannot be <jk>null</jk>. 171 */ 172 protected Builder(UrlEncodingSerializer copyFrom) { 173 super(copyFrom); 174 expandedParams = copyFrom.expandedParams; 175 } 176 177 @Override /* Overridden from Context.Builder<?> */ 178 public UrlEncodingSerializer build() { 179 return cache(CACHE).build(UrlEncodingSerializer.class); 180 } 181 182 @Override /* Overridden from Context.Builder<?> */ 183 public Builder copy() { 184 return new Builder(this); 185 } 186 187 /** 188 * Serialize bean property collections/arrays as separate key/value pairs. 189 * 190 * <p> 191 * By default, serializing the array <c>[1,2,3]</c> results in <c>?key=$a(1,2,3)</c>. 192 * <br>When enabled, serializing the same array results in <c>?key=1&key=2&key=3</c>. 193 * 194 * <p> 195 * This option only applies to beans. 196 * 197 * <h5 class='section'>Notes:</h5><ul> 198 * <li class='note'> 199 * If parsing multi-part parameters, it's highly recommended to use <c>Collections</c> or <c>Lists</c> 200 * as bean property types instead of arrays since arrays have to be recreated from scratch every time a value 201 * is added to it. 202 * </ul> 203 * 204 * <h5 class='section'>Example:</h5> 205 * <p class='bjava'> 206 * <jc>// A sample bean.</jc> 207 * <jk>public class</jk> A { 208 * <jk>public</jk> String[] <jf>f1</jf> = {<js>"a"</js>,<js>"b"</js>}; 209 * <jk>public</jk> List<String> <jf>f2</jf> = Arrays.<jsm>asList</jsm>(<jk>new</jk> String[]{<js>"c"</js>,<js>"d"</js>}); 210 * } 211 * 212 * <jc>// Normal serializer.</jc> 213 * WriterSerializer <jv>serializer1</jv> = UrlEncodingSerializer.<jsf>DEFAULT</jsf>; 214 * 215 * <jc>// Expanded-params serializer.</jc> 216 * WriterSerializer <jv>serializer2</jv> = UrlEncodingSerializer.<jsm>create</jsm>().expandedParams().build(); 217 * 218 * <jc>// Produces "f1=(a,b)&f2=(c,d)"</jc> 219 * String <jv>out1</jv> = <jv>serializer1</jv>.write(<jk>new</jk> A()); 220 * 221 * <jc>// Produces "f1=a&f1=b&f2=c&f2=d"</jc> 222 * String <jv>out2</jv> = <jv>serializer2</jv>.write(<jk>new</jk> A()); 223 * </p> 224 * 225 * @return This object. 226 */ 227 public Builder expandedParams() { 228 return expandedParams(true); 229 } 230 231 /** 232 * Same as {@link #expandedParams()} but allows you to explicitly specify the value. 233 * 234 * @param value The value for this setting. 235 * @return This object. 236 */ 237 public Builder expandedParams(boolean value) { 238 expandedParams = value; 239 return this; 240 } 241 242 @Override /* Overridden from Context.Builder<?> */ 243 public HashKey hashKey() { 244 // @formatter:off 245 return HashKey.of( 246 super.hashKey(), 247 expandedParams 248 ); 249 // @formatter:on 250 } 251 252 253 } 254 255 /** 256 * Equivalent to <code>UrlEncodingSerializer.<jsm>create</jsm>().expandedParams().build();</code>. 257 */ 258 public static class Expanded extends UrlEncodingSerializer { 259 260 /** 261 * Constructor. 262 * 263 * @param builder The builder for this object. 264 */ 265 public Expanded(Builder builder) { 266 super(builder.expandedParams()); 267 } 268 } 269 270 /** 271 * Equivalent to <code>UrlEncodingSerializer.<jsm>create</jsm>().plainTextParts().build();</code>. 272 */ 273 public static class PlainText extends UrlEncodingSerializer { 274 275 /** 276 * Constructor. 277 * 278 * @param builder The builder for this object. 279 */ 280 public PlainText(Builder builder) { 281 super(builder.paramFormatPlain()); 282 } 283 } 284 285 /** 286 * Equivalent to <code>UrlEncodingSerializer.<jsm>create</jsm>().useWhitespace().build();</code>. 287 */ 288 public static class Readable extends UrlEncodingSerializer { 289 290 /** 291 * Constructor. 292 * 293 * @param builder The builder for this object. 294 */ 295 public Readable(Builder builder) { 296 super(builder.useWhitespace()); 297 } 298 } 299 300 /** Reusable instance of {@link UrlEncodingSerializer}, all default settings. */ 301 public static final UrlEncodingSerializer DEFAULT = new UrlEncodingSerializer(create()); 302 /** Reusable instance of {@link UrlEncodingSerializer.PlainText}. */ 303 public static final UrlEncodingSerializer DEFAULT_PLAINTEXT = new PlainText(create()); 304 305 /** Reusable instance of {@link UrlEncodingSerializer.Expanded}. */ 306 public static final UrlEncodingSerializer DEFAULT_EXPANDED = new Expanded(create()); 307 308 /** Reusable instance of {@link UrlEncodingSerializer.Readable}. */ 309 public static final UrlEncodingSerializer DEFAULT_READABLE = new Readable(create()); 310 311 /** 312 * Creates a new builder for this object. 313 * 314 * @return A new builder. 315 */ 316 public static Builder create() { 317 return new Builder(); 318 } 319 320 final boolean expandedParams; 321 322 private final Map<ClassMeta<?>,UrlEncodingClassMeta> urlEncodingClassMetas = new ConcurrentHashMap<>(); 323 private final Map<BeanPropertyMeta,UrlEncodingBeanPropertyMeta> urlEncodingBeanPropertyMetas = new ConcurrentHashMap<>(); 324 325 /** 326 * Constructor. 327 * 328 * @param builder The builder for this object. 329 */ 330 public UrlEncodingSerializer(Builder builder) { 331 super(builder.encoding()); 332 expandedParams = builder.expandedParams; 333 } 334 335 @Override /* Overridden from Context */ 336 public Builder copy() { 337 return new Builder(this); 338 } 339 340 @Override /* Overridden from Context */ 341 public UrlEncodingSerializerSession.Builder createSession() { 342 return UrlEncodingSerializerSession.create(this); 343 } 344 345 @Override /* Overridden from Context */ 346 public UrlEncodingSerializerSession getSession() { return createSession().build(); } 347 348 @Override /* Overridden from UrlEncodingMetaProvider */ 349 public UrlEncodingBeanPropertyMeta getUrlEncodingBeanPropertyMeta(BeanPropertyMeta bpm) { 350 if (bpm == null) 351 return UrlEncodingBeanPropertyMeta.DEFAULT; 352 return urlEncodingBeanPropertyMetas.computeIfAbsent(bpm, k -> new UrlEncodingBeanPropertyMeta(k.getDelegateFor(), this)); 353 } 354 355 @Override /* Overridden from UrlEncodingMetaProvider */ 356 public UrlEncodingClassMeta getUrlEncodingClassMeta(ClassMeta<?> cm) { 357 return urlEncodingClassMetas.computeIfAbsent(cm, k -> new UrlEncodingClassMeta(k, this)); 358 } 359 360 /** 361 * Serialize bean property collections/arrays as separate key/value pairs. 362 * 363 * @see Builder#expandedParams() 364 * @return 365 * <jk>false</jk> if serializing the array <c>[1,2,3]</c> results in <c>?key=$a(1,2,3)</c>. 366 * <br><jk>true</jk> if serializing the same array results in <c>?key=1&key=2&key=3</c>. 367 */ 368 protected final boolean isExpandedParams() { return expandedParams; } 369 370 @Override /* Overridden from UonSerializer */ 371 protected FluentMap<String,Object> properties() { 372 return super.properties() 373 .a("expandedParams", expandedParams); 374 } 375}