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}