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
019
020import static org.apache.juneau.commons.utils.StringUtils.isEmpty;
021
022import java.io.*;
023
024import org.apache.juneau.commons.lang.*;
025import org.apache.juneau.marshall.*;
026import org.apache.juneau.marshall.serializer.*;
027
028/**
029 * Specialized writer for serializing Hjson format.
030 *
031 * <p>
032 * Extends {@link SerializerWriter} with Hjson-specific methods for quoteless strings,
033 * multiline strings, and formatting options.
034 *
035 * <h5 class='section'>See Also:</h5><ul>
036 *    <li class='link'><a class="doclink" href="https://hjson.github.io/syntax.html">Hjson Specification</a>
037 * </ul>
038 */
039@SuppressWarnings({
040   "java:S135", // Multiple break/continue necessary for isNumber char validation loop
041   "resource" // Writer resource managed by calling code
042})
043public class HjsonWriter extends SerializerWriter<HjsonWriter> {
044
045   private static final AsciiSet QUOTE_KEY_CHARS = AsciiSet.of(" \t\n\r{},:[]\"'#/");
046   private static final AsciiSet QUOTE_VALUE_CHARS = AsciiSet.of("\n\r{},:\"'#");
047   private static final AsciiSet ENCODED_CHARS = AsciiSet.of("\n\t\b\f\r\"\\");
048
049   /** Use newlines instead of commas between members. */
050   protected final boolean useNewlineSeparators;
051
052   /** Use quoteless strings for simple values. */
053   protected final boolean useQuotelessStrings;
054
055   /** Use quoteless keys when safe. */
056   protected final boolean useQuotelessKeys;
057
058   /** Omit root braces. */
059   protected final boolean omitRootBraces;
060
061   /** Use multiline ''' for strings with newlines. */
062   protected final boolean useMultilineStrings;
063
064   /** Whether to add space after colon. */
065   protected final boolean spacedKeyValueSeparator;
066
067   /**
068    * Constructor.
069    *
070    * @param out The writer being wrapped.
071    * @param useWhitespace If <jk>true</jk>, use indentation and newlines.
072    * @param maxIndent The maximum indentation level.
073    * @param trimStrings If <jk>true</jk>, trim strings before serialization.
074    * @param useNewlineSeparators Use newlines instead of commas.
075    * @param useQuotelessStrings Use quoteless strings for simple values.
076    * @param useQuotelessKeys Use quoteless keys when safe.
077    * @param omitRootBraces Omit root object braces.
078    * @param useMultilineStrings Use ''' for multiline strings.
079    * @param spacedKeyValueSeparator Add space after colon.
080    * @param uriResolver The URI resolver.
081    */
082   @SuppressWarnings({
083      "java:S107" // Constructor requires many parameters for Hjson writer configuration
084   })
085   protected HjsonWriter(Writer out, boolean useWhitespace, int maxIndent, boolean trimStrings,
086         boolean useNewlineSeparators, boolean useQuotelessStrings, boolean useQuotelessKeys,
087         boolean omitRootBraces, boolean useMultilineStrings, boolean spacedKeyValueSeparator,
088         UriResolver uriResolver) {
089      super(out, useWhitespace, maxIndent, trimStrings, '"', uriResolver);
090      this.useNewlineSeparators = useNewlineSeparators;
091      this.useQuotelessStrings = useQuotelessStrings;
092      this.useQuotelessKeys = useQuotelessKeys;
093      this.omitRootBraces = omitRootBraces;
094      this.useMultilineStrings = useMultilineStrings;
095      this.spacedKeyValueSeparator = spacedKeyValueSeparator;
096   }
097
098   /**
099    * Writes member separator (newline or comma).
100    *
101    * @param depth The current indentation depth.
102    * @return This object.
103    */
104   public HjsonWriter memberSeparator(int depth) {
105      if (useNewlineSeparators) {
106         nl(depth);
107      } else {
108         w(',');
109      }
110      return this;
111   }
112
113   /**
114    * Writes key-value separator (: or : ).
115    *
116    * @return This object.
117    */
118   public HjsonWriter keyValueSeparator() {
119      if (spacedKeyValueSeparator)
120         w(": ");
121      else
122         w(':');
123      return this;
124   }
125
126   /**
127    * Writes object start, optionally omitting braces.
128    *
129    * @param omitBraces If <jk>true</jk>, omit the opening brace.
130    * @param depth The current indentation depth.
131    * @return This object.
132    */
133   public HjsonWriter objectStart(boolean omitBraces, int depth) {
134      if (!omitBraces)
135         w('{');
136      return this;
137   }
138
139   /**
140    * Writes object end, optionally omitting braces.
141    *
142    * @param omitBraces If <jk>true</jk>, omit the closing brace.
143    * @param depth The current indentation depth.
144    * @return This object.
145    */
146   public HjsonWriter objectEnd(boolean omitBraces, int depth) {
147      if (!omitBraces)
148         w('}');
149      return this;
150   }
151
152   /**
153    * Writes array start.
154    *
155    * @return This object.
156    */
157   public HjsonWriter arrayStart() {
158      w('[');
159      return this;
160   }
161
162   /**
163    * Writes array end.
164    *
165    * @return This object.
166    */
167   public HjsonWriter arrayEnd() {
168      w(']');
169      return this;
170   }
171
172   /**
173    * Returns whether the key needs quoting.
174    *
175    * @param key The key to check.
176    *    <br>Can be <jk>null</jk> (returns <jk>true</jk>).
177    * @return <jk>true</jk> if the key must be quoted.
178    */
179   public boolean needsQuoting(String key) {
180      if (isEmpty(key))
181         return true;
182      if (!useQuotelessKeys)
183         return true;
184      for (var i = 0; i < key.length(); i++) {
185         if (QUOTE_KEY_CHARS.contains(key.charAt(i)))
186            return true;
187      }
188      return "true".equals(key) || "false".equals(key) || "null".equals(key) || isNumber(key);
189   }
190
191   /**
192    * Returns whether the value can be written as quoteless.
193    *
194    * @param value The value to check.
195    *    <br>Can be <jk>null</jk> (returns <jk>false</jk>).
196    * @return <jk>true</jk> if the value can be quoteless.
197    */
198   public boolean isSimpleValue(String value) {
199      if (isEmpty(value))
200         return false;
201      if (!useQuotelessStrings)
202         return false;
203      if (value.startsWith("{") || value.startsWith("[") || value.startsWith("\"") || value.startsWith("'"))
204         return false;
205      if (value.startsWith("#") || value.startsWith("//") || value.startsWith("/*"))
206         return false;
207      if (value.contains("\n") || value.contains("\r"))
208         return false;
209      for (var i = 0; i < value.length(); i++) {
210         if (QUOTE_VALUE_CHARS.contains(value.charAt(i)))
211            return false;
212      }
213      if ("true".equals(value) || "false".equals(value) || "null".equals(value))
214         return false;
215      return !isNumber(value);
216   }
217
218   private static boolean isNumber(String s) {
219      if (isEmpty(s))
220         return false;
221      var i = s.startsWith("-") ? 1 : 0;
222      if (i >= s.length())
223         return false;
224      if (s.charAt(i) == '0' && s.length() > i + 1 && s.charAt(i + 1) != '.')
225         return false;
226      for (; i < s.length(); i++) {
227         var c = s.charAt(i);
228         if (c >= '0' && c <= '9')
229            continue;
230         if (c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-')
231            continue;
232         return false;
233      }
234      try {
235         Double.parseDouble(s);
236         return true;
237      } catch (@SuppressWarnings("unused") NumberFormatException e) {
238         return false;
239      }
240   }
241
242   /**
243    * Writes a quoteless string value.
244    *
245    * @param value The value to write.
246    *    <br>Can be <jk>null</jk> (nothing is written).
247    * @return This object.
248    */
249   public HjsonWriter quotelessString(String value) {
250      if (value != null)
251         w(value);
252      return this;
253   }
254
255   /**
256    * Writes a quoted string with JSON escaping.
257    *
258    * @param value The value to write.
259    *    <br>Can be <jk>null</jk> (emitted as the literal <js>"null"</js>).
260    * @return This object.
261    */
262   public HjsonWriter quotedString(String value) {
263      if (value == null) {
264         w("null");
265         return this;
266      }
267      w('"');
268      for (var i = 0; i < value.length(); i++) {
269         var c = value.charAt(i);
270         if (ENCODED_CHARS.contains(c)) {
271            w('\\');
272            w(switch (c) {
273               case '\n' -> 'n';
274               case '\t' -> 't';
275               case '\b' -> 'b';
276               case '\f' -> 'f';
277               case '\r' -> 'r';
278               case '"' -> '"';
279               case '\\' -> '\\';
280               default -> c;
281            });
282         } else {
283            w(c);
284         }
285      }
286      w('"');
287      return this;
288   }
289
290   /**
291    * Writes a multiline string using ''' syntax.
292    *
293    * @param value The value to write.
294    *    <br>Can be <jk>null</jk> (emitted as the literal <js>"null"</js>).
295    * @return This object.
296    */
297   public HjsonWriter multilineString(String value) {
298      if (value == null) {
299         w("null");
300         return this;
301      }
302      w("'''");
303      w(value.replace("\r\n", "\n").replace("\r", "\n"));
304      w("'''");
305      return this;
306   }
307}