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}