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.hocon;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022import java.net.*;
023import java.util.*;
024
025import org.apache.juneau.commons.bean.*;
026import org.apache.juneau.commons.collections.*;
027import org.apache.juneau.marshall.*;
028import org.apache.juneau.marshall.serializer.*;
029import org.apache.juneau.marshall.stream.*;
030import org.apache.juneau.marshall.swap.spi.*;
031
032/**
033 * Serializes POJO models to HOCON (Human-Optimized Config Object Notation).
034 *
035 * <h5 class='topic'>Media types</h5>
036 * <p>
037 * Accepts <c>Accept</c> types:  <bc>application/hocon</bc>
038 * <p>
039 * Produces <c>Content-Type</c> types:  <bc>application/hocon</bc>
040 *
041 * <h5 class='topic'>Description</h5>
042 * <p>
043 * HOCON is a superset of JSON used extensively in the JVM ecosystem for configuration files.
044 * This serializer produces output that is valid HOCON and can be parsed back by {@link HoconParser}.
045 *
046 * <p>
047 * The conversion is as follows:
048 * <ul class='spaced-list'>
049 *    <li>
050 *       {@link HashMap HashMap} / bean → HOCON object with unquoted keys and <c>=</c> separator (<c>{ key = value }</c>).
051 *    <li>
052 *       {@link Collection Collection} / array → HOCON array (<c>[a, b, c]</c>).
053 *    <li>
054 *       {@link String String} (simple, no special chars) → unquoted string (<c>name = myapp</c>).
055 *    <li>
056 *       {@link String String} (contains special chars or is ambiguous like <c>"true"</c>, <c>"42"</c>, <c>"null"</c>) → quoted string (<c>path = "/usr/local/bin"</c>).
057 *    <li>
058 *       {@link String String} (multi-line) → triple-quoted string (<c>desc = """..."""</c>).
059 *    <li>
060 *       {@link Number Number} → number literal (<c>port = 8080</c>).
061 *    <li>
062 *       {@link Boolean Boolean} → <c>true</c> or <c>false</c>.
063 *    <li>
064 *       <c>null</c> → <c>null</c> keyword.
065 *    <li>
066 *       Non-primitive types (e.g. {@link Date Date}, {@link URI URI}) → converted via {@link ObjectSwap} before serialization.
067 * </ul>
068 *
069 * <h5 class='topic'>Behavior-specific subclasses</h5>
070 * <p>
071 * <ul class='spaced-list'>
072 *    <li>
073 *       {@link #DEFAULT} — readable, unquoted keys/values, <c>=</c> separator, root braces omitted (config-file style).
074 *    <li>
075 *       {@link #DEFAULT_BRACES} — same as {@link #DEFAULT} but includes root <c>{ }</c> braces.
076 *    <li>
077 *       {@link #DEFAULT_COMPACT} — single-line compact output, JSON-compatible.
078 * </ul>
079 *
080 * <h5 class='section'>Example:</h5>
081 * <p class='bjava'>
082 *    <jc>// Serialize a bean</jc>
083 *    String <jv>hocon</jv> = HoconSerializer.<jsf>DEFAULT</jsf>.write(<jv>myBean</jv>);
084 *
085 *    <jc>// Create a custom serializer instance</jc>
086 *    HoconSerializer <jv>s</jv> = HoconSerializer.<jsm>create</jsm>().omitRootBraces(<jk>false</jk>).build();
087 *    String <jv>hocon2</jv> = <jv>s</jv>.write(<jv>myBean</jv>);
088 * </p>
089 *
090 * <h5 class='figure'>Example output (bean, root braces omitted):</h5>
091 * <p class='bjson'>
092 * name = myapp
093 * port = 8080
094 * debug = true
095 * database {
096 *   host = localhost
097 *   port = 5432
098 * }
099 * tags = [web, api, rest]
100 * </p>
101 *
102 * <h5 class='figure'>Example output (with root braces — DEFAULT_BRACES):</h5>
103 * <p class='bjson'>
104 * {
105 *   name = myapp
106 *   port = 8080
107 *   debug = true
108 * }
109 * </p>
110 *
111 * <h5 class='section'>Limitations:</h5><ul>
112 *    <li class='note'>Substitutions (<c>${var}</c>) are never emitted — the serializer always writes resolved (concrete) values.
113 *    <li class='note'>Value concatenation is never emitted — the serializer always writes complete strings.
114 *    <li class='note'>Path expressions (<c>a.b.c = value</c>) are never emitted — the serializer always uses nested-brace form.
115 *    <li class='note'><c>include</c> directives are never emitted — these require file-system access outside the scope of a serializer.
116 * </ul>
117 *
118 * <h5 class='section'>Notes:</h5><ul>
119 *    <li class='note'>This class is thread safe and reusable.
120 * </ul>
121 *
122 * <h5 class='section'>See Also:</h5><ul>
123 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/HoconSupport">HOCON</a>
124 * </ul>
125 */
126@SuppressWarnings({
127   "java:S110", // HoconSerializer extends WriterSerializer, which sits on the shared Serializer base-class chain; the depth comes from that format-family design.
128   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
129   "java:S9149", // Per-format static factories intentionally shadow the parent's.
130   "resource" // Closeable resources are owned by the caller's serializer session; Eclipse JDT @Owning warning is by design.
131})
132public class HoconSerializer extends WriterSerializer implements HoconMetaProvider, RecordWritable {
133
134   private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>,HoconClassMeta> hoconClassMetas = new java.util.concurrent.ConcurrentHashMap<>();
135   private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta,HoconBeanPropertyMeta> hoconBeanPropertyMetas = new java.util.concurrent.ConcurrentHashMap<>();
136
137   /** Use = (not :) for key-value separator. */
138   protected final boolean useEqualsSign;
139
140   /** Use unquoted strings for simple values. */
141   protected final boolean useUnquotedStrings;
142
143   /** Use unquoted keys when safe. */
144   protected final boolean useUnquotedKeys;
145
146   /** Omit root object braces. */
147   protected final boolean omitRootBraces;
148
149   /** Use triple-quoted """ for strings with newlines. */
150   protected final boolean useMultilineStrings;
151
152   /** Use newlines instead of commas between members. */
153   protected final boolean useNewlineSeparators;
154
155   /**
156    * Builder class.
157    */
158   public static class Builder extends WriterSerializer.Builder<Builder> {
159
160      private static final Cache<HashKey,HoconSerializer> CACHE = Cache.of(HashKey.class, HoconSerializer.class).build();
161
162      private boolean useEqualsSign = true;
163      private boolean useUnquotedStrings = true;
164      private boolean useUnquotedKeys = true;
165      private boolean omitRootBraces = true;
166      private boolean useMultilineStrings = true;
167      private boolean useNewlineSeparators = true;
168
169      protected Builder() {
170         produces("application/hocon");
171         accept("application/hocon");
172      }
173
174      protected Builder(Builder copyFrom) {
175         super(reqnn("copyFrom", copyFrom));
176         useEqualsSign = copyFrom.useEqualsSign;
177         useUnquotedStrings = copyFrom.useUnquotedStrings;
178         useUnquotedKeys = copyFrom.useUnquotedKeys;
179         omitRootBraces = copyFrom.omitRootBraces;
180         useMultilineStrings = copyFrom.useMultilineStrings;
181         useNewlineSeparators = copyFrom.useNewlineSeparators;
182      }
183
184      protected Builder(HoconSerializer copyFrom) {
185         super(reqnn("copyFrom", copyFrom));
186         useEqualsSign = copyFrom.useEqualsSign;
187         useUnquotedStrings = copyFrom.useUnquotedStrings;
188         useUnquotedKeys = copyFrom.useUnquotedKeys;
189         omitRootBraces = copyFrom.omitRootBraces;
190         useMultilineStrings = copyFrom.useMultilineStrings;
191         useNewlineSeparators = copyFrom.useNewlineSeparators;
192      }
193
194      /**
195       * Uses = for key-value separator (HOCON convention).
196       *
197       * @param value <jk>true</jk> for <c>=</c>, <jk>false</jk> for <c>:</c>.
198       * @return This object.
199       */
200      public Builder useEqualsSign(boolean value) {
201         useEqualsSign = value;
202         return this;
203      }
204
205      /**
206       * Enables unquoted string output when values are unambiguous.
207       *
208       * @param value <jk>true</jk> to emit unquoted strings when safe.
209       * @return This object.
210       */
211      public Builder useUnquotedStrings(boolean value) {
212         useUnquotedStrings = value;
213         return this;
214      }
215
216      /**
217       * Enables unquoted object keys when safe.
218       *
219       * @param value <jk>true</jk> to emit unquoted keys when safe.
220       * @return This object.
221       */
222      public Builder useUnquotedKeys(boolean value) {
223         useUnquotedKeys = value;
224         return this;
225      }
226
227      /**
228       * Omits root-level object braces <c>{}</c> in output.
229       *
230       * @param value <jk>true</jk> to omit root braces.
231       * @return This object.
232       */
233      public Builder omitRootBraces(boolean value) {
234         omitRootBraces = value;
235         return this;
236      }
237
238      /**
239       * Enables triple-quoted strings for values containing newlines.
240       *
241       * @param value <jk>true</jk> to use <c>"""</c> blocks for newline-containing strings.
242       * @return This object.
243       */
244      public Builder useMultilineStrings(boolean value) {
245         useMultilineStrings = value;
246         return this;
247      }
248
249      /**
250       * Uses newlines instead of commas between object members.
251       *
252       * @param value <jk>true</jk> for newline-separated members.
253       * @return This object.
254       */
255      public Builder useNewlineSeparators(boolean value) {
256         useNewlineSeparators = value;
257         return this;
258      }
259
260      /**
261       * Creates compact output (comma-separated, single line).
262       *
263       * @return This object.
264       */
265      public Builder compact() {
266         useNewlineSeparators = false;
267         return this;
268      }
269
270      @Override
271      public Builder useWhitespace() {
272         super.useWhitespace();
273         return this;
274      }
275
276      @Override
277      public Builder useWhitespace(boolean value) {
278         super.useWhitespace(value);
279         return this;
280      }
281
282      @Override /* Overridden from WriterSerializer.Builder<?> */
283      public Builder ws() {
284         return useWhitespace();
285      }
286
287      @Override
288      public HoconSerializer build() {
289         return cache(CACHE).build(HoconSerializer.class);
290      }
291
292      @Override
293      public Builder copy() {
294         return new Builder(this);
295      }
296
297      @Override
298      public HashKey hashKey() {
299         return HashKey.of(super.hashKey(), useEqualsSign, useUnquotedStrings, useUnquotedKeys,
300            omitRootBraces, useMultilineStrings, useNewlineSeparators);
301      }
302   }
303
304   /** Default serializer (readable, root braces omitted). */
305   public static final HoconSerializer DEFAULT = new HoconSerializer(create().ws());
306
307   /** Default serializer with root braces. */
308   public static final HoconSerializer DEFAULT_BRACES = new HoconSerializer(create().ws().omitRootBraces(false));
309
310   /** Compact serializer (single line). */
311   public static final HoconSerializer DEFAULT_COMPACT = new HoconSerializer(create().useWhitespace(false).compact());
312
313   /**
314    * Creates a new builder.
315    *
316    * @return A new builder.
317    */
318   public static Builder create() {
319      return new Builder();
320   }
321
322   /**
323    * Constructor.
324    *
325    * @param builder The builder.
326    */
327   public HoconSerializer(Builder builder) {
328      super(builder);
329      useEqualsSign = builder.useEqualsSign;
330      useUnquotedStrings = builder.useUnquotedStrings;
331      useUnquotedKeys = builder.useUnquotedKeys;
332      omitRootBraces = builder.omitRootBraces;
333      useMultilineStrings = builder.useMultilineStrings;
334      useNewlineSeparators = builder.useNewlineSeparators;
335   }
336
337   @Override
338   public HoconSerializerSession.Builder createSession() {
339      return HoconSerializerSession.create(this);
340   }
341
342   @Override
343   public Builder copy() {
344      return new Builder(this);
345   }
346
347   @Override
348   public HoconBeanPropertyMeta getHoconBeanPropertyMeta(BeanPropertyMeta bpm) {
349      if (bpm == null)
350         return HoconBeanPropertyMeta.DEFAULT;
351      return hoconBeanPropertyMetas.computeIfAbsent(bpm, k -> new HoconBeanPropertyMeta(k, this));
352   }
353
354   @Override
355   public HoconClassMeta getHoconClassMeta(ClassMeta<?> cm) {
356      return hoconClassMetas.computeIfAbsent(cm, k -> new HoconClassMeta(k, this));
357   }
358
359   @Override
360   protected FluentMap<String,Object> properties() {
361      return super.properties()
362         .a("useEqualsSign", useEqualsSign)
363         .a("useUnquotedStrings", useUnquotedStrings)
364         .a("useUnquotedKeys", useUnquotedKeys)
365         .a("omitRootBraces", omitRootBraces)
366         .a("useMultilineStrings", useMultilineStrings)
367         .a("useNewlineSeparators", useNewlineSeparators);
368   }
369
370   /**
371    * Convenience delegator that opens a {@link RecordWriter} over the output using
372    * <b>default session arguments</b> (mirrors {@link #write(Object)}).
373    *
374    * <p>
375    * The real implementation lives on {@link HoconSerializerSession#writeRecords(Object)}.
376    * Callers that need request-derived configuration should call {@link #createSession()} and
377    * invoke {@link HoconSerializerSession#writeRecords(Object)} on the built session instead.
378    *
379    * @param output The output.
380    * @return A new {@link RecordWriter}.
381    * @throws IOException If a problem occurred opening the underlying output.
382    */
383   @Override /* RecordWritable */
384   public RecordWriter writeRecords(Object output) throws IOException {
385      return ((RecordWritable) getSession()).writeRecords(output);
386   }
387
388   @Override /* RecordWritable */
389   public boolean isRecordStreaming() {
390      return false;
391   }
392}