Ninja features one central route file. We think this is important, because you immediately can see what routes your application provides. It especially facilitates designing a nice RESTful application and API, because you have a good overview what is going on.
The route file is a plain old Java file living at conf/Routes.java
.
Routes.java
implements the interface ApplicationRoutes.java
.
This interface defines a method called public void init(Router router) {…}
which allows us to map incoming requests to controllers and their methods.
public class Routes implements ApplicationRoutes { @Override public void init(Router router) { // a GET request to "index" will be handled by a class called // "AppController" its method "index". router.GET().route("/index").with(AppController::index); ... } }
The init(…)
method provides us with the Router
and router allows us to
define what happens for GET, POST, PUT, OPTIONS, HEAD and DELETE requests.
And if you want to route an HTTP method not yet supported by Ninja out of
the box you can always use
route.METHOD(“MY_CUSTOM_HTTP_METHOD”).route(…)…
.
Ninja 6 introduced support for routing using Java 8 lambda expressions. Ninja includes support for various kinds of lambda expressions including method references and anonymous lambdas. See https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html for more info.
The class ninja.ControllerMethods
defines the various interfaces
that are acceptable method signatures for Java 8 lambda expressions. A controller
method returns a ninja.Result
and has anywhere from 0 to 12 arguments.
If you need more than 12 arguments, you can fallback to Ninja's legacy routing
strategy of Class + “method”.
The most common and recommended lambda expression is a reference to an instance method of an arbitrary type.
public class Routes implements ApplicationRoutes { @Override public void init(Router router) { router.GET().route("/").with(AppController::index); } }
You can also use a lambda expression of a reference to an instance method of a particular object. Please note that Guice will not construct this instance and Ninja will use the one referenced instead.
public class Routes implements ApplicationRoutes { @Override public void init(Router router) { AppController controller = new AppController(); router.GET().route("/").with(controller::index); } }
You can also use a lambda expression of an anonymously defined method. The
lambda method can take anywhere of 0 to 12 arguments. Please note that unlike
methods defined in a regular class, Java 8 does not retain parameter annotations
on anonymously defined lambda expressions. Ninja will still try to inject
parameters via Guice into these methods, but it will only handle injection by
type only. So while ninja.Context
can be injected be aware that
@Param(“a”) String a
cannot until a future JDK addresses this
limitation. We recommend using either zero arguments or ninja.Context
with anonymously defined lambdas.
public class Routes implements ApplicationRoutes { @Override public void init(Router router) { router.GET().route("/").with(() -> Results.redirect("/home")); router.GET().route("/home").with((Context context) -> Results.ok()); } }
You can use a Result object to render a static page simply without any controller.
public class Routes implements ApplicationRoutes { @Override public void init(Router router) { // a GET request to "/" will be redirect to "/dashboard" router.GET().route("/").with(() -> Results.redirect("/dashboard")); // show a static page router.GET().route("/dashboard").with(() -> Results.html().template("/dashboard.html")); ... } }
Routes can contain arbitrary Java regular expressions. You can use these regular expressions simply by putting them into the route definition.
Some examples:
// matches for instance "/assets/00012", "/assets/12334", ... router.GET().route("/assets/\\d*").with(AssetsController::serveDigits); // matches for instance "/assets/myasset.xml", "/assets/boing.txt", ... router.GET().route("/assets/.*").with(AssetsController::serveArbitrary);
In the first example \\d*
tells the router to match digits
(defined by \\d
) of arbitrary length (defined by *
).
In the second example .*
lets the router match arbitrary characters
(.
) of arbitrary length (defined by *
).
This example also shows what happens if two routes match.
For instance a request to /assets/00012
is matched by
both route definitions. In that case the first matching route from top will be
executed. In our case method serveDigits
.
More on regular expressions: http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html
A controller usually not only renders stuff, but also takes some inputs and does something with them.
Let's assume we got a request like that:
GET /user/12345/[email protected]/userDashboard
Looks like a GET request to the userDashboard. The request seems to contain
an id
and an email
. As application developer you want to have a convenient
way to get id
and email
inside your controller. Ninja
allows you to define and name parts of your routes via
curly braces {…}
:
router.GET().route("/user/{id}/{email}/userDashboard").with(ApplicationController::userDashboard);
We can then inject id
and email
into our controller via
annotation @PathParam
.
package controllers; @Singleton public class AppController { public Result userDashboard( @PathParam("id") String id, @PathParam("email") String email) { //do something with the parameters... } }
([^/]*)
. That means that variable parts match arbitrary
characters but do not span over any path separators "/". If you need more control
please you'll find more in the next section.
Ninja allows you to specify regular expressions that will be used to match arbitrary parts of your URL.
The syntax is {PRAMETER_NAME: REGEX}
(with a whitespace after the “:”).
For instance a route like /assets/{fileName: .*}
will match everything
after /assets/
. Imagine a request to /assets/css/app.css
.
This request will be handled by the route and accessing the path parameter
fileName
will return css/app.css
.
Routes can contain multiple variable parts with regular expressions.
For example, for a request to /categories/1234/products/5678
, where category is
expected to be an integer value and product to be either integer or string value,
you can define routes like that:
router.GET().route("/categories/{catId: [0-9]+}/products/{productId: [0-9]+}").with(ProductController::product); router.GET().route("/categories/{catId: [0-9]+}/products/{productName: .*}").with(ProductController::productByName);
The request above will be handled by the first route, and request to /categories/1234/products/mouse
will be handled by the second route.
Values of variable parts of a route are injected into our controller (explained above)
and are implicitly validated with regular expressions.
So our controller for routes above would be like that (look at @PathParam
argument types):
package controllers; @Singleton public class ProductController { public Result product( @PathParam("catId") int catId, @PathParam("productId") int productId) { // find product by id in given category } public Result productByName( @PathParam("catId") int catId, @PathParam("productName") String productName) { // find product(s) by name in given category } }
Note at how regular expressions in routes can be used to validate path parameters and to define fine grained routing.
You can use any Java compliant regular expressions for matching. Please refer to the official documentation at http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html.
Because the Route.java is just a plain old java file we can also inject arbitrary stuff into it (via Guice).
A popular use case is to inject ninjaProperties. This allows us to activate / deactivate certain routes based on the environment we are in.
The following example shows that a setup route is not available when running in production:
public class Routes implements ApplicationRoutes { @Inject NinjaProperties ninjaProperties; @Override public void init(Router router) { // a GET request to "index" will be handled by a class called // "AppController" its method "index". router.GET().route("/index").with(AppController::index); ... // only active when not in production mode: if (!ninjaProperties.isProd) { router.GET().route("/setup").with(AppController::setup); } } }
Let's say you want to know the final route of a class ApplicationController.class, method “index”. Assume
that the original raw route looked like “/”
.
You can get the final URL by injecting a ninja.ReverseRouter
into your controller
and calling various with
methods to lookup the route and optionally replace path
or add query string parameters.
@Inject ReverseRouter reverseRouter; public void myMethod() { // will result into "/" String url = reverseRouter.with(ApplicationController::index) .build(); // url would be "/" }
Now consider a more complex example. Say the original raw route contained placeholders on the following form:
/user/{id}/{email}/userDashboard
. You can now ask the router for the final URL
and swap in path parameters in addition to adding query string parameters.
@Inject ReverseRouter reverseRouter; public void myMethod() { String url = reverseRouter.with(ApplicationController::userDashboard) .path("id", "123") .path("email", "[email protected]") .query("paging_size", 100) .query("page", 1) .build(); // url will be "/user/123/test%40example.com/userDashboard?paging_size=100&page=1" }
Redirecting a user to a reverse route is a common use case. The Builder
object returned by the reverse router has a handy redirect()
method to build
a ninja.Result
that will redirect the user.
@Inject ReverseRouter reverseRouter; public Result myMethod() { // will redirect to "/" return reverseRouter.with(ApplicationController::index) .redirect(); }
Encoding / Decoding of URLs is not as easy as you might think it is. Ninja tries to simplify everything as much as possible, but as a user of the API you have to know what you are submitting to Ninja.
We recommend the following excellent article from Lunatech before you use encoding / decoding actively in your application.
Let's reconsider the controller method from above:
package controllers; @Singleton public class ApplicationController { public Result index( @PathParam("id") String id, @PathParam("email") String email, @Param("debug") String debug) { //do something with the parameters... } }
You can expect that String id and String debug are both correctly decoded values. BUT This assumes that you are encoding the values correctly on the client side. And encoding is different for query parameters or stuff in the path. Do not even think about using URLEncoder for encoding URLs. This is wrong.
Simple example that outlines some of the difficulties: Think of a route “/user/{id}/userDashboard”.
Let's say your id is “rootuser/domain”. If you do not encode the slash in the middle you end up with a URL like /user/rootuser/domain/userDashboard. And the this URL does not match the route because of the “/”.
Therefore you have to encode your id correctly. In that case it would be: rootuser%2Fdomain. When the user then visits /user/rootuser%2Fdomain/userDashboard the route matches and a @PathParam(“id”) would then be rootuser/domain as it is decoded by Ninja.
In principle it is really simple. But it is even simpler to mess encoding / decoding up. The article from Lunatech mentioned earlier is awesome and explains everything.
Ninja-jaxy-routes allows you to register your routes using annotations similar to JAX-RS.
You may use the standard Ninja route registration in combination with this route builder or you may replace all your route registrations with annotations.
NOTE: Your annotated controllers must be located somewhere within your application's configured controller package or a subpackage thereof.
<dependency>
<groupId>org.ninjaframework</groupId>
<artifactId>ninja-jaxy-routes</artifactId>
<version>${ninja.version}</version>
</dependency>
JaxyRoutes
in your conf.Routes
class.@Inject
JaxyRoutes jaxyRoutes;
@Override
public void init(Router router) {
jaxyRoutes.init(router);
}
Now you are ready to start annotating your controllers.
Ninja-jaxy-routes supports multiple @Path
specs per controller method and also supports controller class inheritance.
The following example will register two GET routes /base/middle/app/get
and /base/middle/app/retrieve
for the same controller method.
@Path("/base")
class Base {
}
@Path("/middle")
class Middle extends Base {
}
@Path("/app")
class App extends Middle {
@Path({"/get", "/retrieve"})
@GET
Result get() {
return Results.text().renderRaw("Yahoo!");
}
}
If the Base
and Middle
parent classes had each specified multiple paths, all permutations of the complete routes would be registered too.
By default, all routes are assumed to be GET routes unless they are specifically annotated.
The following common HTTP method annotations are available:
@DELETE
@GET
@HEAD
@OPTIONS
@PATCH
@POST
@PUT
If the built-in methods are insufficient :
Set the key ninja.jaxy.custom_http_methods
to true
in your conf/application.conf
.
Then implement your own custom HTTP method:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @HttpMethod(“CUSTOM”) public @interface CUSTOM { }
Since your controllers and methods are reflectively loaded we can not expect a predictable route registration order from the JVM.
To compensate for this, you may specify the @Order
annotation on a controller method to dictate the ordering of routes.
@Order
for every method.Here is an example of specifying an @Order
.
@Path({"/get", "/retrieve"})
@GET
@Order(10)
Result something() {
}
You may include/exclude routes based on the Ninja runtime mode.
@Dev
@Prod
@Test
Here is an example of specifying @Dev
and @Test
.
@Path("/diagnostics")
@GET
@Dev @Test
Result diagnostics() {
}
It is also possible to include/exclude a route based on a NinjaProperties key.
If the key does not exist in your runtime config, the route is not registered.
@Path("/sneaky")
@GET
@Requires("sneaky.key")
Result somethingSneaky() {
}