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.serializer.*; 021 022/** 023 * Serializes POJO models to JSON. 024 * 025 * <h5 class='topic'>Media types</h5> 026 * 027 * Handles <c>Accept</c> types: <bc>application/json, text/json</bc> 028 * <p> 029 * Produces <c>Content-Type</c> types: <bc>application/json</bc> 030 * 031 * <h5 class='topic'>Description</h5> 032 * 033 * The conversion is as follows... 034 * <ul class='spaced-list'> 035 * <li> 036 * Maps (e.g. {@link HashMap HashMaps}, {@link TreeMap TreeMaps}) are converted to JSON objects. 037 * <li> 038 * Collections (e.g. {@link HashSet HashSets}, {@link LinkedList LinkedLists}) and Java arrays are converted to 039 * JSON arrays. 040 * <li> 041 * {@link String Strings} are converted to JSON strings. 042 * <li> 043 * {@link Number Numbers} (e.g. {@link Integer}, {@link Long}, {@link Double}) are converted to JSON numbers. 044 * <li> 045 * {@link Boolean Booleans} are converted to JSON booleans. 046 * <li> 047 * {@code nulls} are converted to JSON nulls. 048 * <li> 049 * {@code arrays} are converted to JSON arrays. 050 * <li> 051 * {@code beans} are converted to JSON objects. 052 * </ul> 053 * 054 * <p> 055 * The types above are considered "JSON-primitive" object types. 056 * Any non-JSON-primitive object types are transformed into JSON-primitive object types through 057 * {@link org.apache.juneau.transform.PojoSwap PojoSwaps} associated through the 058 * {@link BeanContextBuilder#pojoSwaps(Class...)} method. 059 * Several default transforms are provided for transforming Dates, Enums, Iterators, etc... 060 * 061 * <p> 062 * This serializer provides several serialization options. 063 * Typically, one of the predefined DEFAULT serializers will be sufficient. 064 * However, custom serializers can be constructed to fine-tune behavior. 065 * 066 * <h5 class='topic'>Behavior-specific subclasses</h5> 067 * 068 * The following direct subclasses are provided for convenience: 069 * <ul class='spaced-list'> 070 * <li> 071 * {@link SimpleJsonSerializer} - Default serializer, single quotes, simple mode. 072 * <li> 073 * {@link SimpleJsonSerializer.Readable} - Default serializer, single quotes, simple mode, with whitespace. 074 * </ul> 075 * 076 * <h5 class='section'>Example:</h5> 077 * <p class='bcode w800'> 078 * <jc>// Use one of the default serializers to serialize a POJO</jc> 079 * String json = JsonSerializer.<jsf>DEFAULT</jsf>.serialize(someObject); 080 * 081 * <jc>// Create a custom serializer for lax syntax using single quote characters</jc> 082 * JsonSerializer serializer = JsonSerializer.<jsm>create</jsm>().simple().sq().build(); 083 * 084 * <jc>// Clone an existing serializer and modify it to use single-quotes</jc> 085 * JsonSerializer serializer = JsonSerializer.<jsf>DEFAULT</jsf>.builder().sq().build(); 086 * 087 * <jc>// Serialize a POJO to JSON</jc> 088 * String json = serializer.serialize(someObject); 089 * </p> 090 */ 091@ConfigurableContext 092public class JsonSerializer extends WriterSerializer implements JsonMetaProvider, JsonCommon { 093 094 //------------------------------------------------------------------------------------------------------------------- 095 // Configurable properties 096 //------------------------------------------------------------------------------------------------------------------- 097 098 static final String PREFIX = "JsonSerializer"; 099 100 /** 101 * Configuration property: Add <js>"_type"</js> properties when needed. 102 * 103 * <h5 class='section'>Property:</h5> 104 * <ul class='spaced-list'> 105 * <li><b>ID:</b> {@link org.apache.juneau.json.JsonSerializer#JSON_addBeanTypes JSON_addBeanTypes} 106 * <li><b>Name:</b> <js>"JsonSerializer.addBeanTypes.b"</js> 107 * <li><b>Data type:</b> <jk>boolean</jk> 108 * <li><b>System property:</b> <c>JsonSerializer.addBeanTypes</c> 109 * <li><b>Environment variable:</b> <c>JSONSERIALIZER_ADDBEANTYPES</c> 110 * <li><b>Default:</b> <jk>false</jk> 111 * <li><b>Session property:</b> <jk>false</jk> 112 * <li><b>Annotations:</b> 113 * <ul> 114 * <li class='ja'>{@link org.apache.juneau.json.annotation.JsonConfig#addBeanTypes()} 115 * </ul> 116 * <li><b>Methods:</b> 117 * <ul> 118 * <li class='jm'>{@link org.apache.juneau.json.JsonSerializerBuilder#addBeanTypes(boolean)} 119 * </ul> 120 * </ul> 121 * 122 * <h5 class='section'>Description:</h5> 123 * <p> 124 * If <jk>true</jk>, then <js>"_type"</js> properties will be added to beans if their type cannot be inferred 125 * through reflection. 126 * 127 * <p> 128 * When present, this value overrides the {@link #SERIALIZER_addBeanTypes} setting and is 129 * provided to customize the behavior of specific serializers in a {@link SerializerGroup}. 130 */ 131 public static final String JSON_addBeanTypes = PREFIX + ".addBeanTypes.b"; 132 133 /** 134 * Configuration property: Prefix solidus <js>'/'</js> characters with escapes. 135 * 136 * <h5 class='section'>Property:</h5> 137 * <ul class='spaced-list'> 138 * <li><b>ID:</b> {@link org.apache.juneau.json.JsonSerializer#JSON_escapeSolidus JSON_escapeSolidus} 139 * <li><b>Name:</b> <js>"JsonSerializer.escapeSolidus.b"</js> 140 * <li><b>Data type:</b> <jk>boolean</jk> 141 * <li><b>System property:</b> <c>JsonSerializer.escapeSolidus</c> 142 * <li><b>Environment variable:</b> <c>JSONSERIALIZER_ESCAPESOLIDUS</c> 143 * <li><b>Default:</b> <jk>false</jk> 144 * <li><b>Session property:</b> <jk>false</jk> 145 * <li><b>Annotations:</b> 146 * <ul> 147 * <li class='ja'>{@link org.apache.juneau.json.annotation.JsonConfig#escapeSolidus()} 148 * </ul> 149 * <li><b>Methods:</b> 150 * <ul> 151 * <li class='jm'>{@link org.apache.juneau.json.JsonSerializerBuilder#escapeSolidus(boolean)} 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(<jk>new</jk> ObjectMap().append(<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(boolean)} 204 * <li class='jm'>{@link org.apache.juneau.json.JsonSerializerBuilder#simple()} 205 * <li class='jm'>{@link org.apache.juneau.json.JsonSerializerBuilder#ssq()} 206 * </ul> 207 * </ul> 208 * 209 * <h5 class='section'>Description:</h5> 210 * <p> 211 * If <jk>true</jk>, JSON attribute names will only be quoted when necessary. 212 * <br>Otherwise, they are always quoted. 213 * 214 * <p> 215 * Attributes do not need to be quoted when they conform to the following: 216 * <ol class='spaced-list'> 217 * <li>They start with an ASCII character or <js>'_'</js>. 218 * <li>They contain only ASCII characters or numbers or <js>'_'</js>. 219 * <li>They are not one of the following reserved words: 220 * <p class='bcode w800'> 221 * arguments, break, case, catch, class, const, continue, debugger, default, 222 * delete, do, else, enum, eval, export, extends, false, finally, for, function, 223 * if, implements, import, in, instanceof, interface, let, new, null, package, 224 * private, protected, public, return, static, super, switch, this, throw, 225 * true, try, typeof, var, void, while, with, undefined, yield 226 * </p> 227 * </ol> 228 * 229 * <h5 class='section'>Example:</h5> 230 * <p class='bcode w800'> 231 * <jc>// Create a JSON serializer in normal mode.</jc> 232 * WriterSerializer s1 = JsonSerializer 233 * .<jsm>create</jsm>() 234 * .build(); 235 * 236 * <jc>// Create a JSON serializer in simple mode.</jc> 237 * WriterSerializer s2 = JsonSerializer 238 * .<jsm>create</jsm>() 239 * .simple() 240 * .build(); 241 * 242 * ObjectMap m = <jk>new</jk> ObjectMap() 243 * .append(<js>"foo"</js>, <js>"x1"</js>) 244 * .append(<js>"_bar"</js>, <js>"x2"</js>) 245 * .append(<js>" baz "</js>, <js>"x3"</js>) 246 * .append(<js>"123"</js>, <js>"x4"</js>) 247 * .append(<js>"return"</js>, <js>"x5"</js>); 248 * .append(<js>""</js>, <js>"x6"</js>); 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().set(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 .set(JSON_simpleMode, true) 320 .set(WSERIALIZER_quoteChar, '\'') 321 .set(WSERIALIZER_useWhitespace, true) 322 .set(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 RFC2616.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 * Configuration property: 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 * Configuration property: 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 * Configuration property: 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 ObjectMap toMap() { 504 return super.toMap() 505 .append("JsonSerializer", new DefaultFilteringObjectMap() 506 .append("simpleMode", simpleMode) 507 .append("escapeSolidus", escapeSolidus) 508 .append("addBeanTypes", addBeanTypes) 509 ); 510 } 511}