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