sourceafButter::Body.fan

using util

// Can't use streams for request - 'cos we write to body before we open the real socket connection. 
// We would need a callback for when socket out becomes available. 
** Convenience methods for reading and writing content.
class Body {    
    private Buf?                        buffer
    private HttpRequestHeaders?     reqHeaders
    private HttpResponseHeaders?    resHeaders
    
    ** The charset used to en / decode the string and json objects. If left as 'null' then it defaults to the 'Content-Type' HTTP Header, or UTF-8 if not set.
    ** 
    ** 'charset' should be set *before* the body content.
    Charset? charset
    
    ** Gets and sets the body content as a 'Buf'.
    Buf? buf {
        // don't seek so we can add more and more to it
        get { if (buffer != null) buffer.charset = _strCharset; return buffer }
        set { buffer = it }
    }

    ** Gets and sets the body content as a string. The string is en / decoded using a charset found in the following precedence:
    **  - any charset set via the 'charset' field
    **  - the charset defined in a 'Content-Type' HTTP header
    **  - UTF-8
    ** 
    ** When set, the 'Content-Type' is set to 'text/plain' (if it's not been set already).
    ** 
    ** Returns 'null' if the body has not been set, and an empty Str if it's empty.
    Str? str {
        get { (buf == null) ? null : buf.seek(0).readAllStr }
        set {
            if (it != null && reqHeaders.contentType == null)
                reqHeaders.contentType = MimeType("text/plain; charset=${_strCharset}")
            buffer = (it == null) ? null : (buf ?: Buf() { it.charset = _strCharset }).clear.writeChars(it)
        }
    }

    ** Gets and sets the body content as a JSON string. 
    ** 
    ** When set, the 'Content-Type' is set to 'application/json' (if it's not been set already).
    **   
    ** Returns 'null' if the body has not been set or if it is empty.
    Str? json {
        get { str.trimToNull }
        set {
            if (it != null && reqHeaders.contentType == null)
                reqHeaders.contentType = MimeType("application/json; charset=${_strCharset}")
            str = it
        }
    }

    ** Gets and sets the body content as a JSON object. 
    ** 'JsonInStream' / 'JsonOutStream' are used to convert objects to and from JSON strings.
    ** 
    ** When set, the 'Content-Type' is set to 'application/json' (if it's not been set already).
    **   
    ** Returns 'null' if the body has not been set or if it's empty.
    ** 
    ** Note that [JsonOutStream]`util::JsonOutStream` is used for the conversion. See the
    ** [Json library]`pod:afJson` should you need pretty printing or more control over conversion.
    Obj? jsonObj {
        get { 
            (buf == null || buf.isEmpty) ? null : JsonInStream(buf.seek(0).in).readJson 
        }
        set {
            if (it != null && reqHeaders.contentType == null)
                reqHeaders.contentType = MimeType("application/json; charset=${_strCharset}")
            str = (it == null) ? null : JsonOutStream.writeJsonToStr(it)
        }
    }

    ** Gets and set the body content as a JSON list. Convenience for '(Obj?[]?) body.jsonObj'.
    ** 
    ** When set, the 'Content-Type' is set to 'application/json' (if it's not been set already).  
    **   
    ** Returns 'null' if the body has not been set or if it's empty.
    ** 
    ** Note that [JsonOutStream]`util::JsonOutStream` is used for the conversion. See the
    ** [Json library]`pod:afJson` should you need pretty printing or more control over conversion.
    Obj?[]? jsonList {
        get { jsonObj }
        set { jsonObj = it }
    }

    ** Gets and set the body content as a JSON map. Convenience for '([Str:Obj?]?) body.jsonObj'.
    ** 
    ** When set, the 'Content-Type' is set to 'application/json' (if it's not been set already).  
    **   
    ** Returns 'null' if the body has not been set or if it's empty.
    ** 
    ** Note that [JsonOutStream]`util::JsonOutStream` is used for the conversion. See the
    ** [Json library]`pod:afJson` should you need pretty printing or more control over conversion.
    [Str:Obj?]? jsonMap {
        get { jsonObj }
        set { jsonObj = it }
    }

    ** Gets and sets the body content as a URL encoded form (think forms on web pages). 
    ** 'Uri.encodeQuery()' / 'Uri.decodeQuery()' methods are used to convert objects to and from form values.
    ** 
    ** When set, the 'Content-Type' is set to 'application/x-www-form-urlencoded' (if it's not been set already).  
    **   
    ** Returns 'null' if the body has not been set or if it's empty.
    [Str:Str]? form {
        get { (buf == null) ? null : Uri.decodeQuery(str) }
        set {
            if (it != null && reqHeaders.contentType == null)
                reqHeaders.contentType = MimeType("application/x-www-form-urlencoded; charset=${_strCharset}")
            str = (it == null) ? null : Uri.encodeQuery(it)
        }
    }
    
    ** Returns the size of the body in bytes. 
    ** Shortcut for 'buf?.size ?: 0'.
    Int size() {
        buf?.size ?: 0
    }
    
    ** Returns the contents of the body as in 'InStream'.
    ** Shortcut for 'buf?.seek(0)?.in'.
    InStream? in() {
        buf?.seek(0)?.in
    }
    
    internal new makeForReq(HttpRequestHeaders reqHeaders) {
        this.reqHeaders = reqHeaders
        // we start off with a buffer, as that is what most requests will use to set Str content etc
//      buffer = Buf()
        // err... lets not, so we can return null as promised
    }   
    
    internal new makeForResIn(HttpResponseHeaders resHeaders, InStream in) {
        this.resHeaders = resHeaders
        // read in the whole instream only because we need to make sure we close it at some point
        // use 'try' in case the 'in' is empty 
        try buffer = in.readAllBuf.seek(0)
        catch buffer = null
    }
    
    internal new makeForResStr(HttpResponseHeaders resHeaders, Str? str) {
        this.resHeaders = resHeaders
        this.buffer = str?.toBuf?.seek(0)
    }

    internal new makeForResBuf(HttpResponseHeaders resHeaders, Buf? buf) {
        this.resHeaders = resHeaders
        this.buffer = buf?.seek(0)
    }
    
    private Charset _strCharset() {
        if (&charset != null)
            return &charset
        if (resHeaders?.contentType?.charset != null)
            return resHeaders.contentType.charset
        if (reqHeaders?.contentType?.charset != null)
            return reqHeaders.contentType.charset
        return Charset.utf8
    }
}