/*
* 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.net.*
import std.io.*
import std.collection.ArrayList
import stdx.encoding.url.*
public class HttpRequestBuilder {
var _method: String = "GET"
var _url: ?URL = None
// represent it`s not specified by user,
var _version: Protocol = UnknownProtocol("HTTP/1.1")
var _headers: ?HttpHeaders = None
var _body: InputStream = HttpEmptyBody.INSTANCE
var _trailers: ?HttpHeaders = None
var _remoteAddr: ?SocketAddress = None
var priorityValue: String = ""
var _readTimeout: ?Duration = None
var _writeTimeout: ?Duration = None
public init() {}
/**
* Init an HttpRequestBuilder with the input request's attributes.
* Headers and trailers are deep copy of the input request.
* Body is the same stream as the input request.
*/
public init(request: HttpRequest) {
this._method = request.method
this._url = request.url
this._version = request.version
this._headers = request.headers.clone()
this._body = request.body
this._trailers = request.trailers.clone()
this._remoteAddr = request._remoteAddr
this.priorityValue = request.priorityValue
this._readTimeout = request.readTimeout
this._writeTimeout = request.writeTimeout
}
/**
* Sets method of this request.
*
* @param method the request's method,
* method should be consist of tokens.
* @return HttpRequestBuilder whose method has been set.
*
* @throws HttpException, if method is not tokens.
*/
public func method(method: String): HttpRequestBuilder {
if (method.isEmpty()) {
return this
}
if (!isTokenString(method)) {
throw HttpException("Method should be tokens.")
}
this._method = method
return this
}
/**
* Sets url of this request.
*
* @param url the request's url.
* @return HttpRequestBuilder whose url has been set.
*/
public func url(url: URL): HttpRequestBuilder {
_url = url
return this
}
/**
* Sets url of this request.
*
* @param url the raw string url.
* @return HttpRequestBuilder whose url has been set.
*
* @throws UrlSyntaxException if 'rawUrl' is empty or invalid.
* @throws IllegalArgumentException if there is an invalid utf8 leading code in 'rawUrl'.
*/
public func url(rawUrl: String): HttpRequestBuilder {
return url(URL.parse(rawUrl))
}
/**
* Sets the HTTP-version of this request.
*
* @param version the protocol version of the request.
* @return HttpRequestBuilder whose protocol version has been set.
*/
public func version(version: Protocol): HttpRequestBuilder {
this._version = version
return this
}
/**
* Adds a specifies key-value pair to this request 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 HttpRequestBuilder whose header has been set.
*
* @throws HttpException, if field name or value invalid.
*/
public func header(name: String, value: String): HttpRequestBuilder {
headers.add(name, value)
return this
}
/**
* Adds the specifies headers to this request headers.
*
* @param headers the headers to be added into the request.
* @return HttpRequestBuilder whose header has been set.
*/
public func addHeaders(headers: HttpHeaders): HttpRequestBuilder {
this.headers.addAll(headers)
return this
}
/**
* Sets the specifies headers to this request headers.
*
* @param headers the headers to be set to the request.
* @return HttpRequestBuilder whose header has been set.
*/
public func setHeaders(headers: HttpHeaders): HttpRequestBuilder {
_headers = headers
return this
}
/**
* Sets the specified message body for this request.
* If the body exists before, it will be overwritten.
*
* @param body the request's body.
* @return HttpRequestBuilder whose body has been set.
*/
public func body(body: Array<UInt8>): HttpRequestBuilder {
_body = HttpRawBody(body)
return this
}
/**
* Sets the specified message body for this request.
* If the body exists before, it will be overwritten.
*
* @param body the request's body.
* @return HttpRequestBuilder whose body has been set.
*/
public func body(body: InputStream): HttpRequestBuilder {
_body = body
return this
}
/**
* Sets the specified message body for this request.
* If the body exists before, it will be overwritten.
*
* @param body the request's body.
* @return HttpRequestBuilder whose body has been set.
*/
public func body(body: String): HttpRequestBuilder {
_body = HttpRawBody(body)
return this
}
/**
* Adds a specifies key-value pair to this request 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 HttpRequestBuilder whose trailer has been set.
*
* @throws HttpException, if field name or value invalid.
*/
public func trailer(name: String, value: String): HttpRequestBuilder {
trailers.add(name, value)
return this
}
/**
* Adds the specifies trailers to this request trailers
*
* @param trailers the trailers to be added into the request.
* @return HttpRequestBuilder whose trailers has been set.
*/
public func addTrailers(trailers: HttpHeaders): HttpRequestBuilder {
this.trailers.addAll(trailers)
return this
}
/**
* Sets the specifies trailers to this request trailers.
*
* @param trailers the trailers to be set to the request.
* @return HttpRequestBuilder whose trailers has been set.
*/
public func setTrailers(trailers: HttpHeaders): HttpRequestBuilder {
_trailers = trailers
return this
}
// cjlint-ignore -start !G.OTH.03
/**
* For http/2 only.
* Sets request priority.
*
* @param urg between 0 ~ 7, inclusive, represents the urgency of the request, the default value is 3.
* @param inc requests, incremental of which is set to false, are not expected to be handled concurrently.
* See RFC 9218 2.1.1.4. , https://www.rfc-editor.org/rfc/rfc9218.html#name-priority-parameters
* @return HttpRequestBuilder whose priority has been set.
*
* @throws HttpException, if urg is not in 0 ~ 7, inclusive.
*/
// cjlint-ignore -end
public func priority(urg: Int64, inc: Bool): HttpRequestBuilder {
if (urg < 0 || urg > 7) {
throw HttpException("Illegal header, priority urgency should be in range [0, 7].")
}
priorityValue = if (inc) {
"u=${urg}, i"
} else {
"u=${urg}"
}
return this
}
/*
* Set the HttpRequestBuilder's readTimeout, the default value of readTimeout is None.
* If this parameter is set, the read response timeout the requests
* builded by this builder will be based on this parameter.
*
* @param timeout the request's readTimeout configuration.
* @return HttpRequestBuilder whose readTimeout has been configured.
*/
public func readTimeout(timeout: Duration): HttpRequestBuilder {
// checkDuration ensures that the timeout is not negative
_readTimeout = checkDuration(timeout)
return this
}
/*
* Set the HttpRequestBuilder's writeTimeout, the default value of writeTimeout is None.
* If this parameter is set, the write request timeout of the requests
* builded by this builder will be based on this parameter.
*
* @param timeout the request's readTimeout configuration.
* @return HttpRequestBuilder whose readTimeout has been configured.
*/
public func writeTimeout(timeout: Duration): HttpRequestBuilder {
_writeTimeout = checkDuration(timeout)
return this
}
/**
* For server to set remote address, in order to record the client, which has sent the request.
*/
func remoteAddr(addr: SocketAddress): HttpRequestBuilder {
_remoteAddr = addr
return this
}
/**
* Sets the method to "GET", an alternative of method("GET") for convenience.
*
* @return HttpRequestBuilder whose method has been set to GET.
*/
public func get(): HttpRequestBuilder {
this._method = "GET"
return this
}
/**
* Sets the method to "HEAD", an alternative of method("GET") for convenience.
*
* @return HttpRequestBuilder whose method has been set to HEAD.
*/
public func head(): HttpRequestBuilder {
this._method = "HEAD"
return this
}
/**
* Sets the method to "OPTIONS", an alternative of method("GET") for convenience.
*
* @return HttpRequestBuilder whose method has been set to OPTIONS.
*/
public func options(): HttpRequestBuilder {
this._method = "OPTIONS"
return this
}
/**
* Sets the method to "TRACE", an alternative of method("TRACE") for convenience.
*
* @return HttpRequestBuilder whose method has been set to TRACE.
*/
public func trace(): HttpRequestBuilder {
this._method = "TRACE"
return this
}
/**
* Sets the method to "DELETE", an alternative of method("DELETE") for convenience.
*
* @return HttpRequestBuilder whose method has been set to DELETE.
*/
public func delete(): HttpRequestBuilder {
this._method = "DELETE"
return this
}
/**
* Sets the method to "POST", an alternative of method("POST") for convenience.
*
* @return HttpRequestBuilder whose method has been set to POST.
*/
public func post(): HttpRequestBuilder {
this._method = "POST"
return this
}
/**
* Sets the method to "PUT", an alternative of method("PUT") for convenience.
*
* @return HttpRequestBuilder whose method has been set to PUT.
*/
public func put(): HttpRequestBuilder {
this._method = "PUT"
return this
}
/**
* Sets the method to "CONNECT", an alternative of method("CONNECT") for convenience.
*
* @return HttpRequestBuilder whose method has been set to CONNECT.
*/
public func connect(): HttpRequestBuilder {
this._method = "CONNECT"
return this
}
/**
* Builds a request.
*
* @return HttpRequest whose arguments are set by the builder.
*/
public func build(): HttpRequest {
let bodySize: ?Int64 = sizeOf(_body)
if (!priorityValue.isEmpty() && (_headers.isNone() || headers.get("priority").isEmpty())) { // cjlint-ignore !G.EXP.03
headers.add("priority", priorityValue)
}
return HttpRequest(
_version: _version,
_method: _method,
_url: _url,
_headers: _headers,
_body: _body,
_trailers: _trailers,
_bodySize: bodySize,
priorityValue: priorityValue,
_remoteAddr: _remoteAddr,
_readTimeout: _readTimeout,
_writeTimeout: _writeTimeout
)
}
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
}
}
}
}
public class HttpRequest <: ToString {
HttpRequest(
var _method!: String = "GET",
var _url!: ?URL = None,
var _version!: Protocol = HTTP2_0,
var _headers!: ?HttpHeaders = None,
var _body!: InputStream = HttpEmptyBody.INSTANCE,
var _trailers!: ?HttpHeaders = None,
var _bodySize!: ?Int64 = 0,
// contentLength is used to decide how to send the request.body
var contentLength!: ?Int64 = None,
var expectContinuation!: Bool = false,
var priorityValue!: String = "",
var _form!: ?Form = None,
var _remoteAddr!: ?SocketAddress = None,
var _readTimeout!: ?Duration = None,
var _writeTimeout!: ?Duration = None
) {}
var requestLine: ?String = None
private var formRead = false
static let empty = HttpRequest(
_method: "",
_url: None,
_version: UnknownProtocol(""),
_headers: None,
_body: HttpEmptyBody.INSTANCE,
_trailers: None,
_bodySize: None,
priorityValue: ""
)
func reset(): Unit {
_method = "GET"
_url = None
_headers?.reset()
_body = HttpEmptyBody.INSTANCE
_trailers?.reset()
_bodySize = 0
priorityValue = ""
_remoteAddr = None
_readTimeout = None
_writeTimeout = None
}
/**
* Gets the method of this request.
* Default value of method is "GET".
*/
public prop method: String {
get() {
_method
}
}
/**
* Gets the url of this request.
* url must be set.
*/
public prop url: URL {
get() {
return match (_url) {
case Some(v) => v
case None =>
_url = EMPTY_URL
EMPTY_URL
}
}
}
/**
* Gets the Http-version of this request.
* Default value of version is HTTP1_1
*/
public prop version: Protocol {
get() {
_version
}
}
/**
* Gets the headers of this request, which stores the key-value pair of the request 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 request.
* Default value of body is an InputStream with no data.
*/
public prop body: InputStream {
get() {
_body
}
}
/**
* Gets the trailers of this request, 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
t
}
}
}
/**
* Gets the body size of this request.
*
* @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
}
}
/**
* If method is POST, PUT, PATCH, and contentType is "application/x-www-form-urlencoded", form represents the body,
* If method is not POST, PUT, PATCH, form represents the query in URL.
* in this case, body is read to end when get form.
*
* @throws UrlSyntaxException, if body is not invalid as a form
*/
public prop form: Form {
get() {
if (let Some(f) <- _form) {
return f
}
let f = match (this._method) {
case "POST" | "PUT" | "PATCH" => match (headers.getInternal("content-type")) {
case Some(hv) =>
if (hv.splitAnyMatch(SEMICOLON, "application/x-www-form-urlencoded")) {
readFormBody()
} else {
Form()
}
case None => Form()
}
case _ => url.queryForm
}
_form = f
return f
}
}
/**
* For server only.
* Gets address of client, which has sent the request.
* It has no defined format, in default implementation of server it is set to "IP: port".
*/
public prop remoteAddr: String {
get() {
_remoteAddr?.toString() ?? ""
}
}
/**
* For http/1 only.
* For server to determine whether to close the connection after processing the request.
* For client to determine whether to close the connection after receiving response to the request.
*/
public prop isPersistent: Bool {
get() {
if (let Some(hv) <- (_headers?.getInternal("connection") ?? return true)) {
return !hv.splitAnyMatch(SYMBOL_COMMA, "close")
}
return true
}
}
public prop readTimeout: ?Duration {
get() {
return _readTimeout
}
}
public prop writeTimeout: ?Duration {
get() {
return _writeTimeout
}
}
/**
* Prints request start line, headers, body size, trailer.
*
* @return String that can represent the request.
*/
public override func toString(): String {
let str = StringBuilder()
if (let Some(s) <- requestLine) {
str.append(s)
} else {
match (_method) {
case "CONNECT" =>
let host = _url?.hostName ?? ""
let port = _url?.port ?? ""
if (!port.isEmpty()) {
str.append("${_method} ${host}:${port} ${_version.toString()}")
} else {
str.append("${_method} ${host} ${_version.toString()}")
}
case _ => str.append("${_method} ${_url?.path ?? ""} ${_version.toString()}")
}
}
str.append("\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()
}
/**
* Reads body to form.
*/
private func readFormBody(): Form {
let body = ArrayList<UInt8>()
let buffer = Array<Byte>(4096, repeat: 0)
var readLen = 0
do {
readLen = this._body.read(buffer)
body.add(all: buffer.slice(0, readLen))
} while (readLen > 0)
var bodyData = unsafe { String.fromUtf8(body.getRawArray().slice(0, body.size)) }
return Form(bodyData)
}
}