All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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.





© 2015 - 2025 Weber Informatics LLC | Privacy Policy