curly.go 5.5 KB

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