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.commons.utils.SystemUtils.*;
021
022import java.io.*;
023import java.math.*;
024import java.time.*;
025import java.util.*;
026import java.util.concurrent.*;
027
028import org.apache.juneau.commons.bean.*;
029import org.apache.juneau.commons.collections.*;
030import org.apache.juneau.marshall.*;
031import org.apache.juneau.marshall.serializer.*;
032import org.apache.juneau.marshall.stream.*;
033
034/**
035 * Serializes POJO models to BSON (Binary JSON).
036 *
037 * <h5 class='section'>Media types:</h5>
038 *
039 * Handles <c>Accept</c> types:  <bc>application/bson</bc>
040 * <p>
041 * Produces <c>Content-Type</c> types: <bc>application/bson</bc>
042 *
043 * <h5 class='topic'>Description</h5>
044 * <p>
045 * Java-to-BSON type mapping:
046 * <ul class='spaced-list'>
047 *    <li>Maps and beans → BSON document (type 0x03)
048 *    <li>Collections and arrays → BSON array (type 0x04, document with numeric string keys)
049 *    <li>{@link String} / {@link Enum} → BSON string (type 0x02)
050 *    <li>{@link Boolean} → BSON boolean (type 0x08)
051 *    <li>{@code int} / {@code short} / {@code byte} → BSON int32 (type 0x10)
052 *    <li>{@code long} → BSON int64 (type 0x12)
053 *    <li>{@code float} / {@code double} → BSON double (type 0x01)
054 *    <li>{@link Date} / {@link Calendar} / {@link Instant} → BSON datetime (type 0x09) when
055 *       {@code writeDatesAsDatetime=true}; otherwise ISO 8601 string
056 *    <li>{@link Duration} → ISO 8601 duration string
057 *    <li>{@code byte[]} → BSON binary (type 0x05, subtype 0x00)
058 *    <li>{@link BigDecimal} / {@link BigInteger} → BSON decimal128 (type 0x13)
059 *    <li>{@code null} → BSON null (type 0x0A)
060 *    <li>All other types → fallback to {@code toString()} as string
061 * </ul>
062 *
063 * <h5 class='section'>Notes:</h5><ul>
064 *    <li class='note'>This class is thread safe and reusable.
065 * </ul>
066 *
067 * <h5 class='section'>Limitations:</h5><ul>
068 *    <li>BSON requires a top-level document. Standalone scalars, collections, and arrays are wrapped as <c>{value: x}</c>.
069 *    <li>Map keys must be strings. Non-string keys use <c>toString()</c>. Null keys use a configurable placeholder.
070 *    <li>ObjectId (0x07) is not generated; parsing returns 24-character hex strings.
071 *    <li>Deprecated types (Undefined, DBPointer, Symbol, JS with Scope) are skipped during parsing.
072 *    <li>MongoDB-internal types (JavaScript, Timestamp, MinKey, MaxKey) are not supported.
073 *    <li>{@link BigDecimal} / {@link BigInteger} values outside the Decimal128 range (max 34 significant digits,
074 *       exponent -6176..6111) cannot be represented and throw a {@link SerializeException} with a clear message.
075 *    <li>BSON datetime (0x09) is millisecond-resolution, so sub-millisecond (nanosecond) precision of
076 *       {@link Instant}/{@link OffsetDateTime}/{@link ZonedDateTime} is truncated when
077 *       {@code writeDatesAsDatetime=true}. Use {@code writeDatesAsDatetime=false} for lossless ISO-8601 string output.
078 *    <li>Local {@code java.time} types ({@link LocalDate}, {@link LocalDateTime}, {@link LocalTime}, {@link Year},
079 *       etc.) have no instant, so they are always written as ISO-8601 strings even when {@code writeDatesAsDatetime=true}.
080 *    <li>BSON is binary; <c>SpacedHex</c> and <c>Base64</c> subclasses provide text-encodable output.
081 * </ul>
082 *
083 * <h5 class='section'>Example:</h5>
084 * <p class='bjava'>
085 *    <jc>// Serialize a POJO to BSON</jc>
086 *    byte[] <jv>bson</jv> = BsonSerializer.<jsf>DEFAULT</jsf>.write(<jv>someObject</jv>);
087 *
088 *    <jc>// Create a serializer that writes dates as ISO 8601 strings instead of BSON datetime</jc>
089 *    BsonSerializer <jv>s</jv> = BsonSerializer.<jsm>create</jsm>().writeDatesAsDatetime(<jk>false</jk>).build();
090 *
091 *    <jc>// Or use the marshaller for convenience</jc>
092 *    <jv>bson</jv> = Bson.<jsm>of</jsm>(<jv>someObject</jv>);
093 * </p>
094 *
095 * <h5 class='section'>See Also:</h5><ul>
096 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Bson">BSON</a>
097 * </ul>
098 */
099@SuppressWarnings({
100   "java:S110", // Inheritance depth acceptable for this class hierarchy
101   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
102   "java:S9149", // Per-format static factories intentionally shadow the parent's.
103   "resource" // Closeable resources are owned by the caller's serializer session; Eclipse JDT @Owning warning is by design.
104})
105public class BsonSerializer extends OutputStreamSerializer implements BsonMetaProvider, RecordWritable, ArrayRecordWritable {
106
107   private static final String DEFAULT_NULL_KEY = "<NULL>";
108
109   /** Default serializer, BASE64 string output. */
110   public static class Base64 extends BsonSerializer {
111
112      /**
113       * Constructor.
114       *
115       * @param builder The builder for this object.
116       */
117      public Base64(Builder builder) {
118         super(reqnn("builder", builder).binaryFormat(BinaryFormat.BASE64));
119      }
120   }
121
122   /**
123    * Builder class.
124    */
125   public static class Builder extends OutputStreamSerializer.Builder<Builder> {
126
127      private static final Cache<HashKey,BsonSerializer> CACHE = Cache.of(HashKey.class, BsonSerializer.class).build();
128
129      private boolean addBeanTypesBson;
130      private String nullKeyString = DEFAULT_NULL_KEY;
131      private boolean writeDatesAsDatetime = true;
132
133      /**
134       * Constructor, default settings.
135       */
136      protected Builder() {
137         produces("application/bson");
138         accept("application/bson");
139         addBeanTypesBson = env("BsonSerializer.addBeanTypesBson", false);
140         nullKeyString = env("BsonSerializer.nullKeyString", DEFAULT_NULL_KEY);
141         writeDatesAsDatetime = env("BsonSerializer.writeDatesAsDatetime", true);
142      }
143
144      /**
145       * Copy constructor.
146       *
147       * @param copyFrom The builder to copy from.
148       *    <br>Cannot be <jk>null</jk>.
149       */
150      protected Builder(Builder copyFrom) {
151         super(reqnn("copyFrom", copyFrom));
152         addBeanTypesBson = copyFrom.addBeanTypesBson;
153         nullKeyString = copyFrom.nullKeyString;
154         writeDatesAsDatetime = copyFrom.writeDatesAsDatetime;
155      }
156
157      /**
158       * Copy constructor.
159       *
160       * @param copyFrom The bean to copy from.
161       *    <br>Cannot be <jk>null</jk>.
162       */
163      protected Builder(BsonSerializer copyFrom) {
164         super(reqnn("copyFrom", copyFrom));
165         addBeanTypesBson = copyFrom.addBeanTypesBson;
166         nullKeyString = copyFrom.nullKeyString;
167         writeDatesAsDatetime = copyFrom.writeDatesAsDatetime;
168      }
169
170      /**
171       * Add <js>"_type"</js> properties when needed.
172       *
173       * @param value The value for this setting.
174       * @return This object.
175       */
176      public Builder addBeanTypesBson(boolean value) {
177         addBeanTypesBson = value;
178         return this;
179      }
180
181      /**
182       * The string used to represent <jk>null</jk> map keys in BSON documents.
183       *
184       * <p>
185       * BSON document field names cannot be <jk>null</jk>. When serializing a {@link Map} with a <jk>null</jk> key,
186       * this string is used as the field name. When parsing, a field with this name is stored with <jk>null</jk>
187       * as the key.
188       *
189       * @param value The placeholder string for null keys. Default is <js>"&lt;NULL&gt;"</js>.
190       * @return This object.
191       */
192      public Builder nullKeyString(String value) {
193         nullKeyString = value == null ? DEFAULT_NULL_KEY : value;
194         return this;
195      }
196
197      /**
198       * Use BSON datetime type for dates. When <jk>false</jk>, dates are serialized as ISO-8601 strings.
199       *
200       * @param value The value for this setting.
201       * @return This object.
202       */
203      public Builder writeDatesAsDatetime(boolean value) {
204         writeDatesAsDatetime = value;
205         return this;
206      }
207
208      @Override /* OutputStreamSerializer.Builder<?> */
209      public Builder binaryFormat(BinaryFormat value) {
210         super.binaryFormat(value);
211         return this;
212      }
213
214      @Override /* Serializer.Builder<?> */
215      public Builder keepNullProperties() {
216         super.keepNullProperties();
217         return this;
218      }
219
220      @Override /* Serializer.Builder<?> */
221      public Builder keepNullProperties(boolean value) {
222         super.keepNullProperties(value);
223         return this;
224      }
225
226      @Override /* Serializer.Builder<?> */
227      public Builder addRootType() {
228         super.addRootType();
229         return this;
230      }
231
232      @Override /* Serializer.Builder<?> */
233      public Builder addRootType(boolean value) {
234         super.addRootType(value);
235         return this;
236      }
237
238      @Override /* Context.Builder<?> */
239      public BsonSerializer build() {
240         return cache(CACHE).build(BsonSerializer.class);
241      }
242
243      @Override /* Context.Builder<?> */
244      public HashKey hashKey() {
245         return HashKey.of(super.hashKey(), addBeanTypesBson, nullKeyString, writeDatesAsDatetime);
246      }
247
248      @Override /* Context.Builder<?> */
249      public Builder copy() {
250         return new Builder(this);
251      }
252   }
253
254   /** Default serializer, spaced-hex string output. */
255   public static class SpacedHex extends BsonSerializer {
256
257      /**
258       * Constructor.
259       *
260       * @param builder The builder for this object.
261       */
262      public SpacedHex(Builder builder) {
263         super(builder.binaryFormat(BinaryFormat.SPACED_HEX));
264      }
265   }
266
267   /** Default serializer, all default settings. */
268   public static final BsonSerializer DEFAULT = new BsonSerializer(create());
269   /** Default serializer, spaced-hex string output. */
270   public static final BsonSerializer DEFAULT_SPACED_HEX = new SpacedHex(create());
271   /** Default serializer, BASE64 string output. */
272   public static final BsonSerializer DEFAULT_BASE64 = new Base64(create());
273
274   /**
275    * Creates a new builder for this object.
276    *
277    * @return A new builder.
278    */
279   public static Builder create() {
280      return new Builder();
281   }
282
283   protected final boolean addBeanTypesBson;
284   protected final String nullKeyString;
285   protected final boolean writeDatesAsDatetime;
286
287   private final Map<BeanPropertyMeta,BsonBeanPropertyMeta> bsonBeanPropertyMetas = new ConcurrentHashMap<>();
288   private final Map<ClassMeta<?>,BsonClassMeta> bsonClassMetas = new ConcurrentHashMap<>();
289
290   /**
291    * Constructor.
292    *
293    * @param builder The builder for this object.
294    */
295   public BsonSerializer(Builder builder) {
296      super(builder);
297      addBeanTypesBson = builder.addBeanTypesBson;
298      nullKeyString = builder.nullKeyString;
299      writeDatesAsDatetime = builder.writeDatesAsDatetime;
300   }
301
302   @Override /* Context */
303   public Builder copy() {
304      return new Builder(this);
305   }
306
307   @Override /* Context */
308   public BsonSerializerSession.Builder createSession() {
309      return BsonSerializerSession.create(this);
310   }
311
312   @Override /* BsonMetaProvider */
313   public BsonBeanPropertyMeta getBsonBeanPropertyMeta(BeanPropertyMeta bpm) {
314      if (bpm == null)
315         return BsonBeanPropertyMeta.DEFAULT;
316      return bsonBeanPropertyMetas.computeIfAbsent(bpm, k -> new BsonBeanPropertyMeta(k.getDelegateFor(), this));
317   }
318
319   @Override /* BsonMetaProvider */
320   public BsonClassMeta getBsonClassMeta(ClassMeta<?> cm) {
321      return bsonClassMetas.computeIfAbsent(cm, k -> new BsonClassMeta(k, this));
322   }
323
324   @Override /* Context */
325   public BsonSerializerSession getSession() {
326      return createSession().build();
327   }
328
329   @Override
330   protected final boolean isAddBeanTypes() {
331      return addBeanTypesBson || super.isAddBeanTypes();
332   }
333
334   @Override /* OutputStreamSerializer */
335   protected FluentMap<String,Object> properties() {
336      return super.properties()
337         .a("addBeanTypesBson", addBeanTypesBson)
338         .a("nullKeyString", nullKeyString)
339         .a("writeDatesAsDatetime", writeDatesAsDatetime);
340   }
341
342   /**
343    * Convenience delegator for the whole-value {@link RecordWriter} using <b>default session
344    * arguments</b>.  The real implementation lives on
345    * {@link BsonSerializerSession#writeRecords(Object)}.
346    *
347    * @param output The output.
348    * @return A new {@link RecordWriter}.
349    * @throws IOException If a problem occurred opening the underlying output.
350    */
351   @Override /* RecordWritable */
352   public RecordWriter writeRecords(Object output) throws IOException {
353      return getSession().writeRecords(output);
354   }
355
356   /**
357    * Convenience delegator for the buffered array-element {@link RecordWriter} using <b>default
358    * session arguments</b>.  BSON's wire format requires the entire document byte-length up front
359    * (length prefix at every nesting level), so streaming without buffering is not possible.  The
360    * real implementation lives on {@link BsonSerializerSession#writeArrayRecords(Object)}.
361    *
362    * @param output The output.
363    * @return A buffered {@link RecordWriter}.
364    * @throws IOException If a problem occurred opening the underlying output.
365    */
366   @Override /* ArrayRecordWritable */
367   public RecordWriter writeArrayRecords(Object output) throws IOException {
368      return getSession().writeArrayRecords(output);
369   }
370
371   /**
372    * The BSON record writer is buffered/{@link RecordAdapter}-backed, not O(1) streaming.
373    *
374    * @return Always <jk>false</jk>.
375    */
376   @Override /* RecordWritable */
377   public boolean isRecordStreaming() { return false; }
378
379   /**
380    * The BSON array-record writer is buffered/{@link RecordAdapter}-backed, not O(1) streaming.
381    *
382    * @return Always <jk>false</jk>.
383    */
384   @Override /* ArrayRecordWritable */
385   public boolean isArrayRecordStreaming() { return false; }
386}