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.*;
020
021import java.io.*;
022import java.math.*;
023import java.nio.charset.*;
024
025import org.apache.juneau.marshall.serializer.*;
026
027/**
028 * Specialized output stream for serializing BSON (Binary JSON) streams.
029 *
030 * <p>
031 * BSON uses little-endian byte order for all multi-byte values. Documents are length-prefixed: the total
032 * byte count (int32) must appear before the content. Since the size is unknown until all elements are written,
033 * each document is buffered in a {@link ByteArrayOutputStream} child. When complete, the size is calculated
034 * and <c>[int32 size][content][0x00]</c> is written to the parent stream.
035 *
036 * <h5 class='section'>Notes:</h5><ul>
037 *    <li class='note'>
038 *       This class is not intended for external 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   "resource" // OutputStream resource managed by calling code
047})
048public class BsonOutputStream extends OutputStream {
049
050   private static final Charset UTF8 = StandardCharsets.UTF_8;
051   private static final byte BINARY_SUBTYPE_GENERIC = 0x00;
052   private static final byte DOCUMENT_TERMINATOR = 0x00;
053
054   private final OutputStream os;
055   private ByteArrayOutputStream buffer;
056   private boolean inDocument;
057
058   /**
059    * Constructor.
060    *
061    * @param os The output stream being wrapped.
062    */
063   public BsonOutputStream(OutputStream os) {
064      this.os = os;
065   }
066
067   @Override /* Overridden from OutputStream */
068   public void write(int b) {
069      try {
070         target().write(b);
071      } catch (IOException e) {
072         throw new SerializeException(e);
073      }
074   }
075
076   @Override /* Overridden from OutputStream */
077   public void write(byte[] b) {
078      if (b != null && b.length > 0)
079         write(b, 0, b.length);
080   }
081
082   @Override /* Overridden from OutputStream */
083   public void write(byte[] b, int off, int len) {
084      try {
085         target().write(b, off, len);
086      } catch (IOException e) {
087         throw new SerializeException(e);
088      }
089   }
090
091   private OutputStream target() {
092      return or(buffer, os);
093   }
094
095   /**
096    * Begin buffering a new document.
097    */
098   public void startDocument() {
099      buffer = new ByteArrayOutputStream();
100      inDocument = true;
101   }
102
103   /**
104    * Finalize the current document and return the bytes (int32 size + content + 0x00).
105    *
106    * @return The complete document bytes.
107    */
108   public byte[] endDocument() {
109      if (buffer == null)
110         return new byte[0];
111      buffer.write(DOCUMENT_TERMINATOR);
112      var content = buffer.toByteArray();
113      // size = 4 (int32) + content length
114      var size = 4 + content.length;
115      var result = new byte[size];
116      writeLE4To(result, 0, size);
117      System.arraycopy(content, 0, result, 4, content.length);
118      buffer = null;
119      inDocument = false;
120      return result;
121   }
122
123   /**
124    * Finalize the current document and write it to the parent stream.
125    *
126    * @param parent The parent output stream.
127    */
128   public void writeDocumentTo(OutputStream parent) {
129      var bytes = endDocument();
130      try {
131         parent.write(bytes);
132      } catch (IOException e) {
133         throw new SerializeException(e);
134      }
135   }
136
137   /**
138    * Writes a BSON element header (type byte + cstring name).
139    *
140    * @param type The BSON type code.
141    * @param name The element name (cstring).
142    */
143   public void writeElement(int type, String name) {
144      write(type & 0xFF);
145      writeCString(name);
146   }
147
148   /**
149    * Writes a 4-byte little-endian integer.
150    *
151    * @param value The 32-bit value.
152    */
153   public void writeLE4(int value) {
154      write(value & 0xFF);
155      write((value >> 8) & 0xFF);
156      write((value >> 16) & 0xFF);
157      write((value >> 24) & 0xFF);
158   }
159
160   /**
161    * Writes a 4-byte little-endian integer to a byte array.
162    */
163   static void writeLE4To(byte[] buf, int offset, int value) {
164      buf[offset] = (byte)(value & 0xFF);
165      buf[offset + 1] = (byte)((value >> 8) & 0xFF);
166      buf[offset + 2] = (byte)((value >> 16) & 0xFF);
167      buf[offset + 3] = (byte)((value >> 24) & 0xFF);
168   }
169
170   /**
171    * Writes an 8-byte little-endian long.
172    *
173    * @param value The 64-bit value.
174    */
175   public void writeLE8(long value) {
176      write((int)(value & 0xFF));
177      write((int)((value >> 8) & 0xFF));
178      write((int)((value >> 16) & 0xFF));
179      write((int)((value >> 24) & 0xFF));
180      write((int)((value >> 32) & 0xFF));
181      write((int)((value >> 40) & 0xFF));
182      write((int)((value >> 48) & 0xFF));
183      write((int)((value >> 56) & 0xFF));
184   }
185
186   /**
187    * Writes an IEEE 754 double (8 bytes, little-endian).
188    *
189    * @param value The double value.
190    */
191   public void writeDouble(double value) {
192      writeLE8(Double.doubleToLongBits(value));
193   }
194
195   /**
196    * Writes a 32-bit integer (4 bytes, little-endian).
197    *
198    * @param value The integer value.
199    */
200   public void writeInt32(int value) {
201      writeLE4(value);
202   }
203
204   /**
205    * Writes a 64-bit integer (8 bytes, little-endian).
206    *
207    * @param value The long value.
208    */
209   public void writeInt64(long value) {
210      writeLE8(value);
211   }
212
213   /**
214    * Writes a boolean (1 byte: 0x00=false, 0x01=true).
215    *
216    * @param value The boolean value.
217    */
218   public void writeBoolean(boolean value) {
219      write(value ? 0x01 : 0x00);
220   }
221
222   /**
223    * Writes BSON null (no value bytes).
224    */
225   public void writeNull() {
226      // Type byte only, no value
227   }
228
229   /**
230    * Writes a BSON string (int32 length + UTF-8 bytes + 0x00).
231    *
232    * @param value The string to write (treated as empty if <jk>null</jk>).
233    */
234   public void writeString(String value) {
235      if (value == null)
236         value = "";
237      var bytes = value.getBytes(UTF8);
238      // BSON string length includes trailing null
239      writeLE4(bytes.length + 1);
240      write(bytes);
241      write(DOCUMENT_TERMINATOR);
242   }
243
244   /**
245    * Writes a cstring (UTF-8 bytes + 0x00, no length prefix).
246    *
247    * @param value The string to write (treated as empty if <jk>null</jk>).
248    */
249   public void writeCString(String value) {
250      if (value == null)
251         value = "";
252      for (var i = 0; i < value.length(); i++)
253         if (value.charAt(i) == 0)
254            throw new SerializeException("CString must not contain null bytes");
255      write(value.getBytes(UTF8));
256      write(DOCUMENT_TERMINATOR);
257   }
258
259   /**
260    * Writes BSON binary (int32 length + subtype byte + raw bytes).
261    *
262    * @param data The raw bytes (subtype 0x00 generic).
263    */
264   public void writeBinary(byte[] data) {
265      if (data == null)
266         data = new byte[0];
267      writeLE4(data.length);
268      write(BINARY_SUBTYPE_GENERIC);
269      write(data);
270   }
271
272   /**
273    * Writes BSON datetime (int64, UTC milliseconds since epoch).
274    *
275    * @param millis The UTC milliseconds since epoch.
276    */
277   public void writeDateTime(long millis) {
278      writeLE8(millis);
279   }
280
281   /**
282    * Writes BSON decimal128 (16 bytes, IEEE 754-2008) from BigDecimal.
283    *
284    * @param value The decimal value.
285    */
286   public void writeDecimal128(BigDecimal value) {
287      if (value == null)
288         value = BigDecimal.ZERO;
289      BsonDecimal128 d128;
290      try {
291         d128 = BsonDecimal128.fromBigDecimal(value);
292      } catch (NumberFormatException e) {
293         // BSON Decimal128 (0x13) is bounded to 34 significant digits with exponent in [-6176,6111]; values
294         // outside that range are an unavoidable lossy case, so fail cleanly instead of leaking an unchecked
295         // NumberFormatException.  See the BsonSerializer limitations javadoc.
296         var e2 = new SerializeException("Value out of BSON Decimal128 range (max 34 significant digits, exponent -6176..6111): %s", value);
297         e2.initCause(e);
298         throw e2;
299      }
300      writeLE8(d128.getLow());
301      writeLE8(d128.getHigh());
302   }
303
304   /**
305    * Creates a child BsonOutputStream for buffering a nested document.
306    *
307    * @return A new child stream (no parent; use {@link #writeChildDocument} to embed).
308    */
309   public BsonOutputStream createChild() {
310      return new BsonOutputStream(null);
311   }
312
313   /**
314    * Writes the child document's content as an embedded document to this stream.
315    *
316    * @param child The child stream (must have been finalized via endDocument).
317    */
318   public void writeChildDocument(BsonOutputStream child) {
319      // Child buffers to its own ByteArrayOutputStream; we need to write [size][content][0x00]
320      // Child.endDocument() returns [size][content] with 0x00 already in content
321      // So we just need to write the child's endDocument() result
322      var bytes = child.endDocument();
323      // bytes already has size prefix and terminator from endDocument
324      try {
325         target().write(bytes);
326      } catch (IOException e) {
327         throw new SerializeException(e);
328      }
329   }
330
331   /**
332    * Returns whether we are currently buffering a document.
333    *
334    * @return <jk>true</jk> if buffering, <jk>false</jk> otherwise.
335    */
336   public boolean isInDocument() {
337      return inDocument;
338   }
339}