templates.docs.routing.html Maven / Gradle / Ivy
{#==========================================
Docs : "Routing"
==========================================#}
{#==========================================
Routing
==========================================#}     
    
        
        Routing
    
    
    
        Let's learn how the routing process works in Spincast, and how to create
        routes and filters.
    
    
        Note that we won't decribe all the available APIs here! The complete APIs can be found on the
        Routing plugin's page.
    
    
    {#==========================================
    Section "routing / The routing process"
    ==========================================#}
    
        
            
            The routing process
        
        
        
            When an HTTP request arrives, the router is asked to find what route handlers should be used to handle it.
            There can be more than one route handler for a single request because of filters.
            Filters are standard route handlers, but that run before or after the main handler.
        
 
        
            There can be only one main handler but multiple 
            before and after filters. Even if multiple main handlers would match
            a request, the router will only pick one (by default, it keeps the first one matching).
        
 
        
            Those matching routes handlers are called in a
            specific order. As we'll see in the Filters section, a
            filter can have a position which indicates when the filter should run. So it is
            possible to specify that we want a filter to run before or after another one. 
        
 
        
            The routing process we are discussing is called the Found routing type. The Found routing
            type is the one active when at least one main handler matches the request. This is the most frequent case,
            when valid requests are received.
            But what if no main handler matches? What if we receive a request for an invalid URL? Then, 
            we enter the Not Found routing type.
        
 
         
             
            We'll see in the Routing Types section that we can declare some routes
            as supporting this Not Found routing type. Those routes are the only ones going to be considered
            when the Not Found routing type is active.
        
 
            
            Note that we can also enter the Not Found routing
            type if a NotFoundException is thrown by one of the route handlers. Suppose
            you have a /users/${userId} route, and a request for /users/42
            arrives; a main handler is found and called,
            but let's say the user 42 is not found in the system. The handler can then throw a
            NotFoundException exception, and a new Not Found typed routing process
            will be triggered.
        
 
        
             
            The third and final routing type is Exception.
            If an exception occures during a Found or a Not Found
            routing process, the response is reset, and a new Exception routing process starts.
            This is true except for the NotFoundException and 
            some other special exceptions
            which can trigger a different behavior.
        
 
        
            You can define your custom Not Found and Exception
            handlers, and this is highly recommended, but there are some basic provided ones, if you don't.
        
         
             
            There are two important things to remember about the routing process:
            
            
                - 
                    When the routing process of a new type starts, the code to find the matching handlers is restarted from the beginning.
                    For example, if you have some "before" 
filters that
                    have already been run during the initial Found routing process, and then your main handler 
                    throws an exception, the
                    routing process will be restarted, and those filters may be run again. 
                    The only difference between the first routing process and the
                    second is the routing type required for a route to be considered, when trying to find
                    the matching route handlers. Only the routes that have been configured to support the
                    route type of the current routing process are considered.
                 
                - 
                    When a routing process starts, the current response is reset. Its
                    buffer is emptied, and the headers are reset. Of course, if the response's
                    headers have already been flushed, this is not possible, though.
                
   
            
         
    
    
    {#==========================================
    Section "routing / Adding a route"
    ==========================================#}
    
        
            
            Adding a route
        
        
        
            Now, let's learn how to define our routes!
        
        
            First, you get an instance of the router. 
            There are methods on the router to start a route builder. As its name indicates,
            this object uses a builder pattern to help you create a route. Let's see that in details.
        
    
    
    {#==========================================
    Section "routing / HTTP method and path"
    ==========================================#}
    
        
            
            HTTP method and path
        
    
        You start a route builder by choosing the HTTP method 
        and path you want this route to handle.
        
            
                
                    
router.GET("/") ...
router.POST("/users") ...
router.DELETE("/users/${userId}") ...
// Handles all HTTP methods
router.ALL("/books") ...
// Handles POST and PUT requests only
router.SOME("/books", HttpMethod.POST, HttpMethod.PUT) ...
 
                
            
          
      
    
    {#==========================================
    Section "routing / Dynamic parameters"
    ==========================================#}
    
        
            
            Dynamic parameters
        
    
        You can use dynamic parameters in the paths of your routes.
        For example, the following route will match any request with an
        URL starting with /users/ and followed by another token:
        
            
                
                    
router.GET("/users/${userId}")
 
                
            
        
        
            By doing so, your route handlers can then access the actual value of this 
            dynamic parameter. For example : 
        
        
            
                
                    
public void myHandler(IAppRequestContext context) {
    String userId = context.request().getPathParam("userId");
    // Do something with the user id...
} 
                
            
        
        
            If a /users/42 request is handled, userId would be "42", in this
            example.
        
        
             
            Note that this "/users/${userId}" example will only match
            URLs of exactly two tokens! An URL like "/users/42/book/123" 
            won't match!
        
        
            If you want to match more than one path tokens as a single value, you have to use a
            splat parameter. For example, the following path
            does match both "/users/42" and "/users/42/book/123" 
        
        
            
                
                    
router.GET("/users/${userId}/*{remaining}")
 
                
            
        
        
            In that case, the route handler would have access to two path parameters:
            userId, will be "42", and remaining will
            be "book/123".
        
        
            As you can see, a regular dynamic parameter syntax is ${paramName}, where
            a splat parameter syntax is *{paramName}.
        
        
             
            A dynamic parameter can also contain a regular expression pattern. The
            syntax is ${paramName:pattern}, where "pattern" is the regular expression
            to use.
        
        
            In the following example, only requests starting with /users/ and followed by a numeric value 
            will match. In other words, /users/1 and /users/42 would match, 
            but not /users/abc:
        
        
            
                
                    
router.GET("/users/${param1:\\d+}")
 
                
            
        
        
             
            Finally, a dynamic parameter can also contain what we call a pattern alias.
            Instead of having to type the pattern each time you need it, you can 
            use an alias for it. The syntax is ${paramName:<alias>}.
        
        
            Spincast has some built-in pattern aliases:
        
        
            
                
                    
// Matches only alpha characters (A to Z)
router.GET("/${param1:<A>}")
// Matches only numeric characters (0 to 9)
router.GET("/${param1:<N>}")
// Matches only alphanumeric characters (A to Z and 0 to 9)
router.GET("/${param1:<AN>}")
// Matches only alpha characters (A to Z) + "-" and "_"
router.GET("/${param1:<A+>}")
// Matches only numeric characters (0 to 9) + "-" and "_"
router.GET("/${param1:<N+>}")
// Matches only alphanumeric characters (A to Z and 0 to 9) + "-" and "_"
router.GET("/${param1:<AN+>}")
 
                
            
        
        
            You can also specify your own aliases.
            You do that using the addRouteParamPatternAlias(...) method
            on the router. For example :
        
        
            
                
                    
// We register a new alias
router.addRouteParamPatternAlias("USERS", "user|users|usr");
// And we use it! 
router.GET("/${param1:<USERS>}/${userId}")
 
                
            
        
        
            This pattern would match /user/123, /users/123
            and /usr/123, but not /nope/123.
        
      
     
    {#==========================================
    Section "routing / Routing Types"
    ==========================================#}
    
        
            
            Routing Types
        
    
        You can specify for which routing type your route should be
        considered. The three routing types are:
        
        
            - 
                
Found
             
            - 
                
Not Found
             
            - 
                
Exception
             
        
 
        
             
            If no route type is specified when creating a route, Found is used,
            so the route won't be considered during a Not Found or Exception routing process.
        
 
        
            
                
                    
// Only considered during the "Found" routing process
// (this is the default, and it is not required to specify it)
router.GET("/").found()
// Only considered during a "Not Found" routing process
router.GET("/").notFound()
// Only considered during an "Exception" routing process
router.GET("/").exception()
// Always considered!
router.GET("/").allRoutingTypes()
// Only considered during a "Not Found"
// or an "Exception" routing process
router.GET("/").notFound().exception()
 
                
            
        
        
            There are also some shortcuts to quickly define a Not Found
            or an Exception route :
        
        
            
                
                    
// Synonym of : 
// router.ALL("/*{path}").notFound().save(handler)
router.notFound(handler);
// Synonym of : 
// router.ALL("/*{path}").exception().save(handler)
router.exception(handler);
 
                
            
        
        
      
    {#==========================================
    Section "routing / Content-Types"
    ==========================================#}
    
        
            
            Content-Types
        
        
        
            You can also specify if the route should be used only when some
            particular Content-Types are specified as being
            accepted by the request.
        
 
        
            For example, you may have a route handler for a /users URL that will
            produce Json, and a another handler that will produce
            XML, for the very same URL.
        
 
        
            If no Content-Type is specified when building a route, 
            the route doesn't care about it.
        
 
        
        
            
                
                    
// Only considered if the request accepts HTML
router.GET("/users").html() ...
// Only considered if the request accepts Json
router.GET("/users").json() ...
// Only considered if the request accepts XML
router.GET("/users").xml() ...
// Only considered if the request accepts HTML or plain text
router.GET("/users").accept(ContentTypeDefaults.HTML, ContentTypeDefaults.TEXT) ...
// Only considered if the request accepts PDF
router.GET("/users").acceptAsString("application/pdf") ...
 
                
            
             
    
    {#==========================================
    Section "routing / Saving"
    ==========================================#}
    
        
            
            Saving the route
        
        
        
            When your route is complete, you save it to the router by passing
            the route handler to use to the save(...) method. 
            Using Java 8 you can use a method handler or a lambda:
        
            
                
                    
// A method handler
router.GET("/").save(controller::indexHandler);
// A lambda expression
router.GET("/").save(context -> controller.indexHandler(context));
 
                
            
          
        
            Using Java 7, you have to declare an inline handler :
        
        
            
                
                    
router.GET("/").save(new IAppHandler() {
    @Override
    public void handle(IAppRequestContext context) {
        controller.indexHandler(context)
    } 
}); 
                
            
          
        
            A complete example could be :
        
        
            
                
                    
// Will be run for a GET request accepting Json or XML, when the
// user is not found.
router.GET("/users/${userId}").notFound().json().xml().save(usersController::userNotFound);
     
                
            
          
        
    
    
    {#==========================================
    Section "routing / Filters"
    ==========================================#}
    
        
            
            Filters
        
        
        
            Filters are standard route handlers, with the exception 
            that they run before
            or after the main handler.
        
 
        
            You can declare a filter exactly like you would declare a
            standard route, but using the extra "position" property. The
            filter's position indicate when this filter should be run. The lower
            the position number is, the sooner the filter will run. The 
            main handlers are considered as having a position of 0, so filters
            with a position below 0 are before filters, and those with a 
            position greater than 0 are after filters.
        
 
        
            
                
                    
// This filter is a "before" filter and
// will be run first.
router.GET("/").pos(-3).save(ctl::filter1);
// This filter is a "before" filter and
// will be run second.
router.GET("/").pos(-1).save(ctl::filter2);
// This is not a filter, it's a main handler 
// and the ".pos(0)" part is superfluous!
router.GET("/").pos(0).save(ctl::mainHandler);
// This filter is an "after" filter
router.GET("/").pos(100).save(ctl::filter3);
 
                
            
          
        
             
            There also are shortcuts that you can use if you don't need to
            specify fine grain information and just want to add a quick before
            or after filter:
        
 
        
            
                
                    
// Will be applied to any request, at position "-1".
//
// Synonym of : 
// router.ALL("/*{path}").pos(-1).save(ctl::filter)
router.before(ctl::filter);
// Will be applied to any request starting with "/users", 
// at position "-1".
//
// Synonym of : 
// router.ALL("/users").pos(-1).save(ctl::filter)
router.before("/users", ctl::filter);
// Will be applied to any request, at position "1".
//
// Synonym of : 
// router.ALL("/*{path}").pos(1).save(ctl::filter)
router.after(ctl::filter);
// This actually generates two filters.
// Will be applied to any request, both at
// position "-1" and position "1".
router.beforeAndAfter(ctl::filter);
 
                
            
          
        
             
            You can also add inline filters to be run only on
            on a specific route:
        
  
        
            
                
                    
// This route contains four handlers:
// two before filters, the main handler, and one after filter.
router.GET("/users/${userId}")
      .before(ctl::beforeFilter1)
      .before(ctl::beforeFilter2)
      .after(ctl::afterFilter)
      .save(ctl::mainHandler);
 
                
            
          
        
            Inline filters don't have a position: they are
            run in the order they are declared. Also, they always run just before
            or just after the main handler. So they are always run closer to the
            main handler than the global filters. 
        
  
          
            The inline filters have access to
            the same request properties than their associated main handler: same 
            path parameters, same queryString parameters, etc.
        
 
        
        
             
            Finally, note that a filter can decide if it needs to run or not,
            at runtime. Using the routing() add-on,
            a filter can know if the current route type is Found, Not Found
            or Exception, and then decide to run or not! For example:
        
 
        
            
                
                    
public void myFilterHandler(IAppRequestContext context) {
    // The current route is an "Exception" one,
    // we don't apply the filter.
    if(context.routing().isExceptionRoute()) {
        return;
    }
    // Or :
    
    // The current route is a "Not Found" one,
    // we don't apply the filter.
    if(context.routing().isNotFoundRoute()) {
        return;
    }
    // Run the filter...
} 
                
            
          
    
    {#==========================================
    Section "routing / Static resources"
    ==========================================#}
    
        
            
            Static resources
        
        
        
            You can tell Spincast that some files and directories are
            static resources.
            Doing so, the resources will be served by the HTTP server directly,
            and requests for them won't even reach the framework.
        
 
        
            Those static resources can be on the classpath or on the file system. For example:
        
 
        
        
            
                
                    
// Will serve all requests starting with the
// URL "/public" with files under the classpath
// directory "/public_files".
router.dir("/public").classpath("/public_files").save();
// Uses a file system directory (instead of a classpath
// directory) as a static resources root.
router.dir("/public").fileSystem("/user/www/myprojet/public_files").save();
// Will serve the requests for a specific file,
// here "/favicon.ico", with a file from the classpath.
router.file("/favicon.ico").classpath("/public/favicon.ico").save();
// Uses a file system file (instead of a classpath
// file) as the static resource target.
router.file("/favicon.ico").fileSystem("/user/www/myprojet/public_files/favicon.ico").save();
 
                
            
         
        
            Be aware that since requests for static resources don't reach the framework, filters don't apply to them! Even a
            "catch all" filter such as router.ALL("/*{path}").pos(-1).save(handler) 
            won't be applied.
        
 
        
            For the same reason, there are also some limitations about the dynamic parameters the 
            route of a static resource can contain. Only a splat parameter is allowed and only at the very end of
            the path. For example :
            
                - 
                    This is valid : 
/one/two/*{remaining}
                 
                - 
                    This is invalid : 
/one/*{remaining}/two
                 
                - 
                    This is invalid : 
/${param}
                 
            
         
        
            Finally, note that the static resource routes have precedence over all other
            routes, so if you declare router.dir("/", "/public") for example, then
            no route at all would ever reach the framework, everything would be considered
            as static. This is, by the way, a quick way to serve a static website using Spincast!
        
 
  
    
    
    {#==========================================
    Section "routing / Dynamic resources"
    ==========================================#}
    
        
            
            Dynamic resources
        
        
            A variation on static resources is what we call dynamic resources. When
            declaring a static resource, you can provide a generator. The generator is a 
            standard route handler that will receive 
            the request if the static resource is not found. The job of this generator is to
            generate the missing resource and to return it as the response. Spincast will automatically
            intercept the response body, save it and, next time this static resource is requested,
            it will be served directly by the HTTP server!
        
        
            
                
                    
router.dir("/images/tags/${tagId}").fileSystem("/generated_tags")
      .save(new IDefaultHandler() {
          @Override
          public void handle(IDefaultRequestContext context) {
    
              String tagId = context.request().getPathParam("tagId");
              byte[] tagBytes = generateTagImage(tagId);
    
              context.response().sendBytes(tagBytes, "image/png");
          }
      }); 
                
            
         
        
            Or, for a single file :
        
        
            
                
                    
router.file("/css/generated.css")
      .fileSystem("/user/www/myprojet/public_files/css/generated.css")
      .save(new IDefaultHandler() {
          @Override
          public void handle(IDefaultRequestContext context) {
              String css = generateCssFile(context.request().getRequestPath());
              context.response().sendCharacters(css, "text/css");
          }
      }); 
                
            
         
        
            Dynamic resources only work when .fileSystem(...) is used, not
            .classpath(...), and Spincast must have write permissions on the
            target directory.
        
        
    
    
    {#==========================================
    Section "routing / Cors"
    ==========================================#}
    
        
            
            Cors
        
        
            Cors, Cross-Origin Resource Sharing, allows you to
            specify some resources in your application that can be accessed by a browser from another domain. For example,
            let's say your Spincast application runs on domain http://www.example1.com, and you want to allow another
            site, http://www.example2.com, to access some of your APIs, or some of your files. By default, 
            browsers don't allow such cross domains requests. You have to enable cors
            for them to work.
        
        
            There is a provided filter to enable cors on regular routes. Since filters
            are not applied to static resources, there is also a special configuration to add
            cors to those:
        
        
            
                
                    
// Enable cors for every routes of your application,
// except for static resources.
getRouter().cors();
// Enable cors for all routes matching the specified path,
// except for static resources.
getRouter().cors("/api/*{path}");
// Enable cors on a static resource directory.
getRouter().dir("/public").classpath("/public_files")
           .cors().save();
           
// Enable cors on a static resource file.
getRouter().file("/public/test.txt").classpath("/public_files/test.txt")
           .cors().save(); 
                
            
         
        
            There are some parameters available when configuring cors(). The only differences between
            available parameters for regular routes and for static resources, it that you can't specify 
            the HTTP methods for
            static resources: they are always GET, HEAD and OPTIONS.
        
        
            
                
                    
// The default :
// - Allows any origins (domains)
// - Allows cookies
// - Allows any HTTP methods (except for static resources,
//   which allow GET, HEAD and OPTIONS only)
// - Allows any headers to be sent by the browser
// - A Max-Age of 24h is specified for caching purposes
//
// But :
// - Only basic headers are allowed to be read from the browser.
.cors()
// Like the default, but also allows some extra headers
// to be read from the browser. 
.cors(Sets.newHashSet("*"),
      Sets.newHashSet("extra-header-1", "extra-header-2"))
// Only allows the domains "http://example2.com" and "https://example3.com" to access 
// your APIs.
.cors(Sets.newHashSet("http://example2.com", "https://example3.com"))
// Like the default, but only allows the specified extra headers
// to be sent by the browser.
.cors(Sets.newHashSet("*"),
      null,
      Sets.newHashSet("extra-header-1", "extra-header-2"))
// Like the default, but doesn't allow cookies.
.cors(Sets.newHashSet("*"),
      null,
      Sets.newHashSet("*"),
      false)
// Like the default, but only allows the extra "PUT" method
// in addition to GET, POST, HEAD and OPTIONS 
// (which are always allowed). Not applicable to
// static resources.
.cors(Sets.newHashSet("*"),
      null,
      Sets.newHashSet("*"),
      true,
      Sets.newHashSet(HttpMethod.PUT))
// Like the default, but specifies a Max-Age of 60 seconds
// instead of 24 hours.
.cors(Sets.newHashSet("*"),
      null,
      Sets.newHashSet("*"),
      true,
      Sets.newHashSet(HttpMethod.values()),
      60)
 
                
            
         
    
    {#==========================================
    Section "routing / Special exceptions"
    ==========================================#}
    
        
            
            Special exceptions
        
        
            You can throw any exception and this is going to trigger the Exception routing process.
            But some exceptions are provided by Spincast and have a special behavior.
        
        
            
                - 
                    
RedirectException: This exception will stop
                    the current routing process (as any other exceptions),
                    will send a redirection header to the user and will end the exchange. 
 
                    
                        
                            
                                
public void myHandler(IAppRequestContext context) {
    throw new RedirectException("/new-url", true);
} 
                            
                        
                     
                    
                        Note that you can also redirect the user using 
                        context.response().redirect("/new-url", true), instead of throwing a
                        RedirectException exception. The
                        difference is that in the exception version, the remaining handlers
                        are not run and the redirection headers are sent
                        immediately.
                    
                    
                 
                - 
                    
ForwardRouteException: This exception allows
                    you to change the route, to forward the request
                    to another route without doing any client side redirection. A new
                    routing process will start, but this time using the 
                    specified url instead of the url from the original request.
 
                    
                        
                            
                                
public void myHandler(IAppRequestContext context) {
    throw new ForwardRouteException("/new-url");
} 
                            
                        
                     
                 
                - 
                    
SkipRemainingHandlersException: This exception stops
                    the current routing process, but without starting any new one. In other words, the
                    remaining handlers won't be run, and the response will be
                    sent as is, without any more modification.
 
                    
                        
                            
                                
public void myHandler(IAppRequestContext context) {
    context.response().sendPlainText("I'm the last thing sent!");
    throw new SkipRemainingHandlersException();
} 
                            
                        
                     
                 
                - 
                    
CustomStatusCodeException: This exception allows you to
                    set an HTTP status code to return. Then, your custom
                    Exception handler can check this code and display something
                    accordingly. Also, in case you use the provided Exception handler, at least
                    you have control over the status code sent to the user.
                    
                    
                        
                            
                                
public void myHandler(IAppRequestContext context) {
    throw new CustomStatusCodeException("Forbidden!", HttpStatus.SC_FORBIDDEN);
} 
                            
                        
                     
  
                 
                - 
                    
PublicException: This exception allows you to
                    specify a message that will be displayed to the user. This is
                    mainly useful if you use the provided Exception handler since,
                    in a custom handler, you would have full control over what to send
                    as a response anyway...
                    
                    
                        
                            
                                
public void myHandler(IAppRequestContext context) {
    throw new PublicException("You don't have the right to access this page",
                              HttpStatus.SC_FORBIDDEN);
} 
                            
                        
                     
                 
            
           
    
    
    {#==========================================
    Section "routing / Router is dynamic"
    ==========================================#}
    
        
            
            The router is dynamic
        
        
            Note that the router is dynamic, which means you can always add new routes to it. This 
            also means you don't have to define all your routes at the same place, you can let the
            controllers (or even the plugins) define their own routes!
        
        
            
                For example:
                
                    
public class UserController implements IUserController {
    @Inject
    protected void init(IDefaultRouter router) {
        addRoutes(router);
    }
    protected void addRoutes(IDefaultRouter router) {
        router.GET("/users/${userId}").save(this::getUser);
        router.POST("/users").save(this::addUser);
        router.DELETE("/users/${userId}").save(this::deleteUser);
    }
    @Override
    public void getUser(IDefaultRequestContext context) {
        //...
    }
    @Override
    public void addUser(IDefaultRequestContext context) {
        //...
    }
    @Override
    public void deleteUser(IDefaultRequestContext context) {
        //...
    }
} 
                
                
                    Explanation :
                    
                        - 
                            3-6 : An 
init() method
                            receives the router.
                         
                        - 
                            8-12 : The controller adds its own routes. Here, the 
                            
UserController is responsible to add routes related to users.