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.IoUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import java.io.*; 023import java.time.*; 024import java.time.temporal.*; 025import java.util.*; 026import java.util.function.*; 027 028import org.apache.juneau.commons.bean.*; 029import org.apache.juneau.commons.lang.*; 030import org.apache.juneau.marshall.*; 031import org.apache.juneau.marshall.serializer.*; 032import org.apache.juneau.marshall.stream.*; 033 034/** 035 * Session object that lives for the duration of a single use of {@link JsonSerializer}. 036 * 037 * <h5 class='section'>Notes:</h5><ul> 038 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 039 * </ul> 040 * 041 * <h5 class='section'>See Also:</h5><ul> 042 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JsonSupport">JSON</a> 043 * </ul> 044 */ 045@SuppressWarnings({ 046 "java:S110", // Inheritance depth acceptable for this class hierarchy 047 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 048 "java:S1452", // Public/protected API (create(), writeBeanMap(), writeAnything(), getJsonWriter()) returns Builder<?>/SerializerWriter<?>/JsonWriter<?>; callers only need the base type 049 "rawtypes", // Raw types necessary for generic collection/map serialization throughout this session 050 "resource", // Resource management handled externally 051 "unchecked" // Type erasure requires unchecked casts throughout this session 052}) 053public class JsonSerializerSession extends WriterSerializerSession implements TokenWritable, ArrayRecordWritable { 054 055 /** 056 * Builder class. 057 */ 058 @SuppressWarnings({ 059 "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name. 060 }) 061 public abstract static class Builder<SELF extends Builder<SELF>> extends WriterSerializerSession.Builder<SELF> { 062 063 private boolean escapeSolidus; 064 private final JsonSerializer ctx; 065 066 /** 067 * Constructor 068 * 069 * @param ctx The context creating this session. 070 * <br>Cannot be <jk>null</jk>. 071 */ 072 protected Builder(JsonSerializer ctx) { 073 super(reqnn("ctx", ctx)); 074 this.ctx = ctx; 075 escapeSolidus = ctx.isEscapeSolidus(); 076 } 077 078 @Override 079 public JsonSerializerSession build() { 080 return new JsonSerializerSession(this); 081 } 082 083 /** 084 * Prefix solidus characters with escapes. 085 * 086 * @param value The new value for this setting. 087 * @return This object. 088 */ 089 public SELF escapeSolidus(boolean value) { 090 escapeSolidus = value; 091 return self(); 092 } 093 094 @Override /* Overridden from Builder */ 095 public SELF property(String key, Object value) { 096 if (key == null) { super.property(key, value); return self(); } 097 switch (key) { 098 case "escapeSolidus", "JsonSerializerSession.escapeSolidus": 099 return escapeSolidus(cvt(value, Boolean.class)); 100 default: 101 super.property(key, value); 102 return self(); 103 } 104 } 105 106 } 107 108 /** 109 * Concrete default builder leaf for the non-subclassed {@code create()} path (CRTP terminal). 110 */ 111 public static final class DefaultBuilder extends Builder<DefaultBuilder> { 112 113 DefaultBuilder(JsonSerializer ctx) { 114 super(ctx); 115 } 116 } 117 118 /** 119 * Creates a new builder for this object. 120 * 121 * @param ctx The context creating this session. 122 * <br>Cannot be <jk>null</jk>. 123 * @return A new builder. 124 */ 125 public static Builder<?> create(JsonSerializer ctx) { 126 return new DefaultBuilder(reqnn("ctx", ctx)); 127 } 128 129 private final JsonSerializer ctx; 130 private final boolean escapeSolidus; 131 132 /** 133 * Constructor. 134 * 135 * @param builder The builder for this object. 136 */ 137 protected JsonSerializerSession(Builder<?> builder) { 138 super(builder); 139 this.ctx = builder.ctx; 140 escapeSolidus = builder.escapeSolidus; 141 } 142 143 protected SerializerWriter<?> writeBeanMap(JsonWriter<?> out, BeanMap<?> m, String typeName) throws SerializeException { 144 int i = indent; 145 out.w('{'); 146 147 var addComma = Flag.create(); 148 Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x); 149 150 if (nn(typeName)) { 151 BeanPropertyMeta pm = m.getMeta().getTypeProperty(); 152 out.cr(i).attr(pm.getName()).w(':').s(i).stringValue(typeName); 153 addComma.set(); 154 } 155 156 m.forEachValue(checkNull, (pMeta, key, value, thrown) -> { 157 var cMeta = (ClassMeta<?>) pMeta.getBeanInfo(); 158 if (nn(thrown)) 159 onBeanGetterException(pMeta, thrown); 160 161 if (canIgnoreValue(pMeta, key, value)) 162 return; 163 164 addComma.ifSet(() -> out.append(',').smi(i)).set(); 165 166 out.cr(i).attr(key).w(':').s(i); 167 168 writeAnything(out, value, cMeta, key, pMeta); 169 }); 170 171 out.cre(i - 1).w('}'); 172 return out; 173 } 174 175 176 private SerializerWriter<?> writeCollection(JsonWriter<?> out, Collection c, ClassMeta<?> type) throws SerializeException { 177 178 var elementType = type.getElementType(); 179 180 out.w('['); 181 var addComma = Flag.create(); 182 forEachEntry(c, x -> { 183 addComma.ifSet(() -> out.w(',').smi(indent)).set(); 184 out.cr(indent); 185 writeAnything(out, x, elementType, "<iterator>", null); 186 }); 187 188 out.cre(indent - 1).w(']'); 189 return out; 190 } 191 192 private SerializerWriter<?> writeStreamable(JsonWriter<?> out, Object o, ClassMeta<?> sType, ClassMeta<?> type) throws SerializeException { 193 var elementType = type.getElementType(); 194 195 out.w('['); 196 var addComma = Flag.create(); 197 forEachStreamableEntry(o, sType, x -> { 198 addComma.ifSet(() -> out.w(',').smi(indent)).set(); 199 out.cr(indent); 200 writeAnything(out, x, elementType, "<iterator>", null); 201 }); 202 203 out.cre(indent - 1).w(']'); 204 return out; 205 } 206 207 208 protected SerializerWriter<?> writeMap(JsonWriter<?> out, Map m, ClassMeta<?> type) throws SerializeException { 209 210 var keyType = type.getKeyType(); 211 var valueType = type.getValueType(); 212 213 int i = indent; 214 out.w('{'); 215 216 var addComma = Flag.create(); 217 forEachEntry(m, x -> { 218 addComma.ifSet(() -> out.w(',').smi(i)).set(); 219 Object value = x.getValue(); 220 Object key = generalize(x.getKey(), keyType); 221 out.cr(i).attr(toString(key)).w(':').s(i); 222 writeAnything(out, value, valueType, (key == null ? null : toString(key)), null); 223 }); 224 225 out.cre(i - 1).w('}'); 226 227 return out; 228 } 229 230 @Override /* Overridden from SerializerSesssion */ 231 protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException { 232 writeAnything(getJsonWriter(out).i(getInitialDepth()), o, getExpectedRootType(o), "root", null); 233 } 234 235 /** 236 * Returns the language-specific metadata on the specified class. 237 * 238 * @param cm The class to return the metadata on. 239 * @return The metadata. 240 */ 241 protected JsonClassMeta getJsonClassMeta(ClassMeta<?> cm) { 242 return ctx.getJsonClassMeta(cm); 243 } 244 245 /** 246 * Converts the specified output target object to an {@link JsonWriter}. 247 * 248 * @param out The output target object. 249 * <br>Must not be <jk>null</jk>. 250 * @return The output target object wrapped in an {@link JsonWriter}. 251 */ 252 protected JsonWriter<?> getJsonWriter(SerializerPipe out) { 253 var output = out.getRawOutput(); 254 if (output instanceof JsonWriter<?> output2) 255 return output2; 256 var w = new BasicJsonWriter(out.getWriter(), isUseWhitespace(), getMaxIndent(), isEscapeSolidus(), getQuoteChar(), false, isTrimStrings(), getUriResolver()); 257 out.setWriter(w); 258 return w; 259 } 260 261 @Override 262 public final boolean isAddBeanTypes() { return super.isAddBeanTypes(); } 263 264 /** 265 * Prefix solidus <js>'/'</js> characters with escapes. 266 * 267 * @see JsonSerializer.Builder#escapeSolidus() 268 * @return 269 * <jk>true</jk> if solidus (e.g. slash) characters should be escaped. 270 */ 271 protected final boolean isEscapeSolidus() { return escapeSolidus; } 272 273 /** 274 * Whether the token writer emits unquoted (simple) attribute names. 275 * 276 * @return <jk>false</jk> for RFC-8259 strict JSON; JSON5 overrides to <jk>true</jk>. 277 */ 278 protected boolean isSimpleAttrs() { return false; } 279 280 /** 281 * Opens a low-level push generator that emits JSON one structural event at a time, bound to 282 * this session's live config. 283 * 284 * <p> 285 * The writer is purely structural — object swaps and {@code @Schema} annotations are not 286 * applied at this layer. See {@link TokenWriter} for the contract. The higher-level 287 * {@link TokenWriter#object(Object)} bridge walks an arbitrary POJO via {@link PojoWalker} and 288 * DOES honor the databind settings (<c>keepNullProperties</c>, <c>trimEmptyMaps</c>, 289 * <c>trimEmptyCollections</c>, <c>sortMaps</c>, <c>sortCollections</c>, <c>trimStrings</c>). 290 * 291 * @param output The output. Accepts {@link Writer} or {@link OutputStream}. 292 * @return A new {@link JsonTokenWriter}. 293 * @throws IOException If the output type is not supported or could not be opened. 294 */ 295 @Override /* TokenWritable */ 296 public TokenWriter writeTokens(Object output) throws IOException { 297 var walk = new PojoWalker.Options( 298 isKeepNullProperties(), 299 isTrimEmptyMaps(), 300 isTrimEmptyCollections(), 301 isSortMaps(), 302 isSortCollections(), 303 isTrimStrings(), 304 getMarshallingContext()); 305 var settings = new JsonTokenWriter.Settings( 306 isUseWhitespace(), 307 getMaxIndent(), 308 getQuoteChar(), 309 isEscapeSolidus(), 310 isTrimStrings(), 311 isSimpleAttrs(), 312 walk, 313 false /* disableObject */); 314 return JsonTokenWriter.forOutput(output, settings); 315 } 316 317 /** 318 * Streaming array-element {@link RecordWriter} backed by {@link JsonTokenWriter}. 319 * Memory is O(1) in the array length — elements are emitted as they arrive. 320 * 321 * @param output The output. 322 * @return A new element-streamed {@link RecordWriter}. 323 * @throws IOException If a problem occurred opening the underlying output. 324 */ 325 @Override /* ArrayRecordWritable */ 326 public RecordWriter writeArrayRecords(Object output) throws IOException { 327 return StreamingArrayRecord.writer(writeTokens(output)); 328 } 329 330 /** 331 * Workhorse method. 332 * Determines the type of object, and then calls the appropriate type-specific serialization method. 333 * 334 * @param out The output writer. 335 * <br>Must not be <jk>null</jk>. 336 * @param o The object to serialize. 337 * <br>Can be <jk>null</jk> (written as the JSON literal <js>"null"</js>). 338 * @param eType The expected type. 339 * <br>Can be <jk>null</jk> (defaults to {@code Object}). 340 * @param attrName The attribute name. 341 * @param pMeta The bean property currently being serialized. 342 * <br>Can be <jk>null</jk> (the bean-property-level dictionary lookup and URI detection are skipped). 343 * @return The same writer passed in. 344 * @throws SerializeException General serialization error occurred. 345 */ 346 347 @SuppressWarnings({ 348 "java:S3776" // Cognitive complexity acceptable for this specific logic 349 }) 350 protected JsonWriter<?> writeAnything(JsonWriter<?> out, Object o, ClassMeta<?> eType, String attrName, BeanPropertyMeta pMeta) throws SerializeException { 351 352 if (o == null) { 353 out.append("null"); 354 return out; 355 } 356 357 if (eType == null) 358 eType = object(); 359 360 ClassMeta<?> aType = null; // The actual type 361 ClassMeta<?> sType = null; // The serialized type 362 363 aType = push2(attrName, o, eType); 364 boolean isRecursion = aType == null; 365 366 // Handle recursion 367 if (aType == null) { 368 o = null; 369 aType = object(); 370 } 371 372 // Handle Optional<X> 373 if (isOptional(aType)) { 374 o = getOptionalValue(o); 375 eType = getOptionalType(eType); 376 aType = getClassMetaForObject(o, object()); 377 } 378 379 sType = aType; 380 String typeName = getBeanTypeName(this, eType, aType, pMeta); 381 382 // Swap if necessary 383 var swap = aType.getSwap(this); 384 if (nn(swap)) { 385 o = swap(swap, o); 386 sType = swap.getSwapClassMeta(this); 387 388 // If the getSwapClass() method returns Object, we need to figure out 389 // the actual type now. 390 if (sType.isObject()) 391 sType = getClassMetaForObject(o); 392 } 393 394 String wrapperAttr = getJsonClassMeta(sType).getWrapperAttr(); 395 if (nn(wrapperAttr)) { 396 out.w('{').cr(indent).attr(wrapperAttr).w(':').s(indent); 397 indent++; 398 } 399 400 // '\0' characters are considered null. 401 if (o == null || (sType.isChar() && ((Character)o).charValue() == 0)) { 402 out.append("null"); 403 } else if (sType.isBean()) { 404 writeBeanMap(out, toBeanMap(o), typeName); 405 } else if (sType.isMap()) { 406 if (sType.isBeanMap()) 407 writeBeanMap(out, (BeanMap)o, typeName); 408 else 409 writeMap(out, (Map)o, eType); 410 } else if (sType.isCollection()) { 411 writeCollection(out, (Collection)o, eType); 412 } else if (sType.isArray()) { 413 writeCollection(out, toList(sType.inner(), o), eType); 414 } else if (sType.isNumber() || sType.isBoolean()) { 415 out.append(o); 416 } else if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) { 417 out.uriValue(o); 418 } else if (sType.isDate()) { 419 out.stringValue(writeDate((Date)o, sType)); 420 } else if (sType.isCalendar()) { 421 out.stringValue(writeCalendar(o, sType)); 422 } else if (sType.isTemporal()) { 423 out.stringValue(writeTemporal((TemporalAccessor)o, sType)); 424 } else if (sType.isDuration()) { 425 var value = writeDuration((Duration)o); 426 if (getDurationFormat().isNumeric()) 427 out.append(value); 428 else 429 out.stringValue(value); 430 } else if (sType.isPeriod()) { 431 out.stringValue(writePeriod((Period)o)); 432 } else if (sType.isStreamable()) { 433 writeStreamable(out, o, sType, eType); 434 } else if (sType.isReader()) { 435 pipe((Reader)o, out, SerializerSession::handleThrown); 436 } else if (sType.isInputStream()) { 437 pipe((InputStream)o, out, SerializerSession::handleThrown); 438 } else { 439 out.stringValue(toString(o)); 440 } 441 442 if (nn(wrapperAttr)) { 443 indent--; 444 out.cre(indent - 1).w('}'); 445 } 446 447 if (! isRecursion) 448 pop(); 449 return out; 450 } 451 452 /** 453 * Serializes an object to a JSON string using this session's configuration. 454 * 455 * <p> 456 * Protected extension hook that subclasses can call or override when they need to serialize a 457 * value to JSON (e.g. for embedding a nested JSON fragment). 458 * 459 * @param o The object to serialize. 460 * @return The serialized object. 461 * @throws Exception Error occurred. 462 */ 463 @SuppressWarnings({ 464 "java:S112" // throws Exception intentional - callback/lifecycle method for serialization hooks 465 }) 466 protected String writeJson(Object o) throws Exception { 467 var sw = new StringWriter(); 468 writeAnything(getJsonWriter(createPipe(sw)).i(getInitialDepth()), o, getExpectedRootType(o), "root", null); 469 return sw.toString(); 470 } 471}