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.json;
018
019import static org.apache.juneau.commons.utils.StringUtils.*;
020
021import java.io.*;
022
023import org.apache.juneau.commons.collections.*;
024import org.apache.juneau.commons.lang.*;
025import org.apache.juneau.marshall.*;
026import org.apache.juneau.marshall.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/JsonSupport">JSON</a>
038
039 * </ul>
040 */
041@SuppressWarnings({
042   "java:S107", // The constructor and create() take the writer plus all formatting flags, mirroring the serializer settings.
043   "java:S119", // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name.
044   "java:S3776", // attr() and stringValue() need nested character-class and escape branching to decide quoting in one pass.
045   "resource" // Writer resource managed by calling code
046})
047public abstract class JsonWriter<SELF extends JsonWriter<SELF>> extends SerializerWriter<SELF> {
048
049   // Characters that trigger special handling of serializing attribute values.
050   // @formatter:off
051   // Includes all C0 control characters (U+0000-U+001F) and DEL (U+007F), which must be escaped per RFC 8259.
052   private static final AsciiSet encodedChars = AsciiSet.create().range('\u0000', '\u001f').chars("\u007f'\"\\").build();
053   private static final AsciiSet encodedChars2 = AsciiSet.create().range('\u0000', '\u001f').chars("\u007f'\"\\/").build();
054
055   private static final char[] HEX = "0123456789abcdef".toCharArray();
056
057   private static final KeywordSet reservedWords = new KeywordSet(
058      "arguments","break","case","catch","class","const","continue","debugger","default","delete",
059      "do","else","enum","eval","export","extends","false","finally","for","function","if",
060      "implements","import","in","instanceof","interface","let","new","null","package",
061      "private","protected","public","return","static","super","switch","this","throw",
062      "true","try","typeof","var","void","while","with","undefined","yield"
063   );
064   // @formatter:on
065
066   // Characters that represent attribute name characters that don't trigger quoting.
067   // These are actually more strict than the actual Javascript specification, but
068   // can be narrowed in the future if necessary.
069   // For example, we quote attributes that start with $ even though we don't need to.
070   private static final AsciiSet validAttrChars = AsciiSet.create().ranges("a-z", "A-Z", "0-9").chars("_").build();
071
072   private static final AsciiSet validFirstAttrChars = AsciiSet.create().ranges("a-z", "A-Z").chars("_").build();
073   private final boolean simpleAttrs;
074   private final boolean escapeSolidus;
075
076   private final AsciiSet ec;
077
078   /**
079    * Constructor.
080    *
081    * @param out The writer being wrapped.
082    * @param useWhitespace If <jk>true</jk>, tabs and spaces will be used in output.
083    * @param maxIndent The maximum indentation level.
084    * @param escapeSolidus If <jk>true</jk>, forward slashes should be escaped in the output.
085    * @param quoteChar The quote character to use (i.e. <js>'\''</js> or <js>'"'</js>)
086    * @param simpleAttrs If <jk>true</jk>, JSON attributes will only be quoted when necessary.
087    * @param trimStrings If <jk>true</jk>, strings will be trimmed before being serialized.
088    * @param uriResolver The URI resolver for resolving URIs to absolute or root-relative form.
089    */
090   protected JsonWriter(Writer out, boolean useWhitespace, int maxIndent, boolean escapeSolidus, char quoteChar, boolean simpleAttrs, boolean trimStrings, UriResolver uriResolver) {
091      super(out, useWhitespace, maxIndent, trimStrings, quoteChar, uriResolver);
092      this.simpleAttrs = simpleAttrs;
093      this.escapeSolidus = escapeSolidus;
094      this.ec = escapeSolidus ? encodedChars2 : encodedChars;
095   }
096
097   /**
098    * Creates a new JsonWriter instance.
099    * <p>
100    * Provided for subclasses in other packages (e.g. Json5SerializerSession) that need to create JsonWriter instances.
101    *
102    * @param out The writer being wrapped.
103    * @param useWhitespace If <jk>true</jk>, tabs and spaces will be used in output.
104    * @param maxIndent The maximum indentation level.
105    * @param escapeSolidus If <jk>true</jk>, forward slashes should be escaped in the output.
106    * @param quoteChar The quote character to use (i.e. <js>'\''</js> or <js>'"'</js>)
107    * @param simpleAttrs If <jk>true</jk>, JSON attributes will only be quoted when necessary.
108    * @param trimStrings If <jk>true</jk>, strings will be trimmed before being serialized.
109    * @param uriResolver The URI resolver for resolving URIs to absolute or root-relative form.
110    * @return A new JsonWriter instance.
111    */
112   public static BasicJsonWriter create(Writer out, boolean useWhitespace, int maxIndent, boolean escapeSolidus, char quoteChar, boolean simpleAttrs, boolean trimStrings, UriResolver uriResolver) {
113      return new BasicJsonWriter(out, useWhitespace, maxIndent, escapeSolidus, quoteChar, simpleAttrs, trimStrings, uriResolver);
114   }
115
116   /**
117    * Serializes the specified object as a JSON attribute name.
118    *
119    * @param s The object being serialized.
120    *    <br>Can be <jk>null</jk> (emitted as the literal <js>"null"</js>).
121    * @return This object.
122    */
123   public SELF attr(String s) {
124
125      if (trimStrings)
126         s = trim(s);
127
128      /*
129       * Converts a Java string to an acceptable JSON attribute name. If
130       * simpleAttrs is true, then quotes will only be used if the attribute
131       * name consists of only alphanumeric characters.
132       */
133      boolean doConvert = ! simpleAttrs;     // Always convert when not in lax mode.
134
135      if (s == null) {
136         s = "null";
137         doConvert = false;
138
139      } else {
140
141         // Look for characters that would require the attribute to be quoted.
142         // All possible numbers should be caught here.
143         if (! doConvert) {
144            for (var i = 0; i < s.length() && ! doConvert; i++) {
145               var c = s.charAt(i);
146               doConvert |= ! (i == 0 ? validFirstAttrChars.contains(c) : validAttrChars.contains(c));
147            }
148         }
149
150         // Reserved words and blanks must be quoted.
151         if (! doConvert && (s.isEmpty() || reservedWords.contains(s)))
152            doConvert = true;
153      }
154
155      // If no conversion necessary, just print the attribute as-is.
156      if (doConvert)
157         stringValue(s);
158      else
159         w(s);
160
161      return self();
162   }
163
164   /**
165    * Adds a space only if the current indentation level is below maxIndent.
166    *
167    * @param indent The number of spaces to indent.
168    * @return This object.
169    */
170   public SELF s(int indent) {
171      if (indent <= maxIndent)
172         super.s();
173      return self();
174   }
175
176   /**
177    * Performs an indentation only if we're currently past max indentation.
178    *
179    * @param depth The current indentation depth.
180    * @return This object.
181    */
182   public SELF smi(int depth) {
183      if (depth > maxIndent)
184         super.s();
185      return self();
186   }
187
188   /**
189    * Serializes the specified object as a JSON string value.
190    *
191    * @param s The object being serialized.
192    *    <br>Can be <jk>null</jk> (nothing is written).
193    * @return This object.
194    */
195   public SELF stringValue(String s) {
196      if (s == null)
197         return self();
198      boolean doConvert = false;
199      for (var i = 0; i < s.length() && ! doConvert; i++) {
200         var c = s.charAt(i);
201         doConvert |= ec.contains(c);
202      }
203      q();
204      if (! doConvert) {
205         w(s);
206      } else {
207         for (var i = 0; i < s.length(); i++) {
208            var c = s.charAt(i);
209            if (ec.contains(c)) {
210               if (c == '\n')
211                  w('\\').w('n');
212               else if (c == '\t')
213                  w('\\').w('t');
214               else if (c == '\b')
215                  w('\\').w('b');
216               else if (c == '\f')
217                  w('\\').w('f');
218               else if (c == quoteChar)
219                  w('\\').w(quoteChar);
220               else if (c == '\\')
221                  w('\\').w('\\');
222               else if (c == '/' && escapeSolidus)
223                  w('\\').w('/');
224               else if (c == '\r')
225                  w('\\').w('r');
226               else if (c < 0x20 || c == 0x7f)
227                  w('\\').w('u').w('0').w('0').w(HEX[c >> 4]).w(HEX[c & 0xf]);
228               else
229                  w(c);
230            } else {
231               w(c);
232            }
233         }
234      }
235      q();
236      return self();
237   }
238
239   /**
240    * Appends a URI to the output.
241    *
242    * @param uri The URI to append to the output.
243    * @return This object.
244    */
245   public SELF uriValue(Object uri) {
246      return stringValue(uriResolver.resolve(uri));
247   }
248}