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.Shorts.*; 020import static org.apache.juneau.commons.utils.SystemUtils.*; 021 022import java.io.*; 023import java.util.*; 024import java.util.concurrent.*; 025import java.util.concurrent.atomic.*; 026 027import org.apache.juneau.commons.bean.*; 028import org.apache.juneau.commons.collections.*; 029import org.apache.juneau.marshall.*; 030import org.apache.juneau.marshall.json5.*; 031import org.apache.juneau.marshall.serializer.*; 032import org.apache.juneau.marshall.stream.*; 033import org.apache.juneau.marshall.swap.spi.*; 034 035/** 036 * Serializes POJO models to RFC 8259 JSON. 037 * 038 * <p> 039 * Produces strict JSON with double-quoted strings by default. For JSON5-style output (single quotes, 040 * etc.), use {@link Json5Serializer}. 041 * 042 * <h5 class='topic'>Media types</h5> 043 * <p> 044 * Handles <c>Accept</c> types: <bc>application/json, text/json</bc> 045 * <p> 046 * Produces <c>Content-Type</c> types: <bc>application/json</bc> 047 * 048 * <h5 class='topic'>Description</h5> 049 * <p> 050 * The conversion is as follows... 051 * <ul class='spaced-list'> 052 * <li> 053 * Maps (e.g. {@link HashMap HashMaps}, {@link TreeMap TreeMaps}) are converted to JSON objects. 054 * <li> 055 * Collections (e.g. {@link HashSet HashSets}, {@link LinkedList LinkedLists}) and Java arrays are converted to 056 * JSON arrays. 057 * <li> 058 * {@link String Strings} are converted to JSON strings. 059 * <li> 060 * {@link Number Numbers} (e.g. {@link Integer}, {@link Long}, {@link Double}) are converted to JSON numbers. 061 * <li> 062 * {@link Boolean Booleans} are converted to JSON booleans. 063 * <li> 064 * {@code nulls} are converted to JSON nulls. 065 * <li> 066 * {@code arrays} are converted to JSON arrays. 067 * <li> 068 * {@code beans} are converted to JSON objects. 069 * </ul> 070 * 071 * <p> 072 * The types above are considered "JSON-primitive" object types. 073 * Any non-JSON-primitive object types are transformed into JSON-primitive object types through 074 * {@link ObjectSwap ObjectSwaps} associated through the 075 * {@link org.apache.juneau.marshall.MarshallingContext.Builder#swaps(Class...) MarshallingContext.Builder#swaps(Class...)} method. 076 * Several default transforms are provided for transforming Dates, Enums, Iterators, etc... 077 * 078 * <p> 079 * This serializer provides several serialization options. 080 * Typically, one of the predefined DEFAULT serializers will be sufficient. 081 * However, custom serializers can be constructed to fine-tune behavior. 082 * 083 * <h5 class='topic'>Behavior-specific subclasses</h5> 084 * <p> 085 * The following direct subclasses are provided for convenience: 086 * <ul class='spaced-list'> 087 * <li> 088 * {@link Json5Serializer} - Default serializer, single quotes, simple mode. 089 * <li> 090 * {@link org.apache.juneau.marshall.json5.Json5Serializer.Readable Json5Serializer.Readable} - Default serializer, single quotes, simple mode, with whitespace. 091 * </ul> 092 * 093 * <h5 class='section'>Example:</h5> 094 * <p class='bjava'> 095 * <jc>// Use one of the default serializers to serialize a POJO</jc> 096 * String <jv>json</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.write(<jv>someObject</jv>); 097 * 098 * <jc>// Create a custom serializer for lax syntax using single quote characters</jc> 099 * JsonSerializer <jv>serializer</jv> = Json5Serializer.<jsm>create</jsm>().build(); 100 * 101 * <jc>// Clone an existing serializer and modify it to use single-quotes</jc> 102 * <jv>serializer</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.copy().sq().build(); 103 * 104 * <jc>// Serialize a POJO to JSON</jc> 105 * String <jv>json</jv> = <jv>serializer</jv>.write(<jv>someObject</jv>); 106 * </p> 107 * 108 * <h5 class='figure'>Example output (Map of name/age):</h5> 109 * <p class='bjson'> 110 * {<js>"name"</js>:<js>"Alice"</js>,<js>"age"</js>:30} 111 * </p> 112 * 113 * <h5 class='figure'>Complex (nested object + array):</h5> 114 * <p class='bjson'> 115 * {<js>"name"</js>:<js>"Alice"</js>,<js>"age"</js>:30,<js>"address"</js>:{<js>"street"</js>:<js>"123 Main St"</js>,<js>"city"</js>:<js>"Boston"</js>,<js>"state"</js>:<js>"MA"</js>},<js>"tags"</js>:[<js>"a"</js>,<js>"b"</js>,<js>"c"</js>]} 116 * </p> 117 * 118 * <h5 class='section'>Notes:</h5><ul> 119 * <li class='note'>This class is thread safe and reusable. 120 * </ul> 121 * 122 * <h5 class='section'>See Also:</h5><ul> 123 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JsonSupport">JSON</a> 124 * <li class='link'>{@link Json5Serializer} - For JSON5-style output (single quotes, etc.) 125 * </ul> 126 */ 127@SuppressWarnings({ 128 "java:S110", // Inheritance depth acceptable for this class hierarchy 129 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 130 "java:S9149", // Per-format static factories intentionally shadow the parent's. 131 "resource" // writeTokens(...) returns a Closeable owned by the caller; Eclipse JDT @Owning warning is by design. 132}) 133public class JsonSerializer extends WriterSerializer implements JsonMetaProvider, TokenWritable, ArrayRecordWritable { 134 135 /** 136 * Builder class. 137 */ 138 @SuppressWarnings({ 139 "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name. 140 }) 141 public abstract static class Builder<SELF extends Builder<SELF>> extends WriterSerializer.Builder<SELF> { 142 143 private static final Cache<HashKey,JsonSerializer> CACHE = Cache.of(HashKey.class, JsonSerializer.class).build(); 144 145 private boolean addBeanTypesJson; 146 private boolean escapeSolidus; 147 148 /** 149 * Constructor, default settings. 150 */ 151 protected Builder() { 152 produces("application/json"); 153 accept("application/json,text/json"); 154 addBeanTypesJson = env("JsonSerializer.addBeanTypes", false); 155 escapeSolidus = env("JsonSerializer.escapeSolidus", false); 156 } 157 158 /** 159 * Copy constructor. 160 * 161 * @param copyFrom The builder to copy from. 162 * <br>Cannot be <jk>null</jk>. 163 */ 164 protected Builder(Builder<?> copyFrom) { 165 super(reqnn("copyFrom", copyFrom)); 166 addBeanTypesJson = copyFrom.addBeanTypesJson; 167 escapeSolidus = copyFrom.escapeSolidus; 168 } 169 170 /** 171 * Copy constructor. 172 * 173 * @param copyFrom The bean to copy from. 174 * <br>Cannot be <jk>null</jk>. 175 */ 176 protected Builder(JsonSerializer copyFrom) { 177 super(reqnn("copyFrom", copyFrom)); 178 addBeanTypesJson = copyFrom.addBeanTypesJson; 179 escapeSolidus = copyFrom.escapeSolidus; 180 } 181 182 /** 183 * Add <js>"_type"</js> properties when needed. 184 * 185 * <p> 186 * If <jk>true</jk>, then <js>"_type"</js> properties will be added to beans if their type cannot be inferred 187 * through reflection. 188 * 189 * <p> 190 * When present, this value overrides the {@link org.apache.juneau.marshall.serializer.Serializer.Builder#addBeanTypes() Serializer.Builder#addBeanTypes()} setting and is 191 * provided to customize the behavior of specific serializers in a {@link SerializerSet}. 192 * 193 * <p> 194 * <b>Token streams:</b> the cursor's raw structural methods do not emit <c>"_type"</c>; 195 * however this setting IS honored by 196 * {@link TokenWriter#object(Object) TokenWriter.object(Object)} 197 * when walking a bean. 198 * 199 * @return This object. 200 */ 201 public SELF addBeanTypesJson() { 202 return addBeanTypesJson(true); 203 } 204 205 /** 206 * Same as {@link #addBeanTypesJson()} but allows you to explicitly specify the value. 207 * 208 * @param value The value for this setting. 209 * @return This object. 210 */ 211 public SELF addBeanTypesJson(boolean value) { 212 addBeanTypesJson = value; 213 return self(); 214 } 215 216 @Override /* Overridden from Context.Builder<?> */ 217 public JsonSerializer build() { 218 return cache(CACHE).build(JsonSerializer.class); 219 } 220 221 @Override /* Overridden from Context.Builder<?> */ 222 public abstract SELF copy(); 223 224 /** 225 * Prefix solidus <js>'/'</js> characters with escapes. 226 * 227 * <p> 228 * If enabled, solidus (e.g. slash) characters should be escaped. 229 * 230 * <p> 231 * The JSON specification allows for either format. 232 * <br>However, if you're embedding JSON in an HTML script tag, this setting prevents confusion when trying to serialize 233 * <xt><\/script></xt>. 234 * 235 * <h5 class='section'>Example:</h5> 236 * <p class='bjava'> 237 * <jc>// Create a JSON serializer that escapes solidus characters.</jc> 238 * WriterSerializer <jv>serializer</jv> = Json5Serializer 239 * .<jsm>create</jsm>() 240 * .escapeSolidus() 241 * .build(); 242 * 243 * <jc>// Produces: "{foo:'<\/bar>'"</jc> 244 * String <jv>json</jv> = <jv>serializer</jv>.write(JsonMap.<jsm>of</jsm>(<js>"foo"</js>, <js>"</bar>"</js>); 245 * </p> 246 * 247 * @return This object. 248 */ 249 public SELF escapeSolidus() { 250 return escapeSolidus(true); 251 } 252 253 /** 254 * Same as {@link #escapeSolidus()} but allows you to explicitly specify the value. 255 * 256 * @param value The value for this setting. 257 * @return This object. 258 */ 259 public SELF escapeSolidus(boolean value) { 260 escapeSolidus = value; 261 return self(); 262 } 263 264 @Override /* Overridden from Context.Builder<?> */ 265 public HashKey hashKey() { 266 // @formatter:off 267 return HashKey.of( 268 super.hashKey(), 269 addBeanTypesJson, 270 escapeSolidus 271 ); 272 // @formatter:on 273 } 274 275 276 } 277 278 /** 279 * Concrete default builder leaf for the non-subclassed {@link JsonSerializer#create()} / {@link JsonSerializer#copy()} path. 280 */ 281 public static final class DefaultBuilder extends Builder<DefaultBuilder> { 282 283 DefaultBuilder() {} 284 285 DefaultBuilder(JsonSerializer copyFrom) { 286 super(copyFrom); 287 } 288 289 DefaultBuilder(Builder<?> copyFrom) { 290 super(copyFrom); 291 } 292 293 @Override /* Overridden from Context.Builder<?> */ 294 public DefaultBuilder copy() { 295 return new DefaultBuilder(this); 296 } 297 } 298 299 /** Default serializer, with whitespace. */ 300 public static class Readable extends JsonSerializer { 301 302 /** 303 * Constructor. 304 * 305 * @param builder The builder for this object. 306 */ 307 public Readable(Builder<?> builder) { 308 super(builder.useWhitespace()); 309 } 310 } 311 312 /** Default serializer, all default settings.*/ 313 public static final JsonSerializer DEFAULT = new JsonSerializer(create()); 314 315 /** Default serializer, all default settings.*/ 316 public static final JsonSerializer DEFAULT_READABLE = new Readable(create()); 317 318 /** 319 * Creates a new builder for this object. 320 * 321 * @return A new builder. 322 */ 323 @SuppressWarnings({ 324 "java:S1452" // Builder<?> wildcard return intentional; callers chain via fluent API without needing the concrete type 325 }) 326 public static Builder<?> create() { 327 return new DefaultBuilder(); 328 } 329 330 protected final boolean addBeanTypesJson; 331 protected final boolean escapeSolidus; 332 333 private final boolean addBeanTypes2; 334 private final Map<BeanPropertyMeta,JsonBeanPropertyMeta> jsonBeanPropertyMetas = new ConcurrentHashMap<>(); 335 private final Map<ClassMeta<?>,JsonClassMeta> jsonClassMetas = new ConcurrentHashMap<>(); 336 337 private final AtomicReference<JsonSchemaSerializer> schemaSerializer = new AtomicReference<>(); 338 339 /** 340 * Constructor. 341 * 342 * @param builder The builder for this object. 343 */ 344 public JsonSerializer(Builder<?> builder) { 345 super(builder); 346 addBeanTypesJson = builder.addBeanTypesJson; 347 escapeSolidus = builder.escapeSolidus; 348 349 addBeanTypes2 = addBeanTypesJson || super.isAddBeanTypes(); 350 } 351 352 @Override /* Overridden from Context */ 353 public Builder<?> copy() { 354 return new DefaultBuilder(this); 355 } 356 357 @Override /* Overridden from Context */ 358 public JsonSerializerSession.Builder<?> createSession() { 359 return JsonSerializerSession.create(this); 360 } 361 362 @Override /* Overridden from JsonMetaProvider */ 363 public JsonBeanPropertyMeta getJsonBeanPropertyMeta(BeanPropertyMeta bpm) { 364 if (bpm == null) 365 return JsonBeanPropertyMeta.DEFAULT; 366 return jsonBeanPropertyMetas.computeIfAbsent(bpm, k -> new JsonBeanPropertyMeta(k.getDelegateFor(), this)); 367 } 368 369 @Override /* Overridden from JsonMetaProvider */ 370 public JsonClassMeta getJsonClassMeta(ClassMeta<?> cm) { 371 return jsonClassMetas.computeIfAbsent(cm, k -> new JsonClassMeta(k, this)); 372 } 373 374 /** 375 * Returns the schema serializer based on the settings of this serializer. 376 * 377 * <p> 378 * Note that this method creates a builder initialized to all default settings, whereas {@link #copy()} copies 379 * the settings of the object called on. 380 * 381 * @return The schema serializer. 382 */ 383 public JsonSchemaSerializer getSchemaSerializer() { 384 JsonSchemaSerializer result = schemaSerializer.get(); 385 if (result == null) { 386 result = JsonSchemaSerializer.create().marshallingContext(getMarshallingContext()).build(); 387 if (! schemaSerializer.compareAndSet(null, result)) { 388 result = schemaSerializer.get(); 389 } 390 } 391 return result; 392 } 393 394 @Override /* Overridden from Context */ 395 public JsonSerializerSession getSession() { return createSession().build(); } 396 397 /** 398 * Convenience delegator that opens a {@link JsonTokenWriter} over the output using 399 * <b>default session arguments</b> (mirrors {@link #write(Object, Object)}). 400 * 401 * <p> 402 * The real implementation lives on {@link JsonSerializerSession#writeTokens(Object)}. 403 * Callers that need request-derived configuration should call {@link #createSession()} and 404 * invoke {@link JsonSerializerSession#writeTokens(Object)} on the built session instead. 405 * 406 * @param output The output. Accepts {@link Writer}, {@link OutputStream}, {@link File}, or 407 * {@link StringBuilder}. 408 * @return A new {@link JsonTokenWriter}. 409 * @throws IOException If the output type is not supported or could not be opened. 410 */ 411 @Override /* TokenWritable */ 412 public TokenWriter writeTokens(Object output) throws IOException { 413 return getSession().writeTokens(output); 414 } 415 416 /** 417 * Add <js>"_type"</js> properties when needed. 418 * 419 * @see Builder#addBeanTypesJson() 420 * @return 421 * <jk>true</jk> if <js>"_type"</js> properties will be added to beans if their type cannot be inferred 422 * through reflection. 423 */ 424 @Override 425 protected final boolean isAddBeanTypes() { return addBeanTypes2; } 426 427 /** 428 * Prefix solidus <js>'/'</js> characters with escapes. 429 * 430 * @see Builder#escapeSolidus() 431 * @return 432 * <jk>true</jk> if solidus (e.g. slash) characters should be escaped. 433 */ 434 protected final boolean isEscapeSolidus() { return escapeSolidus; } 435 436 @Override /* Overridden from WriterSerializer */ 437 protected FluentMap<String,Object> properties() { 438 return super.properties() 439 .a("addBeanTypesJson", addBeanTypesJson) 440 .a("escapeSolidus", escapeSolidus); 441 } 442 443 /** 444 * Convenience delegator for the streaming array-element {@link RecordWriter} (uses default 445 * session args; see {@link #writeTokens(Object)}). Real impl on 446 * {@link JsonSerializerSession#writeArrayRecords(Object)}. 447 * 448 * @param output The output. 449 * @return A new element-streamed {@link RecordWriter}. 450 * @throws IOException If a problem occurred opening the underlying output. 451 */ 452 @Override /* ArrayRecordWritable */ 453 public RecordWriter writeArrayRecords(Object output) throws IOException { 454 return getSession().writeArrayRecords(output); 455 } 456}