/*
* Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
* This source file is part of the Cangjie project, licensed under Apache-2.0
* with Runtime Library Exception.
*
* See https://cangjie-lang.cn/pages/LICENSE for license information.
*/
package stdx.net.http
import std.io.InputStream
import std.collection.ArrayList
/**
* A builder of HttpResponse, which is used by the server to return responses.
*/
public class HttpResponseBuilder {
var _version: Protocol = HTTP1_1
var _status: ?UInt16 = None
var _headers: ?HttpHeaders = None
var _body: InputStream = HttpEmptyBody.INSTANCE
var _trailers: ?HttpHeaders = None
var _request: ?HttpRequest = None
func reset(): Unit {
_version = HTTP1_1
_status = None
match (_headers) {
case Some(value) => value.reset()
case None => _headers = HttpHeaders()
}
_body = HttpEmptyBody.INSTANCE
_trailers = None
_request = None
}
public init() {}
/**
* Sets the HTTP-version of this response.
*
* @param version the protocol version of the response.
* @return HttpResponseBuilder whose protocol version has been set.
*/
public func version(version: Protocol): HttpResponseBuilder {
this._version = version
return this
}
/**
* Sets the status-code of this response.
*
* @param status the status of the response.
* @return HttpResponseBuilder whose status has been set.
*
* @throws HttpException, if there status not within the specification range.
* RFC 9110 says: All valid status codes are within the range of 100 to 599
*/
public func status(status: UInt16): HttpResponseBuilder {
if (status < 100 || status > 599) {
throw HttpException("Valid status codes are within the range of 100 to 599.")
}
this._status = status
return this
}
/**
* Adds a specifies key-value pair to this response headers
* Name should consist of tokens, value should consist of vchar (visible US-ASCII octet), SP or HTAB.
*
* @param key the field name
* @param value the field value
* @return HttpResponseBuilder whose header has been set.
*
* @throws HttpException, if field name or value invalid.
*/
public func header(name: String, value: String): HttpResponseBuilder {
headers.add(name, value)
return this
}
/**
* Adds the specifies headers to this response headers
*
* @param headers the headers to be added into the response.
* @return HttpResponseBuilder whose header has been set.
*/
public func addHeaders(headers: HttpHeaders): HttpResponseBuilder {
this.headers.addAll(headers)
return this
}
/**
* Sets the specifies headers to this response headers.
*
* @param headers the headers to be set to the response.
* @return HttpResponseBuilder whose header has been set.
*/
public func setHeaders(headers: HttpHeaders): HttpResponseBuilder {
_headers = headers
return this
}
/**
* Sets the specified message body for this response.
* If the body exists before, it will be overwritten.
*
* @param body the response's body.
* @return HttpResponseBuilder whose body has been set.
*/
public func body(body: Array<UInt8>): HttpResponseBuilder {
_body = HttpRawBody(body)
return this
}
/**
* Sets the specified message body for this response.
* If the body exists before, it will be overwritten.
*
* @param body the response's body.
* @return HttpResponseBuilder whose body has been set.
*/
public func body(body: InputStream): HttpResponseBuilder {
_body = body
return this
}
/**
* Sets the specified message body for this response.
* If the body exists before, it will be overwritten.
*
* @param body the response's body.
* @return HttpResponseBuilder whose body has been set.
*/
public func body(body: String): HttpResponseBuilder {
_body = HttpRawBody(body)
return this
}
/**
* Adds a specifies key-value pair to this response trailers
* Name should consist of tokens, value should consist of vchar (visible US-ASCII octet), SP or HTAB.
*
* @param key the field name
* @param value the field value
* @return HttpResponseBuilder whose trailer has been set.
*
* @throws HttpException, if field name or value invalid.
*/
public func trailer(name: String, value: String): HttpResponseBuilder {
trailers.add(name, value)
return this
}
/**
* Adds the specifies trailers to this response trailers
*
* @param trailers the trailers to be added into the response.
* @return HttpResponseBuilder whose trailers has been set.
*/
public func addTrailers(trailers: HttpHeaders): HttpResponseBuilder {
this.trailers.addAll(trailers)
return this
}
/**
* Sets the specifies trailers to this response trailers.
*
* @param trailers the trailers to be set to the response.
* @return HttpResponseBuilder whose trailers has been set.
*/
public func setTrailers(trailers: HttpHeaders): HttpResponseBuilder {
_trailers = trailers
return this
}
/**
* Sets the specifies trailers to the response trailers.
*
* @param request the request related to the response.
* @return HttpResponseBuilder whose request has been set.
*/
public func request(request: HttpRequest): HttpResponseBuilder {
this._request = request
return this
}
/**
* Generates and returns the configured HttpResponse.
*
* @return HttpResponse whose arguments are set by the builder.
*/
public func build(): HttpResponse {
let bodySize: ?Int64 = sizeOf(_body)
return HttpResponse(
_version: _version,
_status: _status ?? HttpStatusCode.STATUS_OK,
_headers: _headers,
_body: _body,
_trailers: _trailers,
_bodySize: bodySize,
_request: _request
)
}
prop headers: HttpHeaders {
get() {
return match (_headers) {
case Some(h) => h
case None =>
let h = HttpHeaders()
_headers = h
return h
}
}
}
prop trailers: HttpHeaders {
get() {
return match (_trailers) {
case Some(t) => t
case None =>
let t = HttpHeaders()
_trailers = t
return t
}
}
}
}
/*HTTP response, which is used by the client to read this response from the server.*/
public class HttpResponse <: ToString {
HttpResponse(
var _version!: Protocol,
var _status!: UInt16,
var _headers!: ?HttpHeaders,
var _body!: InputStream,
var _trailers!: ?HttpHeaders,
var _bodySize!: ?Int64,
var _request!: ?HttpRequest,
var pushResponses!: ?ArrayList<Object> = None,
// upgrade to websocket
var connNode!: ?BodyProviderConn = None
) {}
/**
* Gets the Http-version of this response.
* Default value of version is HTTP1_1.
*/
public prop version: Protocol {
get() {
_version
}
}
/**
* Gets the status-code of this response.
* Default value of status is 200.
*/
public prop status: UInt16 {
get() {
_status
}
}
/**
* Gets the headers of this response, which stores the key-value pair of the response header.
* Default provide an empty HttpHeaders.
*/
public prop headers: HttpHeaders {
get() {
return match (_headers) {
case Some(h) => h
case None =>
let h = HttpHeaders()
_headers = h
return h
}
}
}
/**
* Gets the body of this response.
* Default value of body is an InputStream with no data.
*/
public prop body: InputStream {
get() {
_body
}
}
/**
* Gets the trailers of this response, which save the trailer key-value pair in the same format as the header field.
* Default provide an empty HttpHeaders.
*/
public prop trailers: HttpHeaders {
get() {
return match (_trailers) {
case Some(t) => t
case None =>
let t = HttpHeaders()
_trailers = t
return t
}
}
}
/**
* Gets the body size of this response.
*
* @return Some(0), means the body is empty.
* @return Some(v), means the body size is known, input value of body is Array<UInt8> or String
* or InputStream whose length is valid.
* @return None, means the body size is unknown, that is, the body is self-implemented InputStream
* whose length is invalid.
*/
public prop bodySize: Option<Int64> {
get() {
_bodySize
}
}
/**
* Gets the request corresponding to this response.
* Default value of request is None.
*/
public prop request: Option<HttpRequest> {
get() {
_request
}
}
/**
* If headers contains "Connection: close", this value is set false.
* It indicates that whether the connection will be closed after reading Body.
*/
public prop isPersistent: Bool {
get() {
if (let Some(h) <- _headers) {
return !(h.get("connection") |> splitValuesByComma |> "close".caseInsensitiveMatchOne)
}
return true
}
}
public func close(): Unit {
match (_body) {
case closable: Resource => closable.close()
case _ => ()
}
}
/**
* Prints request start line, headers, body size, trailer.
*
* @return String that can represent the response.
*/
public override func toString(): String {
var str = StringBuilder()
str.append("${_version} ${_status} ${phrase}\r\n")
str.append(_headers?.toString() ?? "\r\n")
match (bodySize) {
case Some(0) => ()
case Some(s) => str.append("body size: ${s}\r\n")
case None => str.append("unknown body size\r\n")
}
str.append(_trailers?.toString() ?? "")
return str.toString()
}
prop phrase: String {
get() {
return STATUS_TEXT.get(status) ?? ""
}
}
}
public class HttpResponseWriter {
public HttpResponseWriter(let ctx: HttpContext) {}
/**
* Send buf to client.
*
* @param buf the buf server wants to send to client.
*/
public func write(buf: Array<Byte>): Unit {
synchronized(ctx.writerMtx) {
if (ctx.responded) {
return
}
if (ctx.upgraded) {
throw HttpException("The connection is upgraded and response cannot be written.")
}
ctx.httpConn.writeResponseByWriter(ctx, buf)
}
}
}