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 &mdash; 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 &mdash; 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}