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.json;
018
019import static org.apache.juneau.commons.utils.StringUtils.*;
020
021import java.io.*;
022
023import org.apache.juneau.*;
024import org.apache.juneau.commons.collections.*;
025import org.apache.juneau.commons.lang.*;
026import org.apache.juneau.serializer.*;
027
028/**
029 * Specialized writer for serializing JSON.
030 *
031 * <h5 class='section'>Notes:</h5><ul>
032 *    <li class='note'>
033 *       This class is not intended for external use.
034 * </ul>
035 *
036 * <h5 class='section'>See Also:</h5><ul>
037 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JsonBasics">JSON Basics</a>
038
039 * </ul>
040 */
041@SuppressWarnings("resource")
042public class JsonWriter extends SerializerWriter {
043
044   // Characters that trigger special handling of serializing attribute values.
045   // @formatter:off
046   private static final AsciiSet
047      encodedChars = AsciiSet.of("\n\t\b\f\r'\"\\"),
048      encodedChars2 = AsciiSet.of("\n\t\b\f\r'\"\\/");
049
050   private static final KeywordSet reservedWords = new KeywordSet(
051      "arguments","break","case","catch","class","const","continue","debugger","default","delete",
052      "do","else","enum","eval","export","extends","false","finally","for","function","if",
053      "implements","import","in","instanceof","interface","let","new","null","package",
054      "private","protected","public","return","static","super","switch","this","throw",
055      "true","try","typeof","var","void","while","with","undefined","yield"
056   );
057   // @formatter:on
058
059   // Characters that represent attribute name characters that don't trigger quoting.
060   // These are actually more strict than the actual Javascript specification, but
061   // can be narrowed in the future if necessary.
062   // For example, we quote attributes that start with $ even though we don't need to.
063   private static final AsciiSet validAttrChars = AsciiSet.create().ranges("a-z", "A-Z", "0-9").chars("_").build();
064
065   private static final AsciiSet validFirstAttrChars = AsciiSet.create().ranges("a-z", "A-Z").chars("_").build();
066   private final boolean simpleAttrs, escapeSolidus;
067
068   private final AsciiSet ec;
069
070   /**
071    * Constructor.
072    *
073    * @param out The writer being wrapped.
074    * @param useWhitespace If <jk>true</jk>, tabs and spaces will be used in output.
075    * @param maxIndent The maximum indentation level.
076    * @param escapeSolidus If <jk>true</jk>, forward slashes should be escaped in the output.
077    * @param quoteChar The quote character to use (i.e. <js>'\''</js> or <js>'"'</js>)
078    * @param simpleAttrs If <jk>true</jk>, JSON attributes will only be quoted when necessary.
079    * @param trimStrings If <jk>true</jk>, strings will be trimmed before being serialized.
080    * @param uriResolver The URI resolver for resolving URIs to absolute or root-relative form.
081    */
082   protected JsonWriter(Writer out, boolean useWhitespace, int maxIndent, boolean escapeSolidus, char quoteChar, boolean simpleAttrs, boolean trimStrings, UriResolver uriResolver) {
083      super(out, useWhitespace, maxIndent, trimStrings, quoteChar, uriResolver);
084      this.simpleAttrs = simpleAttrs;
085      this.escapeSolidus = escapeSolidus;
086      this.ec = escapeSolidus ? encodedChars2 : encodedChars;
087   }
088
089   @Override /* Overridden from SerializerWriter */
090   public JsonWriter append(char c) {
091      super.append(c);
092      return this;
093   }
094
095   @Override /* Overridden from SerializerWriter */
096   public JsonWriter append(char[] value) {
097      super.append(value);
098      return this;
099   }
100
101   @Override /* Overridden from SerializerWriter */
102   public JsonWriter append(int indent, char c) {
103      super.append(indent, c);
104      return this;
105   }
106
107   @Override /* Overridden from SerializerWriter */
108   public JsonWriter append(int indent, String text) {
109      super.append(indent, text);
110      return this;
111   }
112
113   @Override /* Overridden from SerializerWriter */
114   public JsonWriter append(Object text) {
115      super.append(text);
116      return this;
117   }
118
119   @Override /* Overridden from SerializerWriter */
120   public JsonWriter append(String text) {
121      super.append(text);
122      return this;
123   }
124
125   @Override /* Overridden from SerializerWriter */
126   public JsonWriter appendIf(boolean b, char c) {
127      super.appendIf(b, c);
128      return this;
129   }
130
131   @Override /* Overridden from SerializerWriter */
132   public JsonWriter appendIf(boolean b, String text) {
133      super.appendIf(b, text);
134      return this;
135   }
136
137   @Override /* Overridden from SerializerWriter */
138   public JsonWriter appendln(int indent, String text) {
139      super.appendln(indent, text);
140      return this;
141   }
142
143   @Override /* Overridden from SerializerWriter */
144   public JsonWriter appendln(String text) {
145      super.appendln(text);
146      return this;
147   }
148
149   @Override /* Overridden from SerializerWriter */
150   public JsonWriter appendUri(Object value) {
151      super.appendUri(value);
152      return this;
153   }
154
155   /**
156    * Serializes the specified object as a JSON attribute name.
157    *
158    * @param s The object being serialized.
159    * @return This object.
160    */
161   public JsonWriter attr(String s) {
162
163      if (trimStrings)
164         s = trim(s);
165
166      /*
167       * Converts a Java string to an acceptable JSON attribute name. If
168       * simpleAttrs is true, then quotes will only be used if the attribute
169       * name consists of only alphanumeric characters.
170       */
171      boolean doConvert = ! simpleAttrs;     // Always convert when not in lax mode.
172
173      // If the attribute is null, it must always be printed as null without quotes.
174      // Technically, this isn't part of the JSON spec, but it does allow for null key values.
175      if (s == null) {
176         s = "null";
177         doConvert = false;
178
179      } else {
180
181         // Look for characters that would require the attribute to be quoted.
182         // All possible numbers should be caught here.
183         if (! doConvert) {
184            for (var i = 0; i < s.length() && ! doConvert; i++) {
185               var c = s.charAt(i);
186               doConvert |= ! (i == 0 ? validFirstAttrChars.contains(c) : validAttrChars.contains(c));
187            }
188         }
189
190         // Reserved words and blanks must be quoted.
191         if (! doConvert) {
192            if (s.isEmpty() || reservedWords.contains(s))
193               doConvert = true;
194         }
195      }
196
197      // If no conversion necessary, just print the attribute as-is.
198      if (doConvert)
199         stringValue(s);
200      else
201         w(s);
202
203      return this;
204   }
205
206   @Override /* Overridden from SerializerWriter */
207   public JsonWriter cr(int depth) {
208      super.cr(depth);
209      return this;
210   }
211
212   @Override /* Overridden from SerializerWriter */
213   public JsonWriter cre(int depth) {
214      super.cre(depth);
215      return this;
216   }
217
218   @Override /* Overridden from SerializerWriter */
219   public JsonWriter i(int indent) {
220      super.i(indent);
221      return this;
222   }
223
224   @Override /* Overridden from SerializerWriter */
225   public JsonWriter ie(int indent) {
226      super.ie(indent);
227      return this;
228   }
229
230   @Override /* Overridden from SerializerWriter */
231   public JsonWriter nl(int indent) {
232      super.nl(indent);
233      return this;
234   }
235
236   @Override /* Overridden from SerializerWriter */
237   public JsonWriter nlIf(boolean flag, int indent) {
238      super.nlIf(flag, indent);
239      return this;
240   }
241
242   @Override /* Overridden from SerializerWriter */
243   public JsonWriter q() {
244      super.q();
245      return this;
246   }
247
248   @Override /* Overridden from SerializerWriter */
249   public JsonWriter s() {
250      super.s();
251      return this;
252   }
253
254   /**
255    * Adds a space only if the current indentation level is below maxIndent.
256    *
257    * @param indent The number of spaces to indent.
258    * @return This object.
259    */
260   public JsonWriter s(int indent) {
261      if (indent <= maxIndent)
262         super.s();
263      return this;
264   }
265
266   @Override /* Overridden from SerializerWriter */
267   public JsonWriter sIf(boolean flag) {
268      super.sIf(flag);
269      return this;
270   }
271
272   /**
273    * Performs an indentation only if we're currently past max indentation.
274    *
275    * @param depth The current indentation depth.
276    * @return This object.
277    */
278   public JsonWriter smi(int depth) {
279      if (depth > maxIndent)
280         super.s();
281      return this;
282   }
283
284   /**
285    * Serializes the specified object as a JSON string value.
286    *
287    * @param s The object being serialized.
288    * @return This object.
289    */
290   public JsonWriter stringValue(String s) {
291      if (s == null)
292         return this;
293      boolean doConvert = false;
294      for (var i = 0; i < s.length() && ! doConvert; i++) {
295         var c = s.charAt(i);
296         doConvert |= ec.contains(c);
297      }
298      q();
299      if (! doConvert) {
300         w(s);
301      } else {
302         for (var i = 0; i < s.length(); i++) {
303            var c = s.charAt(i);
304            if (ec.contains(c)) {
305               if (c == '\n')
306                  w('\\').w('n');
307               else if (c == '\t')
308                  w('\\').w('t');
309               else if (c == '\b')
310                  w('\\').w('b');
311               else if (c == '\f')
312                  w('\\').w('f');
313               else if (c == quoteChar)
314                  w('\\').w(quoteChar);
315               else if (c == '\\')
316                  w('\\').w('\\');
317               else if (c == '/' && escapeSolidus)
318                  w('\\').w('/');
319               else if (c != '\r')
320                  w(c);
321            } else {
322               w(c);
323            }
324         }
325      }
326      q();
327      return this;
328   }
329
330   /**
331    * Appends a URI to the output.
332    *
333    * @param uri The URI to append to the output.
334    * @return This object.
335    */
336   public SerializerWriter uriValue(Object uri) {
337      return stringValue(uriResolver.resolve(uri));
338   }
339
340   @Override /* Overridden from SerializerWriter */
341   public JsonWriter w(char value) {
342      super.w(value);
343      return this;
344   }
345
346   @Override /* Overridden from SerializerWriter */
347   public JsonWriter w(String value) {
348      super.w(value);
349      return this;
350   }
351}