blob: 79f1f5aa200e41b3462797792fde8e6f4581f7de [file] [log] [blame]
Matthias Andreas Benkard832a54e2019-01-29 09:27:38 +01001package restful
2
3// Copyright 2013 Ernest Micklei. All rights reserved.
4// Use of this source code is governed by a license
5// that can be found in the LICENSE file.
6
7import (
8 "net/http"
9 "regexp"
10 "sort"
11 "strings"
12)
13
14// CurlyRouter expects Routes with paths that contain zero or more parameters in curly brackets.
15type CurlyRouter struct{}
16
17// SelectRoute is part of the Router interface and returns the best match
18// for the WebService and its Route for the given Request.
19func (c CurlyRouter) SelectRoute(
20 webServices []*WebService,
21 httpRequest *http.Request) (selectedService *WebService, selected *Route, err error) {
22
23 requestTokens := tokenizePath(httpRequest.URL.Path)
24
25 detectedService := c.detectWebService(requestTokens, webServices)
26 if detectedService == nil {
27 if trace {
28 traceLogger.Printf("no WebService was found to match URL path:%s\n", httpRequest.URL.Path)
29 }
30 return nil, nil, NewError(http.StatusNotFound, "404: Page Not Found")
31 }
32 candidateRoutes := c.selectRoutes(detectedService, requestTokens)
33 if len(candidateRoutes) == 0 {
34 if trace {
35 traceLogger.Printf("no Route in WebService with path %s was found to match URL path:%s\n", detectedService.rootPath, httpRequest.URL.Path)
36 }
37 return detectedService, nil, NewError(http.StatusNotFound, "404: Page Not Found")
38 }
39 selectedRoute, err := c.detectRoute(candidateRoutes, httpRequest)
40 if selectedRoute == nil {
41 return detectedService, nil, err
42 }
43 return detectedService, selectedRoute, nil
44}
45
46// selectRoutes return a collection of Route from a WebService that matches the path tokens from the request.
47func (c CurlyRouter) selectRoutes(ws *WebService, requestTokens []string) sortableCurlyRoutes {
48 candidates := sortableCurlyRoutes{}
49 for _, each := range ws.routes {
50 matches, paramCount, staticCount := c.matchesRouteByPathTokens(each.pathParts, requestTokens)
51 if matches {
52 candidates.add(curlyRoute{each, paramCount, staticCount}) // TODO make sure Routes() return pointers?
53 }
54 }
55 sort.Sort(sort.Reverse(candidates))
56 return candidates
57}
58
59// matchesRouteByPathTokens computes whether it matches, howmany parameters do match and what the number of static path elements are.
60func (c CurlyRouter) matchesRouteByPathTokens(routeTokens, requestTokens []string) (matches bool, paramCount int, staticCount int) {
61 if len(routeTokens) < len(requestTokens) {
62 // proceed in matching only if last routeToken is wildcard
63 count := len(routeTokens)
64 if count == 0 || !strings.HasSuffix(routeTokens[count-1], "*}") {
65 return false, 0, 0
66 }
67 // proceed
68 }
69 for i, routeToken := range routeTokens {
70 if i == len(requestTokens) {
71 // reached end of request path
72 return false, 0, 0
73 }
74 requestToken := requestTokens[i]
75 if strings.HasPrefix(routeToken, "{") {
76 paramCount++
77 if colon := strings.Index(routeToken, ":"); colon != -1 {
78 // match by regex
79 matchesToken, matchesRemainder := c.regularMatchesPathToken(routeToken, colon, requestToken)
80 if !matchesToken {
81 return false, 0, 0
82 }
83 if matchesRemainder {
84 break
85 }
86 }
87 } else { // no { prefix
88 if requestToken != routeToken {
89 return false, 0, 0
90 }
91 staticCount++
92 }
93 }
94 return true, paramCount, staticCount
95}
96
97// regularMatchesPathToken tests whether the regular expression part of routeToken matches the requestToken or all remaining tokens
98// format routeToken is {someVar:someExpression}, e.g. {zipcode:[\d][\d][\d][\d][A-Z][A-Z]}
99func (c CurlyRouter) regularMatchesPathToken(routeToken string, colon int, requestToken string) (matchesToken bool, matchesRemainder bool) {
100 regPart := routeToken[colon+1 : len(routeToken)-1]
101 if regPart == "*" {
102 if trace {
103 traceLogger.Printf("wildcard parameter detected in route token %s that matches %s\n", routeToken, requestToken)
104 }
105 return true, true
106 }
107 matched, err := regexp.MatchString(regPart, requestToken)
108 return (matched && err == nil), false
109}
110
111var jsr311Router = RouterJSR311{}
112
113// detectRoute selectes from a list of Route the first match by inspecting both the Accept and Content-Type
114// headers of the Request. See also RouterJSR311 in jsr311.go
115func (c CurlyRouter) detectRoute(candidateRoutes sortableCurlyRoutes, httpRequest *http.Request) (*Route, error) {
116 // tracing is done inside detectRoute
117 return jsr311Router.detectRoute(candidateRoutes.routes(), httpRequest)
118}
119
120// detectWebService returns the best matching webService given the list of path tokens.
121// see also computeWebserviceScore
122func (c CurlyRouter) detectWebService(requestTokens []string, webServices []*WebService) *WebService {
123 var best *WebService
124 score := -1
125 for _, each := range webServices {
126 matches, eachScore := c.computeWebserviceScore(requestTokens, each.pathExpr.tokens)
127 if matches && (eachScore > score) {
128 best = each
129 score = eachScore
130 }
131 }
132 return best
133}
134
135// computeWebserviceScore returns whether tokens match and
136// the weighted score of the longest matching consecutive tokens from the beginning.
137func (c CurlyRouter) computeWebserviceScore(requestTokens []string, tokens []string) (bool, int) {
138 if len(tokens) > len(requestTokens) {
139 return false, 0
140 }
141 score := 0
142 for i := 0; i < len(tokens); i++ {
143 each := requestTokens[i]
144 other := tokens[i]
145 if len(each) == 0 && len(other) == 0 {
146 score++
147 continue
148 }
149 if len(other) > 0 && strings.HasPrefix(other, "{") {
150 // no empty match
151 if len(each) == 0 {
152 return false, score
153 }
154 score += 1
155 } else {
156 // not a parameter
157 if each != other {
158 return false, score
159 }
160 score += (len(tokens) - i) * 10 //fuzzy
161 }
162 }
163 return true, score
164}