sourceafBson::BsonWriter.fan


** 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    := Utils.getLog(BsonReader#)

    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) {
        (BsonWriter) cache |->Obj?| {
            if (document != null)
                _writeObject(document, BsonBasicTypeWriter(out))
            return this
        }
    }

    ** 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) {
        cache |->Int| {
            (document == null) ? 0 : _writeObject(document, BsonBasicTypeWriter(null)).bytesWritten
        }
    }
    
    ** 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(ErrMsgs.bsonType_unknownNameType(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?[:] { ordered = true }.addList(obj) |v, i->Str| { i.toStr }
                _writeObject(doc, writer)

            case BsonType.BINARY:
                if (obj is Buf) 
                    obj = Binary(obj, Binary.BIN_GENERIC)

                binary := (Binary) obj
                dataSize := (binary.subtype == Binary.BIN_BINARY_OLD) ? 4 : 0
                dataSize += binary.data.size
                writer.writeInteger32(dataSize)
                writer.writeByte(binary.subtype)
                if (binary.subtype == Binary.BIN_BINARY_OLD) 
                    writer.writeInteger32(binary.data.size)
                writer.writeBinary(binary.data)

            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.toSec)
                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
    }
    
    Obj? cache(|->Obj?| c) {
        try {
            return c.call()
        } finally {
            // clear nameStack in case we're exiting use to an Err and it wasn't popped 
            nameStack.clear
            sizeCache.clear
        }
    }
}


** 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) {
        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
    }
}