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>"<NULL>"</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}