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}