gf/net/ghttp/ghttp_server_router.go
2021-01-27 00:20:23 +08:00

382 lines
12 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 (
"errors"
"fmt"
"github.com/gogf/gf/container/gtype"
"strings"
"github.com/gogf/gf/debug/gdebug"
"github.com/gogf/gf/container/glist"
"github.com/gogf/gf/text/gregex"
"github.com/gogf/gf/text/gstr"
)
const (
stackFilterKey = "/net/ghttp/ghttp"
)
var (
// handlerIdGenerator is handler item id generator.
handlerIdGenerator = gtype.NewInt()
)
// routerMapKey creates and returns an 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 = errors.New("invalid pattern: URI should not be empty")
}
if path != "/" {
path = strings.TrimRight(path, "/")
}
return
}
// setHandler creates router item with 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 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(pattern string, handler *handlerItem) {
handler.itemId = handlerIdGenerator.Add(1)
if handler.source == "" {
_, file, line := gdebug.CallerWithFilter(stackFilterKey)
handler.source = fmt.Sprintf(`%s:%d`, file, line)
}
domain, method, uri, err := s.parsePattern(pattern)
if err != nil {
s.Logger().Fatal("invalid pattern:", pattern, err)
return
}
if len(uri) == 0 || uri[0] != '/' {
s.Logger().Fatal("invalid pattern:", pattern, "URI should lead with '/'")
return
}
// Repeated router checks, this feature can be disabled by server configuration.
routerKey := s.routerMapKey(handler.hookName, method, uri, domain)
if !s.config.RouteOverWrite {
switch handler.itemType {
case handlerTypeHandler, handlerTypeObject, handlerTypeController:
if item, ok := s.routesMap[routerKey]; ok {
s.Logger().Fatalf(
`duplicated route registry "%s" at %s , already registered at %s`,
pattern, handler.source, item[0].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.
lists := make([]*glist.List, 0)
array := ([]string)(nil)
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.
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 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.
item := (*handlerItem)(nil)
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 more 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([]registeredRouteItem, 0)
}
routeItem := registeredRouteItem{
source: handler.source,
handler: handler,
}
switch handler.itemType {
case handlerTypeHandler, handlerTypeObject, handlerTypeController:
// Overwrite the route.
s.routesMap[routerKey] = []registeredRouteItem{routeItem}
default:
// Append the route.
s.routesMap[routerKey] = append(s.routesMap[routerKey], routeItem)
}
}
// 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 insert 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 type of middleware, the priority is according their registered sequence.
if newItem.itemType == handlerTypeMiddleware && oldItem.itemType == handlerTypeMiddleware {
return false
}
// The middleware has the most high priority.
if newItem.itemType == handlerTypeMiddleware && oldItem.itemType != 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.
// Eg:
// /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.
// Eg:
// /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 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.itemType == handlerTypeHandler ||
newItem.itemType == handlerTypeObject ||
newItem.itemType == handlerTypeController {
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 regular expression.
func (s *Server) patternToRegular(rule string) (regular string, names []string) {
if len(rule) < 2 {
return rule, nil
}
regular = "^"
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
}