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

templates.docs.routing.html Maven / Gradle / Ivy

There is a newer version: 2.2.0
Show newest version
{#==========================================
Docs : "Routing"
==========================================#}

{#==========================================
Routing
==========================================#}     

Routing

Let's learn how the routing process works in Spincast, and how to create Routes and Filters.

{#========================================== 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 Route Handler.

There can be only one main Route Handler but multiple before and after Filters. Even if multiple Route Handlers would match a request, the router will only pick one as the main 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.

This initial routing process has a routing process type called "Found". The "Found" routing type is the one active when at least one main Route Handler matches the request. This is the most frequent case, when valid requests are received, using URLs that are managed by our application.

But what if no main Route Handler matches? What if we receive a request that uses an invalid URL? Then, we enter a routing process with a routing process type called "Not Found" (404). We'll see in the Routing Types section that we can define some routes for the Not Found routing process. Those routes are the ones that are going to be considered when a Not Found routing process occures.

So when we receive a request for an invalid URL, the Not Found routing process is triggered and the dedicated Routes are considered as potential handlers. Note that the Not Found routing process is also activated if a NotFoundException is thrown in our application! Suppose you have a "/users/${userId}" Route, and a request for "/users/42" is received... Then a main Route Handler may be found and called, but it is possible that this particular user, with id "42", is not found in the system... Your can at this point throw a NotFoundException exception : this is going to stop the initial "Found" routing process, and start a new Not Found routing process, as if the request was using an invalid URL in the first place!

The third and final routing process type (with "Found" and "Not Found") is "Exception". If an exception occurs during a "Found" or a "Not Found" routing process, then a new "Exception" routing process is started. This enables you to create some Routes explicitly made to manage exceptions.

It's important to note that there are some special exceptions which may have a different behavior though. The best example is the NotFoundException exception we already discussed : this exception, when throw, doesn't start a new "Exception" routing process, but a new "Not Found" routing process.

Also note that if you do not define custom "Not Found" and "Exception" Routes Handlers, some basic ones are provided. It is still highly recommended that you create custom ones.

There are two important things to remember about how a routing process works :

  • When a routing process starts, the process of finding the matching Routes is restarted from the beginning. For example, if you have some "before" Filters that have already been run during an initial "Found" routing process, and then your main Route Handler throws an exception, the routing process will be restarted (this time of type "Exception") and those already ran Filters may be run again if they are also part of the new routing process!

    The only difference between the initial routing process and the second one is the type which will change the Routes that are going to be considered. Only the Routes that have been configured to support the routing process type of the current routing process may match.

  • When a new routing process starts, the current response is reset : its buffer is emptied, and the HTTP headers are reset... But this is only true if the response has not already been flushed though!

Finally, note that Spincast also supports WebSockets, which involve a totally different routing process! Make sure you read the dedicated section about WebSockets to learn more about this.

{#========================================== Section "routing / Adding a route" ==========================================#}

Adding a Route

Now, let's learn how to define our Routes!

First, you have to get the Router instance. If you use the default router, this involves injecting the DefaultRouter object. For example, using constructor injection :

public class AppRouteDefinitions {

    private final DefaultRouter router;
    
    @Inject
    public AppRouteDefinitions(DefaultRouter router) {
        this.router = router;
    }
    
    //...
}

On that Router, there are methods to create 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 create a Route Builder by choosing the HTTP methods and the path you want your 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.methods("/books", HttpMethod.POST, HttpMethod.PUT) ...

{#========================================== Section "routing / Route id" ==========================================#}

Route id

You can assign an id to a route. This id can be useful later, for example to validate at runtime what the route of the current request is.


router.GET("/users").id('users') ...

{#========================================== Section "routing / Dynamic parameters" ==========================================#}

Dynamic parameters

In the paths of your route definitions, you can use what we call "dynamic parameters", which syntax is "${paramName"}. For example, the following Route Builder will generate a Route matching any request for an URL starting with "/users/" and that is followed by another token:

router.GET("/users/${userId}")

By doing so, your associated Route Handlers can later access the actual value of this dynamic parameter. For example :

public void myHandler(AppRequestContext context) {
    
    String userId = context.request().getPathParam("userId");
    
    // Do something with the user id...
}

If a "/users/42" request is received, then the userId would be "42", in this example.

Note that this "/users/${userId}" example will only match URLs containing exactly two tokens! An URL like "/users/42/book/123" won't match!

If you want to match more than one path tokens using a single variable, you have to use a Splat parameter, which syntax is "*{paramName}". For example, the Route generated in following example will match both "/users/42" and "/users/42/book/123" :

router.GET("/users/${userId}/*{remaining}")

In this example, the Route Handlers would have access to two path parameters : userId will be "42", and remaining will be "book/123".

A dynamic parameter can also contain a regular expression pattern. The syntax is "${paramName:pattern}", where "pattern" is the regular expression to use. For example :


router.GET("/users/${userId:\\d+}")

In this 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".

Finally, a dynamic parameter can also contain what we call a pattern alias. Instead of having to type the regular expression pattern each time you need it, you can use an alias for it. The syntax to use an alias is "${paramName:<alias>}".

Spincast has some built-in 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), (0 to 9), "-" and "_"
router.GET("/${param1:<AN+>}")

You can of course create your own aliases. You do that using the addRouteParamPatternAlias(...) method on the Router. For example :

// Registers a new alias
router.addRouteParamPatternAlias("USERS", "user|users|usr");

// Uses the alias! 
router.GET("/${param1:<USERS>}/${userId}")

The Route generated using this pattern would match "/user/123", "/users/123" and "/usr/123", but not "/nope/123".

{#========================================== Section "routing / Routing Types" ==========================================#}

Routing Process Types

You can specify of which type the current routing process must be for your route to be considered.

The three routing process types are:

  • Found
  • Not Found
  • Exception

If no routing process type is specified when a Route is created, "Found" is used by default. This means the Route won't be considered during a Not Found or Exception routing process.

Here's a example of creating Routes using routing process types :

// Only considered during a "Found" routing process
// (this is the default, so 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() ...

// Considered both during a "Not Found"
// or an "Exception" routing process
router.GET("/").notFound().exception() ...

There are some shortcuts to quickly define a Route for a "Not Found" or an "Exception" type, wathever the URL is :

// Synonym of : 
// router.ALL("/*{path}").notFound().handle(handler)
router.notFound(handler);

// Synonym of : 
// router.ALL("/*{path}").exception().handle(handler)
router.exception(handler);

{#========================================== Section "routing / Content-Types" ==========================================#}

Content-Types

You can specify for acceptable content-types for a Route to be considered. For example, you may have a Route Handler for a "/users" URL that will produce Json, and another Route Handler that will produce XML, for the very same URL. You could also have a single handler for both content-types, and let this handler decide what to return as the response : both approaches are valid.

If no content-type is specified when building a Route, the route is always considered in that regard.

Let's see some examples :

// 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 / HTTP Caching" ==========================================#}

HTTP Caching route options

Soon in the documentation, you will find a dedicated HTTP Caching section, containing all the information about HTTP Caching using Spincast. Here, we're only going to list the options available when building a Route.

For both regular Routes and Static Resources Routes, you can use the cache(...) method to send appropriate cache headers to the client :

// Default cache headers will be sent (this default is configurable).
router.GET("/test").cache() ...

// Sends headers so the client caches the resource for 60 seconds.
router.GET("/test").cache(60) ...

// Sends headers so the client caches the resource for 60 seconds.
// Also specifies that this cache should be *private*.
// See : https://goo.gl/VotTdD
router.GET("/test").cache(60, true) ...

// Sends headers so the client caches the resource for 60 seconds,
// but so a CDN (proxy) caches it for 30 seconds only.
router.GET("/test").cache(60, false, 30) ...

// The "cache()" method is also available on Static Resources Routes!
router.file("/favicon.ico").cache(86400).classpath("/public/favicon.ico") ...

On a standard route, it is also possible to use the noCache() method to send headers asking the client to disable any caching :

router.GET("/test").noCache().handle(handler);

Again, make sure you read the dedicated HTTP Caching section for the full documentation about caching.

{#========================================== Section "routing / Saving" ==========================================#}

Saving the route

When your Route definition is complete, you save the generated Route to the router by passing a last parameter to the handle(...) method : the Route Handler to use to handle the Route. With Java 8, you can use a method handler or a lambda for this parameter :

// A method handler
router.GET("/").handle(controller::indexHandler);

// A lambda expression
router.GET("/").handle(context -> controller.indexHandler(context));

Here's a complete example of a Route creation :

// Will be considered on a GET request accepting Json or XML, when a 
// requested user is not found.
// This may occure if you throw a "NotFoundException" after you validated
// the "userId" path parameter...
router.GET("/users/${userId}").notFound().json().xml().handle(usersController::userNotFound);
    

{#========================================== Section "routing / Filters" ==========================================#}

Filters

Filters are plain Route Handlers, with the exception that they run before or after the single main Route Handler.

You can declare a Filter exactly like you would declare a standard Route, but using the extra "pos" ("position") property! The Filter's position indicate when the Filter should be run. The lower that position number is, the sooner the Filter will run. Note that the main Route 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 Filter.

An example :

// This Filter is a "before" Filter and
// will be run first.
router.GET("/").pos(-3).handle(ctl::filter1);

// This Filter is also a "before" Filter and
// will be run second.
router.GET("/").pos(-1).handle(ctl::filter2);

// This is not a Filter, it's a main Route Handler 
// and the ".pos(0)" part is superfluous!
router.GET("/").pos(0).handle(ctl::mainHandler);

// This Filter is an "after" Filter and will run
// after the main Route Handler
router.GET("/").pos(100).handle(ctl::filter3);

A Route definition can disable a Filter that would otherwise be run, by using the skip(...) method. The target Filter must have been declared with an "id" for this to be possible though. He're an example :

// This "myFilter" Filter will be applied on all Routes by default
router.ALL().pos(-100).id("myFilter").handle(ctl::filterHandler);

// ... but this Route disables it!
router.GET("/test").skip("myFilter").handle(ctl::testHandler);

A Route definition can also disable a Filter that would otherwise be run when a Dynamic Resource is generated. To do so, simply call skipResourcesRequests():

// This Filter will be applied on all routes
router.ALL().pos(-100).handle(ctl::someHandler);

// This one on all routes but dynamic resources ones!
router.ALL().pos(-100).skipResourcesRequests().handle(ctl::someHandler);

You can also add inline Filters that are run only on on a specific Route, using the before() and after() methods :

// This route contains four "handlers" :
// two "before" Filters, the main Route Handler, 
// and one "after" Filter.
router.GET("/users/${userId}")
      .before(ctl::beforeFilter1)
      .before(ctl::beforeFilter2)
      .after(ctl::afterFilter)
      .handle(ctl::mainHandler);

The 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 Route Handler. In other words, they are always run closer to the main Route Handler than the global Filters.

The inline filters have access to the same request information than their associated main Route Handler: same path parameters, same queryString parameters, etc.

Finally, note that a Filter can decide by itself if it will run or not, at runtime. For example, using the routing() add-on, a Filter can know if the current Routing Process Type is "Found", "Not Found" or "Exception", and decide to run or not depending on that information. For example :

public void myFilterHandler(AppRequestContext context) {

    // The current Routing Process Type is "Exception",
    // we don't run the Filter.
    if(context.routing().isExceptionRoute()) {
        return;
    }

    // Or, using any other information from the request...
    
    // Some Cookie is set, we don't run the Filter.
    if(context.request().getCookie("someCookie") != null) {
        return;
    }

    // Actually run the Filter...
    
}

{#========================================== Section "routing / WebSockets" ==========================================#}

WebSockets

Because of the particular nature of WebSockets, we decided to aggregate all the documentation about them in a dedicated WebSockets section. Make sure you read that section to learn everything about WebSockets... Here's we're only going to provide a quick WebSocket route definition example, since we're talking about routin.

To create a WebSocket Route, you use the Router object, the same way you do for a regular Route. The big difference is the type of controller that is going to receive the WebSocket request. Here's the quick example :

router.websocket("/chat").before(someFilter).handle(chatWebsocketController);

{#========================================== Section "routing / Static resources" ==========================================#}

Static Resources

You can tell Spincast that some files and directories are Static Resources. Doing so, those files and directories will be served by the HTTP Server directly : the requests for them won't even reach the framework.

Note that queryStrings are ignored when a request is made for a Static Resource. If you need queryStrings to make a difference, have a look at Dynamic Resources.

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").handle();

// Uses an absolute path to a directory on the file system  
// as a static resources root.
router.dir("/public").pathAbsolute("/user/www/myprojet/public_files").handle();

// Uses a path relative to the Spincast writable directory, 
// on the file system, as the root for the static resources.
router.dir("/public").pathRelative("/public_files").handle();

// Will serve the requests for a specific file,
// here "/favicon.ico", using a file from the classpath.
router.file("/favicon.ico").classpath("/public/favicon.ico").handle();

// Uses an absolute path to a file on the file system 
// as the static resource target.
router.file("/favicon.ico").pathAbsolute("/user/www/myprojet/public_files/favicon.ico").handle();

// Uses a path relative to the Spincast writable directory, 
// on the file system, as the static resource target file.
router.file("/favicon.ico").pathRelative("/public_files/favicon.ico").handle();

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().pos(-1).handle(handler) won't be applied...

For the same reason, there are some limitations about the dynamic parameters that the Route definition of a Static Resource can contain... For standard Static Resources, only a dir(...) definition can contain a dynamic part, and it can only be a splat parameter, located at the very end of its route. For example :

  • This is valid! : dir(/one/two/*{remaining})
  • This is not valid : dir(/one/*{remaining}/two)
  • This is not valid : dir(/${param})
  • This is not valid : file(/one/two/*{remaining})

Finally, note that the Static Resource route definitions have precedence over any other Routes, so if you declare router.dir("/", "/public") for example, then no Route at all will ever reach the framework, everything would be considered as static!

This is, by the way, a quick and easy way to serve a purely static website using Spincast!

Hotlinking protection

Hotlinking is the process of embedding in a website a resource that comes from another domain. The typical example is an image from your site that someone embeds in his own website, without copying it to his server first. This would cost you bandwidth for an image that won't even be seen by your visitors!

Spincast provides a way of protecting your static resources from hotlinking. For the default protection, you simply have to add .hotlinkingProtected() on your static resources' routes:

getRouter().file("/some-file")
           .classpath("someFile.txt")
           .hotlinkingProtected()
           .handle();

The default behavior of the hotlinking protection is simply to return a FORBIDDEN HTTP status. So if someone hotlinks one of your resource, it won't be displayed at all on his website.

You can also tweak the way the protection work. You do this by passing an instance of HotlinkingManager.

The HotlinkingManager lets you change:

  • boolean mustHotlinkingProtect(...)
    To determine if the resource needs to be protected or not. By default, the protection will be triggered if one of those:
  • HotlinkingStategy<R> getHotlinkingStategy(...)
    The strategy to use to protect the resource. This can be:
    • FORBIDDEN: a 403 HTTP status would be returned.
    • REDIRECT: the current request will be redirected to the URL generated by #getRedirectUrl(...).
  • String getRedirectUrl(...)
    The URL to redirect to, when the strategy is REDIRECT. For example, you can redirect the request to an URL where a watermarked version of the resource would be served.

Here's an example of a custom HotlinkingManager, redirecting a single image to its watermarked version:


HotlinkingManager manager = new HotlinkingManagerDefault(getSpincastConfig()) {

    @Override
    public HotlinkingStategy getHotlinkingStategy(Object serverExchange, 
                                                  URI resourceURI, 
                                                  StaticResource<?> resource) {
        return HotlinkingStategy.REDIRECT;
    }

    @Override
    public String getRedirectUrl(Object serverExchange, 
                                 URI resourceURI, 
                                 StaticResource<?> resource) {
        return "/public/images/cat-watermarked.jpg";
    }
};

getRouter().file("/public/images/cat.jpg")
           .classpath("/images/cat.jpg")
           .hotlinkingProtected(manager)
           .handle();

In summary, you decide if you want the hotlinked resource to be totally unavailable if it is embedded on a foreign website, or if you want to serve a different version for it. You can combine the hotlinking protection with the Spincast Watermarker plugin when dealing with images: it allows you to serve the hotlinked images with a piece of additional information added to them (in general your logo or your website URL).

{#========================================== Section "routing / Dynamic resources" ==========================================#}

Dynamic Resources

A variation on Static Resources is what we call Dynamic Resources. When you declare a Static Resource, you can provide a "generator". The generator is a standard Route Handler that is going to receive the request if a requested Static Resource is not found. The job of this generator is to generate the missing Static Resource and to return it as the response. Spincast will then automatically intercept the body of this response, save it, and next time this Static Resource is requested, it is going to be served directly by the HTTP server, without reaching the framework anymore!

Here's an example of a Dynamic Resource definition :

router.dir("/images/tags/${tagId}").pathAbsolute("/generated_tags")
      .handle(new DefaultHandler() {

          @Override
          public void handle(DefaultRequestContext 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")
      .pathAbsolute("/user/www/myprojet/public_files/css/generated.css")
      .handle(new DefaultHandler() {

          @Override
          public void handle(DefaultRequestContext context) {

              String css = generateCssFile(context.request().getRequestPath());
              context.response().sendCharacters(css, "text/css");
          }
      });

Dynamic Resources definitions must be define using .pathAbsolute(...) or .pathRelative(...), and not .classpath(...) since the resource will be written to disk once generated. For the same reason, Spincast must have write permissions on the target directory!

By default, if the request for a Dynamic Resource contains a queryString, the resource is always generated, no cached version is used! This allows you to generate a Static Resource which is going to be cached, but also to get a variation on this resource if required, by passing some parameters.

Using the previous example, "/css/generated.css" requests would always return the same generated resource (only reaching the framework the first time), but a "/css/generated.css?test=123" request would not use any cached version and would always reach your generator.

If you don't want a queryString to make a difference, if you always want the first generated resource to be cached and served, you can set the "ignoreQueryString" parameter to true :

router.file("/css/generated.css")
      .pathAbsolute("/user/www/myprojet/public_files/css/generated.css")
      .handle(new DefaultHandler() {

          @Override
          public void handle(DefaultRequestContext context) {

              String css = generateCssFile(context.request().getRequestPath());
              context.response().sendCharacters(css, "text/css");
          }
      }, true);

Doing so, "/css/generated.css" and "/css/generated.css?test=123" would both always return the same cached resource.

The Route of a Dynamic Resource can contain dynamic parameters, but there are some rules :

  • The Route of a file(...) based Dynamic Resource can contain dynamic parameters but no splat parameter. For example :

    
    router.file("/test/${fileName}").pathAbsolute("/usr/someDir/${fileName}").handle(generator);
    

  • The Route of a dir(...) based Dynamic Resource can only contain a splat parameter, at its end. For example :

    
    router.dir("/generated/*{splat}").pathRelative("/someDir").handle(generator);
    

  • The target path of a file(...) based Dynamic Resource can use the dynamic parameters from the URL. For example this is valid :

    
    router.file("/test/${fileName}").pathAbsolute("/usr/someDir/${fileName}").handle(generator);
    

  • But the target path of a dir(...) based Dynamic Resource can not use the splat parameter from the URL :

    // This is NOT valid!
    router.dir("/generated/*{splat}").pathAbsolute("/someDir/*{splat}").handle(generator);
    

Finally, note that when a dynamic resource is generated (in other words when its generator is called), the filters will be applied, as with any reguar route!

If you don't want a specific filter to be applied when a dynamic resource is generated, you can use .skipResourcesRequests() on the route of that filter.

{#========================================== 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 the 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().handle();
           
// Enable CORS on a Static Resource file.
getRouter().file("/public/test.txt").classpath("/public_files/test.txt")
           .cors().handle();

Here are the available options when configuring CORS on a Route :

// 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 / HTTP Authentication" ==========================================#}

HTTP Authentication

If you need a section of your application to be protected so only privileged users can access it, one of the options is to use HTTP Authentication. With HTTP Authentication, the Server itself manages the authentication, so no request will ever reach the framework or a protected resource unless the correct username/password is given.

Here's a Protected page example. To access it, you have to provide the correct username/password combination: Stromgol/Laroche.

You enable HTTP Authentication in two steps :

  • You use the httpAuth(...) method on the Router object to indicate which sections of the application to protect. This method requires the base URL of the section to protect and a name for the realm (a synonym for "a protected section") :

    // Protects the "/admin" section of the website and associates
    // a realm name
    router.httpAuth("/admin", "Protected Admin Section");

    Note that the name of the realm will be displayed to the user when the server asks for the username/password!
  • You add a set of acceptable username/passwords combinaisons for that realm, using the addHttpAuthentication(...) on the Server object :

    // Adds a username/password combinaison to the
    // "Protected Admin Section" realm
    getServer().addHttpAuthentication("Protected Admin Section", "Stromgol", "Laroche")

    If you fail to add any username/password combinaisons, no one will be able to see the protected section!

You can have a look at the Javadoc of the server to see more methods related to HTTP Authentication.

Finally, note that HTTP Authentication is a quick and easy way to protect a section of your application but, if you need more flexibility, a form based authentication is often preferred.

{#========================================== Section "routing / Redirection rules" ==========================================#}

Redirection rules

Using the Router, you can specify that a Route must automatically be redirected to another one. This is useful, for example, when you change the URL of a resource but don't want the existing links to break.

To add a redirection rule, simply use the redirect(...) method on the Router:

router.redirect("/the-original-route").to("/the-new-route");

Here are the available options when creating such redirection rule:

  • redirect(originalRoute): starts the creation of the direction rule by specifying the original Route's path.
  • temporarily(): specifies that the redirection must be temporary (302).
  • permanently(): specifies that the redirection must be permanent (301). This is the default and it is not required to specify it.
  • to(newRoute): saves the redirection rule by specifying the new Route's path.

Note that if the path of the original Route contains dynamic parameters (splat parameters included), you can use those in the definition of the new Route! For example, this redirection rule will redirect "/books/42" to "/catalog/item/42" :

router.redirect("/books/${bookId}").to("/catalog/item/${bookId}");

Those dynamic parameters can be used anywhere in the new Route definition, not just as "full" tokens. For example, this redirection rule will redirect "/types/books/42" to "/catalog-books/42" :

router.redirect("/types/${typeId}/${itemId}").to("/catalog-${typeId}/${itemId}");

Finally, note that a redirection rule is implemented as a "before" Filter and must, in general, be the very first one to run! Its position is configurable using SpincastRouterConfig and its default value is "-1000". When this Filter runs, any other Filters or Route Handlers are skipped, and the redirection header is sent to the client immediately.

{#========================================== Section "routing / Special exceptions" ==========================================#}

Special exceptions

You can throw any exception and it is going to trigger a new "Exception" routing process. But some exceptions provided by Spincast have a special behavior :

  • RedirectException : This exception will stop the current routing process (as any exception does), will send a redirection header to the user, and will end the exchange. Learn more about that in the Redirecting section. Here's an example :

    public void myHandler(AppRequestContext context) {
    
        // Redirects to "/new-url"
        throw new RedirectException("/new-url", true);
        
        // You can also provide a Flash message :
        throw new RedirectException("/new-url", true, myFlashMessage);
    }

  • ForwardRouteException : This exception allows you to change the Route, to forward the request to another Route without any client side redirection. Doing so, a new routing process is started, but this time using the newly specified URL. Learn more about this in the Forwarding section. Here's an example :

    public void myHandler(AppRequestContext 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 Filters/Route Handlers won't be run, and the response will be sent as is, without any more modification. Learn more about this in the SkipRemainingHandlersException section. Here's an example :

    public void myHandler(AppRequestContext context) {
    
        context.response().sendPlainText("I'm the last thing sent!");
        throw new SkipRemainingHandlersException();
    }

  • CustomStatusCodeException : This exception allows you to set an HTTP status to return. Then, your custom Exception Route Handler can check this code and display something accordingly. Also, in case you use the provided Exception Route Handler, you still at least have control over the HTTP status sent to the user. Here's an example :

    public void myHandler(AppRequestContext 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 Route Handler because in a custom handler you would have full control over what you send as a response anyway... Here's an example :

    public void myHandler(AppRequestContext 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 Spincast 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 UserControllerDefault implements UserController {

    @Inject
    protected void init(DefaultRouter router) {
        addRoutes(router);
    }

    protected void addRoutes(DefaultRouter router) {
        router.GET("/users/${userId}").handle(this::getUser);
        router.POST("/users").handle(this::addUser);
        router.DELETE("/users/${userId}").handle(this::deleteUser);
    }

    @Override
    public void getUser(DefaultRequestContext context) {
        //...
    }

    @Override
    public void addUser(DefaultRequestContext context) {
        //...
    }

    @Override
    public void deleteUser(DefaultRequestContext context) {
        //...
    }
}

Explanation :

  • 3-6 : The Router is injected in an init() method.
  • 8-12 : The controller adds its own Routes. Here, the UserController is responsible to add Routes related to users.





© 2015 - 2024 Weber Informatics LLC | Privacy Policy