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