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.collections.*; 021import org.apache.juneau.serializer.*; 022import org.apache.juneau.uon.*; 023 024/** 025 * Serializes POJO models to URL-encoded notation with UON-encoded values (a notation for URL-encoded query paramter values). 026 * 027 * <h5 class='section'>Media types:</h5> 028 * 029 * Handles <c>Accept</c> types: <bc>application/x-www-form-urlencoded</bc> 030 * <p> 031 * Produces <c>Content-Type</c> types: <bc>application/x-www-form-urlencoded</bc> 032 * 033 * <h5 class='topic'>Description</h5> 034 * 035 * This serializer provides several serialization options. 036 * <br>Typically, one of the predefined DEFAULT serializers will be sufficient. 037 * <br>However, custom serializers can be constructed to fine-tune behavior. 038 * 039 * <p> 040 * The following shows a sample object defined in Javascript: 041 * <p class='bcode w800'> 042 * { 043 * id: 1, 044 * name: <js>'John Smith'</js>, 045 * uri: <js>'http://sample/addressBook/person/1'</js>, 046 * addressBookUri: <js>'http://sample/addressBook'</js>, 047 * birthDate: <js>'1946-08-12T00:00:00Z'</js>, 048 * otherIds: <jk>null</jk>, 049 * addresses: [ 050 * { 051 * uri: <js>'http://sample/addressBook/address/1'</js>, 052 * personUri: <js>'http://sample/addressBook/person/1'</js>, 053 * id: 1, 054 * street: <js>'100 Main Street'</js>, 055 * city: <js>'Anywhereville'</js>, 056 * state: <js>'NY'</js>, 057 * zip: 12345, 058 * isCurrent: <jk>true</jk>, 059 * } 060 * ] 061 * } 062 * </p> 063 * 064 * <p> 065 * Using the "strict" syntax defined in this document, the equivalent URL-encoded notation would be as follows: 066 * <p class='bcode w800'> 067 * <ua>id</ua>=<un>1</un> 068 * &<ua>name</ua>=<us>'John+Smith'</us>, 069 * &<ua>uri</ua>=<us>http://sample/addressBook/person/1</us>, 070 * &<ua>addressBookUri</ua>=<us>http://sample/addressBook</us>, 071 * &<ua>birthDate</ua>=<us>1946-08-12T00:00:00Z</us>, 072 * &<ua>otherIds</ua>=<uk>null</uk>, 073 * &<ua>addresses</ua>=@( 074 * ( 075 * <ua>uri</ua>=<us>http://sample/addressBook/address/1</us>, 076 * <ua>personUri</ua>=<us>http://sample/addressBook/person/1</us>, 077 * <ua>id</ua>=<un>1</un>, 078 * <ua>street</ua>=<us>'100+Main+Street'</us>, 079 * <ua>city</ua>=<us>Anywhereville</us>, 080 * <ua>state</ua>=<us>NY</us>, 081 * <ua>zip</ua>=<un>12345</un>, 082 * <ua>isCurrent</ua>=<uk>true</uk> 083 * ) 084 * ) 085 * </p> 086 * 087 * <h5 class='section'>Example:</h5> 088 * <p class='bcode w800'> 089 * <jc>// Serialize a Map</jc> 090 * Map m = OMap.<jsm>ofJson</jsm>(<js>"{a:'b',c:1,d:false,e:['f',1,false],g:{h:'i'}}"</js>); 091 * 092 * <jc>// Serialize to value equivalent to JSON.</jc> 093 * <jc>// Produces "a=b&c=1&d=false&e=@(f,1,false)&g=(h=i)"</jc> 094 * String s = UrlEncodingSerializer.<jsf>DEFAULT</jsf>.serialize(s); 095 * 096 * <jc>// Serialize a bean</jc> 097 * <jk>public class</jk> Person { 098 * <jk>public</jk> Person(String s); 099 * <jk>public</jk> String getName(); 100 * <jk>public int</jk> getAge(); 101 * <jk>public</jk> Address getAddress(); 102 * <jk>public boolean</jk> deceased; 103 * } 104 * 105 * <jk>public class</jk> Address { 106 * <jk>public</jk> String getStreet(); 107 * <jk>public</jk> String getCity(); 108 * <jk>public</jk> String getState(); 109 * <jk>public int</jk> getZip(); 110 * } 111 * 112 * 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>); 113 * 114 * <jc>// Produces "name=John+Doe&age=23&address=(street='123+Main+St',city=Anywhere,state=NY,zip=12345)&deceased=false"</jc> 115 * String s = UrlEncodingSerializer.<jsf>DEFAULT</jsf>.serialize(s); 116 * </p> 117 */ 118@ConfigurableContext 119public class UrlEncodingSerializer extends UonSerializer implements UrlEncodingMetaProvider, UrlEncodingCommon { 120 121 //------------------------------------------------------------------------------------------------------------------- 122 // Configurable properties 123 //------------------------------------------------------------------------------------------------------------------- 124 125 static final String PREFIX = "UrlEncodingSerializer"; 126 127 /** 128 * Configuration property: Serialize bean property collections/arrays as separate key/value pairs. 129 * 130 * <h5 class='section'>Property:</h5> 131 * <ul class='spaced-list'> 132 * <li><b>ID:</b> {@link org.apache.juneau.urlencoding.UrlEncodingSerializer#URLENC_expandedParams URLENC_expandedParams} 133 * <li><b>Name:</b> <js>"UrlEncodingSerializer.expandedParams.b"</js> 134 * <li><b>Data type:</b> <jk>boolean</jk> 135 * <li><b>System property:</b> <c>UrlEncodingSerializer.expandedParams</c> 136 * <li><b>Environment variable:</b> <c>URLENCODINGSERIALIZER_EXPANDEDPARAMS</c> 137 * <li><b>Default:</b> <jk>false</jk> 138 * <li><b>Session property:</b> <jk>false</jk> 139 * <li><b>Annotations:</b> 140 * <ul> 141 * <li class='ja'>{@link org.apache.juneau.urlencoding.annotation.UrlEncodingConfig#expandedParams()} 142 * </ul> 143 * <li><b>Methods:</b> 144 * <ul> 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().setDefault(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().setDefault(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().setDefault(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 ExtRFC2616.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 .setDefault(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 * 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 OMap toMap() { 396 return super.toMap() 397 .a("UrlEncodingSerializer", new DefaultFilteringOMap() 398 .a("expandedParams", expandedParams) 399 ); 400 } 401}