diff --git a/packages/web/src/constants.ts b/packages/web/src/constants.ts index 1a7fbe33..0302cc91 100644 --- a/packages/web/src/constants.ts +++ b/packages/web/src/constants.ts @@ -1,2 +1,16 @@ export const WebProxyBeanName = 'webServerProxy' export const FilterProxyBeanName = 'webFilterProxy' +export const METADATA_KEY = { + Controller: Symbol("@ccms/web:Controller"), + Action: Symbol("@ccms/web:Action"), + Param: Symbol("@ccms/web:Param"), + Middleware: Symbol("@ccms/web:Middleware"), +} +export enum PARAM_TYPE { + QUERY = 'QUERY', + BODY = 'BODY', + HEADER = 'HEADER', + COOKIE = 'COOKIE', + REQUEST = 'REQUEST', + RESPONSE = 'RESPONSE', +} diff --git a/packages/web/src/decorators/index.ts b/packages/web/src/decorators/index.ts index eb3112aa..8d251d6f 100644 --- a/packages/web/src/decorators/index.ts +++ b/packages/web/src/decorators/index.ts @@ -1,30 +1,80 @@ -export const Controller = () => { - return (target: TFunction): ClassDecorator => { +import { decorate, injectable } from "@ccms/container" + +import { METADATA_KEY, PARAM_TYPE } from '../constants' +import { interfaces } from "../interfaces" +import { addControllerMetadata, addControllerAction, addActionParam } from "./utils" + +export const Controller = (metadata?: string | interfaces.ControllerMetadata) => { + return (target: any) => { + if (!metadata) { metadata = target.name.toLowerCase().replace('controller', '') } + if (typeof metadata === "string") { metadata = { path: metadata } } + metadata.name = metadata.name || target.name + metadata.path = metadata.path ?? `/${metadata}` + metadata.path = metadata.path.startsWith('/') ? metadata.path : `/${metadata.path}` + decorate(injectable(), target) + Reflect.defineMetadata(METADATA_KEY.Controller, metadata, target) + addControllerMetadata(metadata) return } } -export const Post = () => { - return (target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor): MethodDecorator => { - return + +function action(method: interfaces.Method) { + return (metadata?: string | interfaces.ActionMetadata) => { + return (target: any, propertyKey: string) => { + if (!metadata) { metadata = propertyKey.toLowerCase() } + if (typeof metadata === "string") { metadata = { path: metadata } } + metadata.path = metadata.path ?? `/${propertyKey}` + metadata.path = metadata.path.startsWith('/') ? metadata.path : `/${metadata.path}` + metadata.method = method + metadata.executor = propertyKey + Reflect.defineMetadata(METADATA_KEY.Action, metadata, target[propertyKey]) + addControllerAction(target, propertyKey) + return + } } } -export const Get = () => { - return (target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor): MethodDecorator => { - return +export const Action = action('ALL') +export const Get = action('GET') +export const Post = action('POST') +export const Put = action('PUT') +export const Patch = action('PATCH') +export const Head = action('HEAD') +export const Delete = action('DELETE') + +function param(type: PARAM_TYPE) { + return (metadata?: string | interfaces.ParamMetadata) => { + return (target: any, propertyKey: string, index: number) => { + if (!metadata) { metadata = `${propertyKey}-${index}` } + if (typeof metadata === "string") { metadata = { name: metadata } } + metadata.type = type + metadata.index = index + metadata.paramtype = Reflect.getMetadata("design:paramtypes", target, propertyKey)[index] + addActionParam(target, propertyKey, metadata) + return + } } } -export const Header = () => { - return (target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor): MethodDecorator => { - return - } -} -export const Param = () => { - return (target: Object, propertyKey: string | symbol, parameterIndex: number): ParameterDecorator => { - return - } -} -export const RequestBody = () => { - return (target: Object, propertyKey: string | symbol, parameterIndex: number): ParameterDecorator => { - return +export const Request = param(PARAM_TYPE.REQUEST) +export const Response = param(PARAM_TYPE.RESPONSE) +export const Header = param(PARAM_TYPE.HEADER) +export const Cookie = param(PARAM_TYPE.COOKIE) +export const Query = param(PARAM_TYPE.QUERY) +export const Param = param(PARAM_TYPE.QUERY) +export const Body = param(PARAM_TYPE.BODY) + +function Middleware() { + return (metadata?: string | interfaces.ActionMetadata) => { + return (target: any, propertyKey: string) => { + if (!metadata) { metadata = propertyKey.toLowerCase() } + if (typeof metadata === "string") { metadata = { path: metadata } } + metadata.path = metadata.path ?? `/${propertyKey}` + metadata.path = metadata.path.startsWith('/') ? metadata.path : `/${metadata.path}` + metadata.executor = propertyKey + Reflect.defineMetadata(METADATA_KEY.Action, metadata, target[propertyKey]) + addControllerAction(target, propertyKey) + return + } } } + +export * from './utils' diff --git a/packages/web/src/decorators/utils.ts b/packages/web/src/decorators/utils.ts new file mode 100644 index 00000000..3f858163 --- /dev/null +++ b/packages/web/src/decorators/utils.ts @@ -0,0 +1,27 @@ +import { interfaces } from '../interfaces' +import { METADATA_KEY } from '../constants' + +export function getControllerMetadatas(): interfaces.ControllerMetadata[] { + return Reflect.getMetadata(METADATA_KEY.Controller, Reflect) || [] +} +export function addControllerMetadata(metadata: interfaces.ControllerMetadata) { + Reflect.defineMetadata(METADATA_KEY.Controller, [metadata, ...getControllerMetadatas()], Reflect) +} +export function getControllerActions(target: any): string[] { + return Reflect.getMetadata(METADATA_KEY.Action, target.constructor) || [] +} +export function addControllerAction(target: any, propertyKey: string) { + Reflect.defineMetadata(METADATA_KEY.Action, [propertyKey, ...getControllerActions(target)], target.constructor) +} +export function getControllerMetadata(target: any): interfaces.ControllerMetadata { + return Reflect.getMetadata(METADATA_KEY.Controller, target) +} +export function getActionMetadata(target: any, propertyKey: string): interfaces.ActionMetadata { + return Reflect.getMetadata(METADATA_KEY.Action, target[propertyKey]) +} +export function getActionParams(target: any, propertyKey: string): interfaces.ParamMetadata[] { + return Reflect.getMetadata(METADATA_KEY.Param, target[propertyKey]) || [] +} +export function addActionParam(target: any, propertyKey: string, metadata: interfaces.ParamMetadata) { + Reflect.defineMetadata(METADATA_KEY.Param, [metadata, ...getActionParams(target, propertyKey)], target[propertyKey]) +} diff --git a/packages/web/src/index.ts b/packages/web/src/index.ts index 449e781d..0137597b 100644 --- a/packages/web/src/index.ts +++ b/packages/web/src/index.ts @@ -6,5 +6,6 @@ /// export * from './server' +export * from './constants' export * from './decorators' export * from './interfaces' diff --git a/packages/web/src/interfaces/context.ts b/packages/web/src/interfaces/context.ts index 674bfb0f..219f276d 100644 --- a/packages/web/src/interfaces/context.ts +++ b/packages/web/src/interfaces/context.ts @@ -5,14 +5,20 @@ export interface InterceptorAdapter { postHandle?(ctx: Context): void } -export type RequestHeader = { [key: string]: string | string[] } -export type RequestParams = { [key: string]: string | string[] } +type StringKeyAndStringValue = { [key: string]: string } +type StringKeyAndStringOrArrayValue = { [key: string]: string | string[] } + +export type RequestHeaders = StringKeyAndStringOrArrayValue +export type RequestParams = StringKeyAndStringOrArrayValue +export type RequestCookies = StringKeyAndStringValue export interface Context { request?: javax.servlet.http.HttpServletRequest response?: javax.servlet.http.HttpServletResponse - header?: RequestHeader + handler?: RequestHandler url?: string + headers?: RequestHeaders + cookies?: RequestCookies params?: RequestParams body?: any result?: any diff --git a/packages/web/src/interfaces/metadata.ts b/packages/web/src/interfaces/metadata.ts index 52339d48..90bd95d2 100644 --- a/packages/web/src/interfaces/metadata.ts +++ b/packages/web/src/interfaces/metadata.ts @@ -1,14 +1,77 @@ -export interface BaseMetadata { - /** - * 名称 为空则为对象名称 - */ - name?: string - /** - * 支持的服务器列表 为空则代表所有 - */ - servers?: string[] -} +import { PARAM_TYPE } from "../constants" -export interface ControllerMetadata extends BaseMetadata { - +export namespace interfaces { + export interface BaseMetadata { + /** + * 名称 为空则为对象名称 + */ + name?: string + /** + * 支持的服务器列表 为空则代表所有 + */ + servers?: string[] + } + export interface WebMetadata extends BaseMetadata { + /** + * 路径 + */ + path: string + /** + * 对象 + */ + target?: string + } + export interface ControllerMetadata extends WebMetadata { + + } + interface Newable { + new(...args: any[]): T + } + interface Abstract { + prototype: T + } + export type ServiceIdentifier = (string | symbol | Newable | Abstract) + export interface MiddlewareMetadata extends BaseMetadata { + /** + * 中间件名称列表 + */ + names: Array> + } + export type Method = 'ALL' | 'GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'TRACE' + export interface ActionMetadata extends WebMetadata { + /** + * 请求方法 + */ + method?: Method + /** + * 执行器 + */ + executor?: string + } + export interface ParamMetadata extends BaseMetadata { + /** + * 参数类型 + */ + type?: PARAM_TYPE + /** + * 默认值 + */ + default?: any + /** + * 参数位置 + */ + index?: number + /** + * 参数对象类型 + */ + paramtype?: NewableFunction + /** + * 是否必传 + */ + require?: boolean + /** + * 异常消息 + */ + message?: string + } } diff --git a/packages/web/src/server.ts b/packages/web/src/server.ts index 2407b126..1138c84e 100644 --- a/packages/web/src/server.ts +++ b/packages/web/src/server.ts @@ -1,10 +1,16 @@ import * as querystring from 'querystring' import { web } from '@ccms/api' -import { provideSingleton, JSClass, postConstruct } from '@ccms/container' +import { provideSingleton, JSClass, postConstruct, Container, ContainerInstance, inject } from '@ccms/container' -import { WebProxyBeanName, FilterProxyBeanName } from './constants' -import { Context, InterceptorAdapter, RequestHandler } from './interfaces' +import { WebProxyBeanName, FilterProxyBeanName, METADATA_KEY, PARAM_TYPE } from './constants' +import { Context, InterceptorAdapter, RequestHandler, interfaces } from './interfaces' +import { getControllerActions, getActionMetadata, getControllerMetadata, getActionParams } from './decorators' + +const HttpServletRequestWrapper = Java.type('javax.servlet.http.HttpServletRequestWrapper') +const HttpServletResponseWrapper = Java.type('javax.servlet.http.HttpServletResponseWrapper') +const ServletInputStream = Java.type('javax.servlet.ServletInputStream') +const ServletOutputStream = Java.type('javax.servlet.ServletOutputStream') @provideSingleton(web.Server) export class Server { @@ -13,11 +19,14 @@ export class Server { @JSClass('pw.yumc.MiaoScript.web.WebFilterProxy') private WebFilterProxy: any + @inject(ContainerInstance) + private container: Container + private StreamUtils = org.springframework.util.StreamUtils private ResponseEntity = org.springframework.http.ResponseEntity private interceptors: Map - private handlerMapping: Map + private methodMappings: Map> private beanFactory: org.springframework.beans.factory.support.DefaultListableBeanFactory @@ -25,7 +34,7 @@ export class Server { initialization() { this.beanFactory = base.getInstance().getAutowireCapableBeanFactory() this.interceptors = new Map() - this.handlerMapping = new Map() + this.methodMappings = new Map() this.start() } @@ -43,13 +52,77 @@ export class Server { } } + registryController(target: any) { + if (!target) { throw new Error('Controller can\'t be undefiend!') } + let controllerMetadata = getControllerMetadata(target) + if (!controllerMetadata) { throw new Error(`Controller ${target.name} must have @Controller decorator!`) } + try { + this.container.rebind(METADATA_KEY.Controller).to(target).inSingletonScope().whenTargetNamed(target.name) + } catch{ + this.container.bind(METADATA_KEY.Controller).to(target).inSingletonScope().whenTargetNamed(target.name) + } + target = this.container.getNamed(METADATA_KEY.Controller, target.name) + let actions = getControllerActions(target) + for (const action of actions) { + let actionMetadata = getActionMetadata(target, action) + let path = `${controllerMetadata.path || ''}${actionMetadata.path || ''}` + if (!path) throw new Error(`Controller ${controllerMetadata.name} Action ${actionMetadata.name} path is empty!`) + if (!this.methodMappings.has(path)) { this.methodMappings.set(path, new Map()) } + console.debug(`Controller ${controllerMetadata.name} Registry ${path} to ${actionMetadata.executor || ''} Action function.`) + this.methodMappings.get(path).set(actionMetadata.method || 'ALL', (ctx: Context) => { + let args = [] + let params = getActionParams(target, action) + for (const index in params) { + let param = params[index] + let paramValue = undefined + switch (param.type) { + case PARAM_TYPE.REQUEST: paramValue = ctx.request; break + case PARAM_TYPE.RESPONSE: paramValue = ctx.response; break + case PARAM_TYPE.QUERY: paramValue = ctx.params[param.name]; break + case PARAM_TYPE.HEADER: paramValue = ctx.headers[param.name]; break + case PARAM_TYPE.BODY: paramValue = ctx.body; break + case PARAM_TYPE.COOKIE: paramValue = ctx.cookies[param.name]; break + } + if (param.require && !paramValue) { + return { + status: 400, + msg: param.message ?? `Param Type ${param.type} require not empty!`, + data: param + } + } + args[param.index] = paramValue ?? param.default + } + return target[actionMetadata.executor].apply(target, args) + }) + } + } + + unregistryController(target: any) { + if (!target) { throw new Error('Controller can\'t be undefiend!') } + let controllerMetadata = getControllerMetadata(target) + if (!controllerMetadata) { throw new Error(`Controller ${target.name} must have @Controller decorator!`) } + try { + target = this.container.getNamed(METADATA_KEY.Controller, target.name) + } catch (error) { + throw new Error(`Controller ${target.name} not registry! err: ${error}`) + } + let actions = getControllerActions(target) + for (const action of actions) { + let actionMetadata = getActionMetadata(target, action) + let path = `${controllerMetadata.path || ''}${actionMetadata.path || ''}` + if (!this.methodMappings.has(path)) { continue } + this.methodMappings.get(path).delete(actionMetadata.method) + } + } + registryMapping(path: string, handler: RequestHandler) { console.debug(`Registry Mapping ${path} to handle ${handler.name || ''} function.`) - this.handlerMapping.set(path, handler) + if (!this.methodMappings.has(path)) { this.methodMappings.set(path, new Map()) } + this.methodMappings.get(path).set("ALL", handler) } unregistryMapping(path: string) { - this.handlerMapping.delete(path) + if (this.methodMappings.has(path)) { this.methodMappings.get(path).delete("ALL") } } registryInterceptor(interceptor: InterceptorAdapter) { @@ -65,28 +138,72 @@ export class Server { try { this.beanFactory.destroySingleton(FilterProxyBeanName) } catch (ex) { } var WebFilterProxyNashorn = Java.extend(this.WebFilterProxy, { doFilter: (servletRequest: javax.servlet.http.HttpServletRequest, servletResponse: javax.servlet.http.HttpServletResponse, filterChain: javax.servlet.FilterChain) => { - console.log('WebFilterProxyNashorn', 'doFilter', servletRequest, servletResponse) filterChain.doFilter(servletRequest, servletResponse) } }) this.beanFactory.registerSingleton(FilterProxyBeanName, new WebFilterProxyNashorn()) } + // private getRequestWrapper(servletRequest: javax.servlet.http.HttpServletRequest) { + // var body = org.springframework.util.StreamUtils.copyToByteArray(servletRequest.getInputStream()) + // var HttpServletRequestWrapperAdapter = Java.extend(HttpServletRequestWrapper, { + // getInputStream: () => { + // var bais = new java.io.ByteArrayInputStream(body) + // return new ServletInputStream({ + // read: () => bais.read(), + // isFinished: () => bais.available() == 0 + // }) + // } + // }) + // var wrapper = new HttpServletRequestWrapperAdapter(servletRequest) + // return wrapper + // } + + // private getResponseWrapper(servletResponse: javax.servlet.http.HttpServletResponse) { + // var HttpServletRequestWrapperAdapter = Java.extend(HttpServletRequestWrapper, { + // getOutputStream: () => { + // return new ServletOutputStream({ + // }) + // } + // }) + // var wrapper = new HttpServletRequestWrapperAdapter(servletResponse) + // return wrapper + // } + + private notFound(method: string, path: string) { + return { + status: 404, + msg: "handlerMapping Not Found!", + method, + path, + timestamp: Date.now() + } + } + private registryWebProxy() { try { this.beanFactory.destroySingleton(WebProxyBeanName) } catch (ex) { } var WebServerProxyNashorn = Java.extend(this.WebServerProxy, { process: (req: javax.servlet.http.HttpServletRequest, resp: javax.servlet.http.HttpServletResponse) => { - let ctx: Context = { request: req, response: resp } + let path = req.getRequestURI() + if (!this.methodMappings.has(path)) return this.notFound(req.getMethod(), path) + let mappings = this.methodMappings.get(req.getRequestURI()) + let handler = mappings.get(req.getMethod()) || mappings.get("ALL") + if (!handler) return this.notFound(req.getMethod(), path) + let ctx: Context = { request: req, response: resp, params: {}, body: {}, handler } ctx.url = req.getRequestURI() // @ts-ignore - ctx.header = { __noSuchProperty__: (name: string) => req.getHeader(name) + '' } + ctx.headers = { __noSuchProperty__: (name: string) => req.getHeader(name) } + ctx.cookies = {} + for (const cookie of (req.getCookies() || [])) { + ctx.cookies[cookie.getName()] = cookie.getValue() + } if (req.getQueryString()) { ctx.url += `?${req.getQueryString()}` ctx.params = querystring.parse(req.getQueryString()) } if (req.getMethod() == "POST") { ctx.body = this.StreamUtils.copyToString(req.getInputStream(), java.nio.charset.StandardCharsets.UTF_8) - if ((ctx.header['Content-Type'] || '').includes('application/json')) { + if ((ctx.headers['Content-Type'] || '').includes('application/json')) { try { ctx.body = JSON.parse(ctx.body) } catch (error) { @@ -158,6 +275,7 @@ export class Server { ===================== MiaoSpring ===================== Request Method : ${ctx.request.getMethod()} Request URL : ${ctx.url} +Request Body : ${JSON.stringify(ctx.body)} Response Body : ${JSON.stringify(Java.asJSONCompatible(ctx.result))} Handle Time : ${Date.now() - startTime}ms ======================================================`) @@ -165,16 +283,8 @@ Handle Time : ${Date.now() - startTime}ms } private execRequestHandle(ctx: Context) { - if (!this.handlerMapping.has(ctx.request.getRequestURI())) { - return { - status: 404, - msg: "handlerMapping Not Found!", - path: ctx.url, - timestamp: Date.now() - } - } try { - return this.handlerMapping.get(ctx.request.getRequestURI())(ctx) + return ctx.handler(ctx) } catch (error) { return { status: 500,