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.uon; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020import static org.apache.juneau.commons.utils.SystemUtils.*; 021 022import java.io.*; 023import java.util.*; 024import java.util.concurrent.*; 025 026import org.apache.juneau.commons.bean.*; 027import org.apache.juneau.commons.collections.*; 028import org.apache.juneau.commons.httppart.*; 029import org.apache.juneau.marshall.*; 030import org.apache.juneau.marshall.httppart.*; 031import org.apache.juneau.marshall.serializer.*; 032import org.apache.juneau.marshall.stream.*; 033 034/** 035 * Serializes POJO models to UON (a notation for URL-encoded query parameter values). 036 * 037 * <h5 class='topic'>Media types</h5> 038 * <p> 039 * Handles <c>Accept</c> types: <bc>text/uon</bc> 040 * <p> 041 * Produces <c>Content-Type</c> types: <bc>text/uon</bc> 042 * 043 * <h5 class='topic'>Description</h5> 044 * <p> 045 * This serializer provides several serialization options. 046 * Typically, one of the predefined DEFAULT serializers will be sufficient. 047 * However, custom serializers can be constructed to fine-tune behavior. 048 * 049 * <p> 050 * The following shows a sample object defined in Javascript: 051 * <p class='bjson'> 052 * { 053 * id: 1, 054 * name: <js>'John Smith'</js>, 055 * uri: <js>'http://sample/addressBook/person/1'</js>, 056 * addressBookUri: <js>'http://sample/addressBook'</js>, 057 * birthDate: <js>'1946-08-12T00:00:00Z'</js>, 058 * otherIds: <jk>null</jk>, 059 * addresses: [ 060 * { 061 * uri: <js>'http://sample/addressBook/address/1'</js>, 062 * personUri: <js>'http://sample/addressBook/person/1'</js>, 063 * id: 1, 064 * street: <js>'100 Main Street'</js>, 065 * city: <js>'Anywhereville'</js>, 066 * state: <js>'NY'</js>, 067 * zip: 12345, 068 * isCurrent: <jk>true</jk>, 069 * } 070 * ] 071 * } 072 * </p> 073 * 074 * <p> 075 * Using the "strict" syntax defined in this document, the equivalent UON notation would be as follows: 076 * <p class='buon'> 077 * ( 078 * <ua>id</ua>=<un>1</un>, 079 * <ua>name</ua>=<us>'John+Smith'</us>, 080 * <ua>uri</ua>=<us>http://sample/addressBook/person/1</us>, 081 * <ua>addressBookUri</ua>=<us>http://sample/addressBook</us>, 082 * <ua>birthDate</ua>=<us>1946-08-12T00:00:00Z</us>, 083 * <ua>otherIds</ua>=<uk>null</uk>, 084 * <ua>addresses</ua>=@( 085 * ( 086 * <ua>uri</ua>=<us>http://sample/addressBook/address/1</us>, 087 * <ua>personUri</ua>=<us>http://sample/addressBook/person/1</us>, 088 * <ua>id</ua>=<un>1</un>, 089 * <ua>street</ua>=<us>'100+Main+Street'</us>, 090 * <ua>city</ua>=<us>Anywhereville</us>, 091 * <ua>state</ua>=<us>NY</us>, 092 * <ua>zip</ua>=<un>12345</un>, 093 * <ua>isCurrent</ua>=<uk>true</uk> 094 * ) 095 * ) 096 * ) 097 * </p> 098 * 099 * <h5 class='figure'>Example output (Map of name/age):</h5> 100 * <p class='bcode'> 101 * (name=Alice,age=30) 102 * </p> 103 * 104 * <h5 class='figure'>Complex (nested object + array):</h5> 105 * <p class='bcode'> 106 * (name=Alice,age=30,address=(street=123+Main+St,city=Boston,state=MA),tags=@(a,b,c)) 107 * </p> 108 * 109 * <h5 class='section'>Example:</h5> 110 * <p class='bjava'> 111 * <jc>// Serialize a Map</jc> 112 * Map <jv>map</jv> = JsonMap.<jsm>ofText</jsm>(<js>"{a:'b',c:1,d:false,e:['f',1,false],g:{h:'i'}}"</js>); 113 * 114 * <jc>// Serialize to value equivalent to JSON.</jc> 115 * <jc>// Produces "(a=b,c=1,d=false,e=@(f,1,false),g=(h=i))"</jc> 116 * String <jv>uon</jv> = UonSerializer.<jsf>DEFAULT</jsf>.write(<jv>map</jv>); 117 * 118 * <jc>// Serialize a bean</jc> 119 * <jk>public class</jk> Person { 120 * <jk>public</jk> Person(String <jv>name</jv>); 121 * <jk>public</jk> String getName(); 122 * <jk>public int</jk> getAge(); 123 * <jk>public</jk> Address getAddress(); 124 * <jk>public boolean</jk> deceased; 125 * } 126 * 127 * <jk>public class</jk> Address { 128 * <jk>public</jk> String getStreet(); 129 * <jk>public</jk> String getCity(); 130 * <jk>public</jk> String getState(); 131 * <jk>public int</jk> getZip(); 132 * } 133 * 134 * Person <jv>person</jv> = <jk>new</jk> Person(<js>"John Doe"</js>, 23, <js>"123 Main St"</js>, <js>"Anywhere"</js>, 135 * <js>"NY"</js>, 12345, <jk>false</jk>); 136 * 137 * <jc>// Produces "(name='John Doe',age=23,address=(street='123 Main St',city=Anywhere,state=NY,zip=12345),deceased=false)"</jc> 138 * String <jv>uon</jv> = UonSerializer.<jsf>DEFAULT</jsf>.write(<jv>person</jv>); 139 * </p> 140 * 141 * <h5 class='section'>Notes:</h5><ul> 142 * <li class='note'>This class is thread safe and reusable. 143 * </ul> 144 * 145 * <h5 class='section'>See Also:</h5><ul> 146 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/UonSupport">UON</a> 147 148 * </ul> 149 */ 150@SuppressWarnings({ 151 "java:S110", // Inheritance depth acceptable for this class hierarchy 152 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 153 "java:S9149", // Per-format static factories intentionally shadow the parent's. 154 "resource" // Closeable resources are owned by the caller's serializer session; Eclipse JDT @Owning warning is by design. 155}) 156public class UonSerializer extends WriterSerializer implements HttpPartSerializer, UonMetaProvider, RecordWritable { 157 158 /** 159 * Builder class. 160 */ 161 @SuppressWarnings({ 162 "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name. 163 }) 164 public abstract static class Builder<SELF extends Builder<SELF>> extends WriterSerializer.Builder<SELF> { 165 166 private static final Cache<HashKey,UonSerializer> CACHE = Cache.of(HashKey.class, UonSerializer.class).build(); 167 168 private boolean addBeanTypesUon; 169 private boolean encoding; 170 private ParamFormat paramFormat; 171 private Character quoteCharUon; 172 173 /** 174 * Constructor, default settings. 175 */ 176 protected Builder() { 177 produces("text/uon"); 178 addBeanTypesUon = env("UonSerializer.addBeanTypesUon", false); 179 encoding = env("UonSerializer.encoding", false); 180 paramFormat = env("UonSerializer.paramFormat", ParamFormat.UON); 181 quoteCharUon = null; 182 } 183 184 /** 185 * Copy constructor. 186 * 187 * @param copyFrom The builder to copy from. 188 * <br>Cannot be <jk>null</jk>. 189 */ 190 protected Builder(Builder<?> copyFrom) { 191 super(reqnn("copyFrom", copyFrom)); 192 addBeanTypesUon = copyFrom.addBeanTypesUon; 193 encoding = copyFrom.encoding; 194 paramFormat = copyFrom.paramFormat; 195 quoteCharUon = copyFrom.quoteCharUon; 196 } 197 198 /** 199 * Copy constructor. 200 * 201 * @param copyFrom The bean to copy from. 202 * <br>Cannot be <jk>null</jk>. 203 */ 204 protected Builder(UonSerializer copyFrom) { 205 super(reqnn("copyFrom", copyFrom)); 206 addBeanTypesUon = copyFrom.addBeanTypesUon; 207 encoding = copyFrom.encoding; 208 paramFormat = copyFrom.paramFormat; 209 quoteCharUon = copyFrom.quoteCharUon; 210 } 211 212 /** 213 * Add <js>"_type"</js> properties when needed. 214 * 215 * <p> 216 * If <jk>true</jk>, then <js>"_type"</js> properties will be added to beans if their type cannot be inferred 217 * through reflection. 218 * 219 * <p> 220 * When present, this value overrides the {@link org.apache.juneau.marshall.serializer.Serializer.Builder#addBeanTypes() Serializer.Builder#addBeanTypes()} setting and is 221 * provided to customize the behavior of specific serializers in a {@link SerializerSet}. 222 * 223 * @return This object. 224 */ 225 public SELF addBeanTypesUon() { 226 return addBeanTypesUon(true); 227 } 228 229 /** 230 * Same as {@link #addBeanTypesUon()} but allows you to explicitly specify the value. 231 * 232 * @param value The value for this setting. 233 * @return This object. 234 */ 235 public SELF addBeanTypesUon(boolean value) { 236 addBeanTypesUon = value; 237 return self(); 238 } 239 240 @Override /* Overridden from Context.Builder<?> */ 241 public UonSerializer build() { 242 return cache(CACHE).build(UonSerializer.class); 243 } 244 245 @Override /* Overridden from Context.Builder<?> */ 246 public abstract SELF copy(); 247 248 /** 249 * Encode non-valid URI characters. 250 * 251 * <p> 252 * Encode non-valid URI characters with <js>"%xx"</js> constructs. 253 * 254 * <p> 255 * If <jk>true</jk>, non-valid URI characters will be converted to <js>"%xx"</js> sequences. 256 * <br>Set to <jk>false</jk> if parameter value is being passed to some other code that will already perform 257 * URL-encoding of non-valid URI characters. 258 * 259 * <h5 class='section'>Example:</h5> 260 * <p class='bjava'> 261 * <jc>// Create a non-encoding UON serializer.</jc> 262 * UonSerializer <jv>serializer1</jv> = UonSerializer. 263 * .<jsm>create</jsm>() 264 * .build(); 265 * 266 * <jc>// Create an encoding UON serializer.</jc> 267 * UonSerializer <jv>serializer2</jv> = UonSerializer. 268 * .<jsm>create</jsm>() 269 * .encoding() 270 * .build(); 271 * 272 * JsonMap <jv>map</jv> = JsonMap.<jsm>of</jsm>(<js>"foo"</js>, <js>"foo bar"</js>); 273 * 274 * <jc>// Produces: "(foo=foo bar)"</jc> 275 * String <jv>uon1</jv> = <jv>serializer1</jv>.write(<jv>map</jv>) 276 * 277 * <jc>// Produces: "(foo=foo%20bar)"</jc> 278 * String <jv>uon2</jv> = <jv>serializer2</jv>.write(<jv>map</jv>) 279 * </p> 280 * 281 * @return This object. 282 */ 283 public SELF encoding() { 284 return encoding(true); 285 } 286 287 /** 288 * Same as {@link #encoding()} but allows you to disable the previous setting. 289 * 290 * @param value The new value for this setting. 291 * @return This object. 292 */ 293 public SELF encoding(boolean value) { 294 encoding = value; 295 return self(); 296 } 297 298 @Override /* Overridden from Context.Builder<?> */ 299 public HashKey hashKey() { 300 // @formatter:off 301 return HashKey.of( 302 super.hashKey(), 303 addBeanTypesUon, 304 encoding, 305 paramFormat, 306 quoteCharUon 307 ); 308 // @formatter:on 309 } 310 311 /** 312 * Format to use for query/form-data/header values. 313 * 314 * <p> 315 * Specifies the format to use for URL GET parameter keys and values. 316 * 317 * <h5 class='section'>Example:</h5> 318 * <p class='bjava'> 319 * <jc>// Create a normal UON serializer.</jc> 320 * UonSerializer <jv>serializer1</jv> = UonSerializer 321 * .<jsm>create</jsm>() 322 * .build(); 323 * 324 * <jc>// Create a plain-text UON serializer.</jc> 325 * UonSerializer <jv>serializer2</jv> = UonSerializer 326 * .<jsm>create</jsm>() 327 * .paramFormat(<jsf>PLAIN_TEXT</jsf>) 328 * .build(); 329 * 330 * JsonMap <jv>map</jv> = JsonMap.<jsm>of</jsm>( 331 * <js>"foo"</js>, <js>"bar"</js>, 332 * <js>"baz"</js>, <jk>new</jk> String[]{<js>"qux"</js>, <js>"true"</js>, <js>"123"</js>} 333 * ); 334 * 335 * <jc>// Produces: "(foo=bar,baz=@(qux,'true','123'))"</jc> 336 * String <jv>uon1</jv> = <jv>serializer1</jv>.write(<jv>map</jv>) 337 * 338 * <jc>// Produces: "foo=bar,baz=qux,true,123"</jc> 339 * String <jv>uon2</jv> = <jv>serializer2</jv>.write(<jv>map</jv>) 340 * </p> 341 * 342 * <p> 343 * <ul class='values javatree'> 344 * <li class='jf'>{@link ParamFormat#UON} (default) - Use UON notation for parameters. 345 * <li class='jf'>{@link ParamFormat#PLAINTEXT} - Use plain text for parameters. 346 * </ul> 347 * 348 * @param value 349 * The new value for this property. 350 * <br>The default value is {@link ParamFormat#UON}. 351 * <br>Cannot be <jk>null</jk>. 352 * @return This object. 353 */ 354 public SELF paramFormat(ParamFormat value) { 355 paramFormat = reqnn("value", value); 356 return self(); 357 } 358 359 /** 360 * Format to use for query/form-data/header values. 361 * 362 * <p> 363 * Specifies plain-text for the format to use for URL GET parameter keys and values. 364 * 365 * <h5 class='section'>Example:</h5> 366 * <p class='bjava'> 367 * <jc>// Create a plain-text UON serializer.</jc> 368 * UonSerializer <jv>serializer</jv> = UonSerializer 369 * .<jsm>create</jsm>() 370 * .paramFormatPlain() 371 * .build(); 372 * 373 * JsonMap <jv>map</jv> = JsonMap.<jsm>of</jsm>( 374 * <js>"foo"</js>, <js>"bar"</js>, 375 * <js>"baz"</js>, <jk>new</jk> String[]{<js>"qux"</js>, <js>"true"</js>, <js>"123"</js>} 376 * ); 377 * 378 * <jc>// Produces: "foo=bar,baz=qux,true,123"</jc> 379 * String <jv>uon</jv> = <jv>serializer</jv>.write(<jv>map</jv>) 380 * </p> 381 * 382 * @return This object. 383 */ 384 public SELF paramFormatPlain() { 385 return paramFormat(ParamFormat.PLAINTEXT); 386 } 387 388 /** 389 * Specifies the quote character. 390 * 391 * @param value The value for this setting. 392 * @return This object. 393 */ 394 public SELF quoteCharUon(char value) { 395 quoteCharUon = value; 396 return self(); 397 } 398 399 400 } 401 402 /** 403 * Concrete default builder leaf for the non-subclassed {@link UonSerializer#create()} / {@link UonSerializer#copy()} path. 404 */ 405 public static final class DefaultBuilder extends Builder<DefaultBuilder> { 406 407 DefaultBuilder() {} 408 409 DefaultBuilder(UonSerializer copyFrom) { 410 super(copyFrom); 411 } 412 413 DefaultBuilder(Builder<?> copyFrom) { 414 super(copyFrom); 415 } 416 417 @Override /* Overridden from Context.Builder<?> */ 418 public DefaultBuilder copy() { 419 return new DefaultBuilder(this); 420 } 421 } 422 423 /** 424 * Equivalent to <code>UonSerializer.<jsm>create</jsm>().encoding().build();</code>. 425 */ 426 public static class Encoding extends UonSerializer { 427 428 /** 429 * Constructor. 430 * 431 * @param builder The builder for this object. 432 */ 433 public Encoding(Builder<?> builder) { 434 super(builder.encoding()); 435 } 436 } 437 438 /** 439 * Equivalent to <code>UonSerializer.<jsm>create</jsm>().ws().build();</code>. 440 */ 441 public static class Readable extends UonSerializer { 442 443 /** 444 * Constructor. 445 * 446 * @param builder The builder for this object. 447 */ 448 public Readable(Builder<?> builder) { 449 super(builder.useWhitespace()); 450 } 451 } 452 453 /** Reusable instance of {@link UonSerializer}, all default settings. */ 454 public static final UonSerializer DEFAULT = new UonSerializer(create()); 455 /** Reusable instance of {@link UonSerializer.Readable}. */ 456 public static final UonSerializer DEFAULT_READABLE = new Readable(create()); 457 458 /** Reusable instance of {@link UonSerializer.Encoding}. */ 459 public static final UonSerializer DEFAULT_ENCODING = new Encoding(create()); 460 461 /** 462 * Creates a new builder for this object. 463 * 464 * @return A new builder. 465 */ 466 @SuppressWarnings({ 467 "java:S1452" // Builder<?> wildcard return intentional; callers chain via fluent API without needing the concrete type 468 }) 469 public static Builder<?> create() { 470 return new DefaultBuilder(); 471 } 472 473 protected final boolean addBeanTypesUon; 474 protected final boolean encoding; 475 protected final Character quoteCharUon; 476 protected final ParamFormat paramFormat; 477 private final boolean addBeanTypes2; 478 479 private final char quoteChar2; 480 private final Map<BeanPropertyMeta,UonBeanPropertyMeta> uonBeanPropertyMetas = new ConcurrentHashMap<>(); 481 private final Map<ClassMeta<?>,UonClassMeta> uonClassMetas = new ConcurrentHashMap<>(); 482 483 /** 484 * Constructor. 485 * 486 * @param builder 487 * The builder for this object. 488 */ 489 public UonSerializer(Builder<?> builder) { 490 super(builder); 491 492 addBeanTypesUon = builder.addBeanTypesUon; 493 encoding = builder.encoding; 494 paramFormat = builder.paramFormat; 495 quoteCharUon = builder.quoteCharUon; 496 497 addBeanTypes2 = addBeanTypesUon || super.isAddBeanTypes(); 498 if (nn(quoteCharUon)) { 499 quoteChar2 = quoteCharUon; 500 } else if (nn(super.quoteChar())) { 501 quoteChar2 = super.quoteChar(); 502 } else { 503 quoteChar2 = '\''; 504 } 505 } 506 507 @Override /* Overridden from Context */ 508 public Builder<?> copy() { 509 return new DefaultBuilder(this); 510 } 511 512 @Override /* Overridden from Context */ 513 public UonSerializerSession.Builder<?> createSession() { 514 return UonSerializerSession.create(this); 515 } 516 517 @Override /* Overridden from HttpPartSerializer */ 518 public UonSerializerSession getPartSession() { return UonSerializerSession.create(this).build(); } 519 520 @Override /* Overridden from Context */ 521 public UonSerializerSession getSession() { return createSession().build(); } 522 523 /** 524 * Convenience delegator that opens a whole-value {@link RecordWriter} over the output using 525 * <b>default session arguments</b> (mirrors {@link #write(Object)}). 526 * 527 * <p> 528 * The real implementation lives on {@link UonSerializerSession#writeRecords(Object)}. 529 * Callers that need request-derived configuration should call {@link #createSession()} and 530 * invoke {@link UonSerializerSession#writeRecords(Object)} on the built session instead. 531 * 532 * @param output The output. 533 * @return A new {@link RecordWriter}. 534 * @throws IOException If a problem occurred opening the underlying output. 535 */ 536 @Override /* RecordWritable */ 537 public RecordWriter writeRecords(Object output) throws IOException { 538 return getSession().writeRecords(output); 539 } 540 541 @Override /* RecordWritable */ 542 public boolean isRecordStreaming() { 543 return false; 544 } 545 546 @Override /* Overridden from UonMetaProvider */ 547 public UonBeanPropertyMeta getUonBeanPropertyMeta(BeanPropertyMeta bpm) { 548 if (bpm == null) 549 return UonBeanPropertyMeta.DEFAULT; 550 return uonBeanPropertyMetas.computeIfAbsent(bpm, k -> new UonBeanPropertyMeta(k.getDelegateFor(), this)); 551 } 552 553 @Override /* Overridden from UonMetaProvider */ 554 public UonClassMeta getUonClassMeta(ClassMeta<?> cm) { 555 return uonClassMetas.computeIfAbsent(cm, k -> new UonClassMeta(k, this)); 556 } 557 558 /** 559 * Converts the specified value to a string that can be used as an HTTP header value, query parameter value, 560 * form-data parameter, or URI path variable. 561 * 562 * <p> 563 * Returned values should NOT be URL-encoded. 564 * 565 * @param partType The category of value being serialized. 566 * @param schema 567 * Schema information about the part. 568 * <br>May be <jk>null</jk>. 569 * <br>Not all part serializers use the schema information. 570 * @param value The value being serialized. 571 * @return The serialized value. 572 * @throws SerializeException If a problem occurred while trying to parse the input. 573 * @throws SchemaValidationException If the output fails schema validation. 574 */ 575 public String write(HttpPartType partType, HttpPartSchema schema, Object value) throws SchemaValidationException, SerializeException { 576 return getPartSession().write(partType, schema, value); 577 } 578 579 /** 580 * Format to use for query/form-data/header values. 581 * 582 * @see Builder#paramFormat(ParamFormat) 583 * @return 584 * Specifies the format to use for URL GET parameter keys and values. 585 */ 586 protected final ParamFormat getParamFormat() { return paramFormat; } 587 588 /** 589 * Quote character. 590 * 591 * @see Builder#quoteCharUon(char) 592 * @return 593 * The character used for quoting attributes and values. 594 */ 595 @Override 596 protected final char getQuoteChar() { return quoteChar2; } 597 598 /** 599 * Add <js>"_type"</js> properties when needed. 600 * 601 * @see Builder#addBeanTypesUon() 602 * @return 603 * <jk>true</jk> if <js>"_type"</js> properties will be added to beans if their type cannot be inferred 604 * through reflection. 605 */ 606 @Override 607 protected final boolean isAddBeanTypes() { return addBeanTypes2; } 608 609 /** 610 * Encode non-valid URI characters. 611 * 612 * @see Builder#encoding() 613 * @return 614 * <jk>true</jk> if non-valid URI characters should be encoded with <js>"%xx"</js> constructs. 615 */ 616 protected final boolean isEncoding() { return encoding; } 617 618 @Override /* Overridden from WriterSerializer */ 619 protected FluentMap<String,Object> properties() { 620 return super.properties() 621 .a("addBeanTypes", addBeanTypes2) 622 .a("encoding", encoding) 623 .a("paramFormat", paramFormat); 624 } 625}