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.hocon;
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 HOCON format.
036 */
037@SuppressWarnings({
038   "java:S110", // Inheritance depth comes from the inherited WriterSerializerSession session hierarchy, not from this class
039   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
040   "java:S3776", // writeBeanMap() and writeAnything() branch over every HOCON value kind (bean, map, collection, date, duration, stream) in one pass
041   "java:S6541", // writeAnything() is the single dispatch point over all serializable value kinds
042   "rawtypes", // writeMap()/writeCollection() take raw Map/Collection because values arrive as Object and are cast after ClassMeta type checks
043   "resource", // The HoconWriter created in getHoconWriter() wraps the pipe's writer and is closed by the SerializerPipe, not by this session
044   "unchecked" // Raw Map/Collection casts in doWrite()/writeAnything()/writeBeanMap() are guarded by the ClassMeta isMap()/isCollection() and instanceof checks
045})
046public class HoconSerializerSession extends WriterSerializerSession implements RecordWritable {
047
048   /**
049    * Builder for HOCON serializer session.
050    */
051   public static class Builder extends WriterSerializerSession.Builder<Builder> {
052
053      private final HoconSerializer ctx;
054
055      protected Builder(HoconSerializer ctx) {
056         super(reqnn("ctx", ctx));
057         this.ctx = ctx;
058      }
059
060      @Override
061      public HoconSerializerSession build() {
062         return new HoconSerializerSession(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(HoconSerializer ctx) {
074      return new Builder(reqnn("ctx", ctx));
075   }
076
077   private final HoconSerializer ctx;
078
079   protected HoconSerializerSession(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 HoconWriter getHoconWriter(SerializerPipe out) {
095      var output = out.getRawOutput();
096      if (output instanceof HoconWriter output2)
097         return output2;
098      var w = new HoconWriter(out.getWriter(), isUseWhitespace(), getMaxIndent(), isTrimStrings(),
099         ctx.useNewlineSeparators, ctx.useUnquotedStrings, ctx.useUnquotedKeys, ctx.omitRootBraces,
100         ctx.useMultilineStrings, ctx.useEqualsSign, 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 = getHoconWriter(out).i(getInitialDepth());
112      ClassMeta<?> sType = aType;
113      var swap = aType.getSwap(this);
114      if (nn(swap)) {
115         o = swap(swap, o);
116         sType = swap.getSwapClassMeta(this);
117         if (sType.isObject())
118            sType = getClassMetaForObject(o);
119      }
120      if (sType.isBean() || sType.isMap()) {
121         var omitRoot = ctx.omitRootBraces;
122         if (sType.isBean())
123            writeBeanMap(hw, toBeanMap(o), getBeanTypeName(this, eType, sType, null), omitRoot);
124         else
125            writeMap(hw, (Map) o, eType, omitRoot);
126      } else {
127         writeAnything(hw, o, eType, "root", null);
128      }
129   }
130
131   private void writeBeanMap(HoconWriter out, BeanMap<?> m, String typeName, boolean omitRootBraces) throws SerializeException {
132      var i = indent;
133      if (!omitRootBraces) {
134         out.objectStart(i);
135         indent++;
136      }
137      var first = Flag.create();
138      Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x);
139
140      if (nn(typeName)) {
141         var pm = m.getMeta().getTypeProperty();
142         writeKeyPrefix(out, pm.getName(), i, first);
143         out.equalsSign();
144         writeString(out, typeName);
145         first.set();
146      }
147
148      m.forEachValue(checkNull, (pMeta, key, value, thrown) -> {
149         var cMeta = (ClassMeta<?>) pMeta.getBeanInfo();
150         if (nn(thrown))
151            onBeanGetterException(pMeta, thrown);
152         if (canIgnoreValue(pMeta, key, value))
153            return;
154         var isObject = cMeta.isBean() || cMeta.isMap();
155         writeKeyPrefix(out, key, i, first);
156         if (isObject) {
157            out.append(" ");
158            if (value instanceof BeanMap value2)
159               writeBeanMap(out, value2, getBeanTypeName(this, cMeta, cMeta, pMeta), false);
160            else if (cMeta.isBean())
161               writeBeanMap(out, toBeanMap(value), getBeanTypeName(this, cMeta, cMeta, pMeta), false);
162            else
163               writeMap(out, (Map) value, cMeta, false);
164         } else {
165            out.equalsSign();
166            writeAnything(out, value, cMeta, key, pMeta);
167         }
168         first.set();
169      });
170
171      if (!omitRootBraces) {
172         indent--;
173         out.objectEnd(indent);
174      }
175   }
176
177   private void writeKeyPrefix(HoconWriter out, Object key, int depth, Flag first) throws SerializeException {
178      if (first.isSet())
179         out.separator(depth);
180      else
181         out.cr(depth);
182      var keyStr = toString(key);
183      out.key(keyStr);
184   }
185
186   private void writeMap(HoconWriter out, Map m, ClassMeta<?> type, boolean omitRootBraces) throws SerializeException {
187      var i = indent;
188      if (!omitRootBraces) {
189         out.objectStart(i);
190         indent++;
191      }
192      var keyType = type.getKeyType();
193      var valueType = type.getValueType();
194      var first = Flag.create();
195
196      forEachEntry(m, x -> {
197         var value = x.getValue();
198         var key = generalize(x.getKey(), keyType);
199         var isObject = value != null && (value instanceof Map || value instanceof BeanMap);
200         writeKeyPrefix(out, key, i, first);
201         if (isObject) {
202            out.append(" ");
203            if (value instanceof BeanMap value2)
204               writeBeanMap(out, value2, getBeanTypeName(this, valueType, getClassMetaForObject(value), null), false);
205            else
206               writeMap(out, (Map) value, object(), false);
207         } else {
208            out.equalsSign();
209            writeAnything(out, value, valueType, key == null ? null : toString(key), null);
210         }
211         first.set();
212      });
213
214      if (!omitRootBraces) {
215         indent--;
216         out.objectEnd(indent);
217      }
218   }
219
220   private void writeCollection(HoconWriter out, Collection c, ClassMeta<?> type) throws SerializeException {
221      var eType = type.getElementType();
222      out.arrayStart();
223      indent++;
224      var first = Flag.create();
225      forEachEntry(c, x -> {
226         if (first.isSet())
227            out.separator(indent);
228         else
229            out.cr(indent);
230         writeAnything(out, x, eType, "<iterator>", null);
231         first.set();
232      });
233      indent--;
234      out.arrayEnd();
235   }
236
237   private void writeString(HoconWriter out, String s) throws SerializeException {
238      if (s == null) {
239         out.w("null");
240         return;
241      }
242      if (ctx.useMultilineStrings && s.contains("\n"))
243         out.tripleQuotedString(s);
244      else if (out.isSimpleValue(s))
245         out.unquotedString(s);
246      else
247         out.quotedString(s);
248   }
249
250   protected HoconWriter writeAnything(HoconWriter hw, Object o, ClassMeta<?> eType, String attrName, BeanPropertyMeta pMeta) throws SerializeException {
251      if (o == null) {
252         hw.w("null");
253         return hw;
254      }
255      if (eType == null)
256         eType = object();
257
258      var aType = push2(attrName, o, eType);
259      var isRecursion = aType == null;
260
261      if (aType == null) {
262         o = null;
263         aType = object();
264      }
265
266      if (isOptional(aType)) {
267         o = getOptionalValue(o);
268         eType = getOptionalType(eType);
269         aType = getClassMetaForObject(o, object());
270      }
271
272      var sType = aType;
273      var swap = aType.getSwap(this);
274      if (nn(swap)) {
275         o = swap(swap, o);
276         sType = swap.getSwapClassMeta(this);
277         if (sType.isObject())
278            sType = getClassMetaForObject(o);
279      }
280
281      if (o == null || (sType.isChar() && ((Character) o).charValue() == 0)) {
282         hw.w("null");
283      } else if (o instanceof byte[] o2) {
284         // byte[] gate: encode as Base64 before isArray would otherwise route it as a generic array.
285         writeString(hw, Base64.getEncoder().encodeToString(o2));
286      } else if (sType.isBean()) {
287         writeBeanMap(hw, toBeanMap(o), getBeanTypeName(this, eType, aType, pMeta), false);
288      } else if (sType.isMap()) {
289         if (sType.isBeanMap())
290            writeBeanMap(hw, (BeanMap) o, getBeanTypeName(this, eType, aType, pMeta), false);
291         else
292            writeMap(hw, (Map) o, eType, false);
293      } else if (sType.isCollection()) {
294         writeCollection(hw, (Collection) o, eType);
295      } else if (sType.isArray()) {
296         writeCollection(hw, toList(sType.inner(), o), eType);
297      } else if (sType.isNumber() || sType.isBoolean()) {
298         hw.append(o);
299      } else if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) {
300         writeString(hw, getUriResolver().resolve(o));
301      } else if (sType.isDate()) {
302         writeString(hw, writeDate((Date)o, sType));
303      } else if (sType.isCalendar()) {
304         writeString(hw, writeCalendar(o, sType));
305      } else if (sType.isTemporal()) {
306         writeString(hw, writeTemporal((TemporalAccessor)o, sType));
307      } else if (sType.isDuration()) {
308         var value = writeDuration((Duration)o);
309         if (getDurationFormat().isNumeric())
310            hw.append(value);
311         else
312            writeString(hw, value);
313      } else if (sType.isPeriod()) {
314         writeString(hw, writePeriod((Period)o));
315      } else if (sType.isStreamable()) {
316         writeStreamable(hw, o, sType, eType);
317      } else if (sType.isReader()) {
318         pipe((Reader) o, hw, SerializerSession::handleThrown);
319      } else if (sType.isInputStream()) {
320         pipe((InputStream) o, hw, SerializerSession::handleThrown);
321      } else {
322         writeString(hw, toString(o));
323      }
324
325      if (!isRecursion)
326         pop();
327      return hw;
328   }
329
330   private void writeStreamable(HoconWriter out, Object o, ClassMeta<?> sType, ClassMeta<?> type) throws SerializeException {
331      var eType = type.getElementType();
332      out.arrayStart();
333      indent++;
334      var first = Flag.create();
335      forEachStreamableEntry(o, sType, x -> {
336         if (first.isSet())
337            out.separator(indent);
338         else
339            out.cr(indent);
340         writeAnything(out, x, eType, "<iterator>", null);
341         first.set();
342      });
343      indent--;
344      out.arrayEnd();
345   }
346}