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.bson;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.marshall.bson.DataType.*;
021
022import java.io.*;
023import java.math.*;
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 BsonSerializer}.
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/Bson">BSON</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   "java:S3776", // Cognitive complexity acceptable for serialization dispatch logic
049   "rawtypes", // Raw types necessary for generic type handling
050   "resource", // Output streams managed by calling code
051   "unchecked" // Cast from raw type is safe by checked conditional above.
052})
053public class BsonSerializerSession extends OutputStreamSerializerSession implements RecordWritable, ArrayRecordWritable {
054
055   /**
056    * Builder class.
057    */
058   public static class Builder extends OutputStreamSerializerSession.Builder<Builder> {
059
060      private final BsonSerializer ctx;
061
062      /**
063       * Constructor
064       *
065       * @param ctx The context creating this session.
066       *    <br>Cannot be <jk>null</jk>.
067       */
068      protected Builder(BsonSerializer ctx) {
069         super(reqnn("ctx", ctx));
070         this.ctx = ctx;
071      }
072
073      @Override
074      public BsonSerializerSession build() {
075         return new BsonSerializerSession(this);
076      }
077   }
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(BsonSerializer ctx) {
087      return new Builder(reqnn("ctx", ctx));
088   }
089
090   private static BsonOutputStream getBsonOutputStream(SerializerPipe out) throws IOException {
091      Object output = out.getRawOutput();
092      if (output instanceof BsonOutputStream output2)
093         return output2;
094      var os = new BsonOutputStream(out.getOutputStream());
095      out.setOutputStream(os);
096      return os;
097   }
098
099   private final BsonSerializer ctx;
100
101   protected BsonSerializerSession(Builder builder) {
102      super(builder);
103      ctx = builder.ctx;
104   }
105
106   /**
107    * Opens a whole-value push generator targeting BSON output, bound to this live session.
108    * {@link RecordWriter#write(Object) write(Object)} delegates to
109    * {@link SerializerSession#write(Object, Object)}.
110    *
111    * @param output The output.
112    * @return A new {@link RecordWriter}.
113    * @throws IOException If a problem occurred opening the underlying output.
114    */
115   @Override /* RecordWritable */
116   public RecordWriter writeRecords(Object output) throws IOException {
117      return RecordAdapter.writer(this, output);
118   }
119
120   /**
121    * Buffered array-element {@link RecordWriter} for BSON, bound to this live session.  BSON's wire
122    * format requires the entire document byte-length up front (length prefix at every nesting
123    * level), so streaming without buffering is not possible.
124    *
125    * @param output The output.
126    * @return A buffered {@link RecordWriter}.
127    * @throws IOException If a problem occurred opening the underlying output.
128    */
129   @Override /* ArrayRecordWritable */
130   public RecordWriter writeArrayRecords(Object output) throws IOException {
131      return RecordAdapter.arrayWriter(this, output);
132   }
133
134   /**
135    * The BSON record writer is buffered/{@link RecordAdapter}-backed, not O(1) streaming.
136    *
137    * @return Always <jk>false</jk>.
138    */
139   @Override /* RecordWritable */
140   public boolean isRecordStreaming() { return false; }
141
142   /**
143    * The BSON array-record writer is buffered/{@link RecordAdapter}-backed, not O(1) streaming.
144    *
145    * @return Always <jk>false</jk>.
146    */
147   @Override /* ArrayRecordWritable */
148   public boolean isArrayRecordStreaming() { return false; }
149
150   private void writeElement(BsonOutputStream out, String name, Object o, ClassMeta<?> eType, BeanPropertyMeta pMeta) throws SerializeException {
151      if (o == null) {
152         out.writeElement(NULL.value, name);
153         return;
154      }
155      if (eType == null)
156         eType = object();
157
158      var aType = push2(name, o, eType);
159      if (aType == null) {
160         out.writeElement(NULL.value, name);
161         pop();
162         return;
163      }
164
165      if (isOptional(aType)) {
166         o = getOptionalValue(o);
167         eType = getOptionalType(eType);
168         aType = getClassMetaForObject(o, object());
169      }
170
171      var sType = aType;
172      var swap = aType.getSwap(this);
173      if (nn(swap)) {
174         o = swap(swap, o);
175         sType = swap.getSwapClassMeta(this);
176         if (sType.isObject())
177            sType = getClassMetaForObject(o);
178      }
179
180      if (o == null || (sType.isChar() && ((Character)o).charValue() == 0)) {
181         out.writeElement(NULL.value, name);
182      } else if (sType.isBean()) {
183         out.writeElement(DOCUMENT.value, name);
184         var child = out.createChild();
185         child.startDocument();
186         writeBeanMap(child, toBeanMap(o), getBeanTypeName(this, eType, aType, pMeta));
187         out.writeChildDocument(child);
188      } else if (sType.isMap()) {
189         if (sType.isBeanMap()) {
190            out.writeElement(DOCUMENT.value, name);
191            var child = out.createChild();
192            child.startDocument();
193            writeBeanMap(child, (BeanMap)o, getBeanTypeName(this, eType, aType, pMeta));
194            out.writeChildDocument(child);
195         } else {
196            out.writeElement(DOCUMENT.value, name);
197            var child = out.createChild();
198            child.startDocument();
199            writeMap(child, (Map)o, eType);
200            out.writeChildDocument(child);
201         }
202      } else if (sType.isByteArray()) {
203         var binaryFormat = getBinaryFormat();
204         if (binaryFormat == BinaryFormat.NOT_SET) {
205            out.writeElement(BINARY.value, name);
206            out.writeBinary((byte[])o);
207         } else {
208            out.writeElement(STRING.value, name);
209            out.writeString(binaryFormat.format((byte[])o));
210         }
211      } else if (sType.isCollection() || sType.isArray()) {
212         out.writeElement(ARRAY.value, name);
213         var child = out.createChild();
214         child.startDocument();
215         writeArray(child, sType.isArray() ? toList(sType.inner(), o) : (Collection)o, eType);
216         out.writeChildDocument(child);
217      } else if (sType.isCharSequence() || sType.isChar() || sType.isEnum()) {
218         out.writeElement(STRING.value, name);
219         out.writeString(trim(o));
220      } else if (sType.isBoolean()) {
221         out.writeElement(BOOLEAN.value, name);
222         out.writeBoolean((Boolean)o);
223      } else if (sType.isNumber()) {
224         if (sType.inner() == int.class || sType.inner() == short.class || sType.inner() == byte.class
225            || sType.inner() == Integer.class || sType.inner() == Short.class || sType.inner() == Byte.class) {
226            out.writeElement(INT32.value, name);
227            out.writeInt32(((Number)o).intValue());
228         } else if (sType.inner() == long.class || sType.inner() == Long.class) {
229            out.writeElement(INT64.value, name);
230            out.writeInt64(((Number)o).longValue());
231         } else if (sType.inner() == BigDecimal.class) {
232            out.writeElement(DECIMAL128.value, name);
233            out.writeDecimal128((BigDecimal)o);
234         } else if (sType.inner() == BigInteger.class) {
235            out.writeElement(DECIMAL128.value, name);
236            out.writeDecimal128(new BigDecimal((BigInteger)o));
237         } else {
238            out.writeElement(DOUBLE.value, name);
239            out.writeDouble(((Number)o).doubleValue());
240         }
241      } else if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) {
242         out.writeElement(STRING.value, name);
243         out.writeString(resolveUri(o.toString()));
244      } else if (sType.isDate()) {
245         if (ctx.writeDatesAsDatetime) {
246            out.writeElement(DATETIME.value, name);
247            out.writeDateTime(toEpochMillis(o));
248         } else {
249            out.writeElement(STRING.value, name);
250            out.writeString(writeDate((Date)o, sType));
251         }
252      } else if (sType.isCalendar()) {
253         if (ctx.writeDatesAsDatetime) {
254            out.writeElement(DATETIME.value, name);
255            out.writeDateTime(toEpochMillis(o));
256         } else {
257            out.writeElement(STRING.value, name);
258            out.writeString(writeCalendar(o, sType));
259         }
260      } else if (sType.isTemporal()) {
261         // BSON datetime (0x09) is int64 millis-since-epoch, so only instant-bearing temporals can use it.
262         // Local temporals (LocalDate/LocalDateTime/LocalTime/Year/...) cannot supply an instant; fall back to
263         // the ISO-string path instead of crashing in Instant.from(...) (matches CBOR/MsgPack temporal handling).
264         if (ctx.writeDatesAsDatetime && isInstantCapable(o)) {
265            out.writeElement(DATETIME.value, name);
266            out.writeDateTime(toEpochMillis(o));
267         } else {
268            out.writeElement(STRING.value, name);
269            out.writeString(writeTemporal((TemporalAccessor)o, sType));
270         }
271      } else if (sType.isDuration()) {
272         writeDuration(out, name, (Duration)o);
273      } else if (sType.isPeriod()) {
274         out.writeElement(STRING.value, name);
275         out.writeString(writePeriod((Period)o));
276      } else if (sType.isStreamable()) {
277         out.writeElement(ARRAY.value, name);
278         var child = out.createChild();
279         child.startDocument();
280         writeArray(child, toListFromStreamable(o, sType), eType);
281         out.writeChildDocument(child);
282      } else {
283         out.writeElement(STRING.value, name);
284         out.writeString(toString(o));
285      }
286      pop();
287   }
288
289   private void writeBeanMap(BsonOutputStream out, BeanMap<?> m, String typeName) throws SerializeException {
290      Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x);
291      var values = new ArrayList<BeanPropertyValue>();
292
293      if (nn(typeName)) {
294         var pm = m.getMeta().getTypeProperty();
295         if (pm != null)
296            values.add(new BeanPropertyValue(pm, pm.getName(), typeName, null));
297      }
298
299      m.forEachValue(checkNull, (pMeta, key, value, thrown) -> {
300         if (nn(thrown)) {
301            onBeanGetterException(pMeta, thrown);
302            return;
303         }
304         var p = new BeanPropertyValue(pMeta, key, value, null);
305         if (!isKeepNullProperties() && willRecurse(p))
306            return;
307         values.add(p);
308      });
309
310      for (var x : values) {
311         var pMeta = x.getMeta();
312         if (pMeta.canRead())
313            writeElement(out, x.getName(), x.getValue(), (ClassMeta<?>) x.getBeanInfo(), pMeta);
314      }
315   }
316
317   private void writeArray(BsonOutputStream out, Collection c, ClassMeta<?> type) throws SerializeException {
318      var elementType = type.getElementType();
319      c = sort(c);
320      var idx = 0;
321      for (var x : c)
322         writeElement(out, String.valueOf(idx++), x, elementType, null);
323   }
324
325   private void writeMap(BsonOutputStream out, Map m, ClassMeta<?> type) throws SerializeException {
326      var valueType = type.getValueType();
327      var keyType = type.getKeyType();
328      m = sort(m);
329      for (Map.Entry<?,?> entry : ((Map<?,?>)m).entrySet()) {
330         var key = generalize(entry.getKey(), keyType);
331         var name = (key == null) ? ctx.nullKeyString : trim(key);
332         writeElement(out, name, entry.getValue(), valueType, null);
333      }
334   }
335
336   /**
337    * Returns whether the given value can supply an instant for BSON datetime (0x09) encoding.
338    *
339    * <p>
340    * Only instant-bearing temporals (such as {@link Instant}, {@link OffsetDateTime}, and {@link ZonedDateTime})
341    * support {@link ChronoField#INSTANT_SECONDS}.  Local temporals ({@link LocalDate}, {@link LocalDateTime},
342    * {@link LocalTime}, {@link Year}, {@link YearMonth}, {@link MonthDay}) cannot, so they must fall back to the
343    * ISO-string path rather than crashing in {@link Instant#from(TemporalAccessor)}.
344    *
345    * @param o The value being serialized.
346    * @return <jk>true</jk> if the value is an instant-bearing temporal.
347    */
348   private static boolean isInstantCapable(Object o) {
349      return o instanceof TemporalAccessor o2 && o2.isSupported(ChronoField.INSTANT_SECONDS);
350   }
351
352   private static long toEpochMillis(Object o) {
353      if (o instanceof Date o2)
354         return o2.getTime();
355      if (o instanceof Calendar o2)
356         return o2.getTimeInMillis();
357      if (o instanceof Instant o2)
358         return o2.toEpochMilli();
359      if (o instanceof Temporal o2)
360         return Instant.from(o2).toEpochMilli();
361      return 0L;
362   }
363
364   private boolean willRecurse(BeanPropertyValue v) throws SerializeException {
365      var aType = push2(v.getName(), v.getValue(), (ClassMeta<?>) v.getBeanInfo());
366      if (nn(aType))
367         pop();
368      return aType == null;
369   }
370
371   @Override
372   protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException {
373      var bsonOut = getBsonOutputStream(out);
374      bsonOut.startDocument();
375
376      var eType = getExpectedRootType(o);
377      var aType = push2("root", o, eType);
378      ClassMeta<?> sType = aType;
379      if (nn(aType)) {
380         var swap = aType.getSwap(this);
381         if (nn(swap)) {
382            o = swap(swap, o);
383            sType = swap.getSwapClassMeta(this);
384         }
385      }
386
387      if (sType != null && sType.isBean()) {
388         pop();
389         writeBeanMap(bsonOut, toBeanMap(o), getBeanTypeName(this, eType, sType, null));
390      } else if (sType != null && sType.isMap() && !sType.isBeanMap()) {
391         pop();
392         writeMap(bsonOut, (Map)o, eType);
393      } else {
394         pop();
395         writeElement(bsonOut, "value", o, eType, null);
396      }
397
398      bsonOut.writeDocumentTo(out.getOutputStream());
399   }
400
401   private void writeDuration(BsonOutputStream out, String name, Duration value) {
402      var f = getDurationFormat();
403      var s = writeDuration(value);
404      if (f == DurationFormat.NANOS || f == DurationFormat.MILLIS) {
405         out.writeElement(INT64.value, name);
406         out.writeInt64(Long.parseLong(s));
407      } else if (f == DurationFormat.SECONDS) {
408         out.writeElement(DOUBLE.value, name);
409         out.writeDouble(Double.parseDouble(s));
410      } else {
411         out.writeElement(STRING.value, name);
412         out.writeString(s);
413      }
414   }
415
416}