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.ini;
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.serializer.*;
027import org.apache.juneau.marshall.stream.*;
028
029/**
030 * Serializes POJO models to INI format.
031 *
032 * <p>
033 * Converts Java POJOs to INI format (section/key-value structure).
034 *
035 * <h5 class='topic'>Media types</h5>
036 * <p>
037 * Produces: <bc>text/ini</bc>
038 * <br>Accepts: <bc>text/ini, text/x-ini</bc>
039 *
040 * <h5 class='topic'>Bean-to-INI mapping</h5>
041 * <ul class='spaced-list'>
042 *    <li>Simple properties → key-value pairs in the default section
043 *    <li>Nested beans → <c>[sectionName]</c> sections
044 *    <li>Deeply nested beans → <c>[path/to/section]</c> section paths
045 *    <li>Collections and complex values → JSON5-encoded inline values
046 *    <li>Maps with string keys → <c>[sectionName]</c> sections
047 *    <li>Null values → unquoted <c>null</c> token
048 * </ul>
049 *
050 * <h5 class='section'>Example:</h5>
051 * <p class='bjava'>
052 *    <jc>// Serialize a bean to INI</jc>
053 *    String <jv>ini</jv> = IniSerializer.<jsf>DEFAULT</jsf>.write(<jv>myBean</jv>);
054 *
055 *    <jc>// Create a custom serializer</jc>
056 *    IniSerializer <jv>s</jv> = IniSerializer.<jsm>create</jsm>().useComments().build();
057 *    <jv>ini</jv> = <jv>s</jv>.write(<jv>myBean</jv>);
058 * </p>
059 *
060 * <h5 class='figure'>Example output (Map of name/age):</h5>
061 * <p class='bini'>
062 *    <ck>name</ck> = <cv>Alice</cv>
063 *    <ck>age</ck> = <cv>30</cv>
064 * </p>
065 *
066 * <h5 class='figure'>Complex (nested object + array):</h5>
067 * <p class='bini'>
068 *    <ck>name</ck> = <cv>Alice</cv>
069 *    <ck>age</ck> = <cv>30</cv>
070 *    <ck>tags</ck> = <cv>['a','b','c']</cv>
071 *
072 *    <cs>[address]</cs>
073 *    <ck>street</ck> = <cv>123 Main St</cv>
074 *    <ck>city</ck> = <cv>Boston</cv>
075 *    <ck>state</ck> = <cv>MA</cv>
076 * </p>
077 *
078 * <h5 class='topic'>Limitations</h5>
079 * <p>
080 * <ul class='spaced-list'>
081 *    <li>Top-level collections, arrays, and scalar values are not supported. The root must be a bean or
082 *       <c>Map&lt;String,?&gt;</c>. Throws {@link SerializeException} for unsupported root types.
083 *    <li>{@link Reader} and {@link InputStream} passthrough is not supported.
084 * </ul>
085 *
086 * <h5 class='section'>Notes:</h5><ul>
087 *    <li class='note'>This class is thread safe and reusable.
088 *    <li class='note'>Complex values (collections, maps with complex values) are embedded as JSON5 inline
089 *       strings and remain fully round-trippable.
090 * </ul>
091 *
092 * <h5 class='section'>See Also:</h5><ul>
093 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Ini">INI Basics</a>
094 * </ul>
095 */
096@SuppressWarnings({
097   "java:S110", // Serializer hierarchy (IniSerializer -> WriterSerializer -> Serializer -> MarshallingContextable -> Context) is inherently deep.
098   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
099   "java:S9149", // Per-format static factories intentionally shadow the parent's.
100   "resource" // Closeable resources are owned by the caller's serializer session; Eclipse JDT @Owning warning is by design.
101})
102public class IniSerializer extends WriterSerializer implements IniMetaProvider, RecordWritable {
103
104   private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>,IniClassMeta> iniClassMetas = new java.util.concurrent.ConcurrentHashMap<>();
105   private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta,IniBeanPropertyMeta> iniBeanPropertyMetas = new java.util.concurrent.ConcurrentHashMap<>();
106
107   /**
108    * Builder class.
109    */
110   public static class Builder extends WriterSerializer.Builder<Builder> {
111
112      private static final Cache<HashKey,IniSerializer> CACHE = Cache.of(HashKey.class, IniSerializer.class).build();
113
114      private char kvSeparator = '=';
115      private boolean spacedSeparator = true;
116      private boolean useComments = false;
117
118      protected Builder() {
119         produces("text/ini");
120         accept("text/ini,text/x-ini");
121      }
122
123      protected Builder(Builder copyFrom) {
124         super(reqnn("copyFrom", copyFrom));
125         kvSeparator = copyFrom.kvSeparator;
126         spacedSeparator = copyFrom.spacedSeparator;
127         useComments = copyFrom.useComments;
128      }
129
130      protected Builder(IniSerializer copyFrom) {
131         super(reqnn("copyFrom", copyFrom));
132         kvSeparator = copyFrom.kvSeparator;
133         spacedSeparator = copyFrom.spacedSeparator;
134         useComments = copyFrom.useComments;
135      }
136
137      /**
138       * Key-value separator character.
139       *
140       * @param value <c>=</c> (default) or <c>:</c>.
141       * @return This object.
142       */
143      public Builder kvSeparator(char value) {
144         kvSeparator = value;
145         return this;
146      }
147
148      /**
149       * Whether to add spaces around the separator.
150       *
151       * @param value <jk>true</jk> for <c>key = value</c>, <jk>false</jk> for <c>key=value</c>.
152       * @return This object.
153       */
154      public Builder spacedSeparator(boolean value) {
155         spacedSeparator = value;
156         return this;
157      }
158
159      /**
160       * Whether to emit <c>#</c> comments from bean property descriptions.
161       *
162       * @param value The flag.
163       * @return This object.
164       */
165      public Builder useComments(boolean value) {
166         useComments = value;
167         return this;
168      }
169
170      /**
171       * Emit <c>#</c> comments from bean property descriptions.
172       *
173       * @return This object.
174       */
175      public Builder useComments() {
176         useComments = true;
177         return this;
178      }
179
180      @Override
181      public Builder useWhitespace() {
182         super.useWhitespace();
183         return this;
184      }
185
186      @Override
187      public Builder useWhitespace(boolean value) {
188         super.useWhitespace(value);
189         return this;
190      }
191
192      @Override
193      public Builder ws() {
194         return useWhitespace();
195      }
196
197      @Override
198      public IniSerializer build() {
199         return cache(CACHE).build(IniSerializer.class);
200      }
201
202      @Override
203      public Builder copy() {
204         return new Builder(this);
205      }
206
207      @Override
208      public HashKey hashKey() {
209         return HashKey.of(super.hashKey(), kvSeparator, spacedSeparator, useComments);
210      }
211   }
212
213   /** Default serializer. */
214   public static final IniSerializer DEFAULT = new IniSerializer(create());
215
216   /** Default serializer with blank lines between sections. */
217   public static final IniSerializer DEFAULT_READABLE = new IniSerializer(create().ws());
218
219   /**
220    * Creates a new builder.
221    *
222    * @return A new builder.
223    */
224   public static Builder create() {
225      return new Builder();
226   }
227
228   /** Key-value separator. */
229   protected final char kvSeparator;
230
231   /** Whether to add spaces around the separator. */
232   protected final boolean spacedSeparator;
233
234   /** Whether to emit property descriptions as comments. */
235   protected final boolean useComments;
236
237   /**
238    * Constructor.
239    *
240    * @param builder The builder.
241    */
242   public IniSerializer(Builder builder) {
243      super(builder);
244      kvSeparator = builder.kvSeparator;
245      spacedSeparator = builder.spacedSeparator;
246      useComments = builder.useComments;
247   }
248
249   @Override
250   public IniSerializerSession.Builder createSession() {
251      return IniSerializerSession.create(this);
252   }
253
254   @Override
255   public Builder copy() {
256      return new Builder(this);
257   }
258
259   @Override
260   public IniBeanPropertyMeta getIniBeanPropertyMeta(BeanPropertyMeta bpm) {
261      if (bpm == null)
262         return IniBeanPropertyMeta.DEFAULT;
263      return iniBeanPropertyMetas.computeIfAbsent(bpm, k -> new IniBeanPropertyMeta(k, this));
264   }
265
266   @Override
267   public IniClassMeta getIniClassMeta(ClassMeta<?> cm) {
268      return iniClassMetas.computeIfAbsent(cm, k -> new IniClassMeta(k, this));
269   }
270
271   @Override
272   protected FluentMap<String,Object> properties() {
273      return super.properties()
274         .a("kvSeparator", String.valueOf(kvSeparator))
275         .a("spacedSeparator", spacedSeparator)
276         .a("useComments", useComments);
277   }
278
279   /**
280    * Convenience delegator that opens a {@link RecordWriter} over the output using
281    * <b>default session arguments</b> (mirrors {@link #write(Object)}).
282    *
283    * <p>
284    * The real implementation lives on {@link IniSerializerSession#writeRecords(Object)}.
285    * Callers that need request-derived configuration should call {@link #createSession()} and
286    * invoke {@link IniSerializerSession#writeRecords(Object)} on the built session instead.
287    *
288    * @param output The output.
289    * @return A new {@link RecordWriter}.
290    * @throws IOException If a problem occurred opening the underlying output.
291    */
292   @Override /* RecordWritable */
293   public RecordWriter writeRecords(Object output) throws IOException {
294      return ((RecordWritable) getSession()).writeRecords(output);
295   }
296
297   @Override /* RecordWritable */
298   public boolean isRecordStreaming() {
299      return false;
300   }
301}