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.msgpack;
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 MsgPackSerializer}.
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/MessagePackSupport">MessagePack</a>
043
044 * </ul>
045 */
046@SuppressWarnings({
047   "java:S110", // Inheritance depth acceptable for serializer session hierarchy
048   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
049   "rawtypes", // writeMap()/writeCollection() take raw Map/Collection because values arrive as Object and are cast after ClassMeta type checks
050   "resource", // Output streams managed by calling code
051   "unchecked" // Type erasure requires unchecked operations
052})
053public class MsgPackSerializerSession extends OutputStreamSerializerSession implements TokenWritable, ArrayRecordWritable {
054
055   /**
056    * Builder class.
057    */
058   public static class Builder extends OutputStreamSerializerSession.Builder<Builder> {
059
060      /**
061       * Constructor
062       *
063       * @param ctx The context creating this session.
064       *    <br>Cannot be <jk>null</jk>.
065       */
066      protected Builder(MsgPackSerializer ctx) {
067         super(reqnn("ctx", ctx));
068      }
069
070      @Override
071      public MsgPackSerializerSession build() {
072         return new MsgPackSerializerSession(this);
073      }
074
075   }
076
077   private record SimpleMapEntry(Object key, Object value) {}
078
079   /**
080    * Creates a new builder for this object.
081    *
082    * @param ctx The context creating this session.
083    *    <br>Cannot be <jk>null</jk>.
084    * @return A new builder.
085    */
086   public static Builder create(MsgPackSerializer ctx) {
087      return new Builder(reqnn("ctx", ctx));
088   }
089
090   /**
091    * Converts the specified output target object to an {@link MsgPackOutputStream}.
092    */
093   private static MsgPackOutputStream getMsgPackOutputStream(SerializerPipe out) throws IOException {
094      Object output = out.getRawOutput();
095      if (output instanceof MsgPackOutputStream output2)
096         return output2;
097      var os = new MsgPackOutputStream(out.getOutputStream());
098      out.setOutputStream(os);
099      return os;
100   }
101
102   /**
103    * Constructor.
104    *
105    * @param builder The builder for this object.
106    */
107   protected MsgPackSerializerSession(Builder builder) {
108      super(builder);
109   }
110
111   /**
112    * Opens a low-level push generator that emits MessagePack one structural event at a time,
113    * bound to this live session.
114    *
115    * <p>
116    * MsgPack containers are length-prefixed with no indefinite-length encoding, so the writer
117    * <b>buffers</b> each open container's body bytes until the matching {@code endXxx} call;
118    * memory cost is O(largest open container).
119    *
120    * <h5 class='section'>Builder properties honored:</h5>
121    * <c>keepNullProperties</c>, <c>trimEmptyMaps</c>, <c>trimEmptyCollections</c>, <c>sortMaps</c>,
122    * <c>sortCollections</c>, <c>trimStrings</c> &mdash; all flow through to
123    * {@link TokenWriter#object(Object) object(Object)}.
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); <c>uriContext</c>, <c>uriResolution</c>, <c>uriRelativity</c>, <c>listener</c>.
128    *
129    * @param output The output.
130    * @return A new {@link MsgPackTokenWriter}.
131    * @throws IOException If the output type is not supported or could not be opened.
132    */
133   @Override /* TokenWritable */
134   public TokenWriter writeTokens(Object output) throws IOException {
135      if (output == null)
136         throw new IOException("Output cannot be null.");
137      if (!(output instanceof OutputStream output2))
138         throw ioex("Cannot convert object of type %s to an OutputStream.", output.getClass().getName());
139      var walk = new PojoWalker.Options(
140         isKeepNullProperties(),
141         isTrimEmptyMaps(),
142         isTrimEmptyCollections(),
143         isSortMaps(),
144         isSortCollections(),
145         isTrimStrings(),
146         getMarshallingContext());
147      return new MsgPackTokenWriter(output2, new MsgPackTokenWriter.Settings(walk));
148   }
149
150   /**
151    * Buffered array-element {@link RecordWriter}.  MsgPack's array wire format is
152    * length-prefixed, so streaming without a known element count requires either buffering
153    * (this method) or the {@link #writeArrayRecords(Object, int)} overload that takes a count.
154    *
155    * @param output The output.
156    * @return A buffered {@link RecordWriter}.
157    * @throws IOException If a problem occurred opening the underlying output.
158    */
159   @Override /* ArrayRecordWritable */
160   public RecordWriter writeArrayRecords(Object output) throws IOException {
161      return RecordAdapter.arrayWriter(this, output);
162   }
163
164   @Override /* ArrayRecordWritable */
165   public boolean isArrayRecordStreaming() { return false; }
166
167   /**
168    * Streaming array-element {@link RecordWriter} that pre-declares the element count and
169    * writes each element directly to the output stream &mdash; no per-element buffering.
170    * The caller must pass exactly {@code expectedCount} {@link RecordWriter#write(Object)}
171    * calls before {@link RecordWriter#close()}; mismatch throws on close.
172    *
173    * @param output The output (must be an {@link OutputStream}).
174    * @param expectedCount The number of elements that will be written.
175    * @return A streaming {@link RecordWriter}.
176    * @throws IOException If a problem occurred opening the underlying output.
177    */
178   @Override /* ArrayRecordWritable */
179   public RecordWriter writeArrayRecords(Object output, int expectedCount) throws IOException {
180      if (!(output instanceof OutputStream))
181         throw new IOException("MsgPack streaming arrayRecordWriter requires an OutputStream");
182      var os = (OutputStream) output;
183      var mpos = new MsgPackOutputStream(os);
184      mpos.startArray(expectedCount);
185      return new RecordWriter() {
186         private int written;
187         private boolean closed;
188
189         @Override public RecordWriter write(Object value) throws IOException {
190            if (closed)
191               throw new IllegalStateException("Array stream is closed.");
192            if (written >= expectedCount)
193               throw new IllegalStateException(
194                  "Array stream is full; declared " + expectedCount + " elements, attempted to write more.");
195            try {
196               MsgPackSerializerSession.this.write(value, os);
197            } catch (SerializeException e) {
198               throw new IOException(e);
199            }
200            written++;
201            return this;
202         }
203         @Override public boolean isStreaming() {
204            return true;
205         }
206         @Override public void flush() throws IOException {
207            os.flush();
208         }
209         @Override public void close() throws IOException {
210            if (closed)
211               return;
212            closed = true;
213            if (written != expectedCount)
214               throw new IOException(
215                  "Array stream count mismatch: declared " + expectedCount + " but wrote " + written);
216            os.close();
217         }
218      };
219   }
220
221   /*
222    * Workhorse method.
223    * Determines the type of object, and then calls the appropriate type-specific serialization method.
224    */
225   @SuppressWarnings({
226      "java:S3776" // Cognitive complexity acceptable for serialization dispatch logic
227   })
228   private MsgPackOutputStream writeAnything(MsgPackOutputStream out, Object o, ClassMeta<?> eType, String attrName, BeanPropertyMeta pMeta) throws SerializeException {
229
230      if (o == null)
231         return out.appendNull();
232
233      if (eType == null)
234         eType = object();
235
236      ClassMeta<?> aType = null;       // The actual type
237      ClassMeta<?> sType = null;       // The serialized type
238
239      aType = push2(attrName, o, eType);
240
241      // Handle recursion
242      if (aType == null)
243         return out.appendNull();
244
245      // Handle Optional<X>
246      if (isOptional(aType)) {
247         o = getOptionalValue(o);
248         eType = getOptionalType(eType);
249         aType = getClassMetaForObject(o, object());
250      }
251
252      sType = aType;
253      String typeName = getBeanTypeName(this, eType, aType, pMeta);
254
255      // Swap if necessary
256      var swap = aType.getSwap(this);
257      if (nn(swap)) {
258         o = swap(swap, o);
259         sType = swap.getSwapClassMeta(this);
260
261         // If the getSwapClass() method returns Object, we need to figure out
262         // the actual type now.
263         if (sType.isObject())
264            sType = getClassMetaForObject(o);
265      }
266
267      // '\0' characters are considered null.
268      if (o == null || (sType.isChar() && ((Character)o).charValue() == 0))
269         out.appendNull();
270      else if (sType.isBean())
271         writeBeanMap(out, toBeanMap(o), typeName);
272      else if (sType.isMap()) {
273         if (sType.isBeanMap())
274            writeBeanMap(out, (BeanMap)o, typeName);
275         else
276            writeMap(out, (Map)o, eType);
277      } else if (sType.isCollection()) {
278         writeCollection(out, (Collection)o, eType);
279      } else if (sType.isByteArray()) {
280         var binaryFormat = getBinaryFormat();
281         if (binaryFormat == BinaryFormat.NOT_SET)
282            out.appendBinary((byte[])o);
283         else
284            out.appendString(binaryFormat.format((byte[])o));
285      } else if (sType.isArray()) {
286         writeCollection(out, toList(sType.inner(), o), eType);
287      } else if (sType.isBoolean()) {
288         out.appendBoolean((Boolean)o);
289      } else if (sType.isNumber()) {
290         out.appendNumber((Number)o);
291      } else if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) {
292         out.appendString(resolveUri(o.toString()));
293      } else if (sType.isDate()) {
294         out.appendString(writeDate((Date)o, sType));
295      } else if (sType.isCalendar()) {
296         out.appendString(writeCalendar(o, sType));
297      } else if (sType.isTemporal()) {
298         out.appendString(writeTemporal((TemporalAccessor)o, sType));
299      } else if (sType.isDuration()) {
300         appendDuration(out, (Duration)o);
301      } else if (sType.isPeriod()) {
302         out.appendString(writePeriod((Period)o));
303      } else if (sType.isStreamable()) {
304         // MsgPack protocol requires array size in header (startArray(size)), so materialization is unavoidable.
305         writeCollection(out, toListFromStreamable(o, sType), eType);
306      } else if (sType.isReader()) {
307         pipe((Reader)o, out, SerializerSession::handleThrown);
308      } else if (sType.isInputStream()) {
309         pipe((InputStream)o, out, SerializerSession::handleThrown);
310      } else
311         out.appendString(toString(o));
312
313      pop();
314      return out;
315   }
316
317   private void writeBeanMap(MsgPackOutputStream out, BeanMap<?> m, String typeName) throws SerializeException {
318
319      Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x);
320
321      var values = new ArrayList<BeanPropertyValue>();
322
323      if (nn(typeName)) {
324         BeanPropertyMeta pm = m.getMeta().getTypeProperty();
325         values.add(new BeanPropertyValue(pm, pm.getName(), typeName, null));
326      }
327
328      m.forEachValue(checkNull, (pMeta, key, value, thrown) -> {
329         if (nn(thrown)) {
330            onBeanGetterException(pMeta, thrown);
331            return;
332         }
333         var p = new BeanPropertyValue(pMeta, key, value, null);
334
335         if ((! isKeepNullProperties()) && willRecurse(p)) {
336            return; // Must handle the case where recursion occurs and property is not serialized.
337         }
338
339         values.add(p);
340      });
341
342      out.startMap(values.size());
343
344      values.forEach(x -> {
345         BeanPropertyMeta pMeta = x.getMeta();
346         if (pMeta.canRead()) {
347            ClassMeta<?> cMeta = (ClassMeta<?>) x.getBeanInfo();
348            String key = x.getName();
349            Object value = x.getValue();
350            writeAnything(out, key, null, null, null);
351            writeAnything(out, value, cMeta, key, pMeta);
352         }
353      });
354   }
355
356   private void writeCollection(MsgPackOutputStream out, Collection c, ClassMeta<?> type) throws SerializeException {
357      var elementType = type.getElementType();
358      List<Object> l = listOfSize(c.size());
359      c = sort(c);
360      l.addAll(c);
361      out.startArray(l.size());
362      l.forEach(x -> writeAnything(out, x, elementType, "<iterator>", null));
363   }
364
365   private void writeMap(MsgPackOutputStream out, Map m, ClassMeta<?> type) throws SerializeException {
366
367      var keyType = type.getKeyType();
368      var valueType = type.getValueType();
369
370      m = sort(m);
371
372      // The map size may change as we're iterating over it, so
373      // grab a snapshot of the entries in a separate list.
374      List<SimpleMapEntry> entries = listOfSize(m.size());
375      m.forEach((k, v) -> entries.add(new SimpleMapEntry(k, v)));
376
377      out.startMap(entries.size());
378
379      entries.forEach(x -> {
380         Object value = x.value();
381         Object key = generalize(x.key(), keyType);
382         writeAnything(out, key, keyType, null, null);
383         writeAnything(out, value, valueType, null, null);
384      });
385   }
386
387   private boolean willRecurse(BeanPropertyValue v) throws SerializeException {
388      var aType = push2(v.getName(), v.getValue(), (ClassMeta<?>) v.getBeanInfo());
389      if (nn(aType))
390         pop();
391      return aType == null;
392   }
393
394   @Override /* Overridden from SerializerSession */
395   protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException {
396      writeAnything(getMsgPackOutputStream(out), o, getExpectedRootType(o), "root", null);
397   }
398
399   @Override
400   public boolean isAddBeanTypes() { return super.isAddBeanTypes(); }
401
402   private void appendDuration(MsgPackOutputStream out, Duration value) {
403      var f = getDurationFormat();
404      var s = writeDuration(value);
405      if (f == DurationFormat.NANOS || f == DurationFormat.MILLIS)
406         out.appendNumber(Long.parseLong(s));
407      else if (f == DurationFormat.SECONDS)
408         out.appendNumber(Double.parseDouble(s));
409      else
410         out.appendString(s);
411   }
412}