beego: router change to method Tree
Showing
5 changed files
with
169 additions
and
33 deletions
| ... | @@ -35,9 +35,16 @@ const ( | ... | @@ -35,9 +35,16 @@ const ( |
| 35 | var ( | 35 | var ( |
| 36 | // custom error when user stop request handler manually. | 36 | // custom error when user stop request handler manually. |
| 37 | USERSTOPRUN = errors.New("User stop run") | 37 | USERSTOPRUN = errors.New("User stop run") |
| 38 | GlobalControllerRouter map[string]map[string]*Tree //pkgpath+controller:method:routertree | 38 | GlobalControllerRouter map[string]*ControllerComments //pkgpath+controller:comments |
| 39 | ) | 39 | ) |
| 40 | 40 | ||
| 41 | // store the comment for the controller method | ||
| 42 | type ControllerComments struct { | ||
| 43 | method string | ||
| 44 | router string | ||
| 45 | allowHTTPMethods []string | ||
| 46 | } | ||
| 47 | |||
| 41 | // Controller defines some basic http request handler operations, such as | 48 | // Controller defines some basic http request handler operations, such as |
| 42 | // http context, template and view, session and xsrf. | 49 | // http context, template and view, session and xsrf. |
| 43 | type Controller struct { | 50 | type Controller struct { |
| ... | @@ -56,7 +63,7 @@ type Controller struct { | ... | @@ -56,7 +63,7 @@ type Controller struct { |
| 56 | AppController interface{} | 63 | AppController interface{} |
| 57 | EnableRender bool | 64 | EnableRender bool |
| 58 | EnableXSRF bool | 65 | EnableXSRF bool |
| 59 | Routers map[string]*Tree //method:routertree | 66 | methodMapping map[string]func() //method:routertree |
| 60 | } | 67 | } |
| 61 | 68 | ||
| 62 | // ControllerInterface is an interface to uniform all controller handler. | 69 | // ControllerInterface is an interface to uniform all controller handler. |
| ... | @@ -74,7 +81,7 @@ type ControllerInterface interface { | ... | @@ -74,7 +81,7 @@ type ControllerInterface interface { |
| 74 | Render() error | 81 | Render() error |
| 75 | XsrfToken() string | 82 | XsrfToken() string |
| 76 | CheckXsrfCookie() bool | 83 | CheckXsrfCookie() bool |
| 77 | HandlerFunc(fn interface{}) | 84 | HandlerFunc(fn string) |
| 78 | URLMapping() | 85 | URLMapping() |
| 79 | } | 86 | } |
| 80 | 87 | ||
| ... | @@ -90,7 +97,7 @@ func (c *Controller) Init(ctx *context.Context, controllerName, actionName strin | ... | @@ -90,7 +97,7 @@ func (c *Controller) Init(ctx *context.Context, controllerName, actionName strin |
| 90 | c.EnableRender = true | 97 | c.EnableRender = true |
| 91 | c.EnableXSRF = true | 98 | c.EnableXSRF = true |
| 92 | c.Data = ctx.Input.Data | 99 | c.Data = ctx.Input.Data |
| 93 | c.Routers = make(map[string]*Tree) | 100 | c.methodMapping = make(map[string]func()) |
| 94 | } | 101 | } |
| 95 | 102 | ||
| 96 | // Prepare runs after Init before request function execution. | 103 | // Prepare runs after Init before request function execution. |
| ... | @@ -139,9 +146,11 @@ func (c *Controller) Options() { | ... | @@ -139,9 +146,11 @@ func (c *Controller) Options() { |
| 139 | } | 146 | } |
| 140 | 147 | ||
| 141 | // call function fn | 148 | // call function fn |
| 142 | func (c *Controller) HandlerFunc(fn interface{}) { | 149 | func (c *Controller) HandlerFunc(fnname string) { |
| 143 | if v, ok := fn.(func()); ok { | 150 | if v, ok := c.methodMapping[fnname]; ok { |
| 144 | v() | 151 | v() |
| 152 | } else { | ||
| 153 | Error("call funcname not exist in the methodMapping: " + fnname) | ||
| 145 | } | 154 | } |
| 146 | } | 155 | } |
| 147 | 156 | ||
| ... | @@ -149,19 +158,8 @@ func (c *Controller) HandlerFunc(fn interface{}) { | ... | @@ -149,19 +158,8 @@ func (c *Controller) HandlerFunc(fn interface{}) { |
| 149 | func (c *Controller) URLMapping() { | 158 | func (c *Controller) URLMapping() { |
| 150 | } | 159 | } |
| 151 | 160 | ||
| 152 | func (c *Controller) Mapping(method, pattern string, fn func()) { | 161 | func (c *Controller) Mapping(method string, fn func()) { |
| 153 | method = strings.ToLower(method) | 162 | c.methodMapping[method] = fn |
| 154 | if !utils.InSlice(method, HTTPMETHOD) && method != "*" { | ||
| 155 | Critical("add mapping method:" + method + " is a valid method") | ||
| 156 | return | ||
| 157 | } | ||
| 158 | if t, ok := c.Routers[method]; ok { | ||
| 159 | t.AddRouter(pattern, fn) | ||
| 160 | } else { | ||
| 161 | t = NewTree() | ||
| 162 | t.AddRouter(pattern, fn) | ||
| 163 | c.Routers[method] = t | ||
| 164 | } | ||
| 165 | } | 163 | } |
| 166 | 164 | ||
| 167 | // Render sends the response with rendered template bytes as text/html type. | 165 | // Render sends the response with rendered template bytes as text/html type. | ... | ... |
| ... | @@ -182,7 +182,15 @@ func (n *Namespace) Handler(rootpath string, h http.Handler) *Namespace { | ... | @@ -182,7 +182,15 @@ func (n *Namespace) Handler(rootpath string, h http.Handler) *Namespace { |
| 182 | //) | 182 | //) |
| 183 | func (n *Namespace) Namespace(ns ...*Namespace) *Namespace { | 183 | func (n *Namespace) Namespace(ns ...*Namespace) *Namespace { |
| 184 | for _, ni := range ns { | 184 | for _, ni := range ns { |
| 185 | n.handlers.routers.AddTree(ni.prefix, ni.handlers.routers) | 185 | for k, v := range ni.handlers.routers { |
| 186 | if t, ok := n.handlers.routers[k]; ok { | ||
| 187 | n.handlers.routers[k].AddTree(ni.prefix, v) | ||
| 188 | } else { | ||
| 189 | t = NewTree() | ||
| 190 | t.AddTree(ni.prefix, v) | ||
| 191 | n.handlers.routers[k] = t | ||
| 192 | } | ||
| 193 | } | ||
| 186 | if n.handlers.enableFilter { | 194 | if n.handlers.enableFilter { |
| 187 | for pos, filterList := range ni.handlers.filters { | 195 | for pos, filterList := range ni.handlers.filters { |
| 188 | for _, mr := range filterList { | 196 | for _, mr := range filterList { |
| ... | @@ -201,7 +209,15 @@ func (n *Namespace) Namespace(ns ...*Namespace) *Namespace { | ... | @@ -201,7 +209,15 @@ func (n *Namespace) Namespace(ns ...*Namespace) *Namespace { |
| 201 | // support multi Namespace | 209 | // support multi Namespace |
| 202 | func AddNamespace(nl ...*Namespace) { | 210 | func AddNamespace(nl ...*Namespace) { |
| 203 | for _, n := range nl { | 211 | for _, n := range nl { |
| 204 | BeeApp.Handlers.routers.AddTree(n.prefix, n.handlers.routers) | 212 | for k, v := range n.handlers.routers { |
| 213 | if t, ok := BeeApp.Handlers.routers[k]; ok { | ||
| 214 | BeeApp.Handlers.routers[k].AddTree(n.prefix, v) | ||
| 215 | } else { | ||
| 216 | t = NewTree() | ||
| 217 | t.AddTree(n.prefix, v) | ||
| 218 | BeeApp.Handlers.routers[k] = t | ||
| 219 | } | ||
| 220 | } | ||
| 205 | if n.handlers.enableFilter { | 221 | if n.handlers.enableFilter { |
| 206 | for pos, filterList := range n.handlers.filters { | 222 | for pos, filterList := range n.handlers.filters { |
| 207 | for _, mr := range filterList { | 223 | for _, mr := range filterList { | ... | ... |
| ... | @@ -4,3 +4,43 @@ | ... | @@ -4,3 +4,43 @@ |
| 4 | // @license http://github.com/astaxie/beego/blob/master/LICENSE | 4 | // @license http://github.com/astaxie/beego/blob/master/LICENSE |
| 5 | // @authors astaxie | 5 | // @authors astaxie |
| 6 | package beego | 6 | package beego |
| 7 | |||
| 8 | import ( | ||
| 9 | "os" | ||
| 10 | "path/filepath" | ||
| 11 | ) | ||
| 12 | |||
| 13 | var globalControllerRouter = `package routers | ||
| 14 | |||
| 15 | import ( | ||
| 16 | "github.com/astaxie/beego" | ||
| 17 | ) | ||
| 18 | |||
| 19 | func init() { | ||
| 20 | {{.globalinfo}} | ||
| 21 | } | ||
| 22 | ` | ||
| 23 | |||
| 24 | func parserPkg(pkgpath string) error { | ||
| 25 | err := filepath.Walk(pkgpath, func(path string, info os.FileInfo, err error) error { | ||
| 26 | if err != nil { | ||
| 27 | Error("error scan app Controller source:", err) | ||
| 28 | return err | ||
| 29 | } | ||
| 30 | //if is normal file or name is temp skip | ||
| 31 | //directory is needed | ||
| 32 | if !info.IsDir() || info.Name() == "tmp" { | ||
| 33 | return nil | ||
| 34 | } | ||
| 35 | |||
| 36 | //fileSet := token.NewFileSet() | ||
| 37 | //astPkgs, err := parser.ParseDir(fileSet, path, func(info os.FileInfo) bool { | ||
| 38 | // name := info.Name() | ||
| 39 | // return !info.IsDir() && !strings.HasPrefix(name, ".") && strings.HasSuffix(name, ".go") | ||
| 40 | //}, parser.ParseComments) | ||
| 41 | |||
| 42 | return nil | ||
| 43 | }) | ||
| 44 | |||
| 45 | return err | ||
| 46 | } | ... | ... |
| ... | @@ -12,7 +12,9 @@ import ( | ... | @@ -12,7 +12,9 @@ import ( |
| 12 | "fmt" | 12 | "fmt" |
| 13 | "net" | 13 | "net" |
| 14 | "net/http" | 14 | "net/http" |
| 15 | "os" | ||
| 15 | "path" | 16 | "path" |
| 17 | "path/filepath" | ||
| 16 | "reflect" | 18 | "reflect" |
| 17 | "runtime" | 19 | "runtime" |
| 18 | "strconv" | 20 | "strconv" |
| ... | @@ -67,7 +69,7 @@ type controllerInfo struct { | ... | @@ -67,7 +69,7 @@ type controllerInfo struct { |
| 67 | 69 | ||
| 68 | // ControllerRegistor containers registered router rules, controller handlers and filters. | 70 | // ControllerRegistor containers registered router rules, controller handlers and filters. |
| 69 | type ControllerRegistor struct { | 71 | type ControllerRegistor struct { |
| 70 | routers *Tree | 72 | routers map[string]*Tree |
| 71 | enableFilter bool | 73 | enableFilter bool |
| 72 | filters map[int][]*FilterRouter | 74 | filters map[int][]*FilterRouter |
| 73 | } | 75 | } |
| ... | @@ -75,7 +77,7 @@ type ControllerRegistor struct { | ... | @@ -75,7 +77,7 @@ type ControllerRegistor struct { |
| 75 | // NewControllerRegistor returns a new ControllerRegistor. | 77 | // NewControllerRegistor returns a new ControllerRegistor. |
| 76 | func NewControllerRegistor() *ControllerRegistor { | 78 | func NewControllerRegistor() *ControllerRegistor { |
| 77 | return &ControllerRegistor{ | 79 | return &ControllerRegistor{ |
| 78 | routers: NewTree(), | 80 | routers: make(map[string]*Tree), |
| 79 | filters: make(map[int][]*FilterRouter), | 81 | filters: make(map[int][]*FilterRouter), |
| 80 | } | 82 | } |
| 81 | } | 83 | } |
| ... | @@ -120,17 +122,69 @@ func (p *ControllerRegistor) Add(pattern string, c ControllerInterface, mappingM | ... | @@ -120,17 +122,69 @@ func (p *ControllerRegistor) Add(pattern string, c ControllerInterface, mappingM |
| 120 | route.methods = methods | 122 | route.methods = methods |
| 121 | route.routerType = routerTypeBeego | 123 | route.routerType = routerTypeBeego |
| 122 | route.controllerType = t | 124 | route.controllerType = t |
| 123 | p.routers.AddRouter(pattern, route) | 125 | if len(methods) == 0 { |
| 126 | for _, m := range HTTPMETHOD { | ||
| 127 | p.addToRouter(m, pattern, route) | ||
| 128 | } | ||
| 129 | } else { | ||
| 130 | for k, _ := range methods { | ||
| 131 | if k == "*" { | ||
| 132 | for _, m := range HTTPMETHOD { | ||
| 133 | p.addToRouter(m, pattern, route) | ||
| 134 | } | ||
| 135 | } else { | ||
| 136 | p.addToRouter(k, pattern, route) | ||
| 137 | } | ||
| 138 | } | ||
| 139 | } | ||
| 140 | } | ||
| 141 | |||
| 142 | func (p *ControllerRegistor) addToRouter(method, pattern string, r *controllerInfo) { | ||
| 143 | if t, ok := p.routers[method]; ok { | ||
| 144 | t.AddRouter(pattern, r) | ||
| 145 | } else { | ||
| 146 | t := NewTree() | ||
| 147 | t.AddRouter(pattern, r) | ||
| 148 | p.routers[method] = t | ||
| 149 | } | ||
| 124 | } | 150 | } |
| 125 | 151 | ||
| 126 | // only when the Runmode is dev will generate router file in the router/auto.go from the controller | 152 | // only when the Runmode is dev will generate router file in the router/auto.go from the controller |
| 127 | // Include(&BankAccount{}, &OrderController{},&RefundController{},&ReceiptController{}) | 153 | // Include(&BankAccount{}, &OrderController{},&RefundController{},&ReceiptController{}) |
| 128 | func (p *ControllerRegistor) Include(cList ...ControllerInterface) { | 154 | func (p *ControllerRegistor) Include(cList ...ControllerInterface) { |
| 129 | if RunMode == "dev" { | 155 | if RunMode == "dev" { |
| 156 | skip := make(map[string]bool, 10) | ||
| 157 | for _, c := range cList { | ||
| 158 | reflectVal := reflect.ValueOf(c) | ||
| 159 | t := reflect.Indirect(reflectVal).Type() | ||
| 160 | gopath := os.Getenv("GOPATH") | ||
| 161 | if gopath == "" { | ||
| 162 | panic("you are in dev mode. So please set gopath") | ||
| 163 | } | ||
| 164 | pkgpath := "" | ||
| 165 | |||
| 166 | wgopath := filepath.SplitList(gopath) | ||
| 167 | for _, wg := range wgopath { | ||
| 168 | wg, _ = filepath.EvalSymlinks(filepath.Join(wg, "src", t.PkgPath())) | ||
| 169 | if utils.FileExists(wg) { | ||
| 170 | pkgpath = wg | ||
| 171 | break | ||
| 172 | } | ||
| 173 | } | ||
| 174 | if pkgpath != "" { | ||
| 175 | if _, ok := skip[pkgpath]; !ok { | ||
| 176 | skip[pkgpath] = true | ||
| 177 | parserPkg(pkgpath) | ||
| 178 | } | ||
| 179 | } | ||
| 180 | } | ||
| 181 | } | ||
| 130 | for _, c := range cList { | 182 | for _, c := range cList { |
| 131 | reflectVal := reflect.ValueOf(c) | 183 | reflectVal := reflect.ValueOf(c) |
| 132 | t := reflect.Indirect(reflectVal).Type() | 184 | t := reflect.Indirect(reflectVal).Type() |
| 133 | t.PkgPath() | 185 | key := t.PkgPath() + ":" + t.Name() |
| 186 | if comm, ok := GlobalControllerRouter[key]; ok { | ||
| 187 | p.Add(comm.router, c, strings.Join(comm.allowHTTPMethods, ",")+":"+comm.method) | ||
| 134 | } | 188 | } |
| 135 | } | 189 | } |
| 136 | } | 190 | } |
| ... | @@ -228,7 +282,15 @@ func (p *ControllerRegistor) AddMethod(method, pattern string, f FilterFunc) { | ... | @@ -228,7 +282,15 @@ func (p *ControllerRegistor) AddMethod(method, pattern string, f FilterFunc) { |
| 228 | methods[method] = method | 282 | methods[method] = method |
| 229 | } | 283 | } |
| 230 | route.methods = methods | 284 | route.methods = methods |
| 231 | p.routers.AddRouter(pattern, route) | 285 | for k, _ := range methods { |
| 286 | if k == "*" { | ||
| 287 | for _, m := range HTTPMETHOD { | ||
| 288 | p.addToRouter(m, pattern, route) | ||
| 289 | } | ||
| 290 | } else { | ||
| 291 | p.addToRouter(k, pattern, route) | ||
| 292 | } | ||
| 293 | } | ||
| 232 | } | 294 | } |
| 233 | 295 | ||
| 234 | // add user defined Handler | 296 | // add user defined Handler |
| ... | @@ -241,7 +303,9 @@ func (p *ControllerRegistor) Handler(pattern string, h http.Handler, options ... | ... | @@ -241,7 +303,9 @@ func (p *ControllerRegistor) Handler(pattern string, h http.Handler, options ... |
| 241 | pattern = path.Join(pattern, "?:all") | 303 | pattern = path.Join(pattern, "?:all") |
| 242 | } | 304 | } |
| 243 | } | 305 | } |
| 244 | p.routers.AddRouter(pattern, route) | 306 | for _, m := range HTTPMETHOD { |
| 307 | p.addToRouter(m, pattern, route) | ||
| 308 | } | ||
| 245 | } | 309 | } |
| 246 | 310 | ||
| 247 | // Add auto router to ControllerRegistor. | 311 | // Add auto router to ControllerRegistor. |
| ... | @@ -270,7 +334,9 @@ func (p *ControllerRegistor) AddAutoPrefix(prefix string, c ControllerInterface) | ... | @@ -270,7 +334,9 @@ func (p *ControllerRegistor) AddAutoPrefix(prefix string, c ControllerInterface) |
| 270 | route.methods = map[string]string{"*": rt.Method(i).Name} | 334 | route.methods = map[string]string{"*": rt.Method(i).Name} |
| 271 | route.controllerType = ct | 335 | route.controllerType = ct |
| 272 | pattern := path.Join(prefix, controllerName, strings.ToLower(rt.Method(i).Name), "*") | 336 | pattern := path.Join(prefix, controllerName, strings.ToLower(rt.Method(i).Name), "*") |
| 273 | p.routers.AddRouter(pattern, route) | 337 | for _, m := range HTTPMETHOD { |
| 338 | p.addToRouter(m, pattern, route) | ||
| 339 | } | ||
| 274 | } | 340 | } |
| 275 | } | 341 | } |
| 276 | } | 342 | } |
| ... | @@ -317,12 +383,13 @@ func (p *ControllerRegistor) UrlFor(endpoint string, values ...string) string { | ... | @@ -317,12 +383,13 @@ func (p *ControllerRegistor) UrlFor(endpoint string, values ...string) string { |
| 317 | } | 383 | } |
| 318 | controllName := strings.Join(paths[:len(paths)-1], ".") | 384 | controllName := strings.Join(paths[:len(paths)-1], ".") |
| 319 | methodName := paths[len(paths)-1] | 385 | methodName := paths[len(paths)-1] |
| 320 | ok, url := p.geturl(p.routers, "/", controllName, methodName, params) | 386 | for _, t := range p.routers { |
| 387 | ok, url := p.geturl(t, "/", controllName, methodName, params) | ||
| 321 | if ok { | 388 | if ok { |
| 322 | return url | 389 | return url |
| 323 | } else { | ||
| 324 | return "" | ||
| 325 | } | 390 | } |
| 391 | } | ||
| 392 | return "" | ||
| 326 | } | 393 | } |
| 327 | 394 | ||
| 328 | func (p *ControllerRegistor) geturl(t *Tree, url, controllName, methodName string, params map[string]string) (bool, string) { | 395 | func (p *ControllerRegistor) geturl(t *Tree, url, controllName, methodName string, params map[string]string) (bool, string) { |
| ... | @@ -436,6 +503,7 @@ func (p *ControllerRegistor) ServeHTTP(rw http.ResponseWriter, r *http.Request) | ... | @@ -436,6 +503,7 @@ func (p *ControllerRegistor) ServeHTTP(rw http.ResponseWriter, r *http.Request) |
| 436 | 503 | ||
| 437 | starttime := time.Now() | 504 | starttime := time.Now() |
| 438 | requestPath := r.URL.Path | 505 | requestPath := r.URL.Path |
| 506 | method := strings.ToLower(r.Method) | ||
| 439 | var runrouter reflect.Type | 507 | var runrouter reflect.Type |
| 440 | var findrouter bool | 508 | var findrouter bool |
| 441 | var runMethod string | 509 | var runMethod string |
| ... | @@ -485,7 +553,7 @@ func (p *ControllerRegistor) ServeHTTP(rw http.ResponseWriter, r *http.Request) | ... | @@ -485,7 +553,7 @@ func (p *ControllerRegistor) ServeHTTP(rw http.ResponseWriter, r *http.Request) |
| 485 | }() | 553 | }() |
| 486 | } | 554 | } |
| 487 | 555 | ||
| 488 | if !utils.InSlice(strings.ToLower(r.Method), HTTPMETHOD) { | 556 | if !utils.InSlice(method, HTTPMETHOD) { |
| 489 | http.Error(w, "Method Not Allowed", 405) | 557 | http.Error(w, "Method Not Allowed", 405) |
| 490 | goto Admin | 558 | goto Admin |
| 491 | } | 559 | } |
| ... | @@ -512,7 +580,8 @@ func (p *ControllerRegistor) ServeHTTP(rw http.ResponseWriter, r *http.Request) | ... | @@ -512,7 +580,8 @@ func (p *ControllerRegistor) ServeHTTP(rw http.ResponseWriter, r *http.Request) |
| 512 | } | 580 | } |
| 513 | 581 | ||
| 514 | if !findrouter { | 582 | if !findrouter { |
| 515 | runObject, p := p.routers.Match(requestPath) | 583 | if t, ok := p.routers[method]; ok { |
| 584 | runObject, p := t.Match(requestPath) | ||
| 516 | if r, ok := runObject.(*controllerInfo); ok { | 585 | if r, ok := runObject.(*controllerInfo); ok { |
| 517 | routerInfo = r | 586 | routerInfo = r |
| 518 | findrouter = true | 587 | findrouter = true |
| ... | @@ -526,6 +595,8 @@ func (p *ControllerRegistor) ServeHTTP(rw http.ResponseWriter, r *http.Request) | ... | @@ -526,6 +595,8 @@ func (p *ControllerRegistor) ServeHTTP(rw http.ResponseWriter, r *http.Request) |
| 526 | } | 595 | } |
| 527 | } | 596 | } |
| 528 | 597 | ||
| 598 | } | ||
| 599 | |||
| 529 | //if no matches to url, throw a not found exception | 600 | //if no matches to url, throw a not found exception |
| 530 | if !findrouter { | 601 | if !findrouter { |
| 531 | middleware.Exception("404", rw, r, "") | 602 | middleware.Exception("404", rw, r, "") | ... | ... |
| ... | @@ -291,6 +291,17 @@ func (a *AdminController) Get() { | ... | @@ -291,6 +291,17 @@ func (a *AdminController) Get() { |
| 291 | a.Ctx.WriteString("hello") | 291 | a.Ctx.WriteString("hello") |
| 292 | } | 292 | } |
| 293 | 293 | ||
| 294 | func TestRouterFunc(t *testing.T) { | ||
| 295 | mux := NewControllerRegistor() | ||
| 296 | mux.Get("/action", beegoFilterFunc) | ||
| 297 | mux.Post("/action", beegoFilterFunc) | ||
| 298 | rw, r := testRequest("GET", "/action") | ||
| 299 | mux.ServeHTTP(rw, r) | ||
| 300 | if rw.Body.String() != "hello" { | ||
| 301 | t.Errorf("TestRouterFunc can't run") | ||
| 302 | } | ||
| 303 | } | ||
| 304 | |||
| 294 | func BenchmarkFunc(b *testing.B) { | 305 | func BenchmarkFunc(b *testing.B) { |
| 295 | mux := NewControllerRegistor() | 306 | mux := NewControllerRegistor() |
| 296 | mux.Get("/action", beegoFilterFunc) | 307 | mux.Get("/action", beegoFilterFunc) | ... | ... |
-
Please register or sign in to post a comment