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.hjson;
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 for serializing objects to Hjson format.
036 */
037@SuppressWarnings({
038   "java:S110", // Session hierarchy (HjsonSerializerSession -> WriterSerializerSession -> SerializerSession -> MarshallingSession ...) is inherently deep.
039   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
040   "java:S3776", // writeAnything() dispatches on every supported value type (bean, map, collection, array, date, etc.) in one if/else chain.
041   "java:S6541", // writeAnything() is the single type-dispatch point of the serializer and is long by design.
042   "rawtypes", // Raw Map, Collection and BeanMap are used in writeMap()/writeCollection() and the casts in doWrite()/writeAnything() because the runtime value type is not known.
043   "resource", // The HjsonWriter wraps the pipe's writer and is closed with the SerializerPipe, not in getHjsonWriter().
044   "unchecked" // Calls to forEachEntry() with the raw Map/Collection arguments in writeMap()/writeCollection() are unchecked.
045})
046public class HjsonSerializerSession extends WriterSerializerSession implements RecordWritable {
047
048   /**
049    * Builder for Hjson serializer session.
050    */
051   public static class Builder extends WriterSerializerSession.Builder<Builder> {
052
053      private final HjsonSerializer ctx;
054
055      protected Builder(HjsonSerializer ctx) {
056         super(reqnn("ctx", ctx));
057         this.ctx = ctx;
058      }
059
060      @Override
061      public HjsonSerializerSession build() {
062         return new HjsonSerializerSession(this);
063      }
064   }
065
066   /**
067    * Creates a session builder.
068    *
069    * @param ctx The serializer context.
070    *    <br>Cannot be <jk>null</jk>.
071    * @return The builder.
072    */
073   public static Builder create(HjsonSerializer ctx) {
074      return new Builder(reqnn("ctx", ctx));
075   }
076
077   private final HjsonSerializer ctx;
078
079   protected HjsonSerializerSession(Builder builder) {
080      super(builder);
081      ctx = builder.ctx;
082   }
083
084   @Override /* RecordWritable */
085   public RecordWriter writeRecords(Object output) throws IOException {
086      return RecordAdapter.writer(this, output);
087   }
088
089   @Override /* RecordWritable */
090   public boolean isRecordStreaming() {
091      return false;
092   }
093
094   protected final HjsonWriter getHjsonWriter(SerializerPipe out) {
095      var output = out.getRawOutput();
096      if (output instanceof HjsonWriter output2)
097         return output2;
098      var w = new HjsonWriter(out.getWriter(), isUseWhitespace(), getMaxIndent(), isTrimStrings(),
099         ctx.useNewlineSeparators, ctx.useQuotelessStrings, ctx.useQuotelessKeys, ctx.omitRootBraces,
100         ctx.useMultilineStrings, isUseWhitespace(), getUriResolver());
101      out.setWriter(w);
102      return w;
103   }
104
105   @Override
106   protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException {
107      if (o == null)
108         return;
109      var aType = getClassMetaForObject(o);
110      var eType = getExpectedRootType(o);
111      var hw = getHjsonWriter(out).i(getInitialDepth());
112      // Apply object swap before type check (e.g., surrogate serializers convert D1 → D2).
113      ClassMeta<?> sType = aType;
114      var swap = aType.getSwap(this);
115      if (nn(swap)) {
116         o = swap(swap, o);
117         sType = swap.getSwapClassMeta(this);
118         if (sType.isObject())
119            sType = getClassMetaForObject(o);
120      }
121      if (sType.isBean() || sType.isMap()) {
122         var omitRoot = ctx.omitRootBraces;
123         if (sType.isBean())
124            writeBeanMap(hw, toBeanMap(o), getBeanTypeName(this, eType, sType, null), omitRoot);
125         else
126            writeMap(hw, (Map) o, eType, omitRoot);
127      } else {
128         writeAnything(hw, o, eType, "root", null);
129      }
130   }
131
132   private void writeBeanMap(HjsonWriter out, BeanMap<?> m, String typeName, boolean omitBraces) throws SerializeException {
133      var i = indent;
134      out.objectStart(omitBraces, i);
135      indent++;
136      var first = Flag.create();
137      Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x);
138
139      if (nn(typeName)) {
140         var pm = m.getMeta().getTypeProperty();
141         writeKey(out, pm.getName(), i, first);
142         writeString(out, typeName);
143         first.set();
144      }
145
146      m.forEachValue(checkNull, (pMeta, key, value, thrown) -> {
147         var cMeta = (ClassMeta<?>) pMeta.getBeanInfo();
148         if (nn(thrown))
149            onBeanGetterException(pMeta, thrown);
150         if (canIgnoreValue(pMeta, key, value))
151            return;
152         writeKey(out, key, i, first);
153         writeAnything(out, value, cMeta, key, pMeta);
154         first.set();
155      });
156
157      indent--;
158      out.objectEnd(omitBraces, indent);
159   }
160
161   private void writeKey(HjsonWriter out, Object key, int depth, Flag first) throws SerializeException {
162      if (first.isSet())
163         out.memberSeparator(depth);
164      else
165         out.cr(depth);
166      var keyStr = toString(key);
167      if (out.needsQuoting(keyStr))
168         out.quotedString(keyStr);
169      else
170         out.quotelessString(keyStr);
171      out.keyValueSeparator();
172   }
173
174   private void writeMap(HjsonWriter out, Map m, ClassMeta<?> type, boolean omitBraces) throws SerializeException {
175      var i = indent;
176      out.objectStart(omitBraces, i);
177      indent++;
178      var keyType = type.getKeyType();
179      var valueType = type.getValueType();
180      var first = Flag.create();
181
182      forEachEntry(m, x -> {
183         var value = x.getValue();
184         var key = generalize(x.getKey(), keyType);
185         writeKey(out, key, i, first);
186         writeAnything(out, value, valueType, key == null ? null : toString(key), null);
187         first.set();
188      });
189
190      indent--;
191      out.objectEnd(omitBraces, indent);
192   }
193
194   private void writeCollection(HjsonWriter out, Collection c, ClassMeta<?> type) throws SerializeException {
195      var eType = type.getElementType();
196      out.arrayStart();
197      indent++;
198      var first = Flag.create();
199      forEachEntry(c, x -> {
200         if (first.isSet())
201            out.memberSeparator(indent);
202         else
203            out.cr(indent);
204         writeAnything(out, x, eType, "<iterator>", null);
205         first.set();
206      });
207      indent--;
208      out.arrayEnd();
209   }
210
211   private void writeString(HjsonWriter out, String s) throws SerializeException {
212      if (s == null) {
213         out.w("null");
214         return;
215      }
216      if (ctx.useMultilineStrings && s.contains("\n"))
217         out.multilineString(s);
218      else if (out.isSimpleValue(s))
219         out.quotelessString(s);
220      else
221         out.quotedString(s);
222   }
223
224   protected HjsonWriter writeAnything(HjsonWriter out, Object o, ClassMeta<?> eType, String attrName, BeanPropertyMeta pMeta) throws SerializeException {
225      if (o == null) {
226         out.w("null");
227         return out;
228      }
229      if (eType == null)
230         eType = object();
231
232      var aType = push2(attrName, o, eType);
233      var isRecursion = aType == null;
234
235      if (aType == null) {
236         o = null;
237         aType = object();
238      }
239
240      if (isOptional(aType)) {
241         o = getOptionalValue(o);
242         eType = getOptionalType(eType);
243         aType = getClassMetaForObject(o, object());
244      }
245
246      var sType = aType;
247      var swap = aType.getSwap(this);
248      if (nn(swap)) {
249         o = swap(swap, o);
250         sType = swap.getSwapClassMeta(this);
251         if (sType.isObject())
252            sType = getClassMetaForObject(o);
253      }
254
255      if (o == null || (sType.isChar() && ((Character) o).charValue() == 0)) {
256         out.w("null");
257      } else if (sType.isBean()) {
258         writeBeanMap(out, toBeanMap(o), getBeanTypeName(this, eType, aType, pMeta), false);
259      } else if (sType.isMap()) {
260         if (sType.isBeanMap())
261            writeBeanMap(out, (BeanMap) o, getBeanTypeName(this, eType, aType, pMeta), false);
262         else
263            writeMap(out, (Map) o, eType, false);
264      } else if (sType.isCollection()) {
265         writeCollection(out, (Collection) o, eType);
266      } else if (sType.isArray()) {
267         writeCollection(out, toList(sType.inner(), o), eType);
268      } else if (sType.isNumber() || sType.isBoolean()) {
269         out.append(o);
270      } else if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) {
271         writeString(out, getUriResolver().resolve(o));
272      } else if (sType.isDate()) {
273         writeString(out, writeDate((Date)o, sType));
274      } else if (sType.isCalendar()) {
275         writeString(out, writeCalendar(o, sType));
276      } else if (sType.isTemporal()) {
277         writeString(out, writeTemporal((TemporalAccessor)o, sType));
278      } else if (sType.isDuration()) {
279         var value = writeDuration((Duration)o);
280         if (getDurationFormat().isNumeric())
281            out.append(value);
282         else
283            writeString(out, value);
284      } else if (sType.isPeriod()) {
285         writeString(out, writePeriod((Period)o));
286      } else if (sType.isStreamable()) {
287         writeStreamable(out, o, sType, eType);
288      } else if (sType.isReader()) {
289         pipe((Reader) o, out, SerializerSession::handleThrown);
290      } else if (sType.isInputStream()) {
291         pipe((InputStream) o, out, SerializerSession::handleThrown);
292      } else {
293         writeString(out, toString(o));
294      }
295
296      if (!isRecursion)
297         pop();
298      return out;
299   }
300
301   private void writeStreamable(HjsonWriter out, Object o, ClassMeta<?> sType, ClassMeta<?> type) throws SerializeException {
302      var eType = type.getElementType();
303      out.arrayStart();
304      indent++;
305      var first = Flag.create();
306      forEachStreamableEntry(o, sType, x -> {
307         if (first.isSet())
308            out.memberSeparator(indent);
309         else
310            out.cr(indent);
311         writeAnything(out, x, eType, "<iterator>", null);
312         first.set();
313      });
314      indent--;
315      out.arrayEnd();
316   }
317}