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.hjson;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022import java.util.*;
023
024import org.apache.juneau.commons.bean.*;
025import org.apache.juneau.commons.collections.*;
026import org.apache.juneau.marshall.*;
027import org.apache.juneau.marshall.serializer.*;
028import org.apache.juneau.marshall.stream.*;
029import org.apache.juneau.marshall.swap.spi.*;
030
031/**
032 * Serializes POJO models to Hjson (Human JSON).
033 *
034 * <h5 class='topic'>Media types</h5>
035 * <p>
036 * Handles <c>Accept</c> types:  <bc>application/hjson, application/hjson+json</bc>
037 * <p>
038 * Produces <c>Content-Type</c> types:  <bc>application/hjson</bc>
039 *
040 * <h5 class='topic'>Description</h5>
041 * <p>
042 * Hjson is a syntax extension of JSON designed for human-friendly configuration files and hand-edited data.
043 * This serializer produces output that is valid Hjson and can be parsed back by {@link HjsonParser}.
044 *
045 * <p>
046 * The conversion is as follows:
047 * <ul class='spaced-list'>
048 *    <li>
049 *       Maps (e.g. {@link HashMap HashMaps}, {@link TreeMap TreeMaps}) are converted to Hjson objects.
050 *    <li>
051 *       Collections (e.g. {@link HashSet HashSets}, {@link LinkedList LinkedLists}) and Java arrays are converted to
052 *       Hjson arrays.
053 *    <li>
054 *       {@link String Strings} are serialized as quoteless values when they contain only safe characters.
055 *       Strings that would be misinterpreted as booleans, null, or numbers are automatically double-quoted.
056 *    <li>
057 *       {@link Number Numbers} (e.g. {@link Integer}, {@link Long}, {@link Double}) are converted to Hjson numbers.
058 *    <li>
059 *       {@link Boolean Booleans} are converted to {@code true} or {@code false}.
060 *    <li>
061 *       {@code null} values are converted to Hjson {@code null}.
062 *    <li>
063 *       Beans are converted to Hjson objects.
064 * </ul>
065 *
066 * <p>
067 * The types above are considered "Hjson-primitive" object types.
068 * Any non-Hjson-primitive object types are transformed into Hjson-primitive object types through
069 * {@link ObjectSwap ObjectSwaps} associated through the
070 * {@link org.apache.juneau.marshall.MarshallingContext.Builder#swaps(Class...) MarshallingContext.Builder.swaps(Class...)} method.
071 * Several default transforms are provided for transforming Dates, Enums, Iterators, etc.
072 *
073 * <h5 class='section'>Example:</h5>
074 * <p class='bjava'>
075 *    <jc>// Use one of the default serializers to serialize a POJO</jc>
076 *    String <jv>hjson</jv> = HjsonSerializer.<jsf>DEFAULT</jsf>.write(<jv>someObject</jv>);
077 *
078 *    <jc>// Create a compact serializer (comma-separated, no newlines between members)</jc>
079 *    HjsonSerializer <jv>compact</jv> = HjsonSerializer.<jsm>create</jsm>().compact().build();
080 *
081 *    <jc>// Clone an existing serializer and enable multiline strings</jc>
082 *    <jv>compact</jv> = HjsonSerializer.<jsf>DEFAULT</jsf>.copy().useMultilineStrings(<jk>true</jk>).build();
083 * </p>
084 *
085 * <h5 class='figure'>Example output - readable mode (Map of name/age):</h5>
086 * <p class='bjson'>
087 *    {
088 *      name: Alice
089 *      age: 30
090 *    }
091 * </p>
092 *
093 * <h5 class='figure'>Example output - compact mode:</h5>
094 * <p class='bjson'>
095 *    {name:Alice,age:30}
096 * </p>
097 *
098 * <h5 class='section'>Notes:</h5><ul>
099 *    <li class='note'>String values equal to <c>true</c>, <c>false</c>, <c>null</c>, or parseable as numbers
100 *       are automatically serialized in double quotes to prevent parse ambiguity.
101 *    <li class='note'>Empty strings are always serialized as <c>""</c>.
102 *    <li class='note'>Root-level objects always include enclosing braces <c>{}</c> by default.
103 *       Use {@link Builder#omitRootBraces(boolean)} to suppress them.
104 *    <li class='note'>In compact mode, strings containing newlines are serialized as double-quoted strings
105 *       with JSON escape sequences rather than multiline <c>'''</c> blocks.
106 *    <li class='note'>Format-specific field-level annotations ({@link Hjson @Hjson}) are supported
107 *       but intentionally minimal in the initial implementation.
108 *    <li class='note'>This class is thread safe and reusable.
109 * </ul>
110 *
111 * <h5 class='section'>See Also:</h5><ul>
112 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Hjson">Hjson</a>
113 *    <li class='link'><a class="doclink" href="https://hjson.github.io/syntax.html">Hjson Specification</a>
114 * </ul>
115 */
116@SuppressWarnings({
117   "java:S110", // Depth comes from the Serializer -> WriterSerializer base chain shared by all text-format serializers
118   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
119   "java:S9149", // Per-format static factories intentionally shadow the parent's.
120   "resource" // Closeable resources are owned by the caller's serializer session; Eclipse JDT @Owning warning is by design.
121})
122public class HjsonSerializer extends WriterSerializer implements HjsonMetaProvider, RecordWritable {
123
124   private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>,HjsonClassMeta> hjsonClassMetas = new java.util.concurrent.ConcurrentHashMap<>();
125   private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta,HjsonBeanPropertyMeta> hjsonBeanPropertyMetas = new java.util.concurrent.ConcurrentHashMap<>();
126
127   /** Use multiline ''' for strings with newlines. */
128   protected final boolean useMultilineStrings;
129
130   /** Use quoteless strings for simple values. */
131   protected final boolean useQuotelessStrings;
132
133   /** Use quoteless keys when safe. */
134   protected final boolean useQuotelessKeys;
135
136   /** Omit root object braces. */
137   protected final boolean omitRootBraces;
138
139   /** Use newlines instead of commas between members. */
140   protected final boolean useNewlineSeparators;
141
142   /**
143    * Builder class.
144    */
145   public static class Builder extends WriterSerializer.Builder<Builder> {
146
147      private static final Cache<HashKey,HjsonSerializer> CACHE = Cache.of(HashKey.class, HjsonSerializer.class).build();
148
149      private boolean useMultilineStrings = true;
150      private boolean useQuotelessStrings = true;
151      private boolean useQuotelessKeys = true;
152      private boolean omitRootBraces = false;
153      private boolean useNewlineSeparators = true;
154
155      protected Builder() {
156         produces("application/hjson");
157         accept("application/hjson,application/hjson+json");
158      }
159
160      protected Builder(Builder copyFrom) {
161         super(reqnn("copyFrom", copyFrom));
162         useMultilineStrings = copyFrom.useMultilineStrings;
163         useQuotelessStrings = copyFrom.useQuotelessStrings;
164         useQuotelessKeys = copyFrom.useQuotelessKeys;
165         omitRootBraces = copyFrom.omitRootBraces;
166         useNewlineSeparators = copyFrom.useNewlineSeparators;
167      }
168
169      protected Builder(HjsonSerializer copyFrom) {
170         super(reqnn("copyFrom", copyFrom));
171         useMultilineStrings = copyFrom.useMultilineStrings;
172         useQuotelessStrings = copyFrom.useQuotelessStrings;
173         useQuotelessKeys = copyFrom.useQuotelessKeys;
174         omitRootBraces = copyFrom.omitRootBraces;
175         useNewlineSeparators = copyFrom.useNewlineSeparators;
176      }
177
178      /**
179       * Enables multiline strings for values containing newlines.
180       *
181       * @param value <jk>true</jk> to use <c>'''</c> blocks for newline-containing strings.
182       * @return This object.
183       */
184      public Builder useMultilineStrings(boolean value) {
185         useMultilineStrings = value;
186         return this;
187      }
188
189      /**
190       * Enables quoteless string output when values are unambiguous.
191       *
192       * @param value <jk>true</jk> to emit unquoted strings when safe.
193       * @return This object.
194       */
195      public Builder useQuotelessStrings(boolean value) {
196         useQuotelessStrings = value;
197         return this;
198      }
199
200      /**
201       * Enables quoteless object keys when safe.
202       *
203       * @param value <jk>true</jk> to emit unquoted keys when safe.
204       * @return This object.
205       */
206      public Builder useQuotelessKeys(boolean value) {
207         useQuotelessKeys = value;
208         return this;
209      }
210
211      /**
212       * Omits root-level object braces <c>{}</c> in output.
213       *
214       * @param value <jk>true</jk> to omit root braces.
215       * @return This object.
216       */
217      public Builder omitRootBraces(boolean value) {
218         omitRootBraces = value;
219         return this;
220      }
221
222      /**
223       * Uses newlines instead of commas between object members.
224       *
225       * @param value <jk>true</jk> for newline-separated members.
226       * @return This object.
227       */
228      public Builder useNewlineSeparators(boolean value) {
229         useNewlineSeparators = value;
230         return this;
231      }
232
233      /**
234       * Creates compact output (comma-separated, single line).
235       *
236       * @return This object.
237       */
238      public Builder compact() {
239         useNewlineSeparators = false;
240         return this;
241      }
242
243      @Override
244      public Builder useWhitespace() {
245         super.useWhitespace();
246         return this;
247      }
248
249      @Override
250      public Builder useWhitespace(boolean value) {
251         super.useWhitespace(value);
252         return this;
253      }
254
255      @Override /* Overridden from WriterSerializer.Builder<?> */
256      public Builder ws() {
257         return useWhitespace();
258      }
259
260      @Override
261      public HjsonSerializer build() {
262         return cache(CACHE).build(HjsonSerializer.class);
263      }
264
265      @Override
266      public Builder copy() {
267         return new Builder(this);
268      }
269
270      @Override
271      public HashKey hashKey() {
272         return HashKey.of(super.hashKey(), useMultilineStrings, useQuotelessStrings, useQuotelessKeys, omitRootBraces, useNewlineSeparators);
273      }
274   }
275
276   /** Default serializer (readable). */
277   public static final HjsonSerializer DEFAULT = new HjsonSerializer(create().ws());
278
279   /** Compact serializer (single line). */
280   public static final HjsonSerializer DEFAULT_COMPACT = new HjsonSerializer(create().useWhitespace(false).compact());
281
282   /**
283    * Creates a new builder.
284    *
285    * @return A new builder.
286    */
287   public static Builder create() {
288      return new Builder();
289   }
290
291   /**
292    * Constructor.
293    *
294    * @param builder The builder.
295    */
296   public HjsonSerializer(Builder builder) {
297      super(builder);
298      useMultilineStrings = builder.useMultilineStrings;
299      useQuotelessStrings = builder.useQuotelessStrings;
300      useQuotelessKeys = builder.useQuotelessKeys;
301      omitRootBraces = builder.omitRootBraces;
302      useNewlineSeparators = builder.useNewlineSeparators;
303   }
304
305   @Override
306   public HjsonSerializerSession.Builder createSession() {
307      return HjsonSerializerSession.create(this);
308   }
309
310   @Override
311   public Builder copy() {
312      return new Builder(this);
313   }
314
315   @Override
316   public HjsonBeanPropertyMeta getHjsonBeanPropertyMeta(BeanPropertyMeta bpm) {
317      if (bpm == null)
318         return HjsonBeanPropertyMeta.DEFAULT;
319      return hjsonBeanPropertyMetas.computeIfAbsent(bpm, k -> new HjsonBeanPropertyMeta(k, this));
320   }
321
322   @Override
323   public HjsonClassMeta getHjsonClassMeta(ClassMeta<?> cm) {
324      return hjsonClassMetas.computeIfAbsent(cm, k -> new HjsonClassMeta(k, this));
325   }
326
327   @Override
328   protected FluentMap<String,Object> properties() {
329      return super.properties()
330         .a("useMultilineStrings", useMultilineStrings)
331         .a("useQuotelessStrings", useQuotelessStrings)
332         .a("useQuotelessKeys", useQuotelessKeys)
333         .a("omitRootBraces", omitRootBraces)
334         .a("useNewlineSeparators", useNewlineSeparators);
335   }
336
337   /**
338    * Convenience delegator that opens a {@link RecordWriter} over the output using
339    * <b>default session arguments</b> (mirrors {@link #write(Object)}).
340    *
341    * <p>
342    * The real implementation lives on {@link HjsonSerializerSession#writeRecords(Object)}.
343    * Callers that need request-derived configuration should call {@link #createSession()} and
344    * invoke {@link HjsonSerializerSession#writeRecords(Object)} on the built session instead.
345    *
346    * @param output The output.
347    * @return A new {@link RecordWriter}.
348    * @throws IOException If a problem occurred opening the underlying output.
349    */
350   @Override /* RecordWritable */
351   public RecordWriter writeRecords(Object output) throws IOException {
352      return ((RecordWritable) getSession()).writeRecords(output);
353   }
354
355   @Override /* RecordWritable */
356   public boolean isRecordStreaming() {
357      return false;
358   }
359}