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.uon; 014 015import java.util.*; 016import java.util.concurrent.*; 017 018import org.apache.juneau.*; 019import org.apache.juneau.annotation.*; 020import org.apache.juneau.httppart.*; 021import org.apache.juneau.serializer.*; 022 023/** 024 * Serializes POJO models to UON (a notation for URL-encoded query parameter values). 025 * 026 * <h5 class='topic'>Media types</h5> 027 * 028 * Handles <c>Accept</c> types: <bc>text/uon</bc> 029 * <p> 030 * Produces <c>Content-Type</c> types: <bc>text/uon</bc> 031 * 032 * <h5 class='topic'>Description</h5> 033 * 034 * This serializer provides several serialization options. 035 * Typically, one of the predefined DEFAULT serializers will be sufficient. 036 * 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 UON notation would be as follows: 065 * <p class='bcode w800'> 066 * ( 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 * ) 086 * </p> 087 * 088 * <h5 class='section'>Example:</h5> 089 * <p class='bcode w800'> 090 * <jc>// Serialize a Map</jc> 091 * Map m = <jk>new</jk> ObjectMap(<js>"{a:'b',c:1,d:false,e:['f',1,false],g:{h:'i'}}"</js>); 092 * 093 * <jc>// Serialize to value equivalent to JSON.</jc> 094 * <jc>// Produces "(a=b,c=1,d=false,e=@(f,1,false),g=(h=i))"</jc> 095 * String s = UonSerializer.<jsf>DEFAULT</jsf>.serialize(s); 096 * 097 * <jc>// Serialize a bean</jc> 098 * <jk>public class</jk> Person { 099 * <jk>public</jk> Person(String s); 100 * <jk>public</jk> String getName(); 101 * <jk>public int</jk> getAge(); 102 * <jk>public</jk> Address getAddress(); 103 * <jk>public boolean</jk> deceased; 104 * } 105 * 106 * <jk>public class</jk> Address { 107 * <jk>public</jk> String getStreet(); 108 * <jk>public</jk> String getCity(); 109 * <jk>public</jk> String getState(); 110 * <jk>public int</jk> getZip(); 111 * } 112 * 113 * Person p = <jk>new</jk> Person(<js>"John Doe"</js>, 23, <js>"123 Main St"</js>, <js>"Anywhere"</js>, 114 * <js>"NY"</js>, 12345, <jk>false</jk>); 115 * 116 * <jc>// Produces "(name='John Doe',age=23,address=(street='123 Main St',city=Anywhere,state=NY,zip=12345),deceased=false)"</jc> 117 * String s = UonSerializer.<jsf>DEFAULT</jsf>.serialize(s); 118 * </p> 119 */ 120@ConfigurableContext 121public class UonSerializer extends WriterSerializer implements HttpPartSerializer, UonMetaProvider, UonCommon { 122 123 //------------------------------------------------------------------------------------------------------------------- 124 // Configurable properties 125 //------------------------------------------------------------------------------------------------------------------- 126 127 static final String PREFIX = "UonSerializer"; 128 129 /** 130 * Configuration property: Add <js>"_type"</js> properties when needed. 131 * 132 * <h5 class='section'>Property:</h5> 133 * <ul class='spaced-list'> 134 * <li><b>ID:</b> {@link org.apache.juneau.uon.UonSerializer#UON_addBeanTypes UON_addBeanTypes} 135 * <li><b>Name:</b> <js>"UonSerializer.addBeanTypes.b"</js> 136 * <li><b>Data type:</b> <jk>boolean</jk> 137 * <li><b>System property:</b> <c>UonSerializer.addBeanTypes</c> 138 * <li><b>Environment variable:</b> <c>UONSERIALIZER_ADDBEANTYPES</c> 139 * <li><b>Default:</b> <jk>false</jk> 140 * <li><b>Session property:</b> <jk>false</jk> 141 * <li><b>Annotations:</b> 142 * <ul> 143 * <li class='ja'>{@link org.apache.juneau.uon.annotation.UonConfig#addBeanTypes()} 144 * </ul> 145 * <li><b>Methods:</b> 146 * <ul> 147 * <li class='jm'>{@link org.apache.juneau.uon.UonSerializerBuilder#addBeanTypes(boolean)} 148 * </ul> 149 * </ul> 150 * 151 * <h5 class='section'>Description:</h5> 152 * <p> 153 * If <jk>true</jk>, then <js>"_type"</js> properties will be added to beans if their type cannot be inferred 154 * through reflection. 155 * 156 * <p> 157 * When present, this value overrides the {@link #SERIALIZER_addBeanTypes} setting and is 158 * provided to customize the behavior of specific serializers in a {@link SerializerGroup}. 159 */ 160 public static final String UON_addBeanTypes = PREFIX + ".addBeanTypes.b"; 161 162 /** 163 * Configuration property: Encode non-valid URI characters. 164 * 165 * <h5 class='section'>Property:</h5> 166 * <ul class='spaced-list'> 167 * <li><b>ID:</b> {@link org.apache.juneau.uon.UonSerializer#UON_encoding UON_encoding} 168 * <li><b>Name:</b> <js>"UonSerializer.encoding.b"</js> 169 * <li><b>Data type:</b> <jk>boolean</jk> 170 * <li><b>System property:</b> <c>UonSerializer.encoding</c> 171 * <li><b>Environment variable:</b> <c>UONSERIALIZER_ENCODING</c> 172 * <li><b>Default:</b> <jk>false</jk> for {@link org.apache.juneau.uon.UonSerializer}, <jk>true</jk> for {@link org.apache.juneau.urlencoding.UrlEncodingSerializer} 173 * <li><b>Session property:</b> <jk>false</jk> 174 * <li><b>Annotations:</b> 175 * <ul> 176 * <li class='ja'>{@link org.apache.juneau.uon.annotation.UonConfig#encoding()} 177 * </ul> 178 * <li><b>Methods:</b> 179 * <ul> 180 * <li class='jm'>{@link org.apache.juneau.uon.UonSerializerBuilder#encoding(boolean)} 181 * <li class='jm'>{@link org.apache.juneau.uon.UonSerializerBuilder#encoding()} 182 * </ul> 183 * </ul> 184 * 185 * <h5 class='section'>Description:</h5> 186 * <p> 187 * Encode non-valid URI characters with <js>"%xx"</js> constructs. 188 * 189 * <p> 190 * If <jk>true</jk>, non-valid URI characters will be converted to <js>"%xx"</js> sequences. 191 * <br>Set to <jk>false</jk> if parameter value is being passed to some other code that will already perform 192 * URL-encoding of non-valid URI characters. 193 * 194 * <h5 class='section'>Example:</h5> 195 * <p class='bcode w800'> 196 * <jc>// Create a non-encoding UON serializer.</jc> 197 * UonSerializer s1 = UonSerializer. 198 * .<jsm>create</jsm>() 199 * .build(); 200 * 201 * <jc>// Create an encoding UON serializer.</jc> 202 * UonSerializer s2 = UonSerializer. 203 * .<jsm>create</jsm>() 204 * .encoding() 205 * .build(); 206 * 207 * ObjectMap m = <jk>new</jk> ObjectMap().append("foo", "foo bar"); 208 * 209 * <jc>// Produces: "(foo=foo bar)"</jc> 210 * String uon1 = s1.serialize(m) 211 * 212 * <jc>// Produces: "(foo=foo%20bar)"</jc> 213 * String uon2 = s2.serialize(m) 214 * </p> 215 */ 216 public static final String UON_encoding = PREFIX + ".encoding.b"; 217 218 /** 219 * Configuration property: Format to use for query/form-data/header values. 220 * 221 * <h5 class='section'>Property:</h5> 222 * <ul class='spaced-list'> 223 * <li><b>ID:</b> {@link org.apache.juneau.uon.UonSerializer#UON_paramFormat UON_paramFormat} 224 * <li><b>Name:</b> <js>"UonSerializer.paramFormat.s"</js> 225 * <li><b>Data type:</b> {@link org.apache.juneau.uon.ParamFormat} 226 * <li><b>System property:</b> <c>UonSerializer.paramFormat</c> 227 * <li><b>Environment variable:</b> <c>UONSERIALIZER_PARAMFORMAT</c> 228 * <li><b>Default:</b> <js>"UON"</js> 229 * <li><b>Session property:</b> <jk>false</jk> 230 * <li><b>Annotations:</b> 231 * <ul> 232 * <li class='ja'>{@link org.apache.juneau.uon.annotation.UonConfig#paramFormat()} 233 * </ul> 234 * <li><b>Methods:</b> 235 * <ul> 236 * <li class='jm'>{@link org.apache.juneau.uon.UonSerializerBuilder#paramFormat(ParamFormat)} 237 * <li class='jm'>{@link org.apache.juneau.uon.UonSerializerBuilder#paramFormatPlain()} 238 * </ul> 239 * </ul> 240 * 241 * <h5 class='section'>Description:</h5> 242 * <p> 243 * Specifies the format to use for URL GET parameter keys and values. 244 * 245 * <p> 246 * Possible values: 247 * <ul class='javatree'> 248 * <li class='jf'>{@link ParamFormat#UON} - Use UON notation for parameters. 249 * <li class='jf'>{@link ParamFormat#PLAINTEXT} - Use plain text for parameters. 250 * </ul> 251 * 252 * <h5 class='section'>Example:</h5> 253 * <p class='bcode w800'> 254 * <jc>// Create a normal UON serializer.</jc> 255 * UonSerializer s1 = UonSerializer. 256 * .<jsm>create</jsm>() 257 * .build(); 258 * 259 * <jc>// Create a plain-text UON serializer.</jc> 260 * UonSerializer s2 = UonSerializer. 261 * .<jsm>create</jsm>() 262 * .paramFormat(<jsf>PLAIN_TEXT</jsf>) 263 * .build(); 264 * 265 * ObjectMap m = <jk>new</jk> ObjectMap() 266 * .append(<js>"foo"</js>, <js>"bar"</js>); 267 * .append(<js>"baz"</js>, <jk>new</jk> String[]{<js>"qux"</js>, <js>"true"</js>, <js>"123"</js>}); 268 * 269 * <jc>// Produces: "(foo=bar,baz=@(qux,'true','123'))"</jc> 270 * String uon1 = s1.serialize(m) 271 * 272 * <jc>// Produces: "foo=bar,baz=qux,true,123"</jc> 273 * String uon2 = s2.serialize(m) 274 * </p> 275 */ 276 public static final String UON_paramFormat = PREFIX + ".paramFormat.s"; 277 278 279 //------------------------------------------------------------------------------------------------------------------- 280 // Predefined instances 281 //------------------------------------------------------------------------------------------------------------------- 282 283 /** Reusable instance of {@link UonSerializer}, all default settings. */ 284 public static final UonSerializer DEFAULT = new UonSerializer(PropertyStore.DEFAULT); 285 286 /** Reusable instance of {@link UonSerializer.Readable}. */ 287 public static final UonSerializer DEFAULT_READABLE = new Readable(PropertyStore.DEFAULT); 288 289 /** Reusable instance of {@link UonSerializer.Encoding}. */ 290 public static final UonSerializer DEFAULT_ENCODING = new Encoding(PropertyStore.DEFAULT); 291 292 293 //------------------------------------------------------------------------------------------------------------------- 294 // Predefined subclasses 295 //------------------------------------------------------------------------------------------------------------------- 296 297 /** 298 * Equivalent to <code>UonSerializer.<jsm>create</jsm>().ws().build();</code>. 299 */ 300 public static class Readable extends UonSerializer { 301 302 /** 303 * Constructor. 304 * 305 * @param ps The property store containing all the settings for this object. 306 */ 307 public Readable(PropertyStore ps) { 308 super(ps.builder().set(WSERIALIZER_useWhitespace, true).build()); 309 } 310 } 311 312 /** 313 * Equivalent to <code>UonSerializer.<jsm>create</jsm>().encoding().build();</code>. 314 */ 315 public static class Encoding extends UonSerializer { 316 317 /** 318 * Constructor. 319 * 320 * @param ps The property store containing all the settings for this object. 321 */ 322 public Encoding(PropertyStore ps) { 323 super(ps.builder().set(UON_encoding, true).build()); 324 } 325 } 326 327 328 //------------------------------------------------------------------------------------------------------------------- 329 // Instance 330 //------------------------------------------------------------------------------------------------------------------- 331 332 private final boolean 333 encoding, 334 addBeanTypes; 335 336 private final ParamFormat 337 paramFormat; 338 339 private final Map<ClassMeta<?>,UonClassMeta> uonClassMetas = new ConcurrentHashMap<>(); 340 private final Map<BeanPropertyMeta,UonBeanPropertyMeta> uonBeanPropertyMetas = new ConcurrentHashMap<>(); 341 342 /** 343 * Constructor. 344 * 345 * @param ps 346 * The property store containing all the settings for this object. 347 */ 348 public UonSerializer(PropertyStore ps) { 349 this(ps, "text/uon", (String)null); 350 } 351 352 /** 353 * Constructor. 354 * 355 * @param ps 356 * The property store containing all the settings for this object. 357 * @param produces 358 * The media type that this serializer produces. 359 * @param accept 360 * The accept media types that the serializer can handle. 361 * <p> 362 * Can contain meta-characters per the <c>media-type</c> specification of {@doc RFC2616.section14.1} 363 * <p> 364 * If empty, then assumes the only media type supported is <c>produces</c>. 365 * <p> 366 * For example, if this serializer produces <js>"application/json"</js> but should handle media types of 367 * <js>"application/json"</js> and <js>"text/json"</js>, then the arguments should be: 368 * <p class='bcode w800'> 369 * <jk>super</jk>(ps, <js>"application/json"</js>, <js>"application/json,text/json"</js>); 370 * </p> 371 * <br>...or... 372 * <p class='bcode w800'> 373 * <jk>super</jk>(ps, <js>"application/json"</js>, <js>"*​/json"</js>); 374 * </p> 375 * <p> 376 * The accept value can also contain q-values. 377 */ 378 public UonSerializer(PropertyStore ps, String produces, String accept) { 379 super(ps, produces, accept); 380 encoding = getBooleanProperty(UON_encoding, false); 381 addBeanTypes = getBooleanProperty(UON_addBeanTypes, getBooleanProperty(SERIALIZER_addBeanTypes, false)); 382 paramFormat = getProperty(UON_paramFormat, ParamFormat.class, ParamFormat.UON); 383 } 384 385 @Override /* Context */ 386 public UonSerializerBuilder builder() { 387 return new UonSerializerBuilder(getPropertyStore()); 388 } 389 390 /** 391 * Instantiates a new clean-slate {@link UonSerializerBuilder} object. 392 * 393 * <p> 394 * This is equivalent to simply calling <code><jk>new</jk> UonSerializerBuilder()</code>. 395 * 396 * <p> 397 * Note that this method creates a builder initialized to all default settings, whereas {@link #builder()} copies 398 * the settings of the object called on. 399 * 400 * @return A new {@link UonSerializerBuilder} object. 401 */ 402 public static UonSerializerBuilder create() { 403 return new UonSerializerBuilder(); 404 } 405 406 407 //----------------------------------------------------------------------------------------------------------------- 408 // Entry point methods 409 //----------------------------------------------------------------------------------------------------------------- 410 411 @Override /* Context */ 412 public UonSerializerSession createSession() { 413 return createSession(createDefaultSessionArgs()); 414 } 415 416 @Override /* Serializer */ 417 public UonSerializerSession createSession(SerializerSessionArgs args) { 418 return new UonSerializerSession(this, null, args); 419 } 420 421 @Override /* HttpPartSerializer */ 422 public UonSerializerSession createPartSession(SerializerSessionArgs args) { 423 return new UonSerializerSession(this, null, args); 424 } 425 426 @Override /* HttpPartSerializer */ 427 public UonSerializerSession createPartSession() { 428 return createPartSession(null); 429 } 430 431 @Override /* HttpPartSerializer */ 432 public String serialize(HttpPartType partType, HttpPartSchema schema, Object value) throws SchemaValidationException, SerializeException { 433 return createPartSession().serialize(partType, schema, value); 434 } 435 436 @Override /* HttpPartSerializer */ 437 public String serialize(HttpPartSchema schema, Object value) throws SchemaValidationException, SerializeException { 438 return createPartSession().serialize(null, schema, value); 439 } 440 441 //----------------------------------------------------------------------------------------------------------------- 442 // Extended metadata 443 //----------------------------------------------------------------------------------------------------------------- 444 445 @Override /* UonMetaProvider */ 446 public UonClassMeta getUonClassMeta(ClassMeta<?> cm) { 447 UonClassMeta m = uonClassMetas.get(cm); 448 if (m == null) { 449 m = new UonClassMeta(cm, this); 450 uonClassMetas.put(cm, m); 451 } 452 return m; 453 } 454 455 @Override /* UonMetaProvider */ 456 public UonBeanPropertyMeta getUonBeanPropertyMeta(BeanPropertyMeta bpm) { 457 if (bpm == null) 458 return UonBeanPropertyMeta.DEFAULT; 459 UonBeanPropertyMeta m = uonBeanPropertyMetas.get(bpm); 460 if (m == null) { 461 m = new UonBeanPropertyMeta(bpm.getDelegateFor(), this); 462 uonBeanPropertyMetas.put(bpm, m); 463 } 464 return m; 465 } 466 467 //----------------------------------------------------------------------------------------------------------------- 468 // Properties 469 //----------------------------------------------------------------------------------------------------------------- 470 471 /** 472 * Configuration property: Add <js>"_type"</js> properties when needed. 473 * 474 * @see #UON_addBeanTypes 475 * @return 476 * <jk>true</jk> if <js>"_type"</js> properties will be added to beans if their type cannot be inferred 477 * through reflection. 478 */ 479 @Override 480 protected final boolean isAddBeanTypes() { 481 return addBeanTypes; 482 } 483 484 /** 485 * Configuration property: Encode non-valid URI characters. 486 * 487 * @see #UON_encoding 488 * @return 489 * <jk>true</jk> if non-valid URI characters should be encoded with <js>"%xx"</js> constructs. 490 */ 491 protected final boolean isEncoding() { 492 return encoding; 493 } 494 495 /** 496 * Configuration property: Format to use for query/form-data/header values. 497 * 498 * @see #UON_paramFormat 499 * @return 500 * Specifies the format to use for URL GET parameter keys and values. 501 */ 502 protected final ParamFormat getParamFormat() { 503 return paramFormat; 504 } 505 506 //----------------------------------------------------------------------------------------------------------------- 507 // Other methods 508 //----------------------------------------------------------------------------------------------------------------- 509 510 @Override /* Context */ 511 public ObjectMap toMap() { 512 return super.toMap() 513 .append("UonSerializer", new DefaultFilteringObjectMap() 514 .append("encoding", encoding) 515 .append("addBeanTypes", addBeanTypes) 516 .append("paramFormat", paramFormat) 517 ); 518 } 519}