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 java.util.*; 016import java.util.concurrent.*; 017 018import org.apache.juneau.*; 019import org.apache.juneau.annotation.*; 020import org.apache.juneau.serializer.*; 021import org.apache.juneau.uon.*; 022 023/** 024 * Serializes POJO models to URL-encoded notation with UON-encoded values (a notation for URL-encoded query paramter values). 025 * 026 * <h5 class='section'>Media types:</h5> 027 * 028 * Handles <c>Accept</c> types: <bc>application/x-www-form-urlencoded</bc> 029 * <p> 030 * Produces <c>Content-Type</c> types: <bc>application/x-www-form-urlencoded</bc> 031 * 032 * <h5 class='topic'>Description</h5> 033 * 034 * This serializer provides several serialization options. 035 * <br>Typically, one of the predefined DEFAULT serializers will be sufficient. 036 * <br>However, custom serializers can be constructed to fine-tune behavior. 037 * 038 * <p> 039 * The following shows a sample object defined in Javascript: 040 * <p class='bcode w800'> 041 * { 042 * id: 1, 043 * name: <js>'John Smith'</js>, 044 * uri: <js>'http://sample/addressBook/person/1'</js>, 045 * addressBookUri: <js>'http://sample/addressBook'</js>, 046 * birthDate: <js>'1946-08-12T00:00:00Z'</js>, 047 * otherIds: <jk>null</jk>, 048 * addresses: [ 049 * { 050 * uri: <js>'http://sample/addressBook/address/1'</js>, 051 * personUri: <js>'http://sample/addressBook/person/1'</js>, 052 * id: 1, 053 * street: <js>'100 Main Street'</js>, 054 * city: <js>'Anywhereville'</js>, 055 * state: <js>'NY'</js>, 056 * zip: 12345, 057 * isCurrent: <jk>true</jk>, 058 * } 059 * ] 060 * } 061 * </p> 062 * 063 * <p> 064 * Using the "strict" syntax defined in this document, the equivalent URL-encoded notation would be as follows: 065 * <p class='bcode w800'> 066 * <ua>id</ua>=<un>1</un> 067 * &<ua>name</ua>=<us>'John+Smith'</us>, 068 * &<ua>uri</ua>=<us>http://sample/addressBook/person/1</us>, 069 * &<ua>addressBookUri</ua>=<us>http://sample/addressBook</us>, 070 * &<ua>birthDate</ua>=<us>1946-08-12T00:00:00Z</us>, 071 * &<ua>otherIds</ua>=<uk>null</uk>, 072 * &<ua>addresses</ua>=@( 073 * ( 074 * <ua>uri</ua>=<us>http://sample/addressBook/address/1</us>, 075 * <ua>personUri</ua>=<us>http://sample/addressBook/person/1</us>, 076 * <ua>id</ua>=<un>1</un>, 077 * <ua>street</ua>=<us>'100+Main+Street'</us>, 078 * <ua>city</ua>=<us>Anywhereville</us>, 079 * <ua>state</ua>=<us>NY</us>, 080 * <ua>zip</ua>=<un>12345</un>, 081 * <ua>isCurrent</ua>=<uk>true</uk> 082 * ) 083 * ) 084 * </p> 085 * 086 * <h5 class='section'>Example:</h5> 087 * <p class='bcode w800'> 088 * <jc>// Serialize a Map</jc> 089 * Map m = <jk>new</jk> ObjectMap(<js>"{a:'b',c:1,d:false,e:['f',1,false],g:{h:'i'}}"</js>); 090 * 091 * <jc>// Serialize to value equivalent to JSON.</jc> 092 * <jc>// Produces "a=b&c=1&d=false&e=@(f,1,false)&g=(h=i)"</jc> 093 * String s = UrlEncodingSerializer.<jsf>DEFAULT</jsf>.serialize(s); 094 * 095 * <jc>// Serialize a bean</jc> 096 * <jk>public class</jk> Person { 097 * <jk>public</jk> Person(String s); 098 * <jk>public</jk> String getName(); 099 * <jk>public int</jk> getAge(); 100 * <jk>public</jk> Address getAddress(); 101 * <jk>public boolean</jk> deceased; 102 * } 103 * 104 * <jk>public class</jk> Address { 105 * <jk>public</jk> String getStreet(); 106 * <jk>public</jk> String getCity(); 107 * <jk>public</jk> String getState(); 108 * <jk>public int</jk> getZip(); 109 * } 110 * 111 * Person p = <jk>new</jk> Person(<js>"John Doe"</js>, 23, <js>"123 Main St"</js>, <js>"Anywhere"</js>, <js>"NY"</js>, 12345, <jk>false</jk>); 112 * 113 * <jc>// Produces "name=John+Doe&age=23&address=(street='123+Main+St',city=Anywhere,state=NY,zip=12345)&deceased=false"</jc> 114 * String s = UrlEncodingSerializer.<jsf>DEFAULT</jsf>.serialize(s); 115 * </p> 116 */ 117@ConfigurableContext 118public class UrlEncodingSerializer extends UonSerializer implements UrlEncodingMetaProvider, UrlEncodingCommon { 119 120 //------------------------------------------------------------------------------------------------------------------- 121 // Configurable properties 122 //------------------------------------------------------------------------------------------------------------------- 123 124 static final String PREFIX = "UrlEncodingSerializer"; 125 126 /** 127 * Configuration property: Serialize bean property collections/arrays as separate key/value pairs. 128 * 129 * <h5 class='section'>Property:</h5> 130 * <ul class='spaced-list'> 131 * <li><b>ID:</b> {@link org.apache.juneau.urlencoding.UrlEncodingSerializer#URLENC_expandedParams URLENC_expandedParams} 132 * <li><b>Name:</b> <js>"UrlEncodingSerializer.expandedParams.b"</js> 133 * <li><b>Data type:</b> <jk>boolean</jk> 134 * <li><b>System property:</b> <c>UrlEncodingSerializer.expandedParams</c> 135 * <li><b>Environment variable:</b> <c>URLENCODINGSERIALIZER_EXPANDEDPARAMS</c> 136 * <li><b>Default:</b> <jk>false</jk> 137 * <li><b>Session property:</b> <jk>false</jk> 138 * <li><b>Annotations:</b> 139 * <ul> 140 * <li class='ja'>{@link org.apache.juneau.urlencoding.annotation.UrlEncodingConfig#expandedParams()} 141 * </ul> 142 * <li><b>Methods:</b> 143 * <ul> 144 * <li class='jm'>{@link org.apache.juneau.urlencoding.UrlEncodingSerializerBuilder#expandedParams(boolean)} 145 * <li class='jm'>{@link org.apache.juneau.urlencoding.UrlEncodingSerializerBuilder#expandedParams()} 146 * </ul> 147 * </ul> 148 * 149 * <h5 class='section'>Description:</h5> 150 * <p> 151 * If <jk>false</jk>, serializing the array <c>[1,2,3]</c> results in <c>?key=$a(1,2,3)</c>. 152 * <br>If <jk>true</jk>, serializing the same array results in <c>?key=1&key=2&key=3</c>. 153 * 154 * <p> 155 * This option only applies to beans. 156 * 157 * <ul class='notes'> 158 * <li> 159 * If parsing multi-part parameters, it's highly recommended to use <c>Collections</c> or <c>Lists</c> 160 * as bean property types instead of arrays since arrays have to be recreated from scratch every time a value 161 * is added to it. 162 * </ul> 163 * 164 * <h5 class='section'>Example:</h5> 165 * <p class='bcode w800'> 166 * <jc>// A sample bean.</jc> 167 * <jk>public class</jk> A { 168 * <jk>public</jk> String[] f1 = {<js>"a"</js>,<js>"b"</js>}; 169 * <jk>public</jk> List<String> f2 = Arrays.<jsm>asList</jsm>(<jk>new</jk> String[]{<js>"c"</js>,<js>"d"</js>}); 170 * } 171 * 172 * <jc>// Normal serializer.</jc> 173 * WriterSerializer s1 = UrlEncodingSerializer.<jsf>DEFAULT</jsf>; 174 * 175 * <jc>// Expanded-params serializer.</jc> 176 * WriterSerializer s2 = UrlEncodingSerializer.<jsm>create</jsm>().expandedParams().build(); 177 * 178 * <jc>// Produces "f1=(a,b)&f2=(c,d)"</jc> 179 * String ss1 = s1.serialize(<jk>new</jk> A()); 180 * 181 * <jc>// Produces "f1=a&f1=b&f2=c&f2=d"</jc> 182 * String ss2 = s2.serialize(<jk>new</jk> A()); <jc> 183 * </p> 184 * 185 */ 186 public static final String URLENC_expandedParams = PREFIX + ".expandedParams.b"; 187 188 189 //------------------------------------------------------------------------------------------------------------------- 190 // Predefined instances 191 //------------------------------------------------------------------------------------------------------------------- 192 193 /** Reusable instance of {@link UrlEncodingSerializer}, all default settings. */ 194 public static final UrlEncodingSerializer DEFAULT = new UrlEncodingSerializer(PropertyStore.DEFAULT); 195 196 /** Reusable instance of {@link UrlEncodingSerializer.PlainText}. */ 197 public static final UrlEncodingSerializer DEFAULT_PLAINTEXT = new PlainText(PropertyStore.DEFAULT); 198 199 /** Reusable instance of {@link UrlEncodingSerializer.Expanded}. */ 200 public static final UrlEncodingSerializer DEFAULT_EXPANDED = new Expanded(PropertyStore.DEFAULT); 201 202 /** Reusable instance of {@link UrlEncodingSerializer.Readable}. */ 203 public static final UrlEncodingSerializer DEFAULT_READABLE = new Readable(PropertyStore.DEFAULT); 204 205 206 //------------------------------------------------------------------------------------------------------------------- 207 // Predefined subclasses 208 //------------------------------------------------------------------------------------------------------------------- 209 210 /** 211 * Equivalent to <code>UrlEncodingSerializer.<jsm>create</jsm>().expandedParams().build();</code>. 212 */ 213 public static class Expanded extends UrlEncodingSerializer { 214 215 /** 216 * Constructor. 217 * 218 * @param ps The property store containing all the settings for this object. 219 */ 220 public Expanded(PropertyStore ps) { 221 super(ps.builder().set(URLENC_expandedParams, true).build()); 222 } 223 } 224 225 /** 226 * Equivalent to <code>UrlEncodingSerializer.<jsm>create</jsm>().useWhitespace().build();</code>. 227 */ 228 public static class Readable extends UrlEncodingSerializer { 229 230 /** 231 * Constructor. 232 * 233 * @param ps The property store containing all the settings for this object. 234 */ 235 public Readable(PropertyStore ps) { 236 super(ps.builder().set(WSERIALIZER_useWhitespace, true).build()); 237 } 238 } 239 240 /** 241 * Equivalent to <code>UrlEncodingSerializer.<jsm>create</jsm>().plainTextParts().build();</code>. 242 */ 243 public static class PlainText extends UrlEncodingSerializer { 244 245 /** 246 * Constructor. 247 * 248 * @param ps The property store containing all the settings for this object. 249 */ 250 public PlainText(PropertyStore ps) { 251 super(ps.builder().set(UON_paramFormat, "PLAINTEXT").build()); 252 } 253 } 254 255 256 //------------------------------------------------------------------------------------------------------------------- 257 // Instance 258 //------------------------------------------------------------------------------------------------------------------- 259 260 private final boolean 261 expandedParams; 262 private final Map<ClassMeta<?>,UrlEncodingClassMeta> urlEncodingClassMetas = new ConcurrentHashMap<>(); 263 private final Map<BeanPropertyMeta,UrlEncodingBeanPropertyMeta> urlEncodingBeanPropertyMetas = new ConcurrentHashMap<>(); 264 265 /** 266 * Constructor. 267 * 268 * @param ps 269 * The property store containing all the settings for this object. 270 */ 271 public UrlEncodingSerializer(PropertyStore ps) { 272 this(ps, "application/x-www-form-urlencoded", (String)null); 273 } 274 275 /** 276 * Constructor. 277 * 278 * @param ps 279 * The property store containing all the settings for this object. 280 * @param produces 281 * The media type that this serializer produces. 282 * @param accept 283 * The accept media types that the serializer can handle. 284 * <p> 285 * Can contain meta-characters per the <c>media-type</c> specification of {@doc RFC2616.section14.1} 286 * <p> 287 * If empty, then assumes the only media type supported is <c>produces</c>. 288 * <p> 289 * For example, if this serializer produces <js>"application/json"</js> but should handle media types of 290 * <js>"application/json"</js> and <js>"text/json"</js>, then the arguments should be: 291 * <p class='bcode w800'> 292 * <jk>super</jk>(ps, <js>"application/json"</js>, <js>"application/json,text/json"</js>); 293 * </p> 294 * <br>...or... 295 * <p class='bcode w800'> 296 * <jk>super</jk>(ps, <js>"application/json"</js>, <js>"*​/json"</js>); 297 * </p> 298 * <p> 299 * The accept value can also contain q-values. 300 */ 301 public UrlEncodingSerializer(PropertyStore ps, String produces, String accept) { 302 super( 303 ps.builder() 304 .set(UON_encoding, true) 305 .build(), 306 produces, 307 accept 308 ); 309 expandedParams = getBooleanProperty(URLENC_expandedParams, false); 310 } 311 312 @Override /* Context */ 313 public UrlEncodingSerializerBuilder builder() { 314 return new UrlEncodingSerializerBuilder(getPropertyStore()); 315 } 316 317 /** 318 * Instantiates a new clean-slate {@link UrlEncodingSerializerBuilder} object. 319 * 320 * <p> 321 * This is equivalent to simply calling <code><jk>new</jk> UrlEncodingSerializerBuilder()</code>. 322 * 323 * <p> 324 * Note that this method creates a builder initialized to all default settings, whereas {@link #builder()} copies 325 * the settings of the object called on. 326 * 327 * @return A new {@link UrlEncodingSerializerBuilder} object. 328 */ 329 public static UrlEncodingSerializerBuilder create() { 330 return new UrlEncodingSerializerBuilder(); 331 } 332 333 334 //----------------------------------------------------------------------------------------------------------------- 335 // Entry point methods 336 //----------------------------------------------------------------------------------------------------------------- 337 338 @Override /* Context */ 339 public UrlEncodingSerializerSession createSession() { 340 return createSession(createDefaultSessionArgs()); 341 } 342 343 @Override /* Serializer */ 344 public UrlEncodingSerializerSession createSession(SerializerSessionArgs args) { 345 return new UrlEncodingSerializerSession(this, null, args); 346 } 347 348 //----------------------------------------------------------------------------------------------------------------- 349 // Extended metadata 350 //----------------------------------------------------------------------------------------------------------------- 351 352 @Override /* UrlEncodingMetaProvider */ 353 public UrlEncodingClassMeta getUrlEncodingClassMeta(ClassMeta<?> cm) { 354 UrlEncodingClassMeta m = urlEncodingClassMetas.get(cm); 355 if (m == null) { 356 m = new UrlEncodingClassMeta(cm, this); 357 urlEncodingClassMetas.put(cm, m); 358 } 359 return m; 360 } 361 362 @Override /* UrlEncodingMetaProvider */ 363 public UrlEncodingBeanPropertyMeta getUrlEncodingBeanPropertyMeta(BeanPropertyMeta bpm) { 364 if (bpm == null) 365 return UrlEncodingBeanPropertyMeta.DEFAULT; 366 UrlEncodingBeanPropertyMeta m = urlEncodingBeanPropertyMetas.get(bpm); 367 if (m == null) { 368 m = new UrlEncodingBeanPropertyMeta(bpm.getDelegateFor(), this); 369 urlEncodingBeanPropertyMetas.put(bpm, m); 370 } 371 return m; 372 } 373 374 //----------------------------------------------------------------------------------------------------------------- 375 // Properties 376 //----------------------------------------------------------------------------------------------------------------- 377 378 /** 379 * Configuration property: Serialize bean property collections/arrays as separate key/value pairs. 380 * 381 * @see #URLENC_expandedParams 382 * @return 383 * <jk>false</jk> if serializing the array <c>[1,2,3]</c> results in <c>?key=$a(1,2,3)</c>. 384 * <br><jk>true</jk> if serializing the same array results in <c>?key=1&key=2&key=3</c>. 385 */ 386 protected final boolean isExpandedParams() { 387 return expandedParams; 388 } 389 390 //----------------------------------------------------------------------------------------------------------------- 391 // Other methods 392 //----------------------------------------------------------------------------------------------------------------- 393 394 @Override /* Context */ 395 public ObjectMap toMap() { 396 return super.toMap() 397 .append("UrlEncodingSerializer", new DefaultFilteringObjectMap() 398 .append("expandedParams", expandedParams) 399 ); 400 } 401}