001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.marshall.json; 018 019import static org.apache.juneau.commons.utils.StringUtils.*; 020 021import java.io.*; 022 023import org.apache.juneau.commons.collections.*; 024import org.apache.juneau.commons.lang.*; 025import org.apache.juneau.marshall.*; 026import org.apache.juneau.marshall.serializer.*; 027 028/** 029 * Specialized writer for serializing JSON. 030 * 031 * <h5 class='section'>Notes:</h5><ul> 032 * <li class='note'> 033 * This class is not intended for external use. 034 * </ul> 035 * 036 * <h5 class='section'>See Also:</h5><ul> 037 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JsonSupport">JSON</a> 038 039 * </ul> 040 */ 041@SuppressWarnings({ 042 "java:S107", // The constructor and create() take the writer plus all formatting flags, mirroring the serializer settings. 043 "java:S119", // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name. 044 "java:S3776", // attr() and stringValue() need nested character-class and escape branching to decide quoting in one pass. 045 "resource" // Writer resource managed by calling code 046}) 047public abstract class JsonWriter<SELF extends JsonWriter<SELF>> extends SerializerWriter<SELF> { 048 049 // Characters that trigger special handling of serializing attribute values. 050 // @formatter:off 051 // Includes all C0 control characters (U+0000-U+001F) and DEL (U+007F), which must be escaped per RFC 8259. 052 private static final AsciiSet encodedChars = AsciiSet.create().range('\u0000', '\u001f').chars("\u007f'\"\\").build(); 053 private static final AsciiSet encodedChars2 = AsciiSet.create().range('\u0000', '\u001f').chars("\u007f'\"\\/").build(); 054 055 private static final char[] HEX = "0123456789abcdef".toCharArray(); 056 057 private static final KeywordSet reservedWords = new KeywordSet( 058 "arguments","break","case","catch","class","const","continue","debugger","default","delete", 059 "do","else","enum","eval","export","extends","false","finally","for","function","if", 060 "implements","import","in","instanceof","interface","let","new","null","package", 061 "private","protected","public","return","static","super","switch","this","throw", 062 "true","try","typeof","var","void","while","with","undefined","yield" 063 ); 064 // @formatter:on 065 066 // Characters that represent attribute name characters that don't trigger quoting. 067 // These are actually more strict than the actual Javascript specification, but 068 // can be narrowed in the future if necessary. 069 // For example, we quote attributes that start with $ even though we don't need to. 070 private static final AsciiSet validAttrChars = AsciiSet.create().ranges("a-z", "A-Z", "0-9").chars("_").build(); 071 072 private static final AsciiSet validFirstAttrChars = AsciiSet.create().ranges("a-z", "A-Z").chars("_").build(); 073 private final boolean simpleAttrs; 074 private final boolean escapeSolidus; 075 076 private final AsciiSet ec; 077 078 /** 079 * Constructor. 080 * 081 * @param out The writer being wrapped. 082 * @param useWhitespace If <jk>true</jk>, tabs and spaces will be used in output. 083 * @param maxIndent The maximum indentation level. 084 * @param escapeSolidus If <jk>true</jk>, forward slashes should be escaped in the output. 085 * @param quoteChar The quote character to use (i.e. <js>'\''</js> or <js>'"'</js>) 086 * @param simpleAttrs If <jk>true</jk>, JSON attributes will only be quoted when necessary. 087 * @param trimStrings If <jk>true</jk>, strings will be trimmed before being serialized. 088 * @param uriResolver The URI resolver for resolving URIs to absolute or root-relative form. 089 */ 090 protected JsonWriter(Writer out, boolean useWhitespace, int maxIndent, boolean escapeSolidus, char quoteChar, boolean simpleAttrs, boolean trimStrings, UriResolver uriResolver) { 091 super(out, useWhitespace, maxIndent, trimStrings, quoteChar, uriResolver); 092 this.simpleAttrs = simpleAttrs; 093 this.escapeSolidus = escapeSolidus; 094 this.ec = escapeSolidus ? encodedChars2 : encodedChars; 095 } 096 097 /** 098 * Creates a new JsonWriter instance. 099 * <p> 100 * Provided for subclasses in other packages (e.g. Json5SerializerSession) that need to create JsonWriter instances. 101 * 102 * @param out The writer being wrapped. 103 * @param useWhitespace If <jk>true</jk>, tabs and spaces will be used in output. 104 * @param maxIndent The maximum indentation level. 105 * @param escapeSolidus If <jk>true</jk>, forward slashes should be escaped in the output. 106 * @param quoteChar The quote character to use (i.e. <js>'\''</js> or <js>'"'</js>) 107 * @param simpleAttrs If <jk>true</jk>, JSON attributes will only be quoted when necessary. 108 * @param trimStrings If <jk>true</jk>, strings will be trimmed before being serialized. 109 * @param uriResolver The URI resolver for resolving URIs to absolute or root-relative form. 110 * @return A new JsonWriter instance. 111 */ 112 public static BasicJsonWriter create(Writer out, boolean useWhitespace, int maxIndent, boolean escapeSolidus, char quoteChar, boolean simpleAttrs, boolean trimStrings, UriResolver uriResolver) { 113 return new BasicJsonWriter(out, useWhitespace, maxIndent, escapeSolidus, quoteChar, simpleAttrs, trimStrings, uriResolver); 114 } 115 116 /** 117 * Serializes the specified object as a JSON attribute name. 118 * 119 * @param s The object being serialized. 120 * <br>Can be <jk>null</jk> (emitted as the literal <js>"null"</js>). 121 * @return This object. 122 */ 123 public SELF attr(String s) { 124 125 if (trimStrings) 126 s = trim(s); 127 128 /* 129 * Converts a Java string to an acceptable JSON attribute name. If 130 * simpleAttrs is true, then quotes will only be used if the attribute 131 * name consists of only alphanumeric characters. 132 */ 133 boolean doConvert = ! simpleAttrs; // Always convert when not in lax mode. 134 135 if (s == null) { 136 s = "null"; 137 doConvert = false; 138 139 } else { 140 141 // Look for characters that would require the attribute to be quoted. 142 // All possible numbers should be caught here. 143 if (! doConvert) { 144 for (var i = 0; i < s.length() && ! doConvert; i++) { 145 var c = s.charAt(i); 146 doConvert |= ! (i == 0 ? validFirstAttrChars.contains(c) : validAttrChars.contains(c)); 147 } 148 } 149 150 // Reserved words and blanks must be quoted. 151 if (! doConvert && (s.isEmpty() || reservedWords.contains(s))) 152 doConvert = true; 153 } 154 155 // If no conversion necessary, just print the attribute as-is. 156 if (doConvert) 157 stringValue(s); 158 else 159 w(s); 160 161 return self(); 162 } 163 164 /** 165 * Adds a space only if the current indentation level is below maxIndent. 166 * 167 * @param indent The number of spaces to indent. 168 * @return This object. 169 */ 170 public SELF s(int indent) { 171 if (indent <= maxIndent) 172 super.s(); 173 return self(); 174 } 175 176 /** 177 * Performs an indentation only if we're currently past max indentation. 178 * 179 * @param depth The current indentation depth. 180 * @return This object. 181 */ 182 public SELF smi(int depth) { 183 if (depth > maxIndent) 184 super.s(); 185 return self(); 186 } 187 188 /** 189 * Serializes the specified object as a JSON string value. 190 * 191 * @param s The object being serialized. 192 * <br>Can be <jk>null</jk> (nothing is written). 193 * @return This object. 194 */ 195 public SELF stringValue(String s) { 196 if (s == null) 197 return self(); 198 boolean doConvert = false; 199 for (var i = 0; i < s.length() && ! doConvert; i++) { 200 var c = s.charAt(i); 201 doConvert |= ec.contains(c); 202 } 203 q(); 204 if (! doConvert) { 205 w(s); 206 } else { 207 for (var i = 0; i < s.length(); i++) { 208 var c = s.charAt(i); 209 if (ec.contains(c)) { 210 if (c == '\n') 211 w('\\').w('n'); 212 else if (c == '\t') 213 w('\\').w('t'); 214 else if (c == '\b') 215 w('\\').w('b'); 216 else if (c == '\f') 217 w('\\').w('f'); 218 else if (c == quoteChar) 219 w('\\').w(quoteChar); 220 else if (c == '\\') 221 w('\\').w('\\'); 222 else if (c == '/' && escapeSolidus) 223 w('\\').w('/'); 224 else if (c == '\r') 225 w('\\').w('r'); 226 else if (c < 0x20 || c == 0x7f) 227 w('\\').w('u').w('0').w('0').w(HEX[c >> 4]).w(HEX[c & 0xf]); 228 else 229 w(c); 230 } else { 231 w(c); 232 } 233 } 234 } 235 q(); 236 return self(); 237 } 238 239 /** 240 * Appends a URI to the output. 241 * 242 * @param uri The URI to append to the output. 243 * @return This object. 244 */ 245 public SELF uriValue(Object uri) { 246 return stringValue(uriResolver.resolve(uri)); 247 } 248}