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.util.*; 016import java.util.concurrent.*; 017 018import org.apache.juneau.*; 019import org.apache.juneau.annotation.*; 020import org.apache.juneau.collections.*; 021import org.apache.juneau.serializer.*; 022 023/** 024 * Serializes POJO models to JSON. 025 * 026 * <h5 class='topic'>Media types</h5> 027 * 028 * Handles <c>Accept</c> types: <bc>application/json, text/json</bc> 029 * <p> 030 * Produces <c>Content-Type</c> types: <bc>application/json</bc> 031 * 032 * <h5 class='topic'>Description</h5> 033 * 034 * The conversion is as follows... 035 * <ul class='spaced-list'> 036 * <li> 037 * Maps (e.g. {@link HashMap HashMaps}, {@link TreeMap TreeMaps}) are converted to JSON objects. 038 * <li> 039 * Collections (e.g. {@link HashSet HashSets}, {@link LinkedList LinkedLists}) and Java arrays are converted to 040 * JSON arrays. 041 * <li> 042 * {@link String Strings} are converted to JSON strings. 043 * <li> 044 * {@link Number Numbers} (e.g. {@link Integer}, {@link Long}, {@link Double}) are converted to JSON numbers. 045 * <li> 046 * {@link Boolean Booleans} are converted to JSON booleans. 047 * <li> 048 * {@code nulls} are converted to JSON nulls. 049 * <li> 050 * {@code arrays} are converted to JSON arrays. 051 * <li> 052 * {@code beans} are converted to JSON objects. 053 * </ul> 054 * 055 * <p> 056 * The types above are considered "JSON-primitive" object types. 057 * Any non-JSON-primitive object types are transformed into JSON-primitive object types through 058 * {@link org.apache.juneau.transform.PojoSwap PojoSwaps} associated through the 059 * {@link BeanContextBuilder#swaps(Object...)} method. 060 * Several default transforms are provided for transforming Dates, Enums, Iterators, etc... 061 * 062 * <p> 063 * This serializer provides several serialization options. 064 * Typically, one of the predefined DEFAULT serializers will be sufficient. 065 * However, custom serializers can be constructed to fine-tune behavior. 066 * 067 * <h5 class='topic'>Behavior-specific subclasses</h5> 068 * 069 * The following direct subclasses are provided for convenience: 070 * <ul class='spaced-list'> 071 * <li> 072 * {@link SimpleJsonSerializer} - Default serializer, single quotes, simple mode. 073 * <li> 074 * {@link SimpleJsonSerializer.Readable} - Default serializer, single quotes, simple mode, with whitespace. 075 * </ul> 076 * 077 * <h5 class='section'>Example:</h5> 078 * <p class='bcode w800'> 079 * <jc>// Use one of the default serializers to serialize a POJO</jc> 080 * String json = JsonSerializer.<jsf>DEFAULT</jsf>.serialize(someObject); 081 * 082 * <jc>// Create a custom serializer for lax syntax using single quote characters</jc> 083 * JsonSerializer serializer = JsonSerializer.<jsm>create</jsm>().simple().sq().build(); 084 * 085 * <jc>// Clone an existing serializer and modify it to use single-quotes</jc> 086 * JsonSerializer serializer = JsonSerializer.<jsf>DEFAULT</jsf>.builder().sq().build(); 087 * 088 * <jc>// Serialize a POJO to JSON</jc> 089 * String json = serializer.serialize(someObject); 090 * </p> 091 */ 092@ConfigurableContext 093public class JsonSerializer extends WriterSerializer implements JsonMetaProvider, JsonCommon { 094 095 //------------------------------------------------------------------------------------------------------------------- 096 // Configurable properties 097 //------------------------------------------------------------------------------------------------------------------- 098 099 static final String PREFIX = "JsonSerializer"; 100 101 /** 102 * Configuration property: Add <js>"_type"</js> properties when needed. 103 * 104 * <h5 class='section'>Property:</h5> 105 * <ul class='spaced-list'> 106 * <li><b>ID:</b> {@link org.apache.juneau.json.JsonSerializer#JSON_addBeanTypes JSON_addBeanTypes} 107 * <li><b>Name:</b> <js>"JsonSerializer.addBeanTypes.b"</js> 108 * <li><b>Data type:</b> <jk>boolean</jk> 109 * <li><b>System property:</b> <c>JsonSerializer.addBeanTypes</c> 110 * <li><b>Environment variable:</b> <c>JSONSERIALIZER_ADDBEANTYPES</c> 111 * <li><b>Default:</b> <jk>false</jk> 112 * <li><b>Session property:</b> <jk>false</jk> 113 * <li><b>Annotations:</b> 114 * <ul> 115 * <li class='ja'>{@link org.apache.juneau.json.annotation.JsonConfig#addBeanTypes()} 116 * </ul> 117 * <li><b>Methods:</b> 118 * <ul> 119 * <li class='jm'>{@link org.apache.juneau.json.JsonSerializerBuilder#addBeanTypes()} 120 * </ul> 121 * </ul> 122 * 123 * <h5 class='section'>Description:</h5> 124 * <p> 125 * If <jk>true</jk>, then <js>"_type"</js> properties will be added to beans if their type cannot be inferred 126 * through reflection. 127 * 128 * <p> 129 * When present, this value overrides the {@link #SERIALIZER_addBeanTypes} setting and is 130 * provided to customize the behavior of specific serializers in a {@link SerializerGroup}. 131 */ 132 public static final String JSON_addBeanTypes = PREFIX + ".addBeanTypes.b"; 133 134 /** 135 * Configuration property: Prefix solidus <js>'/'</js> characters with escapes. 136 * 137 * <h5 class='section'>Property:</h5> 138 * <ul class='spaced-list'> 139 * <li><b>ID:</b> {@link org.apache.juneau.json.JsonSerializer#JSON_escapeSolidus JSON_escapeSolidus} 140 * <li><b>Name:</b> <js>"JsonSerializer.escapeSolidus.b"</js> 141 * <li><b>Data type:</b> <jk>boolean</jk> 142 * <li><b>System property:</b> <c>JsonSerializer.escapeSolidus</c> 143 * <li><b>Environment variable:</b> <c>JSONSERIALIZER_ESCAPESOLIDUS</c> 144 * <li><b>Default:</b> <jk>false</jk> 145 * <li><b>Session property:</b> <jk>false</jk> 146 * <li><b>Annotations:</b> 147 * <ul> 148 * <li class='ja'>{@link org.apache.juneau.json.annotation.JsonConfig#escapeSolidus()} 149 * </ul> 150 * <li><b>Methods:</b> 151 * <ul> 152 * <li class='jm'>{@link org.apache.juneau.json.JsonSerializerBuilder#escapeSolidus()} 153 * </ul> 154 * </ul> 155 * 156 * <h5 class='section'>Description:</h5> 157 * <p> 158 * If <jk>true</jk>, solidus (e.g. slash) characters should be escaped. 159 * The JSON specification allows for either format. 160 * <br>However, if you're embedding JSON in an HTML script tag, this setting prevents confusion when trying to serialize 161 * <xt><\/script></xt>. 162 * 163 * <h5 class='section'>Example:</h5> 164 * <p class='bcode w800'> 165 * <jc>// Create a JSON serializer that escapes solidus characters.</jc> 166 * WriterSerializer s = JsonSerializer 167 * .<jsm>create</jsm>() 168 * .simple() 169 * .escapeSolidus() 170 * .build(); 171 * 172 * <jc>// Same, but use property.</jc> 173 * WriterSerializer s = JsonSerializer 174 * .<jsm>create</jsm>() 175 * .simple() 176 * .set(<jsf>JSON_escapeSolidus</jsf>, <jk>true</jk>) 177 * .build(); 178 * 179 * <jc>// Produces: "{foo:'<\/bar>'"</jc> 180 * String json = s.serialize(OMap.<jsm>of</jsm>(<js>"foo"</js>, <js>"</bar>"</js>); 181 * </p> 182 */ 183 public static final String JSON_escapeSolidus = PREFIX + ".escapeSolidus.b"; 184 185 /** 186 * Configuration property: Simple JSON mode. 187 * 188 * <h5 class='section'>Property:</h5> 189 * <ul class='spaced-list'> 190 * <li><b>ID:</b> {@link org.apache.juneau.json.JsonSerializer#JSON_simpleMode JSON_simpleMode} 191 * <li><b>Name:</b> <js>"JsonSerializer.simpleMode.b"</js> 192 * <li><b>Data type:</b> <jk>boolean</jk> 193 * <li><b>System property:</b> <c>JsonSerializer.simpleMode</c> 194 * <li><b>Environment variable:</b> <c>JSONSERIALIZER_SIMPLEMODE</c> 195 * <li><b>Default:</b> <jk>false</jk> 196 * <li><b>Session property:</b> <jk>false</jk> 197 * <li><b>Annotations:</b> 198 * <ul> 199 * <li class='ja'>{@link org.apache.juneau.json.annotation.JsonConfig#simpleMode()} 200 * </ul> 201 * <li><b>Methods:</b> 202 * <ul> 203 * <li class='jm'>{@link org.apache.juneau.json.JsonSerializerBuilder#simple()} 204 * <li class='jm'>{@link org.apache.juneau.json.JsonSerializerBuilder#ssq()} 205 * </ul> 206 * </ul> 207 * 208 * <h5 class='section'>Description:</h5> 209 * <p> 210 * If <jk>true</jk>, JSON attribute names will only be quoted when necessary. 211 * <br>Otherwise, they are always quoted. 212 * 213 * <p> 214 * Attributes do not need to be quoted when they conform to the following: 215 * <ol class='spaced-list'> 216 * <li>They start with an ASCII character or <js>'_'</js>. 217 * <li>They contain only ASCII characters or numbers or <js>'_'</js>. 218 * <li>They are not one of the following reserved words: 219 * <p class='bcode w800'> 220 * arguments, break, case, catch, class, const, continue, debugger, default, 221 * delete, do, else, enum, eval, export, extends, false, finally, for, function, 222 * if, implements, import, in, instanceof, interface, let, new, null, package, 223 * private, protected, public, return, static, super, switch, this, throw, 224 * true, try, typeof, var, void, while, with, undefined, yield 225 * </p> 226 * </ol> 227 * 228 * <h5 class='section'>Example:</h5> 229 * <p class='bcode w800'> 230 * <jc>// Create a JSON serializer in normal mode.</jc> 231 * WriterSerializer s1 = JsonSerializer 232 * .<jsm>create</jsm>() 233 * .build(); 234 * 235 * <jc>// Create a JSON serializer in simple mode.</jc> 236 * WriterSerializer s2 = JsonSerializer 237 * .<jsm>create</jsm>() 238 * .simple() 239 * .build(); 240 * 241 * OMap m = OMap.<jsm>of</jsm>( 242 * <js>"foo"</js>, <js>"x1"</js>, 243 * <js>"_bar"</js>, <js>"x2"</js>, 244 * <js>" baz "</js>, <js>"x3"</js>, 245 * <js>"123"</js>, <js>"x4"</js>, 246 * <js>"return"</js>, <js>"x5"</js>, 247 * <js>""</js>, <js>"x6"</js> 248 * ); 249 * 250 * <jc>// Produces:</jc> 251 * <jc>// {</jc> 252 * <jc>// "foo": "x1"</jc> 253 * <jc>// "_bar": "x2"</jc> 254 * <jc>// " baz ": "x3"</jc> 255 * <jc>// "123": "x4"</jc> 256 * <jc>// "return": "x5"</jc> 257 * <jc>// "": "x6"</jc> 258 * <jc>// }</jc> 259 * String json1 = s1.serialize(m); 260 * 261 * <jc>// Produces:</jc> 262 * <jc>// {</jc> 263 * <jc>// foo: "x1"</jc> 264 * <jc>// _bar: "x2"</jc> 265 * <jc>// " baz ": "x3"</jc> 266 * <jc>// "123": "x4"</jc> 267 * <jc>// "return": "x5"</jc> 268 * <jc>// "": "x6"</jc> 269 * <jc>// }</jc> 270 * String json2 = s2.serialize(m); 271 * </p> 272 */ 273 public static final String JSON_simpleMode = PREFIX + ".simpleMode.b"; 274 275 //------------------------------------------------------------------------------------------------------------------- 276 // Predefined instances 277 //------------------------------------------------------------------------------------------------------------------- 278 279 /** Default serializer, all default settings.*/ 280 public static final JsonSerializer DEFAULT = new JsonSerializer(PropertyStore.DEFAULT); 281 282 /** Default serializer, all default settings.*/ 283 public static final JsonSerializer DEFAULT_READABLE = new Readable(PropertyStore.DEFAULT); 284 285 286 //------------------------------------------------------------------------------------------------------------------- 287 // Predefined subclasses 288 //------------------------------------------------------------------------------------------------------------------- 289 290 /** Default serializer, with whitespace. */ 291 public static class Readable extends JsonSerializer { 292 293 /** 294 * Constructor. 295 * 296 * @param ps The property store containing all the settings for this object. 297 */ 298 public Readable(PropertyStore ps) { 299 super( 300 ps.builder().setDefault(WSERIALIZER_useWhitespace, true).build() 301 ); 302 } 303 } 304 305 /** 306 * Default serializer, single quotes, simple mode, with whitespace and recursion detection. 307 * Note that recursion detection introduces a small performance penalty. 308 */ 309 public static class ReadableSafe extends JsonSerializer { 310 311 /** 312 * Constructor. 313 * 314 * @param ps The property store containing all the settings for this object. 315 */ 316 public ReadableSafe(PropertyStore ps) { 317 super( 318 ps.builder() 319 .setDefault(JSON_simpleMode, true) 320 .setDefault(WSERIALIZER_quoteChar, '\'') 321 .setDefault(WSERIALIZER_useWhitespace, true) 322 .setDefault(BEANTRAVERSE_detectRecursions, true) 323 .build() 324 ); 325 } 326 } 327 328 329 //------------------------------------------------------------------------------------------------------------------- 330 // Instance 331 //------------------------------------------------------------------------------------------------------------------- 332 333 private final boolean 334 simpleMode, 335 escapeSolidus, 336 addBeanTypes; 337 private final Map<ClassMeta<?>,JsonClassMeta> jsonClassMetas = new ConcurrentHashMap<>(); 338 private final Map<BeanPropertyMeta,JsonBeanPropertyMeta> jsonBeanPropertyMetas = new ConcurrentHashMap<>(); 339 340 private volatile JsonSchemaSerializer schemaSerializer; 341 342 /** 343 * Constructor. 344 * 345 * @param ps 346 * The property store containing all the settings for this object. 347 */ 348 public JsonSerializer(PropertyStore ps) { 349 this(ps, "application/json", "application/json,text/json"); 350 } 351 352 /** 353 * Constructor. 354 * 355 * @param ps 356 * The property store containing all the settings for this object. 357 * @param produces 358 * The media type that this serializer produces. 359 * @param accept 360 * The accept media types that the serializer can handle. 361 * <p> 362 * Can contain meta-characters per the <c>media-type</c> specification of {@doc ExtRFC2616.section14.1} 363 * <p> 364 * If empty, then assumes the only media type supported is <c>produces</c>. 365 * <p> 366 * For example, if this serializer produces <js>"application/json"</js> but should handle media types of 367 * <js>"application/json"</js> and <js>"text/json"</js>, then the arguments should be: 368 * <p class='bcode w800'> 369 * <jk>super</jk>(ps, <js>"application/json"</js>, <js>"application/json,text/json"</js>); 370 * </p> 371 * <br>...or... 372 * <p class='bcode w800'> 373 * <jk>super</jk>(ps, <js>"application/json"</js>, <js>"*​/json"</js>); 374 * </p> 375 * <p> 376 * The accept value can also contain q-values. 377 */ 378 public JsonSerializer(PropertyStore ps, String produces, String accept) { 379 super(ps, produces, accept); 380 381 simpleMode = getBooleanProperty(JSON_simpleMode, false); 382 escapeSolidus = getBooleanProperty(JSON_escapeSolidus, false); 383 addBeanTypes = getBooleanProperty(JSON_addBeanTypes, getBooleanProperty(SERIALIZER_addBeanTypes, false)); 384 } 385 386 @Override /* Context */ 387 public JsonSerializerBuilder builder() { 388 return new JsonSerializerBuilder(getPropertyStore()); 389 } 390 391 /** 392 * Instantiates a new clean-slate {@link JsonSerializerBuilder} object. 393 * 394 * <p> 395 * This is equivalent to simply calling <code><jk>new</jk> JsonSerializerBuilder()</code>. 396 * 397 * @return A new {@link JsonSerializerBuilder} object. 398 */ 399 public static JsonSerializerBuilder create() { 400 return new JsonSerializerBuilder(); 401 } 402 403 /** 404 * Returns the schema serializer based on the settings of this serializer. 405 * 406 * <p> 407 * Note that this method creates a builder initialized to all default settings, whereas {@link #builder()} copies 408 * the settings of the object called on. 409 * 410 * @return The schema serializer. 411 */ 412 public JsonSchemaSerializer getSchemaSerializer() { 413 if (schemaSerializer == null) 414 schemaSerializer = builder().build(JsonSchemaSerializer.class); 415 return schemaSerializer; 416 } 417 418 //----------------------------------------------------------------------------------------------------------------- 419 // Entry point methods 420 //----------------------------------------------------------------------------------------------------------------- 421 422 @Override /* Context */ 423 public JsonSerializerSession createSession() { 424 return createSession(createDefaultSessionArgs()); 425 } 426 427 @Override /* Serializer */ 428 public JsonSerializerSession createSession(SerializerSessionArgs args) { 429 return new JsonSerializerSession(this, args); 430 } 431 432 //----------------------------------------------------------------------------------------------------------------- 433 // Extended metadata 434 //----------------------------------------------------------------------------------------------------------------- 435 436 @Override /* JsonMetaProvider */ 437 public JsonClassMeta getJsonClassMeta(ClassMeta<?> cm) { 438 JsonClassMeta m = jsonClassMetas.get(cm); 439 if (m == null) { 440 m = new JsonClassMeta(cm, this); 441 jsonClassMetas.put(cm, m); 442 } 443 return m; 444 } 445 446 @Override /* JsonMetaProvider */ 447 public JsonBeanPropertyMeta getJsonBeanPropertyMeta(BeanPropertyMeta bpm) { 448 if (bpm == null) 449 return JsonBeanPropertyMeta.DEFAULT; 450 JsonBeanPropertyMeta m = jsonBeanPropertyMetas.get(bpm); 451 if (m == null) { 452 m = new JsonBeanPropertyMeta(bpm.getDelegateFor(), this); 453 jsonBeanPropertyMetas.put(bpm, m); 454 } 455 return m; 456 } 457 458 //----------------------------------------------------------------------------------------------------------------- 459 // Properties 460 //----------------------------------------------------------------------------------------------------------------- 461 462 /** 463 * Add <js>"_type"</js> properties when needed. 464 * 465 * @see #JSON_addBeanTypes 466 * @return 467 * <jk>true</jk> if <js>"_type"</js> properties will be added to beans if their type cannot be inferred 468 * through reflection. 469 */ 470 @Override 471 protected final boolean isAddBeanTypes() { 472 return addBeanTypes; 473 } 474 475 /** 476 * Prefix solidus <js>'/'</js> characters with escapes. 477 * 478 * @see #JSON_escapeSolidus 479 * @return 480 * <jk>true</jk> if solidus (e.g. slash) characters should be escaped. 481 */ 482 protected final boolean isEscapeSolidus() { 483 return escapeSolidus; 484 } 485 486 /** 487 * Simple JSON mode. 488 * 489 * @see #JSON_simpleMode 490 * @return 491 * <jk>true</jk> if JSON attribute names will only be quoted when necessary. 492 * <br>Otherwise, they are always quoted. 493 */ 494 protected final boolean isSimpleMode() { 495 return simpleMode; 496 } 497 498 //----------------------------------------------------------------------------------------------------------------- 499 // Other methods 500 //----------------------------------------------------------------------------------------------------------------- 501 502 @Override /* Context */ 503 public OMap toMap() { 504 return super.toMap() 505 .a("JsonSerializer", new DefaultFilteringOMap() 506 .a("simpleMode", simpleMode) 507 .a("escapeSolidus", escapeSolidus) 508 .a("addBeanTypes", addBeanTypes) 509 ); 510 } 511}