Router

package revel

type Route struct {
    Method         string   // e.g. GET
    Path           string   // e.g. /app/:id
    Action         string   // e.g. "Application.ShowApp", "404"
    ControllerName string   // e.g. "Application", ""
    MethodName     string   // e.g. "ShowApp", ""
    FixedParams    []string // e.g. "arg1","arg2","arg3" (CSV formatting)
    TreePath       string   // e.g. "/GET/app/:id"

}

type RouteMatch struct {
    Action         string // e.g. 404
    ControllerName string // e.g. Application
    MethodName     string // e.g. ShowApp
    FixedParams    []string
    Params         map[string][]string // e.g. {id: 123}
}

// Prepares the route to be used in matching.
func NewRoute(method, path, action, fixedArgs, routesPath string, line int) (r *Route) {
    // Handle fixed arguments
    argsReader := strings.NewReader(fixedArgs)
    csv := csv.NewReader(argsReader)
    fargs, err := csv.Read()
    if err != nil && err != io.EOF {
        ERROR.Printf("Invalid fixed parameters (%v): for string '%v'", err.Error(), fixedArgs)
    }

    r = &Route{
        Method:      strings.ToUpper(method),
        Path:        path,
        Action:      action,
        FixedParams: fargs,
        TreePath:    treePath(strings.ToUpper(method), path),
        routesPath:  routesPath,
        line:        line,
    }

    // URL pattern
    if !strings.HasPrefix(r.Path, "/") {
        ERROR.Print("Absolute URL required.")
        return
    }

    actionSplit := strings.Split(action, ".")
    if len(actionSplit) == 2 {
        r.ControllerName = actionSplit[0]
        r.MethodName = actionSplit[1]
    }

    return
}

type Router struct {
    Routes []*Route
    Tree   *pathtree.Node

}

func (router *Router) Route(req *http.Request) *RouteMatch {
    leaf, expansions := router.Tree.Find(treePath(req.Method, req.URL.Path))
    if leaf == nil {
        return nil
    }
    route := leaf.Value.(*Route)

    // Create a map of the route parameters.
    var params url.Values
    if len(expansions) > 0 {
        params = make(url.Values)
        for i, v := range expansions {
            params[leaf.Wildcards[i]] = []string{v}
        }
    }

    // Special handling for explicit 404's.
    if route.Action == "404" {
        return notFound
    }

    // If the action is variablized, replace into it with the captured args.
    controllerName, methodName := route.ControllerName, route.MethodName
    if controllerName[0] == ':' {
        controllerName = params[controllerName[1:]][0]
    }
    if methodName[0] == ':' {
        methodName = params[methodName[1:]][0]
    }

    return &RouteMatch{
        ControllerName: controllerName,
        MethodName:     methodName,
        Params:         params,
        FixedParams:    route.FixedParams,
    }
}

// Refresh re-reads the routes file and re-calculates the routing table.
// Returns an error if a specified action could not be found.
func (router *Router) Refresh() (err *Error) {
    router.Routes, err = parseRoutesFile(router.path, "", true)
    if err != nil {
        return
    }
    err = router.updateTree()
    return
}

func NewRouter(routesPath string) *Router {
    return &Router{
        Tree: pathtree.New(),
        path: routesPath,
    }
}

type ActionDefinition struct {
    Host, Method, Url, Action string
    Star                      bool
    Args                      map[string]string
}

func (a *ActionDefinition) String() string {
    return a.Url
}

func (router *Router) Reverse(action string, argValues map[string]string) *ActionDefinition {
    actionSplit := strings.Split(action, ".")
    if len(actionSplit) != 2 {
        ERROR.Print("revel/router: reverse router got invalid action ", action)
        return nil
    }
    controllerName, methodName := actionSplit[0], actionSplit[1]

    for _, route := range router.Routes {
        // Skip routes without either a ControllerName or MethodName
        if route.ControllerName == "" || route.MethodName == "" {
            continue
        }

        // Check that the action matches or is a wildcard.
        controllerWildcard := route.ControllerName[0] == ':'
        methodWildcard := route.MethodName[0] == ':'
        if (!controllerWildcard && route.ControllerName != controllerName) ||
            (!methodWildcard && route.MethodName != methodName) {
            continue
        }
        if controllerWildcard {
            argValues[route.ControllerName[1:]] = controllerName
        }
        if methodWildcard {
            argValues[route.MethodName[1:]] = methodName
        }

        // Build up the URL.
        var (
            queryValues  = make(url.Values)
            pathElements = strings.Split(route.Path, "/")
        )
        for i, el := range pathElements {
            if el == "" || el[0] != ':' {
                continue
            }

            val, ok := argValues[el[1:]]
            if !ok {
                val = "<nil>"
                ERROR.Print("revel/router: reverse route missing route arg ", el[1:])
            }
            pathElements[i] = val
            delete(argValues, el[1:])
            continue
        }

        // Add any args that were not inserted into the path into the query string.
        for k, v := range argValues {
            queryValues.Set(k, v)
        }

        // Calculate the final URL and Method
        url := strings.Join(pathElements, "/")
        if len(queryValues) > 0 {
            url += "?" + queryValues.Encode()
        }

        method := route.Method
        star := false
        if route.Method == "*" {
            method = "GET"
            star = true
        }

        return &ActionDefinition{
            Url:    url,
            Method: method,
            Star:   star,
            Action: action,
            Args:   argValues,
            Host:   "TODO",
        }
    }
    ERROR.Println("Failed to find reverse route:", action, argValues)
    return nil
}

func RouterFilter(c *Controller, fc []Filter) {
    // Figure out the Controller/Action
    var route *RouteMatch = MainRouter.Route(c.Request.Request)
    if route == nil {
        c.Result = c.NotFound("No matching route found")
        return
    }

    // The route may want to explicitly return a 404.
    if route.Action == "404" {
        c.Result = c.NotFound("(intentionally)")
        return
    }

    // Set the action.
    if err := c.SetAction(route.ControllerName, route.MethodName); err != nil {
        c.Result = c.NotFound(err.Error())
        return
    }

    // Add the route and fixed params to the Request Params.
    c.Params.Route = route.Params

    // Add the fixed parameters mapped by name.
    // TODO: Pre-calculate this mapping.
    for i, value := range route.FixedParams {
        if c.Params.Fixed == nil {
            c.Params.Fixed = make(url.Values)
        }
        if i < len(c.MethodType.Args) {
            arg := c.MethodType.Args[i]
            c.Params.Fixed.Set(arg.Name, value)
        } else {
            WARN.Println("Too many parameters to", route.Action, "trying to add", value)
            break
        }
    }

    fc[0](c, fc[1:])
}