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}