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.cbor;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.IoUtils.*;
021import static org.apache.juneau.commons.utils.Shorts.*;
022
023import java.io.*;
024import java.time.*;
025import java.time.temporal.*;
026import java.util.*;
027import java.util.function.*;
028
029import org.apache.juneau.commons.bean.*;
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 CborSerializer}.
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/Cbor">CBOR</a>
043 * </ul>
044 */
045@SuppressWarnings({
046   "java:S110", // Inheritance depth acceptable for serializer session hierarchy
047   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
048   "rawtypes", // Raw BeanMap/Map/Collection casts in writeAnything() and the writeMap()/writeCollection() helpers, where element types are only known at runtime.
049   "resource", // Output streams managed by calling code
050   "unchecked" // Type erasure requires unchecked operations
051})
052public class CborSerializerSession extends OutputStreamSerializerSession implements TokenWritable, ArrayRecordWritable {
053
054   /**
055    * Builder class.
056    */
057   public static class Builder extends OutputStreamSerializerSession.Builder<Builder> {
058
059      /**
060       * Constructor
061       *
062       * @param ctx The context creating this session.
063       *    <br>Cannot be <jk>null</jk>.
064       */
065      protected Builder(CborSerializer ctx) {
066         super(reqnn("ctx", ctx));
067      }
068
069      @Override
070      public CborSerializerSession build() {
071         return new CborSerializerSession(this);
072      }
073
074   }
075
076   private record SimpleMapEntry(Object key, Object value) {}
077
078   /**
079    * Creates a new builder for this object.
080    *
081    * @param ctx The context creating this session.
082    *    <br>Cannot be <jk>null</jk>.
083    * @return A new builder.
084    */
085   public static Builder create(CborSerializer ctx) {
086      return new Builder(reqnn("ctx", ctx));
087   }
088
089   /**
090    * Converts the specified output target object to an {@link CborOutputStream}.
091    */
092   private static CborOutputStream getCborOutputStream(SerializerPipe out) throws IOException {
093      Object output = out.getRawOutput();
094      if (output instanceof CborOutputStream output2)
095         return output2;
096      var os = new CborOutputStream(out.getOutputStream());
097      out.setOutputStream(os);
098      return os;
099   }
100
101   /**
102    * Constructor.
103    *
104    * @param builder The builder for this object.
105    */
106   protected CborSerializerSession(Builder builder) {
107      super(builder);
108   }
109
110   /**
111    * Opens a low-level push generator that emits CBOR (RFC 8949) one structural event at a time,
112    * bound to this live session.
113    *
114    * <p>
115    * Containers ({@code startObject} / {@code startArray}) are emitted as CBOR
116    * <b>indefinite-length</b> markers (<c>0xBF</c> / <c>0x9F</c>) so the caller doesn't have to
117    * know element counts up front; matching {@code endXxx} calls emit the BREAK byte
118    * (<c>0xFF</c>).  Resulting bytes are valid RFC-8949 CBOR.
119    *
120    * <h5 class='section'>Builder properties honored:</h5>
121    * Databind-level settings flow through to {@link TokenWriter#object(Object) object(Object)}:
122    * <c>keepNullProperties</c>, <c>trimEmptyMaps</c>, <c>trimEmptyCollections</c>,
123    * <c>sortMaps</c>, <c>sortCollections</c>, <c>trimStrings</c>.
124    *
125    * <h5 class='section'>Builder properties NOT honored:</h5>
126    * <c>useWhitespace</c>, <c>maxIndent</c>, <c>quoteChar</c>, <c>escapeSolidus</c> (binary
127    * format &mdash; no text-formatting concepts apply); <c>uriContext</c>,
128    * <c>uriResolution</c>, <c>uriRelativity</c>, <c>listener</c>.
129    *
130    * @param output The output.  Accepts an {@link OutputStream}.
131    * @return A new {@link CborTokenWriter}.
132    * @throws IOException If the output type is not supported or could not be opened.
133    */
134   @Override /* TokenWritable */
135   public TokenWriter writeTokens(Object output) throws IOException {
136      if (output == null)
137         throw new IOException("Output cannot be null.");
138      if (!(output instanceof OutputStream output2))
139         throw ioex("Cannot convert object of type %s to an OutputStream.", output.getClass().getName());
140      var walk = new PojoWalker.Options(
141         isKeepNullProperties(),
142         isTrimEmptyMaps(),
143         isTrimEmptyCollections(),
144         isSortMaps(),
145         isSortCollections(),
146         isTrimStrings(),
147         getMarshallingContext());
148      return new CborTokenWriter(output2, new CborTokenWriter.Settings(walk));
149   }
150
151   /**
152    * Streaming array-element {@link RecordWriter} backed by {@link CborTokenWriter}.
153    * Memory is O(1) &mdash; CBOR's writer always uses indefinite-length arrays
154    * (0x9F .. 0xFF) so element count is not required up front.
155    *
156    * @param output The output.
157    * @return A new element-streamed {@link RecordWriter}.
158    * @throws IOException If a problem occurred opening the underlying output.
159    */
160   @Override /* ArrayRecordWritable */
161   public RecordWriter writeArrayRecords(Object output) throws IOException {
162      return StreamingArrayRecord.writer(writeTokens(output));
163   }
164
165   /*
166    * Workhorse method.
167    * Determines the type of object, and then calls the appropriate type-specific serialization method.
168    */
169   @SuppressWarnings({
170      "java:S3776" // Cognitive complexity acceptable for serialization dispatch logic
171   })
172   private CborOutputStream writeAnything(CborOutputStream out, Object o, ClassMeta<?> eType, String attrName, BeanPropertyMeta pMeta) throws SerializeException {
173
174      if (o == null)
175         return out.appendNull();
176
177      if (eType == null)
178         eType = object();
179
180      ClassMeta<?> aType = null;
181      ClassMeta<?> sType = null;
182
183      aType = push2(attrName, o, eType);
184
185      if (aType == null)
186         return out.appendNull();
187
188      if (isOptional(aType)) {
189         o = getOptionalValue(o);
190         eType = getOptionalType(eType);
191         aType = getClassMetaForObject(o, object());
192      }
193
194      sType = aType;
195      String typeName = getBeanTypeName(this, eType, aType, pMeta);
196
197      var swap = aType.getSwap(this);
198      if (nn(swap)) {
199         o = swap(swap, o);
200         sType = swap.getSwapClassMeta(this);
201
202         if (sType.isObject())
203            sType = getClassMetaForObject(o);
204      }
205
206      if (o == null || (sType.isChar() && ((Character)o).charValue() == 0))
207         out.appendNull();
208      else if (sType.isBean())
209         writeBeanMap(out, toBeanMap(o), typeName);
210      else if (sType.isMap()) {
211         if (sType.isBeanMap())
212            writeBeanMap(out, (BeanMap)o, typeName);
213         else
214            writeMap(out, (Map)o, eType);
215      } else if (sType.isCollection()) {
216         writeCollection(out, (Collection)o, eType);
217      } else if (sType.isByteArray()) {
218         var binaryFormat = getBinaryFormat();
219         if (binaryFormat == BinaryFormat.NOT_SET)
220            out.appendBinary((byte[])o);
221         else
222            out.appendString(binaryFormat.format((byte[])o));
223      } else if (sType.isArray()) {
224         writeCollection(out, toList(sType.inner(), o), eType);
225      } else if (sType.isBoolean()) {
226         out.appendBoolean((Boolean)o);
227      } else if (sType.isNumber()) {
228         out.appendNumber((Number)o);
229      } else if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) {
230         out.appendString(resolveUri(o.toString()));
231      } else if (sType.isDate()) {
232         out.appendString(writeDate((Date)o, sType));
233      } else if (sType.isCalendar()) {
234         out.appendString(writeCalendar(o, sType));
235      } else if (sType.isTemporal()) {
236         out.appendString(writeTemporal((TemporalAccessor)o, sType));
237      } else if (sType.isDuration()) {
238         appendDuration(out, (Duration)o);
239      } else if (sType.isPeriod()) {
240         out.appendString(writePeriod((Period)o));
241      } else if (sType.isStreamable()) {
242         writeCollection(out, toListFromStreamable(o, sType), eType);
243      } else if (sType.isReader()) {
244         pipe((Reader)o, out, SerializerSession::handleThrown);
245      } else if (sType.isInputStream()) {
246         pipe((InputStream)o, out, SerializerSession::handleThrown);
247      } else
248         out.appendString(toString(o));
249
250      pop();
251      return out;
252   }
253
254   private void writeBeanMap(CborOutputStream out, BeanMap<?> m, String typeName) throws SerializeException {
255
256      Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x);
257
258      var values = new ArrayList<BeanPropertyValue>();
259
260      if (nn(typeName)) {
261         BeanPropertyMeta pm = m.getMeta().getTypeProperty();
262         values.add(new BeanPropertyValue(pm, pm.getName(), typeName, null));
263      }
264
265      m.forEachValue(checkNull, (pMeta, key, value, thrown) -> {
266         if (nn(thrown)) {
267            onBeanGetterException(pMeta, thrown);
268            return;
269         }
270         var p = new BeanPropertyValue(pMeta, key, value, null);
271
272         if ((! isKeepNullProperties()) && willRecurse(p)) {
273            return;
274         }
275
276         values.add(p);
277      });
278
279      out.startMap(values.size());
280
281      values.forEach(x -> {
282         BeanPropertyMeta pMeta = x.getMeta();
283         if (pMeta.canRead()) {
284            ClassMeta<?> cMeta = (ClassMeta<?>) x.getBeanInfo();
285            String key = x.getName();
286            Object value = x.getValue();
287            writeAnything(out, key, null, null, null);
288            writeAnything(out, value, cMeta, key, pMeta);
289         }
290      });
291   }
292
293   private void writeCollection(CborOutputStream out, Collection c, ClassMeta<?> type) throws SerializeException {
294      var elementType = type.getElementType();
295      List<Object> l = listOfSize(c.size());
296      c = sort(c);
297      l.addAll(c);
298      out.startArray(l.size());
299      l.forEach(x -> writeAnything(out, x, elementType, "<iterator>", null));
300   }
301
302   private void writeMap(CborOutputStream out, Map m, ClassMeta<?> type) throws SerializeException {
303
304      var keyType = type.getKeyType();
305      var valueType = type.getValueType();
306
307      m = sort(m);
308
309      List<SimpleMapEntry> entries = listOfSize(m.size());
310      m.forEach((k, v) -> entries.add(new SimpleMapEntry(k, v)));
311
312      out.startMap(entries.size());
313
314      entries.forEach(x -> {
315         Object value = x.value();
316         Object key = generalize(x.key(), keyType);
317         writeAnything(out, key, keyType, null, null);
318         writeAnything(out, value, valueType, null, null);
319      });
320   }
321
322   private boolean willRecurse(BeanPropertyValue v) throws SerializeException {
323      var aType = push2(v.getName(), v.getValue(), (ClassMeta<?>) v.getBeanInfo());
324      if (nn(aType))
325         pop();
326      return aType == null;
327   }
328
329   @Override /* Overridden from SerializerSession */
330   protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException {
331      writeAnything(getCborOutputStream(out), o, getExpectedRootType(o), "root", null);
332   }
333
334   private void appendDuration(CborOutputStream out, Duration value) {
335      var f = getDurationFormat();
336      var s = writeDuration(value);
337      if (f == DurationFormat.NANOS || f == DurationFormat.MILLIS)
338         out.appendNumber(Long.parseLong(s));
339      else if (f == DurationFormat.SECONDS)
340         out.appendNumber(Double.parseDouble(s));
341      else
342         out.appendString(s);
343   }
344
345}