RrunningW```
966a187b创建于 2月10日历史提交
/*
Copyright (c) 2025 WuJingrun(吴京润)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
 */
package f_mvc

public class RequestMeta <: HttpRequestHandler & ToString & Comparable<RequestMeta> {
    private static let log = LoggerFactory.getLogger<RequestMeta>()
    static let currentResponseStatus = ThreadLocal<UInt16>()
    static func setResponseStatus(status: HttpStatus) {
        currentResponseStatus.set(status.value)
    }
    private static func getResponseStatus(): UInt16 {
        match (currentResponseStatus.get()) {
            case Some(c) => c
            case _ => HttpStatus.OK.value
        }
    }
    private var ignoreAuth_ = false
    private var ignorePrivilege_ = false

    private let produces = HashSet<String>()
    private let consumes = HashSet<String>()

    private let args = RequestArgMeta()

    var pattern = None<HttpRequestPathPatterns>

    RequestMeta(
        public let path: String,
        public let method: RequestMethod,
        private let params!: RequestCondition = DummyRequestCondition.instance,
        private let headers!: RequestCondition = DummyRequestCondition.instance
    ) {}

    public func compare(other: RequestMeta): Ordering {
        var cmp = path.compare(other.path)
        if (cmp == Ordering.EQ) {
            cmp = method.compare(other.method)
        }
        if (cmp == Ordering.EQ) {
            let tree1 = TreeSet<String>(consumes).iterator()
            let tree2 = TreeSet<String>(other.consumes).iterator()
            while (cmp == Ordering.EQ && let Some(c1) <- tree1.next() && let Some(c2) <- tree2.next()) {
                cmp = c1.compare(c2)
            }
        }
        if (cmp == Ordering.EQ) {
            cmp = consumes.size.compare(other.consumes.size)
        }
        return cmp
    }
    public func toString(): String {
        'path: ${path}; method: ${method}; consumes: ${consumes}; consumesSize: ${consumes.size}'
    }

    private var handle_: (HttpContext) -> Unit = {
        ctx: HttpContext => throw MVCException('Request was not handled')
    }

    public func handle(ctx: HttpContext): Unit {
        handle_(ctx)
    }

    private func populateResponseContentType(ctx: HttpContext) {
        let accept = if (let Some(accept) <- ctx.request.headers.getFirst('Accept')) {
            accept
        } else {
            '*/*'
        }
        ctx.responseBuilder.header('Content-Type', accept)
    }
    public prop ignoreAuth: Bool {
        get() {
            ignoreAuth_
        }
    }
    public prop ignorePrivilege: Bool {
        get() {
            ignorePrivilege_
        }
    }
    internal func iterateConsumes(fn: (String) -> Unit): Unit {
        for (c in consumes) {
            fn(c)
        }
    }
    private func checkHeader(ctx: HttpContext, headers: HashSet<String>, name: String): Bool {
        headers.isEmpty() || headers.contains(ctx.request.headers.getFirst(name) ?? '') || headers.contains('*/*') ||
            headers.contains("*") || headers.contains('')
    }
    public func checkProduces(ctx: HttpContext) {
        if (let Some(accept) <- ctx.request.headers.getFirst('Accept')) {
            accept.isEmpty() || accept == '*' || accept == '*/*' || produces.contains(accept) || if (accept
                .indexOf(',')
                .isSome()) {
                let priority = AcceptQueue(accept)
                while (let Some((q, idx, c)) <- priority.remove()) {
                    log.debug {'RequestMeta.checkProduces.MultiAccept:${q};${idx};${c}'}
                    if (c.startsWith('*/*') || produces.contains(c)) {
                        ctx.request.headers.set('Accept', c)
                        return true
                    }
                }
                false
            } else {
                false
            }
        } else {
            true
        }
    }

    public func check(ctx: HttpContext): Bool {
        let request = ctx.request
        this.headers.check(request.headers) && this.params.check(request.form, request.url)
    }
    public func setHandle<T>(fn: (T, HttpContext, HttpRequestPathPatterns) -> Any): Unit where T <: Object {
        let pattern = this.pattern.getOrThrow()
        this.handle_ = {
            ctx =>
                let result = try {
                    CurrentHttpContext.set(ctx)
                    fn(BeanFactory.instance.getFirst<T>().getOrThrow(), ctx, pattern)
                } finally {
                    CurrentHttpContext.clear()
                }
                match (result) {
                    case x: JsonValue => respond(x, ctx)
                    case x: InputStream => respond(x, ctx)
                    case x: Array<Byte> => respond(x, ctx)
                    case _: Unit => ()
                    case x: ToData => respond(x, ctx)
                    case x: Redirect => respond(x, ctx)
                    case _ =>
                        let request = ctx.request
                        log.warn {'fountain.mvc.handle ${request.method}:${request.url};${TypeInfo.of(result)}'}
                        MultiRequestMethodHandler.NOT_ACCEPTABLE.handle(ctx)
                }
        }
    }
    public func checkAuth(ctx: HttpContext, args: ArrayList<Any>): Bool {
        let checked = if (ignoreAuth && ignorePrivilege) {
            return true
        } else {
            AuthHandlerProxy.instance.check(AuthParam(ctx, path, args, ignoreAuth, ignorePrivilege))
        }
        let (check, status, resp) = match (checked) {
            case OK => return true
            case SessionNotFound(status, resp) => ('SessionNotFound', status, resp)
            case SessionNotFound(resp) => ('SessionNotFound', HttpStatus.OK, resp)
            case InvalidSession(status, resp) => ('InvalidSession', status, resp)
            case InvalidSession(resp) => ('InvalidSession', HttpStatus.OK, resp)
            case SessionError(status, resp) => ('SessionError', status, resp)
            case SessionError(resp) => ('SessionError', HttpStatus.OK, resp)
            case PrivilegeError(status, resp) => ('PrivilegeError', status, resp)
            case PrivilegeError(resp) => ('PrivilegeError', HttpStatus.OK, resp)
            case NoPrivilege(status, resp) => ('NoPrivilege', status, resp)
            case NoPrivilege(resp) => ('NoPrivilege', HttpStatus.OK, resp)
        }
        setResponseStatus(status)
        let accept = ctx.request.headers.getFirst('Accept') ?? '*/*'
        let builder = ctx.responseBuilder.status(status.value).header('Content-Type', accept)
        func respondStatusReasonPhrase() {
            builder.body(if (status == HttpStatus.OK) {
                HttpStatus.UNAUTHORIZED.reasonPhrase
            } else {
                status.reasonPhrase
            })
        }
        log.warn {
            'fountain.mvc.check ${this}; accept: ${accept}; status: ${status.value}; CheckHandler returns ${TypeInfo.of(resp)}; checked: ${check}'
        }
        match (resp) {
            case x: String => builder.body(x)
            case x: Array<Byte> => builder.body(x)
            case x: InputStream => builder.body(x)
            case x: ToString => builder.body(x.toString())
            case x: ToData where accept != '*/*' => match (MediaTypes.tryParse(accept)) {
                case Some(mt) => builder.body(mt.fromData(x.toData()))
                case _ => respondStatusReasonPhrase()
            }
            case _ => respondStatusReasonPhrase()
        }
        false
    }
    func setHandle<T>(wsmeta: WSMeta<T>) where T <: Object {
        this.handle_ = {
            ctx => wsmeta.handle(BeanFactory.instance.getFirst<T>().getOrThrow(), ctx, pattern.getOrThrow())
        }
        produces.add('*')
        consumes.add('*')
    }
    private func respond(method: String, fn: () -> Unit): Unit {
        try {
            fn()
        } finally {
            OverallStopwatch.elapsed(method)
        }
    }
    private func respond<R>(result: R, ctx: HttpContext): Unit where R <: ToData {
        respond(ctx.request.method) {
            let accept = ctx.request.headers.getFirst('Accept')
            if (!(result is Unit) && let Some(accept) <- accept) {
                if (let Some(mediaType) <- MediaTypes.tryParse(accept)) {
                    let bytes = mediaType.fromData(result.toData())
                    ctx.responseBuilder.header('Content-Type', accept).body(bytes)
                } else if (accept == '*/*') {
                    let acc = this.produces.iterator().next() ?? 'application/json'
                    let mt = MediaTypes.parse(acc)
                    let bytes = mt.fromData(result.toData())
                    ctx.responseBuilder.header('Content-Type', acc).body(bytes)
                } else {
                    if (let r: Resource <- result) {
                        try {
                            r.close()
                        } catch (e: Exception) {
                            log.warn(e){'Error occurred on closing ${TypeInfo.of(result)}; path: ${path}; accept: ${accept}'}
                        }
                    }
                    MultiRequestMethodHandler.NOT_ACCEPTABLE.handle(ctx)
                }
            } else if ((result is Unit) && let Some(accept) <- accept && accept == '*/*') {
                ctx.responseBuilder.header('Content-Type', accept).body('')
            } else if ((result is Unit) && accept.isNone()) {
                ctx.responseBuilder.body('')
            } else {
                MultiRequestMethodHandler.NOT_ACCEPTABLE.handle(ctx)
            }
        }
    }
    private func respond(response: JsonValue, ctx: HttpContext): Unit {
        respond(ctx.request.method) {
            if (let Some(accept) <- ctx.request.headers.getFirst('Accept')) {
                if (let Some(mediaType) <- MediaTypes.tryParse(accept) && let jmt: JsonMediaType <- mediaType) {
                    let bytes = jmt.fromData(response)
                    ctx.responseBuilder.header('Content-Type', accept).body(bytes)
                } else if (accept == '*/*') {
                    ctx.responseBuilder.header('Content-Type', 'application/json').body(response.toString())
                } else {
                    MultiRequestMethodHandler.NOT_ACCEPTABLE.handle(ctx)
                }
            } else {
                ctx.responseBuilder.header('Content-Type', 'application/json').body(response.toString())
            }
        }
    }
    private func respond(redirect: Redirect, ctx: HttpContext): Unit {
        respond(ctx.request.method) {
            ctx.responseBuilder.status(redirect.status.value).header('Location', redirect.location)
        }
    }
    private func respond(response: InputStream, ctx: HttpContext): Unit {
        respond(ctx.request.method) {
            populateResponseContentType(ctx)
            ctx.responseBuilder.body(response)
        }
    }
    private func respond(response: Array<Byte>, ctx: HttpContext): Unit {
        respond(ctx.request.method) {
            populateResponseContentType(ctx)
            ctx.responseBuilder.body(response)
        }
    }
    private func respond(response: String, ctx: HttpContext): Unit {
        respond(ctx.request.method) {
            populateResponseContentType(ctx)
            ctx.responseBuilder.body(response.unsafeBytes())
        }
    }
    
    public func isMultipart(name: String) {
        args[name] is MultipartFormData
    }

    public func extract<T, P>(ctx: HttpContext, name: String, default: ?P): T where P <: DataFields<P> {
        match (default) {
            case _: ?HttpContext => (ctx as T).getOrThrow()
            case _: ?HttpRequest => (ctx.request as T).getOrThrow()
            case _: ?HttpHeaders => (ctx.request.headers as T).getOrThrow()
            case _: ?HttpResponseBuilder => (ctx.responseBuilder as T).getOrThrow()
            case _: ?Array<Byte> => (this.extractBytes(ctx) as T).getOrThrow()
            case _: ?Form => (ctx.request.form as T).getOrThrow()
            case _ => match (default) {
                case Some(d: P) =>
                    let val = args.extract<P>(ctx, name, d, pattern)
                    if (let x: T <- val) {
                        x
                    } else if (let x: T <- Some(val)) {
                        x
                    } else {
                        throw UnreachableException()
                    }
                case _ =>
                    let val = args.extract<P>(ctx, name, pattern)
                    match (val) {
                        case x: T => x
                        case Some(x: T) => x
                        case _ => throw HttpRequestException(
                            'NO param for controller func, which request param name is ${name}')
                    }
            }
        }
    }

    public func extractBytes(ctx: HttpContext): Array<Byte> {
        let buf = ByteBuffer()
        copy(ctx.request.body, to: buf)
        buf.bytes()
    }
    private func addMediaTypes(set: HashSet<String>, types: Array<String>, allowAny: Bool, allowEmpty: Bool): Unit {
        if (types.isEmpty()) {
            if (!allowEmpty) {
                throw IllegalArgumentException('mime type must not be empty string')
            }
            set.add('')
            return
        }
        for (t in types) {
            match (t.trimAscii()) {
                case '*' | '*/*' where !allowAny => throw IllegalArgumentException(
                    "Invalid mime type: ${t} in ${types.toString().replace(', ', '|').replace('[', '').replace(']', '')}")
                case '*' | '*/*' =>
                    set.add('*')
                    set.add('*/*')
                case '' where !allowEmpty => throw IllegalArgumentException(
                    "mime type must not be empty string in ${types.toString().replace(', ', '|').replace('[', '').replace(']', '')}")
                case x => set.add(x)
            }
        }
    }
    private func addMediaTypes(set: HashSet<String>, types: String, allowAny: Bool, allowEmpty: Bool): Unit {
        addMediaTypes(set, types.split("|"), allowAny, allowEmpty)
    }
    private func addProduces(types: String) {
        addMediaTypes(produces, types, true, false)
    }
    private func addConsumes(types: String) {
        addMediaTypes(consumes, types, false, true)
    }
    public static func generate<T>(fnName: String, argTypes: Array<TypeInfo>): ArrayList<RequestMeta> where T <: Object {
        let typeInfo = TypeInfo.of<T>()
        let metas = ArrayList<RequestMeta>()
        let function = typeInfo.getInstanceFunction(fnName, argTypes)
        let functionParamMap = HashMap<String, ParameterInfo>()
        for (param in function.parameters) {
            functionParamMap[param.name] = param
        }
        let args = HashMap<String, (ControllerFuncParam, Validator, ?AbstractDateTimeConverter)>()
        let zero = unsafeZeroValue<ControllerFuncParam>()
        for (param in function.parameters) {
            let pname = param.name
            var toThrow = true
            for (annotation in param.annotations) {
                match (annotation) {
                    case x: ControllerFuncParam => match (args.get(pname)) {
                        case Some((p, v, c)) where refEq(p, zero) =>
                            args[pname] = (x, v, c)
                            toThrow = false
                            break
                        case None =>
                            args[pname] = (x, DummyValidator.instance, None<AbstractDateTimeConverter>)
                            toThrow = false
                        case _ => throw UnreachableException()
                    }
                    case x: Validator => match (args.get(pname)) {
                        case Some((p, v, c)) where v is DummyValidator =>
                            args[pname] = (p, x, c)
                            toThrow = false
                            break
                        case None => args[pname] = (zero, x, None<AbstractDateTimeConverter>)
                        case _ => throw UnreachableException()
                    }
                    case x: AbstractDateTimeConverter => match (args.get(pname)) {
                        case Some((p, v, c)) where c.isNone() =>
                            args[pname] = (p, v, x)
                            toThrow = false
                            break
                        case None => args[pname] = (zero, DummyValidator.instance, x)
                        case _ => throw UnreachableException()
                    }
                    case _ => continue
                }
            }
            if (toThrow) {
                throw MVCException(
                    'parameter of controller function must be with one and only one Annotation as @RequestBody or @RequestParam or @RequestHeader or @PathVariable or @MultipartFormData')
            }
        }
        let ignoreSecurity = function.findAnnotation<IgnoreSecurity>().isSome()
        let ignoreAuthGlobal = ignoreSecurity || function.findAnnotation<IgnoreAuth>().isSome()
        let ignorePrivilegeGlobal = ignoreSecurity || function.findAnnotation<IgnorePrivilege>().isSome()
        for (annotation in function.annotations) {
            if (let a: RequestMapping <- annotation) {
                let method = a.method
                let ignoreAuth = a.ignoreAuth || ignoreAuthGlobal
                let ignorePrivilege = a.ignorePrivilege || ignorePrivilegeGlobal
                let params = RequestCondition.compile(a.params)
                let headers = RequestCondition.compile(a.headers)
                for (path in a.path.split('|')) {
                    let meta = RequestMeta(path, method, params: params, headers: headers)
                    meta.addProduces(a.produces)
                    meta.addConsumes(a.consumes)
                    meta.ignoreAuth_ = ignoreAuth
                    meta.ignorePrivilege_ = ignorePrivilege
                    meta.args.add(all: args)
                    RequestMetas.instance.add(meta)
                    metas.add(meta)
                }
            }
        }
        metas
    }
    public func accessLog<T>(start: MonoTime, ctx: HttpContext, args: ArrayList<Any>, returned: ?Any, e: ?Exception) where T <: Object {
        func anyToString(any: Any) {
            let arg = match (any) {
                case x: HttpContext => x
                case x: HttpRequest => x
                case x: HttpHeaders => x
                case x: HttpResponseBuilder => x
                case x: Form => return x.toEncodeString()
                case x: String => return x
                case x: ToString => return x.toString()
                case x: Array<Byte> => return toBase64String(x)
                case x: MultipartFile => return JsonString(x.toString()).toString()
                case x: ToData => return JsonValue.tryFromData(x.toData()).toString()
                case x: Data => return JsonValue.tryFromData(x).toString()
                case x => x
            }
            TypeInfo.of(arg).toString()
        }
        let request = ctx.request
        let argGen = StringGenerator().append('[')
        for (arg in args) {
            if (argGen.size > 1) {
                argGen.append(',')
            }
            let s = anyToString(arg)
            argGen.append(s)
        }
        argGen.append(']')
        let result = match (returned) {
            case Some(x) => anyToString(x)
            case _ => 'None'
        }
        let log = LoggerFactory.getLogger<T>()
        func logContent(){
            'MVC.accessLog:${request.method}:${request.url}?${request.form.toEncodeString()}; consumes:${request.headers.getFirst('Content-Type')}; params:${argGen}; returned:${result}; status:${getResponseStatus()}; elapsed:${MonoTime.now() - start}'
        }
        if (let Some(ex) <- e) {
            let exce = try {
                MultiRequestMethodHandler.INTERNAL_SERVER_ERROR.handle(ctx, ex)
                ex
            } catch (e: Exception) {
                let status = HttpStatus.INTERNAL_SERVER_ERROR
                let b = ctx.responseBuilder
                b.status(status.value)
                b.body(status.reasonPhrase)
                setResponseStatus(status)
                let mvcex = MVCException(e)
                mvcex.addSuppressed(ex)
                mvcex
            }
            log.error(exce, logContent)
        } else {
            log.info(logContent)
        }
    }
}