001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.json;
014
015import java.io.*;
016
017import org.apache.juneau.*;
018import org.apache.juneau.internal.*;
019import org.apache.juneau.serializer.*;
020
021/**
022 * Specialized writer for serializing JSON.
023 *
024 * <h5 class='section'>Notes:</h5>
025 * <ul class='spaced-list'>
026 *    <li>
027 *       This class is not intended for external use.
028 * </ul>
029 */
030public final class JsonWriter extends SerializerWriter {
031
032   private final boolean simpleMode, escapeSolidus;
033
034   // Characters that trigger special handling of serializing attribute values.
035   private static final AsciiSet
036      encodedChars = AsciiSet.create("\n\t\b\f\r'\"\\"),
037      encodedChars2 = AsciiSet.create("\n\t\b\f\r'\"\\/");
038
039   private static final KeywordSet reservedWords = new KeywordSet(
040      "arguments","break","case","catch","class","const","continue","debugger","default","delete",
041      "do","else","enum","eval","export","extends","false","finally","for","function","if",
042      "implements","import","in","instanceof","interface","let","new","null","package",
043      "private","protected","public","return","static","super","switch","this","throw",
044      "true","try","typeof","var","void","while","with","undefined","yield"
045   );
046
047
048   // Characters that represent attribute name characters that don't trigger quoting.
049   // These are actually more strict than the actual Javascript specification, but
050   // can be narrowed in the future if necessary.
051   // For example, we quote attributes that start with $ even though we don't need to.
052   private static final AsciiSet validAttrChars = AsciiSet.create().ranges("a-z","A-Z","0-9").chars("_").build();
053   private static final AsciiSet validFirstAttrChars = AsciiSet.create().ranges("a-z","A-Z").chars("_").build();
054
055   private final AsciiSet ec;
056
057   /**
058    * Constructor.
059    *
060    * @param out The writer being wrapped.
061    * @param useWhitespace If <jk>true</jk>, tabs and spaces will be used in output.
062    * @param maxIndent The maximum indentation level.
063    * @param escapeSolidus If <jk>true</jk>, forward slashes should be escaped in the output.
064    * @param quoteChar The quote character to use (i.e. <js>'\''</js> or <js>'"'</js>)
065    * @param simpleMode If <jk>true</jk>, JSON attributes will only be quoted when necessary.
066    * @param trimStrings If <jk>true</jk>, strings will be trimmed before being serialized.
067    * @param uriResolver The URI resolver for resolving URIs to absolute or root-relative form.
068    */
069   protected JsonWriter(Writer out, boolean useWhitespace, int maxIndent, boolean escapeSolidus, char quoteChar,
070         boolean simpleMode, boolean trimStrings, UriResolver uriResolver) {
071      super(out, useWhitespace, maxIndent, trimStrings, quoteChar, uriResolver);
072      this.simpleMode = simpleMode;
073      this.escapeSolidus = escapeSolidus;
074      this.ec = escapeSolidus ? encodedChars2 : encodedChars;
075   }
076
077   /**
078    * Serializes the specified object as a JSON string value.
079    *
080    * @param s The object being serialized.
081    * @return This object (for method chaining).
082    * @throws IOException Should never happen.
083    */
084   public JsonWriter stringValue(String s) throws IOException {
085      if (s == null)
086         return this;
087      boolean doConvert = false;
088      for (int i = 0; i < s.length() && ! doConvert; i++) {
089         char c = s.charAt(i);
090         doConvert |= ec.contains(c);
091      }
092      q();
093      if (! doConvert) {
094         out.append(s);
095      } else {
096         for (int i = 0; i < s.length(); i++) {
097            char c = s.charAt(i);
098            if (ec.contains(c)) {
099               if (c == '\n')
100                  out.append('\\').append('n');
101               else if (c == '\t')
102                  out.append('\\').append('t');
103               else if (c == '\b')
104                  out.append('\\').append('b');
105               else if (c == '\f')
106                  out.append('\\').append('f');
107               else if (c == quoteChar)
108                  out.append('\\').append(quoteChar);
109               else if (c == '\\')
110                  out.append('\\').append('\\');
111               else if (c == '/' && escapeSolidus)
112                  out.append('\\').append('/');
113               else if (c != '\r')
114                  out.append(c);
115            } else {
116               out.append(c);
117            }
118         }
119      }
120      q();
121      return this;
122   }
123
124   /**
125    * Serializes the specified object as a JSON attribute name.
126    *
127    * @param s The object being serialized.
128    * @return This object (for method chaining).
129    * @throws IOException Should never happen.
130    */
131   public JsonWriter attr(String s) throws IOException {
132      /*
133       * Converts a Java string to an acceptable JSON attribute name. If
134       * simpleMode is true, then quotes will only be used if the attribute
135       * name consists of only alphanumeric characters.
136       */
137      boolean doConvert = trimStrings || ! simpleMode;      // Always convert when not in lax mode.
138
139      // If the attribute is null, it must always be printed as null without quotes.
140      // Technically, this isn't part of the JSON spec, but it does allow for null key values.
141      if (s == null) {
142         s = "null";
143         doConvert = false;
144
145      } else {
146
147         // Look for characters that would require the attribute to be quoted.
148         // All possible numbers should be caught here.
149         if (! doConvert) {
150            for (int i = 0; i < s.length() && ! doConvert; i++) {
151               char c = s.charAt(i);
152               doConvert |= ! (i == 0 ? validFirstAttrChars.contains(c) : validAttrChars.contains(c));
153            }
154         }
155
156         // Reserved words and blanks must be quoted.
157         if (! doConvert) {
158            if (s.isEmpty() || reservedWords.contains(s))
159               doConvert = true;
160         }
161      }
162
163      // If no conversion necessary, just print the attribute as-is.
164      if (doConvert)
165         stringValue(s);
166      else
167         out.append(s);
168
169      return this;
170   }
171
172   /**
173    * Appends a URI to the output.
174    *
175    * @param uri The URI to append to the output.
176    * @return This object (for method chaining).
177    * @throws IOException
178    */
179   public SerializerWriter uriValue(Object uri) throws IOException {
180      return stringValue(uriResolver.resolve(uri));
181   }
182
183   //-----------------------------------------------------------------------------------------------------------------
184   // Overridden methods
185   //-----------------------------------------------------------------------------------------------------------------
186
187   @Override /* SerializerWriter */
188   public JsonWriter cr(int depth) throws IOException {
189      super.cr(depth);
190      return this;
191   }
192
193   @Override /* SerializerWriter */
194   public JsonWriter cre(int depth) throws IOException {
195      super.cre(depth);
196      return this;
197   }
198
199   /**
200    * Performs an indentation only if we're currently past max indentation.
201    *
202    * @param depth The current indentation depth.
203    * @return This object (for method chaining).
204    * @throws IOException
205    */
206   public JsonWriter smi(int depth) throws IOException {
207      if (depth > maxIndent)
208         super.s();
209      return this;
210   }
211
212   @Override /* SerializerWriter */
213   public JsonWriter appendln(int indent, String text) throws IOException {
214      super.appendln(indent, text);
215      return this;
216   }
217
218   @Override /* SerializerWriter */
219   public JsonWriter appendln(String text) throws IOException {
220      super.appendln(text);
221      return this;
222   }
223
224   @Override /* SerializerWriter */
225   public JsonWriter append(int indent, String text) throws IOException {
226      super.append(indent, text);
227      return this;
228   }
229
230   @Override /* SerializerWriter */
231   public JsonWriter append(int indent, char c) throws IOException {
232      super.append(indent, c);
233      return this;
234   }
235
236   @Override /* SerializerWriter */
237   public JsonWriter s() throws IOException {
238      super.s();
239      return this;
240   }
241
242   /**
243    * Adds a space only if the current indentation level is below maxIndent.
244    *
245    * @param indent
246    * @return This object (for method chaining).
247    * @throws IOException
248    */
249   public JsonWriter s(int indent) throws IOException {
250      if (indent <= maxIndent)
251         super.s();
252      return this;
253   }
254
255   @Override /* SerializerWriter */
256   public JsonWriter q() throws IOException {
257      super.q();
258      return this;
259   }
260
261   @Override /* SerializerWriter */
262   public JsonWriter i(int indent) throws IOException {
263      super.i(indent);
264      return this;
265   }
266
267   @Override /* SerializerWriter */
268   public JsonWriter nl(int indent) throws IOException {
269      super.nl(indent);
270      return this;
271   }
272
273   @Override /* SerializerWriter */
274   public JsonWriter append(Object text) throws IOException {
275      super.append(text);
276      return this;
277   }
278
279   @Override /* SerializerWriter */
280   public JsonWriter append(String text) throws IOException {
281      super.append(text);
282      return this;
283   }
284
285   @Override /* SerializerWriter */
286   public JsonWriter appendIf(boolean b, String text) throws IOException {
287      super.appendIf(b, text);
288      return this;
289   }
290
291   @Override /* SerializerWriter */
292   public JsonWriter appendIf(boolean b, char c) throws IOException {
293      super.appendIf(b, c);
294      return this;
295   }
296
297   @Override /* SerializerWriter */
298   public JsonWriter append(char c) throws IOException {
299      super.append(c);
300      return this;
301   }
302}