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