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 * Converts a Java string to an acceptable JSON attribute name. If 133 * simpleMode is true, then quotes will only be used if the attribute 134 * name consists of only alphanumeric characters. 135 */ 136 boolean doConvert = trimStrings || ! simpleMode; // Always convert when not in lax mode. 137 138 // If the attribute is null, it must always be printed as null without quotes. 139 // Technically, this isn't part of the JSON spec, but it does allow for null key values. 140 if (s == null) { 141 s = "null"; 142 doConvert = false; 143 144 } else { 145 146 // Look for characters that would require the attribute to be quoted. 147 // All possible numbers should be caught here. 148 if (! doConvert) { 149 for (int i = 0; i < s.length() && ! doConvert; i++) { 150 char c = s.charAt(i); 151 doConvert |= ! (i == 0 ? validFirstAttrChars.contains(c) : validAttrChars.contains(c)); 152 } 153 } 154 155 // Reserved words and blanks must be quoted. 156 if (! doConvert) { 157 if (s.isEmpty() || reservedWords.contains(s)) 158 doConvert = true; 159 } 160 } 161 162 // If no conversion necessary, just print the attribute as-is. 163 if (doConvert) 164 stringValue(s); 165 else 166 out.append(s); 167 168 return this; 169 } 170 171 /** 172 * Appends a URI to the output. 173 * 174 * @param uri The URI to append to the output. 175 * @return This object (for method chaining). 176 * @throws IOException Thrown by underlying stream. 177 */ 178 public SerializerWriter uriValue(Object uri) throws IOException { 179 return stringValue(uriResolver.resolve(uri)); 180 } 181 182 //----------------------------------------------------------------------------------------------------------------- 183 // Overridden methods 184 //----------------------------------------------------------------------------------------------------------------- 185 186 @Override /* SerializerWriter */ 187 public JsonWriter cr(int depth) throws IOException { 188 super.cr(depth); 189 return this; 190 } 191 192 @Override /* SerializerWriter */ 193 public JsonWriter cre(int depth) throws IOException { 194 super.cre(depth); 195 return this; 196 } 197 198 /** 199 * Performs an indentation only if we're currently past max indentation. 200 * 201 * @param depth The current indentation depth. 202 * @return This object (for method chaining). 203 * @throws IOException Thrown by underlying stream. 204 */ 205 public JsonWriter smi(int depth) throws IOException { 206 if (depth > maxIndent) 207 super.s(); 208 return this; 209 } 210 211 @Override /* SerializerWriter */ 212 public JsonWriter appendln(int indent, String text) throws IOException { 213 super.appendln(indent, text); 214 return this; 215 } 216 217 @Override /* SerializerWriter */ 218 public JsonWriter appendln(String text) throws IOException { 219 super.appendln(text); 220 return this; 221 } 222 223 @Override /* SerializerWriter */ 224 public JsonWriter append(int indent, String text) throws IOException { 225 super.append(indent, text); 226 return this; 227 } 228 229 @Override /* SerializerWriter */ 230 public JsonWriter append(int indent, char c) throws IOException { 231 super.append(indent, c); 232 return this; 233 } 234 235 @Override /* SerializerWriter */ 236 public JsonWriter s() throws IOException { 237 super.s(); 238 return this; 239 } 240 241 /** 242 * Adds a space only if the current indentation level is below maxIndent. 243 * 244 * @param indent The number of spaces to indent. 245 * @return This object (for method chaining). 246 * @throws IOException Thrown by underlying stream. 247 */ 248 public JsonWriter s(int indent) throws IOException { 249 if (indent <= maxIndent) 250 super.s(); 251 return this; 252 } 253 254 @Override /* SerializerWriter */ 255 public JsonWriter q() throws IOException { 256 super.q(); 257 return this; 258 } 259 260 @Override /* SerializerWriter */ 261 public JsonWriter i(int indent) throws IOException { 262 super.i(indent); 263 return this; 264 } 265 266 @Override /* SerializerWriter */ 267 public JsonWriter nl(int indent) throws IOException { 268 super.nl(indent); 269 return this; 270 } 271 272 @Override /* SerializerWriter */ 273 public JsonWriter append(Object text) throws IOException { 274 super.append(text); 275 return this; 276 } 277 278 @Override /* SerializerWriter */ 279 public JsonWriter append(String text) throws IOException { 280 super.append(text); 281 return this; 282 } 283 284 @Override /* SerializerWriter */ 285 public JsonWriter appendIf(boolean b, String text) throws IOException { 286 super.appendIf(b, text); 287 return this; 288 } 289 290 @Override /* SerializerWriter */ 291 public JsonWriter appendIf(boolean b, char c) throws IOException { 292 super.appendIf(b, c); 293 return this; 294 } 295 296 @Override /* SerializerWriter */ 297 public JsonWriter append(char c) throws IOException { 298 super.append(c); 299 return this; 300 } 301}