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.toml;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022
023import org.apache.juneau.commons.bean.*;
024import org.apache.juneau.commons.collections.*;
025import org.apache.juneau.marshall.*;
026import org.apache.juneau.marshall.json.*;
027import org.apache.juneau.marshall.serializer.*;
028import org.apache.juneau.marshall.stream.*;
029
030/**
031 * Serializes POJOs to TOML v1.0.0 format.
032 *
033 * <p>
034 * TOML (Tom's Obvious Minimal Language) maps unambiguously to hash tables with native typing.
035 * See <a href="https://toml.io/en/v1.0.0">TOML v1.0.0</a> specification.
036 *
037 * <h5 class='topic'>Media types</h5>
038 * <p>
039 * Produces: <bc>application/toml</bc>
040 * <br>Accepts: <bc>application/toml</bc>
041 *
042 * <h5 class='topic'>Bean-to-TOML mapping</h5>
043 * <ul class='spaced-list'>
044 *    <li>Simple properties → key-value pairs
045 *    <li>Nested beans → <c>[tableName]</c> sections
046 *    <li>Collections of beans → <c>[[tableName]]</c> array of tables
047 *    <li>Collections of simple values → <c>key = [a, b, c]</c>
048 *    <li>Maps with string keys → <c>[mapName]</c> table
049 *    <li>Null properties are omitted (TOML has no null type)
050 * </ul>
051 *
052 * <h5 class='section'>Example:</h5>
053 * <p class='bjava'>
054 *    <jc>// Serialize a bean to TOML</jc>
055 *    String <jv>toml</jv> = TomlSerializer.<jsf>DEFAULT</jsf>.write(<jv>myBean</jv>);
056 *
057 *    <jc>// Serialize a map (e.g. config with nested sections)</jc>
058 *    Map&lt;String, Object&gt; <jv>config</jv> = Map.of(<js>"name"</js>, <js>"myapp"</js>, <js>"port"</js>, 8080);
059 *    <jv>toml</jv> = TomlSerializer.<jsf>DEFAULT</jsf>.write(<jv>config</jv>);
060 *
061 *    <jc>// Use the marshaller for convenience</jc>
062 *    <jv>toml</jv> = Toml.<jsm>of</jsm>(<jv>config</jv>);
063 *
064 *    <jc>// Custom serializer with whitespace for readability</jc>
065 *    TomlSerializer <jv>s</jv> = TomlSerializer.<jsm>create</jsm>().ws().build();
066 *    <jv>toml</jv> = <jv>s</jv>.write(<jv>config</jv>);
067 * </p>
068 *
069 * <h5 class='figure'>Example output (Map of name/age):</h5>
070 * <p class='bcode'>
071 *    name = "Alice"
072 *    age = 30
073 * </p>
074 *
075 * <h5 class='figure'>Complex (nested table + array):</h5>
076 * <p class='bcode'>
077 *    name = "Alice"
078 *    age = 30
079 *    tags = ["a", "b", "c"]
080 *
081 *    [address]
082 *    street = "123 Main St"
083 *    city = "Boston"
084 *    state = "MA"
085 * </p>
086 *
087 * <h5 class='topic'>Limitations compared to JSON</h5>
088 * <p>
089 * TOML is a configuration format with a more restricted type system than JSON.
090 * The following features supported by {@link JsonSerializer}
091 * are not available or limited in TOML:
092 * </p>
093 * <ul class='spaced-list'>
094 *    <li><b>Null values</b> — TOML has no null type. By default, null properties are omitted.
095 *       Use <c>keepNullProperties()</c> to write the configured <c>nullValue</c> string (e.g. <c>&lt;NULL&gt;</c>) instead.
096 *    <li><b>Map keys</b> — TOML tables only support string keys. Maps with non-string keys
097 *       (e.g. <c>Map&lt;Integer,String&gt;</c>) have keys converted via <c>toString()</c>; ensure results are valid TOML keys.
098 *       Null keys are serialized as the string <c>null</c>.
099 *    <li><b>Polymorphic types</b> — <c>addBeanTypes()</c> and <c>addRootType()</c> add <c>_type</c>
100 *       discriminators for polymorphic parsing; use with care as TOML structure differs from JSON.
101 *    <li><b>Root-level collections</b> — TOML expects a root table. Root arrays use array-of-tables
102 *       (<c>[[item]]</c>) for beans/maps, or a <c>_value</c> wrapper for primitives and simple arrays.
103 *    <li><b>Duplicate keys</b> — TOML forbids duplicate keys; structures that would produce duplicate
104 *       keys may yield invalid TOML.
105 * </ul>
106 *
107 * <h5 class='section'>Notes:</h5><ul>
108 *    <li class='note'>This class is thread safe and reusable.
109 * </ul>
110 */
111@SuppressWarnings({
112   "java:S110", // Depth comes from the Serializer -> WriterSerializer base chain shared by all text-format serializers
113   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
114   "java:S9149", // Per-format static factories intentionally shadow the parent's.
115   "resource" // Closeable resources are owned by the caller's serializer session; Eclipse JDT @Owning warning is by design.
116})
117public class TomlSerializer extends WriterSerializer implements TomlMetaProvider, RecordWritable {
118
119   private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>,TomlClassMeta> tomlClassMetas = new java.util.concurrent.ConcurrentHashMap<>();
120   private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta,TomlBeanPropertyMeta> tomlBeanPropertyMetas = new java.util.concurrent.ConcurrentHashMap<>();
121
122   /**
123    * Builder class.
124    */
125   public static class Builder extends WriterSerializer.Builder<Builder> {
126
127      private static final Cache<HashKey,TomlSerializer> CACHE = Cache.of(HashKey.class, TomlSerializer.class).build();
128
129      private int inlineTableThreshold = 3;
130      private boolean useInlineTables = true;
131      private boolean sortKeys = false;
132      private String nullValue = "<NULL>";
133
134      protected Builder() {
135         produces("application/toml");
136         accept("application/toml");
137      }
138
139      protected Builder(Builder copyFrom) {
140         super(reqnn("copyFrom", copyFrom));
141         inlineTableThreshold = copyFrom.inlineTableThreshold;
142         useInlineTables = copyFrom.useInlineTables;
143         sortKeys = copyFrom.sortKeys;
144         nullValue = copyFrom.nullValue;
145      }
146
147      protected Builder(TomlSerializer copyFrom) {
148         super(reqnn("copyFrom", copyFrom));
149         inlineTableThreshold = copyFrom.inlineTableThreshold;
150         useInlineTables = copyFrom.useInlineTables;
151         sortKeys = copyFrom.sortKeys;
152         nullValue = copyFrom.nullValue;
153      }
154
155      /**
156       * String to write for null values. Parser treats values matching this as null.
157       *
158       * <p>
159       * Default is {@code <NULL>} to avoid confusion with the literal string {@code "null"}.
160       *
161       * @param value The null marker string.
162       * @return This object.
163       */
164      public Builder nullValue(String value) {
165         nullValue = value;
166         return this;
167      }
168
169      /**
170       * Maximum properties for inline table format.
171       *
172       * @param value The threshold.
173       * @return This object.
174       */
175      public Builder inlineTableThreshold(int value) {
176         inlineTableThreshold = value;
177         return this;
178      }
179
180      /**
181       * Use inline tables when possible.
182       *
183       * @param value The flag.
184       * @return This object.
185       */
186      public Builder useInlineTables(boolean value) {
187         useInlineTables = value;
188         return this;
189      }
190
191      /**
192       * Sort keys alphabetically in output.
193       *
194       * @param value The flag.
195       * @return This object.
196       */
197      public Builder sortKeys(boolean value) {
198         sortKeys = value;
199         return this;
200      }
201
202      @Override
203      public Builder useWhitespace() {
204         super.useWhitespace();
205         return this;
206      }
207
208      @Override
209      public Builder useWhitespace(boolean value) {
210         super.useWhitespace(value);
211         return this;
212      }
213
214      @Override
215      public Builder ws() {
216         return useWhitespace();
217      }
218
219      @Override
220      public TomlSerializer build() {
221         return cache(CACHE).build(TomlSerializer.class);
222      }
223
224      @Override
225      public Builder copy() {
226         return new Builder(this);
227      }
228
229      @Override
230      public HashKey hashKey() {
231         return HashKey.of(super.hashKey(), inlineTableThreshold, useInlineTables, sortKeys, nullValue);
232      }
233   }
234
235   /** Default serializer. */
236   public static final TomlSerializer DEFAULT = new TomlSerializer(create());
237
238   /** Default serializer with blank lines between sections. */
239   public static final TomlSerializer DEFAULT_READABLE = new TomlSerializer(create().ws());
240
241   public static Builder create() {
242      return new Builder();
243   }
244
245   protected final int inlineTableThreshold;
246   protected final boolean useInlineTables;
247   protected final boolean sortKeys;
248   /** String written for null values. */
249   protected final String nullValue;
250
251   /**
252    * Constructor.
253    *
254    * @param builder The builder.
255    */
256   public TomlSerializer(Builder builder) {
257      super(builder);
258      inlineTableThreshold = builder.inlineTableThreshold;
259      useInlineTables = builder.useInlineTables;
260      sortKeys = builder.sortKeys;
261      nullValue = or(builder.nullValue, "<NULL>");
262   }
263
264   /**
265    * Returns the string written for null values.
266    *
267    * @return The null marker.
268    */
269   public String getNullValue() {
270      return nullValue;
271   }
272
273   @Override
274   public TomlSerializerSession.Builder createSession() {
275      return TomlSerializerSession.create(this);
276   }
277
278   @Override /* Overridden from TomlMetaProvider */
279   public TomlBeanPropertyMeta getTomlBeanPropertyMeta(BeanPropertyMeta bpm) {
280      if (bpm == null)
281         return TomlBeanPropertyMeta.DEFAULT;
282      return tomlBeanPropertyMetas.computeIfAbsent(bpm, k -> new TomlBeanPropertyMeta(k, this));
283   }
284
285   @Override /* Overridden from TomlMetaProvider */
286   public TomlClassMeta getTomlClassMeta(ClassMeta<?> cm) {
287      return tomlClassMetas.computeIfAbsent(cm, k -> new TomlClassMeta(k, this));
288   }
289
290   @Override
291   public Builder copy() {
292      return new Builder(this);
293   }
294
295   @Override
296   protected FluentMap<String,Object> properties() {
297      return super.properties()
298         .a("inlineTableThreshold", inlineTableThreshold)
299         .a("useInlineTables", useInlineTables)
300         .a("sortKeys", sortKeys)
301         .a("nullValue", nullValue);
302   }
303
304   /**
305    * Convenience delegator that opens a {@link RecordWriter} over the output using
306    * <b>default session arguments</b> (mirrors {@link #write(Object)}).
307    *
308    * <p>
309    * The real implementation lives on {@link TomlSerializerSession#writeRecords(Object)}.
310    * Callers that need request-derived configuration should call {@link #createSession()} and
311    * invoke {@link TomlSerializerSession#writeRecords(Object)} on the built session instead.
312    *
313    * @param output The output.
314    * @return A new {@link RecordWriter}.
315    * @throws IOException If a problem occurred opening the underlying output.
316    */
317   @Override /* RecordWritable */
318   public RecordWriter writeRecords(Object output) throws IOException {
319      return ((RecordWritable) getSession()).writeRecords(output);
320   }
321
322   @Override /* RecordWritable */
323   public boolean isRecordStreaming() {
324      return false;
325   }
326}