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.json;
018
019import static org.apache.juneau.commons.utils.AssertionUtils.*;
020import static org.apache.juneau.commons.utils.IoUtils.*;
021import static org.apache.juneau.commons.utils.Utils.*;
022
023import java.io.*;
024import java.lang.reflect.*;
025import java.nio.charset.*;
026import java.util.*;
027import java.util.function.*;
028
029import org.apache.juneau.*;
030import org.apache.juneau.commons.lang.*;
031import org.apache.juneau.httppart.*;
032import org.apache.juneau.serializer.*;
033import org.apache.juneau.svl.*;
034
035/**
036 * Session object that lives for the duration of a single use of {@link JsonSerializer}.
037 *
038 * <h5 class='section'>Notes:</h5><ul>
039 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
040 * </ul>
041 *
042 * <h5 class='section'>See Also:</h5><ul>
043 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JsonBasics">JSON Basics</a>
044 * </ul>
045 */
046@SuppressWarnings("resource")
047public class JsonSerializerSession extends WriterSerializerSession {
048   /**
049    * Builder class.
050    */
051   public static class Builder extends WriterSerializerSession.Builder {
052
053      private JsonSerializer ctx;
054
055      /**
056       * Constructor
057       *
058       * @param ctx The context creating this session.
059       *    <br>Cannot be <jk>null</jk>.
060       */
061      protected Builder(JsonSerializer ctx) {
062         super(assertArgNotNull("ctx", ctx));
063         this.ctx = ctx;
064      }
065
066      @Override /* Overridden from Builder */
067      public <T> Builder apply(Class<T> type, Consumer<T> apply) {
068         super.apply(type, apply);
069         return this;
070      }
071
072      @Override
073      public JsonSerializerSession build() {
074         return new JsonSerializerSession(this);
075      }
076
077      @Override /* Overridden from Builder */
078      public Builder debug(Boolean value) {
079         super.debug(value);
080         return this;
081      }
082
083      @Override /* Overridden from Builder */
084      public Builder fileCharset(Charset value) {
085         super.fileCharset(value);
086         return this;
087      }
088
089      @Override /* Overridden from Builder */
090      public Builder javaMethod(Method value) {
091         super.javaMethod(value);
092         return this;
093      }
094
095      @Override /* Overridden from Builder */
096      public Builder locale(Locale value) {
097         super.locale(value);
098         return this;
099      }
100
101      @Override /* Overridden from Builder */
102      public Builder mediaType(MediaType value) {
103         super.mediaType(value);
104         return this;
105      }
106
107      @Override /* Overridden from Builder */
108      public Builder mediaTypeDefault(MediaType value) {
109         super.mediaTypeDefault(value);
110         return this;
111      }
112
113      @Override /* Overridden from Builder */
114      public Builder properties(Map<String,Object> value) {
115         super.properties(value);
116         return this;
117      }
118
119      @Override /* Overridden from Builder */
120      public Builder property(String key, Object value) {
121         super.property(key, value);
122         return this;
123      }
124
125      @Override /* Overridden from Builder */
126      public Builder resolver(VarResolverSession value) {
127         super.resolver(value);
128         return this;
129      }
130
131      @Override /* Overridden from Builder */
132      public Builder schema(HttpPartSchema value) {
133         super.schema(value);
134         return this;
135      }
136
137      @Override /* Overridden from Builder */
138      public Builder schemaDefault(HttpPartSchema value) {
139         super.schemaDefault(value);
140         return this;
141      }
142
143      @Override /* Overridden from Builder */
144      public Builder streamCharset(Charset value) {
145         super.streamCharset(value);
146         return this;
147      }
148
149      @Override /* Overridden from Builder */
150      public Builder timeZone(TimeZone value) {
151         super.timeZone(value);
152         return this;
153      }
154
155      @Override /* Overridden from Builder */
156      public Builder timeZoneDefault(TimeZone value) {
157         super.timeZoneDefault(value);
158         return this;
159      }
160
161      @Override /* Overridden from Builder */
162      public Builder unmodifiable() {
163         super.unmodifiable();
164         return this;
165      }
166
167      @Override /* Overridden from Builder */
168      public Builder uriContext(UriContext value) {
169         super.uriContext(value);
170         return this;
171      }
172
173      @Override /* Overridden from Builder */
174      public Builder useWhitespace(Boolean value) {
175         super.useWhitespace(value);
176         return this;
177      }
178   }
179
180   /**
181    * Creates a new builder for this object.
182    *
183    * @param ctx The context creating this session.
184    *    <br>Cannot be <jk>null</jk>.
185    * @return A new builder.
186    */
187   public static Builder create(JsonSerializer ctx) {
188      return new Builder(assertArgNotNull("ctx", ctx));
189   }
190
191   private final JsonSerializer ctx;
192
193   /**
194    * Constructor.
195    *
196    * @param builder The builder for this object.
197    */
198   protected JsonSerializerSession(Builder builder) {
199      super(builder);
200      this.ctx = builder.ctx;
201   }
202
203   private SerializerWriter serializeBeanMap(JsonWriter out, BeanMap<?> m, String typeName) throws SerializeException {
204      int i = indent;
205      out.w('{');
206
207      var addComma = Flag.create();
208      Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x);
209
210      if (nn(typeName)) {
211         BeanPropertyMeta pm = m.getMeta().getTypeProperty();
212         out.cr(i).attr(pm.getName()).w(':').s(i).stringValue(typeName);
213         addComma.set();
214      }
215
216      m.forEachValue(checkNull, (pMeta, key, value, thrown) -> {
217         var cMeta = pMeta.getClassMeta();
218         if (nn(thrown))
219            onBeanGetterException(pMeta, thrown);
220
221         if (canIgnoreValue(cMeta, key, value))
222            return;
223
224         addComma.ifSet(() -> out.append(',').smi(i)).set();
225
226         out.cr(i).attr(key).w(':').s(i);
227
228         serializeAnything(out, value, cMeta, key, pMeta);
229      });
230
231      out.cre(i - 1).w('}');
232      return out;
233   }
234
235   @SuppressWarnings({ "rawtypes", "unchecked" })
236   private SerializerWriter serializeCollection(JsonWriter out, Collection c, ClassMeta<?> type) throws SerializeException {
237
238      var elementType = type.getElementType();
239
240      out.w('[');
241      var addComma = Flag.create();
242      forEachEntry(c, x -> {
243         addComma.ifSet(() -> out.w(',').smi(indent)).set();
244         out.cr(indent);
245         serializeAnything(out, x, elementType, "<iterator>", null);
246      });
247
248      out.cre(indent - 1).w(']');
249      return out;
250   }
251
252   @SuppressWarnings({ "rawtypes", "unchecked" })
253   private SerializerWriter serializeMap(JsonWriter out, Map m, ClassMeta<?> type) throws SerializeException {
254
255      var keyType = type.getKeyType();
256      var valueType = type.getValueType();
257
258      int i = indent;
259      out.w('{');
260
261      var addComma = Flag.create();
262      forEachEntry(m, x -> {
263         addComma.ifSet(() -> out.w(',').smi(i)).set();
264         Object value = x.getValue();
265         Object key = generalize(x.getKey(), keyType);
266         out.cr(i).attr(toString(key)).w(':').s(i);
267         serializeAnything(out, value, valueType, (key == null ? null : toString(key)), null);
268      });
269
270      out.cre(i - 1).w('}');
271
272      return out;
273   }
274
275   @Override /* Overridden from SerializerSesssion */
276   protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException {
277      serializeAnything(getJsonWriter(out).i(getInitialDepth()), o, getExpectedRootType(o), "root", null);
278   }
279
280   /**
281    * Returns the language-specific metadata on the specified class.
282    *
283    * @param cm The class to return the metadata on.
284    * @return The metadata.
285    */
286   protected JsonClassMeta getJsonClassMeta(ClassMeta<?> cm) {
287      return ctx.getJsonClassMeta(cm);
288   }
289
290   /**
291    * Converts the specified output target object to an {@link JsonWriter}.
292    *
293    * @param out The output target object.
294    * @return The output target object wrapped in an {@link JsonWriter}.
295    * @throws IOException Thrown by underlying stream.
296    */
297   protected final JsonWriter getJsonWriter(SerializerPipe out) throws IOException {
298      var output = out.getRawOutput();
299      if (output instanceof JsonWriter output2)
300         return output2;
301      var w = new JsonWriter(out.getWriter(), isUseWhitespace(), getMaxIndent(), isEscapeSolidus(), getQuoteChar(), isSimpleAttrs(), isTrimStrings(), getUriResolver());
302      out.setWriter(w);
303      return w;
304   }
305
306   /**
307    * Add <js>"_type"</js> properties when needed.
308    *
309    * @see JsonSerializer.Builder#addBeanTypesJson()
310    * @return
311    *    <jk>true</jk> if <js>"_type"</js> properties will be added to beans if their type cannot be inferred
312    *    through reflection.
313    */
314   @Override
315   protected final boolean isAddBeanTypes() { return ctx.isAddBeanTypes(); }
316
317   /**
318    * Prefix solidus <js>'/'</js> characters with escapes.
319    *
320    * @see JsonSerializer.Builder#escapeSolidus()
321    * @return
322    *    <jk>true</jk> if solidus (e.g. slash) characters should be escaped.
323    */
324   protected final boolean isEscapeSolidus() { return ctx.isEscapeSolidus(); }
325
326   /**
327    * Simple JSON attributes.
328    *
329    * @see JsonSerializer.Builder#simpleAttrs()
330    * @return
331    *    <jk>true</jk> if JSON attribute names will only be quoted when necessary.
332    *    <br>Otherwise, they are always quoted.
333    */
334   protected final boolean isSimpleAttrs() { return ctx.isSimpleAttrs(); }
335
336   /**
337    * Workhorse method.
338    * Determines the type of object, and then calls the appropriate type-specific serialization method.
339    *
340    * @param out The output writer.
341    * @param o The object to serialize.
342    * @param eType The expected type.
343    * @param attrName The attribute name.
344    * @param pMeta The bean property currently being parsed.
345    * @return The same writer passed in.
346    * @throws SerializeException General serialization error occurred.
347    */
348   @SuppressWarnings({ "rawtypes" })
349   protected JsonWriter serializeAnything(JsonWriter out, Object o, ClassMeta<?> eType, String attrName, BeanPropertyMeta pMeta) throws SerializeException {
350
351      if (o == null) {
352         out.append("null");
353         return out;
354      }
355
356      if (eType == null)
357         eType = object();
358
359      var aType = (ClassMeta<?>)null;        // The actual type
360      var sType = (ClassMeta<?>)null;        // The serialized type
361
362      aType = push2(attrName, o, eType);
363      boolean isRecursion = aType == null;
364
365      // Handle recursion
366      if (aType == null) {
367         o = null;
368         aType = object();
369      }
370
371      // Handle Optional<X>
372      if (isOptional(aType)) {
373         o = getOptionalValue(o);
374         eType = getOptionalType(eType);
375         aType = getClassMetaForObject(o, object());
376      }
377
378      sType = aType;
379      String typeName = getBeanTypeName(this, eType, aType, pMeta);
380
381      // Swap if necessary
382      var swap = aType.getSwap(this);
383      if (nn(swap)) {
384         o = swap(swap, o);
385         sType = swap.getSwapClassMeta(this);
386
387         // If the getSwapClass() method returns Object, we need to figure out
388         // the actual type now.
389         if (sType.isObject())
390            sType = getClassMetaForObject(o);
391      }
392
393      String wrapperAttr = getJsonClassMeta(sType).getWrapperAttr();
394      if (nn(wrapperAttr)) {
395         out.w('{').cr(indent).attr(wrapperAttr).w(':').s(indent);
396         indent++;
397      }
398
399      // '\0' characters are considered null.
400      if (o == null || (sType.isChar() && ((Character)o).charValue() == 0)) {
401         out.append("null");
402      } else if (sType.isNumber() || sType.isBoolean()) {
403         out.append(o);
404      } else if (sType.isBean()) {
405         serializeBeanMap(out, toBeanMap(o), typeName);
406      } else if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) {
407         out.uriValue(o);
408      } else if (sType.isMap()) {
409         if (o instanceof BeanMap)
410            serializeBeanMap(out, (BeanMap)o, typeName);
411         else
412            serializeMap(out, (Map)o, eType);
413      } else if (sType.isCollection()) {
414         serializeCollection(out, (Collection)o, eType);
415      } else if (sType.isArray()) {
416         serializeCollection(out, toList(sType.inner(), o), eType);
417      } else if (sType.isReader()) {
418         pipe((Reader)o, out, SerializerSession::handleThrown);
419      } else if (sType.isInputStream()) {
420         pipe((InputStream)o, out, SerializerSession::handleThrown);
421      } else {
422         out.stringValue(toString(o));
423      }
424
425      if (nn(wrapperAttr)) {
426         indent--;
427         out.cre(indent - 1).w('}');
428      }
429
430      if (! isRecursion)
431         pop();
432      return out;
433   }
434
435   /**
436    * Method that can be called from subclasses to serialize an object to JSON.
437    *
438    * <p>
439    * Used by {@link JsonSchemaSerializerSession} for serializing examples to JSON.
440    *
441    * @param o The object to serialize.
442    * @return The serialized object.
443    * @throws Exception Error occurred.
444    */
445   protected String serializeJson(Object o) throws Exception {
446      var sw = new StringWriter();
447      serializeAnything(getJsonWriter(createPipe(sw)).i(getInitialDepth()), o, getExpectedRootType(o), "root", null);
448      return sw.toString();
449   }
450}