Skip to content

Latest commit

 

History

History
1419 lines (1101 loc) · 26.6 KB

File metadata and controls

1419 lines (1101 loc) · 26.6 KB

Router

The javadoc:Router[] is the heart of Jooby and consist of:

  • Routing algorithm (radix tree)

  • One or more javadoc:Route[text="routes"]

  • Collection of operator over javadoc:Route[text="routes"]

Route

A javadoc:Route[] consists of three part:

Routes:
{

  // (1) (2)
  get("/foo", ctx -> {
    return "foo"; // (3)
  });

  // Get example with path variable
  get("/foo/{id}", ctx -> {
    return ctx.path("id").value();
  });

  // Post example
  post("/", ctx -> {
    return ctx.body().value();
  });
}
Kotlin
{

  // (1) (2)
  get("/foo") {
    "foo" // (3)
  }

  // Get example with path variable
  get("/foo/{id}") {
    ctx.path("id").value()
  }

  // Post example
  post("/") {
    ctx.body().value()
  }
}
  1. HTTP method/verb, like: GET, POST, etc…​

  2. Path pattern, like: /foo, /foo/{id}, etc…​

  3. Handler function

The javadoc:Route.Handler[text="handler"] function always produces a result, which is send it back to the client.

Attributes

Attributes let you annotate a route at application bootstrap time. It functions like static metadata available at runtime:

Java
{
  get("/foo", ctx -> "Foo")
    .attribute("foo", "bar");
}
Kotlin
{
  get("/foo") {
    "Foo"
  }.attribute("foo", "bar")
}

An attribute consist of a name and value. Values can be any object. Attributes can be accessed at runtime in a request/response cycle. For example, a security module might check for a role attribute.

Java
{
  use(next -> ctx -> {
    User user = ...;
    String role = ctx.getRoute().attribute("Role");

    if (user.hasRole(role)) {
        return next.apply(ctx);
    }

    throw new StatusCodeException(StatusCode.FORBIDDEN);
  });
}
Kotlin
{
use(
  val user = ...
  val role = ctx.route.attribute("Role")

  if (user.hasRole(role)) {
    return next.apply(ctx)
  } else {
    throw StatusCodeException(StatusCode.FORBIDDEN)
  }
}

In MVC routes you can set attributes via annotations:

Java
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Role {
  String value();
}

@Path("/path")
public class AdminResource {

  @Role("admin")
  public Object doSomething() {
    ...
  }

}

{
  use(next -> ctx -> {
    System.out.println(ctx.getRoute().attribute("Role"));
  });
}
Kotlin
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Role (val value: String)

@Path("/path")
class AdminResource {

  @Role("admin")
  fun doSomething() : Any {
    ...
  }

}

{
  use {
    println(ctx.route.attribute("Role"))
  }
}

The previous example will print: admin. You can retrieve all the attributes of the route by calling ctx.getRoute().getAttributes().

Any runtime annotation is automatically added as route attributes following these rules: - If the annotation has a value method, then we use the annotation’s name as the attribute name. - Otherwise, we use the method name as the attribute name.

Path Pattern

Static

Java
{
  get("/foo", ctx -> "Foo");
}
Koltin
{
  get("/foo") { "Foo" }
}

Variable

Single path variable:
{
  // (1)
  get("/user/{id}", ctx -> {
    int id = ctx.path("id").intValue(); // (2)
    return id;
  });
}
Kotlin
{
  // (1)
  get("/user/{id}") {
    val id = ctx.path("id").intValue() // (2)
    id
  }
}
  1. Defines a path variable id

  2. Retrieve the variable id as int

Multiple path variables:
{
  // (1)
  get("/file/{file}.{ext}", ctx -> {
    String filename = ctx.path("file").value(); // (2)
    String ext = ctx.path("ext").value();   // (3)
    return filename + "." + ext;
  });
}
Kotlin
{
  // (1)
  get("/file/{file}.{ext}") {
    val filename = ctx.path("file").value() // (2)
    val ext = ctx.path("ext").value()       // (3)
    filename + "." + ext
  }
}
  1. Defines two path variables: file and ext

  2. Retrieve string variable: file

  3. Retrieve string variable: ext

Optional path variable:
{
  // (1)
  get("/profile/{id}?", ctx -> {
    String id = ctx.path("id").value("self"); // (2)
    return id;
  });
}
Kotlin
{
  // (1)
  get("/profile/{id}?") {
    val id = ctx.path("id").value("self") // (2)
    id
  }
}
  1. Defines an optional path variable id. The trailing ? make it optional.

  2. Retrieve the variable id as String when present or use a default value: self.

The trailing ? makes the path variable optional. The route matches:

  • /profile

  • /profile/eespina

Regex

Regex path variable:
{
  // (1)
  get("/user/{id:[0-9]+}", ctx -> {
    int id = ctx.path("id").intValue(); // (2)
    return id;
  });
}
Kotlin
{
  // (1)
  get("/user/{id:[0-9]+}") {
    val id = ctx.path("id").intValue() // (2)
    id
  }
}`
  1. Defines a path variable: id. Regex expression is everything after the first :, like: [0-9]+

  2. Retrieve an int value

Optional syntax is also supported for regex path variable: /user/{id:[0-9]+}?:

  • matches /user

  • matches /user/123

* Catchall

catchall
{
  // (1)
  get("/articles/*", ctx -> {
    String catchall = ctx.path("*").value(); // (2)
    return catchall;
  });

  get("/articles/*path", ctx -> {
    String path = ctx.path("path").value(); // (3)
    return path;
  });
}
Kotlin
{
  // (1)
  get("/articles/*") {
    val catchall = ctx.path("*").value() // (2)
    catchall
  }

  get("/articles/*path") {
    val path = ctx.path("path").value()  // (3)
    path
  }
}
  1. The trailing * defines a catchall pattern

  2. We access to the catchall value using the * character

  3. Same example, but this time we named the catchall pattern and we access to it using path variable name.

Note

A catchall pattern must be defined at the end of the path pattern.

Handler

Application logic goes inside a javadoc:Route.Handler[text=handler]. A javadoc:Route.Handler[text=handler] is a function that accepts a javadoc:Context[text=context] object and produces a result.

A javadoc:Context[text=context] allows you to interact with the HTTP Request and manipulate the HTTP Response.

Note

Incoming request matches exactly ONE route handler. If there is no handler, produces a 404 response.

Java
{
  get("/user/{id}", ctx -> ctx.path("id").value());  // (1)

  get("/user/me", ctx -> "my profile");              // (2)

  get("/users", ctx -> "users");                     // (3)

  get("/users", ctx -> "new users");                 // (4)
}
Kotlin
{
  get("/user/{id}") { ctx.path("id").value() }  // (1)

  get("/user/me") { "my profile" }              // (2)

  get("/users") { "users" }                     // (3)

  get("/users") { "new users" }                 // (4)
}

Output:

  1. GET /user/ppicapiedrappicapiedra

  2. GET /user/memy profile

  3. Unreachable ⇒ override it by next route

  4. GET /usersnew users not users

Routes with most specific path pattern (2 vs 1) has more precedence. Also, is one or more routes result in the same path pattern, like 3 and 4, last registered route hides/overrides previous route.

Filter

Cross cutting concerns such as response modification, verification, security, tracing, etc. is available via javadoc:Route.Filter[].

A filter takes the next handler in the pipeline and produces a new handler:

interface Filter {
  Handler apply(Handler next);
}
Timing filter example:
{
  use(next -> ctx -> {
    long start = System.currentTimeMillis();       // (1)

    Object response = next.apply(ctx);             // (2)

    long end = System.currentTimeMillis();
    long took = end - start;

    System.out.println("Took: " + took + "ms");   // (3)

    return response;                              // (4)
  });

  get("/", ctx -> {
    return "filter";
  });
}
Kotlin
{
  /** Kotlin uses implicit variables: `ctx` and `next` */
  filter {
    val start = System.currentTimeMillis()  // (1)

    val response = next.apply(ctx)          // (2)

    val end = System.currentTimeMillis()
    val took = end - start

    println("Took: " + took + "ms")         // (3)

    response                                // (4)
  }

  get("/") {
    "filter"
  }
}
  1. Saves start time

  2. Proceed with execution (pipeline)

  3. Compute and print latency

  4. Returns a response

Note

One or more filter on top of a handler produces a new handler.

Before

The javadoc:Route.Before[text=before] filter runs before a handler.

A before filter takes a context as argument and don’t produces a response. It expected to operates via side effects (usually modifying the HTTP response).

interface Before {
  void apply(Context ctx);
}
Example
{
  before(ctx -> {
    ctx.setResponseHeader("Server", "Jooby");
  });

  get("/", ctx -> {
    return "...";
  });
}
Kotlin
{
  before {
    ctx.setResponseHeader("Server", "Jooby")
  }

  get("/") {
    "..."
  }
}

After

The javadoc:Route.After[text=after] filter runs after a handler.

An after filter takes three arguments. The first argument is the HTTP context, the second argument is the result/response from a functional handler or null for side-effects handler, the third and last argument is an exception generates from handler.

It expected to operates via side effects, usually modifying the HTTP response (if possible) or for cleaning/trace execution.

interface After {
  void apply(Context ctx, Object result, Throwable failure);
}
Functional Handler:
{
  after((ctx, result, failure) -> {
    System.out.println(result);          (1)
    ctx.setResponseHeader("foo", "bar"); (2)
  });

  get("/", ctx -> {
    return "Jooby";
  });
}
Kotlin
{
  after {
    println("Hello $result")             (1)
    ctx.setResponseHeader("foo", "bar")  (2)
  }

  get("/") {
    "Jooby"
  }
}
  1. Prints Jooby

  2. Add a response header (modifies the HTTP response)

If the target handler is a functional handler modification of HTTP response is allowed it.

For side effects handler the after filter is invoked with a null value and isn’t allowed to modify the HTTP response.

Side-Effect Handler:
{
  after((ctx, result, failure) -> {
    System.out.println(result);          (1)
    ctx.setResponseHeader("foo", "bar"); (2)
  });

  get("/", ctx -> {
    return ctx.send("Jooby");
  });
}
Kotlin
{
  after {
    println("Hello $result")             (1)
    ctx.setResponseHeader("foo", "bar")  (2)
  }

  get("/") {
    ctx.send("Jooby")
  }
}
  1. Prints null (no value)

  2. Produces an error/exception

Exception occurs because response was already started and its impossible to alter/modify it.

Side-effects handler are all that make use of family of send methods, responseOutputStream and responseWriter.

You can check whenever you can modify the response by checking the state of javadoc:Context[isResponseStarted]:

Safe After:
{
  after((ctx, result, failure) -> {
    if (ctx.isResponseStarted()) {
      // Don't modify response
    } else {
      // Safe to modify response
    }
  });
}
Kotlin
{
  after {
    if (ctx.responseStarted) {
      // Don't modify response
    } else {
      // Safe to modify response
    }
  }
}
Note

An after handler is always invoked.

The next examples demonstrate some use cases for dealing with errored responses, but keep in mind that an after handler is not a mechanism for handling and reporting exceptions that’s is a task for an Error Handler.

Run code depending of success or failure responses:
{
  after((ctx, result, failure) -> {
    if (failure == null) {
      db.commit();                   (1)
    } else {
      db.rollback();                 (2)
    }
  });
}
Kotlin
{
  after {
    if (failure == null) {
      db.commit()                    (1)
    } else {
      db.rollback()                  (2)
    }
  }
}

Here the exception is still propagated given the chance to the Error Handler to jump in.

Recover fom exception and produces an alternative output:
{
  after((ctx, result, failure) -> {
    if (failure instanceOf MyBusinessException) {
      ctx.send("Recovering from something");        (1)
    }
  });
}
Kotlin
{
  after {
    if (failure is MyBusinessException) {
      ctx.send("Recovering from something")         (1)
    }
  }
}
  1. Recover and produces an alternative output

Here the exception wont be propagated due we produces a response, so error handler won’t be execute it.

In case where the after handler produces a new exception, that exception will be add to the original exception as suppressed exception.

Suppressed exceptions:
{
  after((ctx, result, failure) -> {
    ...
    throw new AnotherException();
  });

  get("/", ctx -> {
    ...
    throw new OriginalException();
  });

  error((ctx, failure, code) -> {
    Throwable originalException = failure;                       (1)
    Throwable anotherException  = failure.getSuppressed()[0];    (2)
  });
}
Kotlin
{
  after {
    ...
    throw AnotherException();
  }

  get("/") { ctx ->
    ...
    throw OriginalException()
  }

  error { ctx, failure, code) ->
    val originalException = failure                              (1)
    val anotherException  = failure.getSuppressed()[0]           (2)
  }
}
  1. Will be OriginalException

  2. Will be AnotherException

Complete

The javadoc:Route.Complete[text=complete] listener run at the completion of a request/response cycle (i.e. when the request has been completely read, and the response has been fully written).

At this point it is too late to modify the exchange further. They are attached to a running context (not like a filter/before/after filters).

Example
{
   use(next -> ctx -> {
     long start = System.currentTimeInMillis();
     ctx.onComplete(context -> {                      (1)
       long end = System.currentTimeInMillis();       (2)
       System.out.println("Took: " + (end - start));
     });
   });
}
Kotlin
{
   use {
     val start = System.currentTimeMillis()
     ctx.onComplete {                                 (1)
       val end = System.currentTimeMillis()           (2)
       println("Took: " + (end - start))
     }
   }
}
  1. Attach a completion listener

  2. Run after response has been fully written

Completion listeners are invoked in reverse order.

Pipeline

Route pipeline (a.k.a route stack) is a composition of one or more use(s) tied to a single handler:

Java
{
  // Increment +1
  use(next -> ctx -> {
    Number n = (Number) next.apply(ctx);
    return 1 + n.intValue();
  });

  // Increment +1
  use(next -> ctx -> {
    Number n = (Number) next.apply(ctx);
    return 1 + n.intValue();
  });

  get("/1", ctx -> 1); // (1)

  get("/2", ctx -> 2); // (2)
}
Kotlin
{
  // Increment +1
  use {
    val n = next.apply(ctx) as Int
    1 + n
  }

  // Increment +1
  use {
    val n = next.apply(ctx) as Int
    1 + n
  }

  get("/1") { 1 } // (1)

  get("/2") { 2 } // (2)
}

Output:

  1. /13

  2. /24

Behind the scene, Jooby builds something like:

{
  // Increment +1
  var increment = use(next -> ctx -> {
    Number n = (Number) next.apply(ctx);
    return 1 + n.intValue();
  });

  Handler one = ctx -> 1;

  Handler two = ctx -> 2;

  Handler handler1 = increment.then(increment).then(one);
  Handler handler2 = increment.then(increment).then(two);

  get("/1", handler1);

  get("/2", handler2);
}

Any filter defined on top of the handler will be stacked/chained into a new handler.

Note
Filter without path pattern

This was a hard decision to make, but we know is the right one. Jooby 1.x uses a path pattern to define filter.

The pipeline in Jooby 1.x consists of multiple filters and handlers. They are match sequentially one by one. The following filter is always executed in Jooby 1.x

Jooby 1.x
{
   use("/*", (req, rsp, chain) -> {
     // remote call, db call
   });

   // ...
}

Suppose there is a bot trying to access and causing lot of 404 responses (path doesn’t exist). In Jooby 1.x the filter is executed for every single request sent by the bot just to realize there is NO matching route and all we need is a 404.

In Jooby 2.x this won’t happen anymore. If there is a matching handler, the pipeline will be executed. Otherwise, nothing will do!

Order

Order follows the what you see is what you get approach. Routes are stacked in the way they were added/defined.

Order example:
{
  // Increment +1
  use(next -> ctx -> {
    Number n = (Number) next.apply(ctx);
    return 1 + n.intValue();
  });

  get("/1", ctx -> 1);                // (1)

  // Increment +1
  use(next -> ctx -> {
    Number n = (Number) next.apply(ctx);
    return 1 + n.intValue();
  });

  get("/2", ctx -> 2);               // (2)
}
Kotlin
{
  // Increment +1
  use {
    val n = next.apply(ctx) as Int
    1 + n
  }

  get("/1") { 1 }                // (1)

  // Increment +1
  use {
    val n = next.apply(ctx) as Int
    1 + n
  }

  get("/2") { 2 }                // (2)
}

Output:

  1. /12

  2. /24

Scoped Filter

The javadoc:Router[route, java.lang.Runnable] and javadoc:Router[path, java.lang.String, java.lang.Runnable] operators are used to group one or more routes.

A scoped filter looks like:

Scoped filter:
{
  // Increment +1
  use(next -> ctx -> {
    Number n = (Number) next.apply(ctx);
    return 1 + n.intValue();
  });

  routes(() -> {                          // (1)
    // Multiply by 2
    use(next -> ctx -> {
      Number n = (Number) next.apply(ctx);
      return 2 * n.intValue();
    });

    get("/4", ctx -> 4);                 // (2)
  });

  get("/1", ctx -> 1);                   // (3)
}
Kotlin
{
  // Increment +1
  filter {
    val n = next.apply(ctx) as Int
    return 1 + n
  }

  routes {                                // (1)
    // Multiply by 2
    filter {
      val n = next.apply(ctx) as Int
      2 * n
    }

    get("/4") { 4 }               // (2)
  }

  get("/1") { 1 }                 // (3)
}

Output:

  1. Introduce a new scope via route operator

  2. /49

  3. /12

It is a normal filter inside of one of the group operators.

Grouping routes

As showed previously, the javadoc:Router[route, java.lang.Runnable] operator push a new route scope and allows you to selectively apply one or more routes.

Route operator
{
  routes(() -> {

    get("/", ctx -> "Hello");

  });
}
Kotlin
{
  routes {

    get("/") {
      "Hello"
    }

  }
}

Route operator is for grouping one or more routes and apply cross cutting concerns to all them.

In similar fashion the javadoc:Router[path, java.lang.String, java.lang.Runnable] operator groups one or more routes under a common path pattern.

Routes with path prefix:
{
   path("/api/user", () -> {    // (1)

     get("/{id}", ctx -> ...);  // (2)

     get("/", ctx -> ...);      // (3)

     post("/", ctx -> ...);     // (4)

     ...
   });
}
Kotlin
{
   path("/api/user") {     // (1)

     get("/{id}") { ...}   // (2)

     get("/") { ...}       // (3)

     post("/") { ...}      // (4)

     ...
   });
}
  1. Set common prefix /api/user

  2. GET /api/user/{id}

  3. GET /api/user

  4. POST /api/user

Composing

Mount

Composition is a technique for building modular applications. You can compose one or more router into a new one.

Composition is available through the javadoc:Router[mount, io.jooby.Router] operator:

Composing
public class Foo extends Jooby {
  {
    get("/foo", Context::getRequestPath);
  }
}

public class Bar extends Jooby {
  {
    get("/bar", Context::getRequestPath);
  }
}

public class App extends Jooby {
  {
    mount(new Foo());                       // (1)

    mount(new Bar());                       // (2)

    get("/app", Context::getRequestPath);   // (3)
  }
}
Kotlin
class Foo: Kooby({

  get("/foo") { ctx.getRequestPath() }

})

class Bar: Kooby({

  get("/bar") { ctx.getRequestPath() }

})

class App: Kooby({
  mount(Foo())                         // (1)

  mount(Bar())                         // (2)

  get("/app") { ctx.getRequestPath() } // (3)
})
  1. Imports all routes from Foo. Output: /foo/foo

  2. Imports all routes from Bar. Output: /bar/bar

  3. Add more routes . Output /app/app

Composing with path prefix
public class Foo extends Jooby {
  {
    get("/foo", Context::getRequestPath);
  }
}

public class App extends Jooby {
  {
    mount("/prefix", new Foo());  // (1)
  }
}
Kotlin
class Foo: Kooby({

  get("/foo") { ctx.getRequestPath() }

})

class App: Kooby({

  mount("/prefix", Foo())  // (1)

})
  1. Now all routes from Foo will be prefixed with /prefix. Output: /prefix/foo/prefix/foo

The mount operator only import routes. Services, callbacks, etc…​ are not imported. Main application is responsible for assembly all the resources and services required by imported applications.

Install

Alternatively, you can install a standalone application into another one using the javadoc:Jooby[install, io.jooby.Jooby] operator:

Installing
public class Foo extends Jooby {
  {
    get("/foo", ctx -> ...);
  }
}

public class Bar extends Jooby {
  {
    get("/bar", ctx -> ...);
  }
}

public class App extends Jooby {
  {
    install(Foo::new);                       // (1)

    install(Bar::new);                       // (2)
  }
}
Kotlin
class Foo: Kooby({

  get("/foo") { ... }

})

class Bar: Kooby({

  get("/bar") { ... }

})

class App: Kooby({
  install(::Foo)                         // (1)

  install(::Bar)                         // (2)
})
  1. Imports all routes, services, callbacks, etc…​ from Foo. Output: /foo/foo

  2. Imports all routes, services, callbacks, etc…​ from Bar. Output: /bar/bar

This operator lets you for example to deploy Foo as a standalone application or integrate it into a main one called App3508. The install operator shares the state of the main application, so lazy initialization (and therefore instantiation) of any child applications is mandatory.

For example, this won’t work:

Java
{
  Foo foo = new Foo();
  install(() -> foo);        // Won't work
}

The Foo application must be lazy initialized:

Java
{
  install(() -> new Foo());  // Works!
}

Dynamic Routing

Dynamic routing looks similar to composition but enables/disables routes at runtime using a predicate.

Suppose you own two versions of an API and for some time you need to support both the old and new APIs:

Dynamic Routing
public class V1 extends Jooby {
  {
    get("/api", ctx -> "v1");
  }
}

public class V2 extends Jooby {
  {
    get("/api", ctx -> "v2");
  }
}

public class App extends Jooby {
  {
    mount(ctx -> ctx.header("version").value().equals("v1"), new V1()); // (1)

    mount(ctx -> ctx.header("version").value().equals("v2"), new V2()); // (2)
  }
}
Kotlin
class V1: Kooby({

  get("/api") { "v1" }

})

class V2: Kooby({

  get("/api") { "v2" }

})

class App: Kooby({

  mount(ctx -> ctx.header("version").value().equals("v1"), V1()); // (1)

  mount(ctx -> ctx.header("version").value().equals("v2"), V2()); // (2)

})

Output:

  1. /apiv1; when version header is v1

  2. /apiv2; when version header is v2

Done {love}!

Multiple routers

This model let you run multiple applications on single server instance. Each application works like a standalone application, they don’t share any kind of services.

Multiple routers
public class Foo extends Jooby {
  {
    setContextPath("/foo");
    get("/hello", ctx -> ...);
  }
}

public class Bar extends Jooby {
  {
    setContextPath("/bar");
    get("/hello", ctx -> ...);
  }
}

import static io.jooby.Jooby.runApp;

public class MultiApp {
  public static void main(String[] args) {
    runApp(args, List.of(Foo::new, Bar::new));
  }
}
Kotlin
import io.jooby.kt.Kooby.runApp

fun main(args: Array<String>) {
  runApp(args, ::Foo, ::Bar)
}

You write your application as always and them you deploy them using the runApp method.

Important

Due to nature of logging framework (static loading and initialization) the logging bootstrap might not work as you expected. It is recommend to use just the logback.xml or log4j.xml file.