gf/net/ghttp/ghttp_server_router.go
2022-05-06 20:25:21 +08:00

433 lines
13 KiB
Go

// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package ghttp
import (
"context"
"fmt"
"reflect"
"runtime"
"strings"
"github.com/gogf/gf/v2/container/glist"
"github.com/gogf/gf/v2/container/gtype"
"github.com/gogf/gf/v2/debug/gdebug"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/utils"
"github.com/gogf/gf/v2/protocol/goai"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gmeta"
)
const (
stackFilterKey = "/net/ghttp/ghttp"
)
var (
// handlerIdGenerator is handler item id generator.
handlerIdGenerator = gtype.NewInt()
)
// routerMapKey creates and returns a unique router key for given parameters.
// This key is used for Server.routerMap attribute, which is mainly for checks for
// repeated router registering.
func (s *Server) routerMapKey(hook, method, path, domain string) string {
return hook + "%" + s.serveHandlerKey(method, path, domain)
}
// parsePattern parses the given pattern to domain, method and path variable.
func (s *Server) parsePattern(pattern string) (domain, method, path string, err error) {
path = strings.TrimSpace(pattern)
domain = DefaultDomainName
method = defaultMethod
if array, err := gregex.MatchString(`([a-zA-Z]+):(.+)`, pattern); len(array) > 1 && err == nil {
path = strings.TrimSpace(array[2])
if v := strings.TrimSpace(array[1]); v != "" {
method = v
}
}
if array, err := gregex.MatchString(`(.+)@([\w\.\-]+)`, path); len(array) > 1 && err == nil {
path = strings.TrimSpace(array[1])
if v := strings.TrimSpace(array[2]); v != "" {
domain = v
}
}
if path == "" {
err = gerror.NewCode(gcode.CodeInvalidParameter, "invalid pattern: URI should not be empty")
}
if path != "/" {
path = strings.TrimRight(path, "/")
}
return
}
type setHandlerInput struct {
Prefix string
Pattern string
HandlerItem *HandlerItem
}
// setHandler creates router item with a given handler and pattern and registers the handler to the router tree.
// The router tree can be treated as a multilayer hash table, please refer to the comment in the following codes.
// This function is called during server starts up, which cares little about the performance. What really cares
// is the well-designed router storage structure for router searching when the request is under serving.
func (s *Server) setHandler(ctx context.Context, in setHandlerInput) {
var (
prefix = in.Prefix
pattern = in.Pattern
handler = in.HandlerItem
)
if handler.Name == "" {
handler.Name = runtime.FuncForPC(handler.Info.Value.Pointer()).Name()
}
handler.Id = handlerIdGenerator.Add(1)
if handler.Source == "" {
_, file, line := gdebug.CallerWithFilter([]string{utils.StackFilterKeyForGoFrame})
handler.Source = fmt.Sprintf(`%s:%d`, file, line)
}
domain, method, uri, err := s.parsePattern(pattern)
if err != nil {
s.Logger().Fatalf(ctx, `invalid pattern "%s", %+v`, pattern, err)
return
}
// Change the registered route according to meta info from its request structure.
if handler.Info.Type != nil && handler.Info.Type.NumIn() == 2 {
var objectReq = reflect.New(handler.Info.Type.In(1))
if v := gmeta.Get(objectReq, goai.TagNamePath); !v.IsEmpty() {
uri = v.String()
}
if v := gmeta.Get(objectReq, goai.TagNameMethod); !v.IsEmpty() {
method = v.String()
}
if v := gmeta.Get(objectReq, goai.TagNameDomain); !v.IsEmpty() {
domain = v.String()
}
}
// Prefix for URI feature.
if prefix != "" {
uri = prefix + "/" + strings.TrimLeft(uri, "/")
}
uri = strings.TrimRight(uri, "/")
if uri == "" {
uri = "/"
}
if len(uri) == 0 || uri[0] != '/' {
s.Logger().Fatalf(ctx, `invalid pattern "%s", URI should lead with '/'`, pattern)
return
}
// Repeated router checks, this feature can be disabled by server configuration.
var routerKey = s.routerMapKey(handler.HookName, method, uri, domain)
if !s.config.RouteOverWrite {
switch handler.Type {
case HandlerTypeHandler, HandlerTypeObject:
if items, ok := s.routesMap[routerKey]; ok {
var duplicatedHandler *HandlerItem
for i, item := range items {
switch item.Type {
case HandlerTypeHandler, HandlerTypeObject:
duplicatedHandler = items[i]
break
}
}
if duplicatedHandler != nil {
s.Logger().Fatalf(
ctx,
`duplicated route registry "%s" at %s , already registered at %s`,
pattern, handler.Source, duplicatedHandler.Source,
)
}
return
}
}
}
// Create a new router by given parameter.
handler.Router = &Router{
Uri: uri,
Domain: domain,
Method: strings.ToUpper(method),
Priority: strings.Count(uri[1:], "/"),
}
handler.Router.RegRule, handler.Router.RegNames = s.patternToRegular(uri)
if _, ok := s.serveTree[domain]; !ok {
s.serveTree[domain] = make(map[string]interface{})
}
// List array, very important for router registering.
// There may be multiple lists adding into this array when searching from root to leaf.
var (
array []string
lists = make([]*glist.List, 0)
)
if strings.EqualFold("/", uri) {
array = []string{"/"}
} else {
array = strings.Split(uri[1:], "/")
}
// Multilayer hash table:
// 1. Each node of the table is separated by URI path which is split by char '/'.
// 2. The key "*fuzz" specifies this node is a fuzzy node, which has no certain name.
// 3. The key "*list" is the list item of the node, MOST OF THE NODES HAVE THIS ITEM,
// especially the fuzzy node. NOTE THAT the fuzzy node must have the "*list" item,
// and the leaf node also has "*list" item. If the node is not a fuzzy node either
// a leaf, it neither has "*list" item.
// 2. The "*list" item is a list containing registered router items ordered by their
// priorities from high to low.
// 3. There may be repeated router items in the router lists. The lists' priorities
// from root to leaf are from low to high.
var p = s.serveTree[domain]
for i, part := range array {
// Ignore empty URI part, like: /user//index
if part == "" {
continue
}
// Check if it's a fuzzy node.
if gregex.IsMatchString(`^[:\*]|\{[\w\.\-]+\}|\*`, part) {
part = "*fuzz"
// If it's a fuzzy node, it creates a "*list" item - which is a list - in the hash map.
// All the sub router items from this fuzzy node will also be added to its "*list" item.
if v, ok := p.(map[string]interface{})["*list"]; !ok {
newListForFuzzy := glist.New()
p.(map[string]interface{})["*list"] = newListForFuzzy
lists = append(lists, newListForFuzzy)
} else {
lists = append(lists, v.(*glist.List))
}
}
// Make a new bucket for the current node.
if _, ok := p.(map[string]interface{})[part]; !ok {
p.(map[string]interface{})[part] = make(map[string]interface{})
}
// Loop to next bucket.
p = p.(map[string]interface{})[part]
// The leaf is a hash map and must have an item named "*list", which contains the router item.
// The leaf can be furthermore extended by adding more ket-value pairs into its map.
// Note that the `v != "*fuzz"` comparison is required as the list might be added in the former
// fuzzy checks.
if i == len(array)-1 && part != "*fuzz" {
if v, ok := p.(map[string]interface{})["*list"]; !ok {
leafList := glist.New()
p.(map[string]interface{})["*list"] = leafList
lists = append(lists, leafList)
} else {
lists = append(lists, v.(*glist.List))
}
}
}
// It iterates the list array of `lists`, compares priorities and inserts the new router item in
// the proper position of each list. The priority of the list is ordered from high to low.
var item *HandlerItem
for _, l := range lists {
pushed := false
for e := l.Front(); e != nil; e = e.Next() {
item = e.Value.(*HandlerItem)
// Checks the priority whether inserting the route item before current item,
// which means it has higher priority.
if s.compareRouterPriority(handler, item) {
l.InsertBefore(e, handler)
pushed = true
goto end
}
}
end:
// Just push back in default.
if !pushed {
l.PushBack(handler)
}
}
// Initialize the route map item.
if _, ok := s.routesMap[routerKey]; !ok {
s.routesMap[routerKey] = make([]*HandlerItem, 0)
}
switch handler.Type {
case HandlerTypeHandler, HandlerTypeObject:
// Overwrite the route.
s.routesMap[routerKey] = []*HandlerItem{handler}
default:
// Append the route.
s.routesMap[routerKey] = append(s.routesMap[routerKey], handler)
}
}
// compareRouterPriority compares the priority between `newItem` and `oldItem`. It returns true
// if `newItem`'s priority is higher than `oldItem`, else it returns false. The higher priority
// item will be inserted into the router list before the other one.
//
// Comparison rules:
// 1. The middleware has the most high priority.
// 2. URI: The deeper, the higher (simply check the count of char '/' in the URI).
// 3. Route type: {xxx} > :xxx > *xxx.
func (s *Server) compareRouterPriority(newItem *HandlerItem, oldItem *HandlerItem) bool {
// If they're all types of middleware, the priority is according to their registered sequence.
if newItem.Type == HandlerTypeMiddleware && oldItem.Type == HandlerTypeMiddleware {
return false
}
// The middleware has the most high priority.
if newItem.Type == HandlerTypeMiddleware && oldItem.Type != HandlerTypeMiddleware {
return true
}
// URI: The deeper, the higher (simply check the count of char '/' in the URI).
if newItem.Router.Priority > oldItem.Router.Priority {
return true
}
if newItem.Router.Priority < oldItem.Router.Priority {
return false
}
// Compare the length of their URI,
// but the fuzzy and named parts of the URI are not calculated to the result.
// Example:
// /admin-goods-{page} > /admin-{page}
// /{hash}.{type} > /{hash}
var uriNew, uriOld string
uriNew, _ = gregex.ReplaceString(`\{[^/]+?\}`, "", newItem.Router.Uri)
uriOld, _ = gregex.ReplaceString(`\{[^/]+?\}`, "", oldItem.Router.Uri)
uriNew, _ = gregex.ReplaceString(`:[^/]+?`, "", uriNew)
uriOld, _ = gregex.ReplaceString(`:[^/]+?`, "", uriOld)
uriNew, _ = gregex.ReplaceString(`\*[^/]*`, "", uriNew) // Replace "/*" and "/*any".
uriOld, _ = gregex.ReplaceString(`\*[^/]*`, "", uriOld) // Replace "/*" and "/*any".
if len(uriNew) > len(uriOld) {
return true
}
if len(uriNew) < len(uriOld) {
return false
}
// Route type checks: {xxx} > :xxx > *xxx.
// Example:
// /name/act > /{name}/:act
var (
fuzzyCountFieldNew int
fuzzyCountFieldOld int
fuzzyCountNameNew int
fuzzyCountNameOld int
fuzzyCountAnyNew int
fuzzyCountAnyOld int
fuzzyCountTotalNew int
fuzzyCountTotalOld int
)
for _, v := range newItem.Router.Uri {
switch v {
case '{':
fuzzyCountFieldNew++
case ':':
fuzzyCountNameNew++
case '*':
fuzzyCountAnyNew++
}
}
for _, v := range oldItem.Router.Uri {
switch v {
case '{':
fuzzyCountFieldOld++
case ':':
fuzzyCountNameOld++
case '*':
fuzzyCountAnyOld++
}
}
fuzzyCountTotalNew = fuzzyCountFieldNew + fuzzyCountNameNew + fuzzyCountAnyNew
fuzzyCountTotalOld = fuzzyCountFieldOld + fuzzyCountNameOld + fuzzyCountAnyOld
if fuzzyCountTotalNew < fuzzyCountTotalOld {
return true
}
if fuzzyCountTotalNew > fuzzyCountTotalOld {
return false
}
// If the counts of their fuzzy rules are equal.
// Eg: /name/{act} > /name/:act
if fuzzyCountFieldNew > fuzzyCountFieldOld {
return true
}
if fuzzyCountFieldNew < fuzzyCountFieldOld {
return false
}
// Eg: /name/:act > /name/*act
if fuzzyCountNameNew > fuzzyCountNameOld {
return true
}
if fuzzyCountNameNew < fuzzyCountNameOld {
return false
}
// It then compares the accuracy of their http method,
// the more accurate the more priority.
if newItem.Router.Method != defaultMethod {
return true
}
if oldItem.Router.Method != defaultMethod {
return true
}
// If they have different router type,
// the new router item has more priority than the other one.
if newItem.Type == HandlerTypeHandler || newItem.Type == HandlerTypeObject {
return true
}
// Other situations, like HOOK items,
// the old router item has more priority than the other one.
return false
}
// patternToRegular converts route rule to according to regular expression.
func (s *Server) patternToRegular(rule string) (regular string, names []string) {
if len(rule) < 2 {
return rule, nil
}
regular = "^"
var array = strings.Split(rule[1:], "/")
for _, v := range array {
if len(v) == 0 {
continue
}
switch v[0] {
case ':':
if len(v) > 1 {
regular += `/([^/]+)`
names = append(names, v[1:])
} else {
regular += `/[^/]+`
}
case '*':
if len(v) > 1 {
regular += `/{0,1}(.*)`
names = append(names, v[1:])
} else {
regular += `/{0,1}.*`
}
default:
// Special chars replacement.
v = gstr.ReplaceByMap(v, map[string]string{
`.`: `\.`,
`+`: `\+`,
`*`: `.*`,
})
s, _ := gregex.ReplaceStringFunc(`\{[\w\.\-]+\}`, v, func(s string) string {
names = append(names, s[1:len(s)-1])
return `([^/]+)`
})
if strings.EqualFold(s, v) {
regular += "/" + v
} else {
regular += "/" + s
}
}
}
regular += `$`
return
}