1package 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 := make(sortableCurlyRoutes, 0, 8) 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(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} 165