Matthias Andreas Benkard | 832a54e | 2019-01-29 09:27:38 +0100 | [diff] [blame^] | 1 | package 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 | |
| 7 | import ( |
| 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. |
| 15 | type 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. |
| 19 | func (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. |
| 47 | func (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. |
| 60 | func (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]} |
| 99 | func (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 | |
| 111 | var 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 |
| 115 | func (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 |
| 122 | func (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. |
| 137 | func (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 | } |