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}