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.serializer;
018
019import static org.apache.juneau.commons.utils.IoUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021import static org.apache.juneau.commons.utils.SystemUtils.*;
022import static org.apache.juneau.commons.utils.ThrowableUtils.*;
023
024import java.nio.charset.*;
025
026import org.apache.juneau.commons.collections.*;
027import org.apache.juneau.commons.function.*;
028import org.apache.juneau.commons.logging.*;
029import org.apache.juneau.marshall.json5.*;
030
031/**
032 * Subclass of {@link Serializer} for character-based serializers.
033 *
034 * <h5 class='section'>Notes:</h5><ul>
035 *    <li class='note'>This class is thread safe and reusable.
036 * </ul>
037 *
038 * <h5 class='section'>See Also:</h5><ul>
039 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SerializersAndParsers">Serializers and Parsers</a>
040 * </ul>
041 */
042@SuppressWarnings({
043   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
044   "java:S9149" // Per-format static factories intentionally shadow the parent's.
045})
046public class WriterSerializer extends Serializer implements ThrowingFunction<Object,String> {
047
048   /**
049    * Builder class.
050    */
051   @SuppressWarnings({
052      "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name.
053   })
054   public abstract static class Builder<SELF extends Builder<SELF>> extends Serializer.Builder<SELF> {
055
056      private boolean useWhitespace;
057      private Character quoteChar;
058      private Character quoteCharOverride;
059      private Charset streamCharset;
060      private int maxIndent;
061
062      /**
063       * Constructor, default settings.
064       */
065      protected Builder() {
066         streamCharset = UTF8;
067         maxIndent = env("WriterSerializer.maxIndent", 100);
068         quoteChar = env("WriterSerializer.quoteChar").map(x -> (x.isEmpty() ? null : x.charAt(0))).orElse(null);
069         quoteCharOverride = env("WriterSerializer.quoteCharOverride").map(x -> (x.isEmpty() ? null : x.charAt(0))).orElse(null);
070         useWhitespace = env("WriterSerializer.useWhitespace", false);
071      }
072
073      /**
074       * Copy constructor.
075       *
076       * @param copyFrom The builder to copy from.
077       *    <br>Cannot be <jk>null</jk>.
078       */
079      protected Builder(Builder<?> copyFrom) {
080         super(reqnn("copyFrom", copyFrom));
081         streamCharset = copyFrom.streamCharset;
082         maxIndent = copyFrom.maxIndent;
083         quoteChar = copyFrom.quoteChar;
084         quoteCharOverride = copyFrom.quoteCharOverride;
085         useWhitespace = copyFrom.useWhitespace;
086      }
087
088      /**
089       * Copy constructor.
090       *
091       * @param copyFrom The bean to copy from.
092       *    <br>Cannot be <jk>null</jk>.
093       */
094      protected Builder(WriterSerializer copyFrom) {
095         super(reqnn("copyFrom", copyFrom));
096         streamCharset = copyFrom.getStreamCharset();
097         maxIndent = copyFrom.maxIndent;
098         quoteChar = copyFrom.quoteChar;
099         quoteCharOverride = copyFrom.quoteCharOverride;
100         useWhitespace = copyFrom.useWhitespace;
101      }
102
103      @Override /* Overridden from Context.Builder<?> */
104      public WriterSerializer build() {
105         return build(WriterSerializer.class);
106      }
107
108      @Override /* Overridden from Context.Builder<?> */
109      public abstract SELF copy();
110
111      @Override /* Overridden from Context.Builder<?> */
112      public HashKey hashKey() {
113         // @formatter:off
114         return HashKey.of(
115            super.hashKey(),
116            streamCharset,
117            maxIndent,
118            quoteChar,
119            quoteCharOverride,
120            useWhitespace
121         );
122         // @formatter:on
123      }
124
125      /**
126       * Maximum indentation.
127       *
128       * <p>
129       * Specifies the maximum indentation level in the serialized document.
130       *
131       * <h5 class='section'>Notes:</h5><ul>
132       *    <li class='note'>This setting does not apply to the RDF serializers.
133       * </ul>
134       *
135       * <h5 class='section'>Example:</h5>
136       * <p class='bjava'>
137       *    <jc>// Create a serializer that indents a maximum of 20 tabs.</jc>
138       *    WriterSerializer <jv>serializer</jv> = JsonSerializer
139       *       .<jsm>create</jsm>()
140       *       .ws()  <jc>// Enable whitespace</jc>
141       *       .maxIndent(20)
142       *       .build();
143       * </p>
144       *
145       * @param value
146       *    The new value for this property.
147       *    <br>The default is <c>100</c>.
148       * @return This object.
149       */
150      public SELF maxIndent(int value) {
151         maxIndent = value;
152         return self();
153      }
154
155      /**
156       *  Quote character.
157       *
158       * <p>
159       * Specifies the character to use for quoting attributes and values.
160       *
161       * <h5 class='section'>Notes:</h5><ul>
162       *    <li class='note'>This setting does not apply to the RDF serializers.
163       * </ul>
164       *
165       * <h5 class='section'>Example:</h5>
166       * <p class='bjava'>
167       *    <jc>// Create a serializer that uses single quotes.</jc>
168       *    WriterSerializer <jv>serializer</jv> = JsonSerializer
169       *       .<jsm>create</jsm>()
170       *       .quoteChar(<js>'\''</js>)
171       *       .build();
172       *
173       *    <jc>// A bean with a single property</jc>
174       *    <jk>public class</jk> MyBean {
175       *       <jk>public</jk> String <jf>foo</jf> = <js>"bar"</js>;
176       *    }
177       *
178       *    <jc>// Produces {'foo':'bar'}</jc>
179       *    String <jv>json</jv> = <jv>serializer</jv>.toString(<jk>new</jk> MyBean());
180       * </p>
181       *
182       * @param value
183       *    The new value for this property.
184       *    <br>The default is <js>'"'</js>.
185       * @return This object.
186       */
187      public SELF quoteChar(char value) {
188         quoteChar = value;
189         return self();
190      }
191
192      /**
193       * Quote character override.
194       *
195       * <p>
196       * Similar to {@link #quoteChar(char)} but takes precedence over that setting.
197       *
198       * <p>
199       * Allows you to override the quote character even if it's set by a subclass such as {@link Json5Serializer}.
200       *
201       *
202       * @param value
203       *    The new value for this property.
204       *    <br>The default is <jk>null</jk>.
205       * @return This object.
206       */
207      public SELF quoteCharOverride(char value) {
208         quoteCharOverride = value;
209         return self();
210      }
211
212      /**
213       *  Quote character.
214       *
215       * <p>
216       * Specifies to use single quotes for quoting attributes and values.
217       *
218       * <h5 class='section'>Notes:</h5><ul>
219       *    <li class='note'>This setting does not apply to the RDF serializers.
220       * </ul>
221       *
222       * <h5 class='section'>Example:</h5>
223       * <p class='bjava'>
224       *    <jc>// Create a serializer that uses single quotes.</jc>
225       *    WriterSerializer <jv>serializer</jv> = JsonSerializer
226       *       .<jsm>create</jsm>()
227       *       .sq()
228       *       .build();
229       *
230       *    <jc>// A bean with a single property</jc>
231       *    <jk>public class</jk> MyBean {
232       *       <jk>public</jk> String <jf>foo</jf> = <js>"bar"</js>;
233       *    }
234       *
235       *    <jc>// Produces {'foo':'bar'}</jc>
236       *    String <jv>json</jv> = <jv>serializer</jv>.toString(<jk>new</jk> MyBean());
237       * </p>
238       *
239       * @return This object.
240       */
241      public SELF sq() {
242         return quoteChar('\'');
243      }
244
245      /**
246       * Output stream charset.
247       *
248       * <p>
249       * The character set to use when writing to <c>OutputStreams</c>.
250       *
251       * <p>
252       * Used when passing in output streams and byte arrays to {@link WriterSerializer#write(Object, Object)}.
253       *
254       * <h5 class='section'>Example:</h5>
255       * <p class='bjava'>
256       *    <jc>// Create a serializer that writes UTF-8 files.</jc>
257       *    WriterSerializer <jv>serializer</jv> = JsonSerializer
258       *       .<jsm>create</jsm>()
259       *       .streamCharset(Charset.<jsm>forName</jsm>(<js>"UTF-8"</js>))
260       *       .build();
261       *
262       *    <jc>// Use it to write to a UTF-8 encoded output stream.</jc>
263       *    <jv>serializer</jv>.serializer(<jk>new</jk> FileOutputStreamStream(<js>"MyBean.txt"</js>), <jv>myBean</jv>);
264       * </p>
265       *
266       * @param value
267       *    The new value for this property.
268       *    <br>The default is the system JVM setting.
269       *    <br>Can be <jk>null</jk> (defaults to UTF-8).
270       * @return This object.
271       */
272      public SELF streamCharset(Charset value) {
273         streamCharset = value;
274         return self();
275      }
276
277
278      /**
279       *  Use whitespace.
280       *
281       * <p>
282       * When enabled, whitespace is added to the output to improve readability.
283       *
284       * <h5 class='section'>Example:</h5>
285       * <p class='bjava'>
286       *    <jc>// Create a serializer with whitespace enabled.</jc>
287       *    WriterSerializer <jv>serializer</jv> = JsonSerializer
288       *       .<jsm>create</jsm>()
289       *       .useWhitespace()
290       *       .build();
291       *
292       *    <jc>// A bean with a single property</jc>
293       *    <jk>public class</jk> MyBean {
294       *       <jk>public</jk> String <jf>foo</jf> = <js>"bar"</js>;
295       *    }
296       *
297       *    <jc>// Produces "\{\n\t"foo": "bar"\n\}\n"</jc>
298       *    String <jv>json</jv> = <jv>serializer</jv>.write(<jk>new</jk> MyBean());
299       * </p>
300       *
301       * @return This object.
302       */
303      public SELF useWhitespace() {
304         return useWhitespace(true);
305      }
306
307      /**
308       * Same as {@link #useWhitespace()} but allows you to explicitly specify the value.
309       *
310       * @param value The value for this setting.
311       * @return This object.
312       */
313      public SELF useWhitespace(boolean value) {
314         useWhitespace = value;
315         return self();
316      }
317
318      /**
319       *  Use whitespace.
320       *
321       * <p>
322       * When enabled, whitespace is added to the output to improve readability.
323       *
324       * <h5 class='section'>Example:</h5>
325       * <p class='bjava'>
326       *    <jc>// Create a serializer with whitespace enabled.</jc>
327       *    WriterSerializer <jv>serializer</jv> = JsonSerializer
328       *       .<jsm>create</jsm>()
329       *       .ws()
330       *       .build();
331       *
332       *    <jc>// A bean with a single property</jc>
333       *    <jk>public class</jk> MyBean {
334       *       <jk>public</jk> String <jf>foo</jf> = <js>"bar"</js>;
335       *    }
336       *
337       *    <jc>// Produces "\{\n\t"foo": "bar"\n\}\n"</jc>
338       *    String <jv>json</jv> = <jv>serializer</jv>.write(<jk>new</jk> MyBean());
339       * </p>
340       *
341       * @return This object.
342       */
343      public SELF ws() {
344         return useWhitespace();
345      }
346   }
347
348   /**
349    * Concrete default builder leaf for the non-subclassed {@link WriterSerializer#create()} / {@link WriterSerializer#copy()} path.
350    */
351   @SuppressWarnings({
352      "java:S110" // Inheritance depth follows the serializer builder chain; intentional layered design
353   })
354   public static final class DefaultBuilder extends Builder<DefaultBuilder> {
355
356      DefaultBuilder() {}
357
358      DefaultBuilder(WriterSerializer copyFrom) {
359         super(copyFrom);
360      }
361
362      DefaultBuilder(Builder<?> copyFrom) {
363         super(copyFrom);
364      }
365
366      @Override /* Overridden from Context.Builder<?> */
367      public DefaultBuilder copy() {
368         return new DefaultBuilder(this);
369      }
370   }
371
372   /**
373    * Creates a new builder for this object.
374    *
375    * @return A new builder.
376    */
377   @SuppressWarnings({
378      "java:S1452" // Builder<?> wildcard return intentional; callers chain via fluent API without needing the concrete type
379   })
380   public static Builder<?> create() {
381      return new DefaultBuilder();
382   }
383
384   protected final boolean useWhitespace;
385   protected final Character quoteChar;
386   protected final Character quoteCharOverride;
387   private final Charset streamCharset;
388   protected final int maxIndent;
389
390   private final char quoteCharValue;
391
392   /**
393    * Constructor.
394    *
395    * @param builder
396    *    The builder for this object.
397    *    <br>Cannot be <jk>null</jk>.
398    */
399   protected WriterSerializer(Builder<?> builder) {
400      super(builder);
401
402      maxIndent = builder.maxIndent;
403      quoteChar = builder.quoteChar;
404      quoteCharOverride = builder.quoteCharOverride;
405      streamCharset = builder.streamCharset;
406      useWhitespace = builder.useWhitespace;
407
408      if (nn(quoteCharOverride)) {
409         quoteCharValue = quoteCharOverride;
410      } else if (nn(quoteChar)) {
411         quoteCharValue = quoteChar;
412      } else {
413         quoteCharValue = '"';
414      }
415   }
416
417   @Override /* Overridden from Context */
418   public Builder<?> copy() {
419      return new DefaultBuilder(this);
420   }
421
422   @Override /* Overridden from Context */
423   public WriterSerializerSession.Builder<?> createSession() {
424      return WriterSerializerSession.create(this);
425   }
426
427   @Override /* Overridden from Context */
428   public WriterSerializerSession getSession() { return createSession().build(); }
429
430   @Override /* Overridden from Serializer */
431   public final boolean isWriterSerializer() { return true; }
432
433   /**
434    * Convenience method for serializing an object and sending it to STDOUT.
435    *
436    * @param o The object to serialize.
437    * @return This object.
438    */
439   public final WriterSerializer println(Object o) {
440      RichLogger.getLogger(WriterSerializer.class).info(toString(o));  // NOT DEBUG
441      return this;
442   }
443
444   /**
445    * Convenience method for serializing an object to a <c>String</c>.
446    *
447    * @param o The object to serialize.
448    * @return The output serialized to a string.
449    * @throws SerializeException If a problem occurred trying to convert the output.
450    */
451   @Override /* Overridden from Serializer */
452   public final String write(Object o) throws SerializeException {
453      return getSession().write(o);
454   }
455
456   /**
457    * Allows this serializer to be used as a {@link ThrowingFunction} that converts an object to its serialized string form.
458    *
459    * <p>
460    * Because {@link WriterSerializer} implements {@link ThrowingFunction ThrowingFunction&lt;Object,String&gt;}, a serializer
461    * instance can be passed directly wherever a {@link java.util.function.Function Function} or {@link ThrowingFunction} is
462    * expected (e.g. assertion transform methods).
463    *
464    * <h5 class='section'>Example:</h5>
465    * <p class='bjava'>
466    *    <jsm>assertObject</jsm>(<jv>myPojo</jv>).asString(Json5Serializer.<jsf>DEFAULT</jsf>).is(<js>"{...}"</js>);
467    * </p>
468    *
469    * @param o The object to serialize.
470    * @return The output serialized to a string.
471    * @throws Exception If a problem occurred trying to convert the output.
472    */
473   @Override /* Overridden from ThrowingFunction */
474   public final String applyThrows(Object o) throws Exception {
475      return write(o);
476   }
477
478   /**
479    * Identical to {@link #write(Object)} except throws a {@link RuntimeException} instead of a {@link SerializeException}.
480    *
481    * <p>
482    * This is typically good enough for debugging purposes.
483    *
484    * @param o The object to serialize.
485    * @return The serialized object.
486    */
487   public final String toString(Object o) {
488      try {
489         return write(o);
490      } catch (Exception e) {
491         throw toRex(e);
492      }
493   }
494
495   /**
496    * Maximum indentation.
497    *
498    * @see Builder#maxIndent(int)
499    * @return
500    *    The maximum indentation level in the serialized document.
501    */
502   protected final int getMaxIndent() { return maxIndent; }
503
504   /**
505    * Quote character.
506    *
507    * @see Builder#quoteChar(char)
508    * @return
509    *    The character used for quoting attributes and values.
510    */
511   protected char getQuoteChar() { return quoteCharValue; }
512
513   /**
514    * Output stream charset.
515    *
516    * @see Builder#streamCharset(Charset)
517    * @return
518    *    The character set to use when writing to <c>OutputStreams</c> and byte arrays.
519    */
520   protected final Charset getStreamCharset() { return streamCharset; }
521
522   /**
523    * Use whitespace.
524    *
525    * @see Builder#useWhitespace()
526    * @return
527    *    When enabled, whitespace is added to the output to improve readability.
528    */
529   protected final boolean isUseWhitespace() { return useWhitespace; }
530
531   @Override /* Overridden from Serializer */
532   protected FluentMap<String,Object> properties() {
533      return super.properties()
534         .a("maxIndent", maxIndent)
535         .a("quoteChar", quoteChar)
536         .a("streamCharset", streamCharset)
537         .a("useWhitespace", useWhitespace);
538   }
539
540   /**
541    * Quote character.
542    *
543    * @see Builder#quoteChar(char)
544    * @return
545    *    The character used for quoting attributes and values.
546    */
547   @SuppressWarnings({
548      "java:S1845" // Method name intentionally matches field name
549   })
550   protected Character quoteChar() {
551      return nn(quoteCharOverride) ? quoteCharOverride : quoteChar;
552   }
553}