templates.docs.routing.html Maven / Gradle / Ivy
Show all versions of spincast-website Show documentation
{#==========================================
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 at least one of those is true:
-
There is an "
origin
" header in the request and its value does not match the host of the application
(as returned by SpincastConfig#getPublicServerHost()).
-
There is a "
referer
" header in the request and its value does not match the host of the application
(as returned by SpincastConfig#getPublicServerHost()).
-
HotlinkingStategy 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}");
If you need more control over the URL to redirect to, you can also
specify a RedirectHandler
in order to dynamically generate it:
router.redirect("/test").to(context, originalPath) -> originalPath + "/en";
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.