** Wraps an 'OutStream' to write BSON objects.
**
** > **CAUTION:** 'INTEGER_32' values will be read as [Int]`sys::Int` values.
** If you then write its containing document, the storage type will be converted to 'INTEGER_64'.
**
** This is only of concern if other, non Fantom drivers, are writing to the database.
class BsonWriter {
private static const Log log := BsonReader#.pod.log
private Str[] nameStack := [,]
private Str:Int sizeCache := [:]
** The underlying 'OutStream'.
OutStream? out {
private set
}
** Creates a 'BsonWriter', wrapping the given 'OutSteam'
** As per the BSON spec, the stream's endian is set to 'little'.
**
** 'out' may be 'null' if the writer is just being used to size documents.
new make(OutStream? out) {
this.out = out
if (out != null)
out.endian = Endian.little
}
** Serialises the given BSON Document to the underlying 'OutStream'.
This writeDocument([Obj:Obj?]? document) {
if (document == null)
return this
try {
_writeObject(document, BsonBasicTypeWriter(out))
return this
} finally {
// clear nameStack in case we're exiting use to an Err and it wasn't popped
nameStack.clear
sizeCache.clear
}
}
** Calculates the size (in bytes) of the given BSON Document should it be serialised.
** Nothing is written to the 'OutStream'.
Int sizeDocument([Obj:Obj?]? document) {
if (document == null)
return 0
try return _writeObject(document, BsonBasicTypeWriter(null)).bytesWritten
finally {
// clear nameStack in case we're exiting use to an Err and it wasn't popped
nameStack.clear
sizeCache.clear
}
}
** Writes a 'null' terminated BSON string to 'OutStream'.
This writeCString(Str cstr) {
BsonBasicTypeWriter(out).writeCString(cstr)
return this
}
** Calculates the size (in bytes) of the given Str should it be serialised as a null terminated
** 'CString'.
** Nothing is written to the 'OutStream'.
Int sizeCString(Str cstr) {
BsonBasicTypeWriter(null).writeCString(cstr).bytesWritten
}
** Writes a 32 bit integer value to 'OutStream'.
** Unlike storing 'Ints' in a Document, this method *will* write an actual 'INTEGER_32'.
This writeInteger32(Int int32) {
BsonBasicTypeWriter(out).writeInteger32(int32)
return this
}
** Writes a 64 bit integer value to 'OutStream'.
This writeInteger64(Int int64) {
BsonBasicTypeWriter(out).writeInteger64(int64)
return this
}
** Flushes the underlying 'OutStream'.
This flush() {
out?.flush
return this
}
private Int _sizeObject(Obj? object, BsonBasicTypeWriter writer) {
// use toCode() to prevent names from masquerading as multiple keys, e.g. func.code.scope
name := nameStack.toCode
if (sizeCache.containsKey(name))
return sizeCache[name]
// prevent us from recursively sizing objects when we're not actually writing any data
if (writer.out == null)
return -1
size := _writeObject(object, BsonBasicTypeWriter(null)).bytesWritten
sizeCache.add(name, size) // use add() to make sure we don't overwrite any existing keys!
return size
}
private BsonBasicTypeWriter _writeObject(Obj? obj, BsonBasicTypeWriter writer) {
type := BsonType.fromObj(obj, true)
switch (type) {
case BsonType.DOUBLE:
writer.writeDouble(obj)
case BsonType.STRING:
writer.writeString(obj)
case BsonType.DOCUMENT:
docSize := _sizeObject(obj, writer)
writer.writeInteger32(docSize)
((Obj:Obj?) obj).each |val, name| {
// a controversial decision - we check individual key types, not the map key type
// because with [:] it's far too easy to declare Obj maps without knowing it
// if I were to check the paramaterized Map type, people would soon hate me!
if (name isnot Str)
throw ArgErr("BSON Document names must be 'Str', not : ${name.typeof.signature} - ${name}")
nameStack.push(name)
valType := BsonType.fromObj(val, true)
writer.writeByte(valType.value)
writer.writeCString(name)
_writeObject(val, writer)
nameStack.pop
}
writer.writeByte(BsonType.EOO.value)
case BsonType.ARRAY:
doc := Str:Obj?[:]
doc.ordered = true
doc.addList(obj, #rocket01.func)
_writeObject(doc, writer)
case BsonType.BINARY:
buff := (Buf) (obj is Buf ? obj : ((Binary) obj).data)
bint := (Int) (obj is Buf ? Binary.BIN_GENERIC : ((Binary) obj).subtype)
dataSize := (bint == Binary.BIN_BINARY_OLD) ? 4 : 0
dataSize += buff.size
writer.writeInteger32(dataSize)
writer.writeByte(bint)
if (bint == Binary.BIN_BINARY_OLD)
writer.writeInteger32(buff.size)
writer.writeBinary(buff)
case BsonType.OBJECT_ID:
writer.writeObjectId(obj)
case BsonType.BOOLEAN:
writer.writeByte(obj ? 0x01 : 0x00)
case BsonType.DATE:
millisecs := ((DateTime) obj).toJava
writer.writeInteger64(millisecs)
case BsonType.NULL:
null?.toStr // No-op
case BsonType.REGEX:
// Regex flags are not supported by Fantom but flag characters can be embedded into
// the pattern itself --> /(?i)case-insensitive/
// see Java's Pattern class for a list of supported flags --> dimsuxU
// see http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html#special
writer.writeCString(obj.toStr) // --> pattern
writer.writeCString("") // --> flags
case BsonType.CODE:
writer.writeString(((Code) obj).code)
case BsonType.CODE_W_SCOPE:
code := (Code) obj
nameStack.push("code")
codeSize := _sizeObject(code, writer)
writer.writeInteger32(codeSize)
writer.writeString(code.code)
nameStack.push("scope")
_writeObject(code.scope, writer)
nameStack.pop
nameStack.pop
case BsonType.TIMESTAMP:
timestamp := (Timestamp) obj
writer.writeInteger32(timestamp.seconds)
writer.writeInteger32(timestamp.increment)
case BsonType.INTEGER_64:
writer.writeInteger64(obj)
case BsonType.MIN_KEY:
null?.toStr // No-op
case BsonType.MAX_KEY:
null?.toStr // No-op
}
return writer
}
private static Str rocket01(Obj? v, Int i) { i.toStr }
}
** Writes basic BSON types and keeps count of the number of bytes written.
internal class BsonBasicTypeWriter {
Int bytesWritten
OutStream? out
new make(OutStream? out) {
this.out = out
}
This writeCString(Str str) {
writeBinary(str.toBuf).writeNull
return this
}
This writeString(Str str) {
buf := str.toBuf
writeInteger32(buf.size + 1)
writeBinary(buf).writeNull
return this
}
This writeByte(Int byte) {
out?.write(byte)
bytesWritten += 1
return this
}
This writeBinary(Buf binary) {
if (binary.isImmutable) {
// we can't seek on immutable bufs
out?.writeBuf(binary)
bytesWritten += binary.size
} else {
origPos := binary.pos
binary.seek(0)
out?.writeBuf(binary)
bytesWritten += binary.size
binary.seek(origPos)
}
return this
}
This writeDouble(Float double) {
out?.writeF8(double)
bytesWritten += 8
return this
}
This writeInteger32(Int int) {
out?.writeI4(int)
bytesWritten += 4
return this
}
This writeInteger64(Int int) {
out?.writeI8(int)
bytesWritten += 8
return this
}
This writeObjectId(ObjectId objectId) {
if (out != null) {
origEndian := out.endian
out.endian = Endian.big
out.writeBuf(objectId.toBuf)
out.endian = origEndian
}
bytesWritten += 12
return this
}
private This writeNull() {
out?.write(0)
bytesWritten += 1
return this
}
}