This manual is a work in progress and is currently incomplete.
If you'd like to help improve it, and we hope you do, please see the README.

1 Introduction

Ratpack is a set of Java libraries that facilitate fast, efficient, evolvable and well tested HTTP applications. It is built on the highly performant and efficient Netty event-driven networking engine.

Ratpack is purely a runtime. There is no installable package and no coupled build tooling (e.g. Rails, Play, Grails). To build Ratpack applications, you can use any JVM build tool. The Ratpack project provides specific support for Gradle through plugins, but any could be used.

Ratpack is published as a set of library JARs. The ratpack-core library is the only strictly required library. Others such as ratpack-groovy, ratpack-guice, ratpack-jackson, ratpack-test etc. are optional.

1.1 Goals

Ratpack’s goals are:

  1. To be fast, scalable, and efficient
  2. To allow applications to evolve in complexity without compromise
  3. To leverage the benefits of non-blocking programming and reduce the costs
  4. To be flexible and unopinionated when it comes to integrating other tools and libraries
  5. To allow applications to be easily and thoroughly tested

Ratpacks’s goals are not:

  1. To be a fully integrated, “full stack” solution
  2. Provide every feature you might need in a neat box
  3. To provide an architecture or framework for “business logic”

2.1 About this documentation

The documentation for Ratpack is spread over this manual and the Javadoc API reference. The manual introduces topics and concepts at a high level and links through to the Javadoc for detailed API information. The majority of the information is contained within the Javadoc. It is expected that once you have an understanding of the core Ratpack concepts, the manual becomes less useful and the Javadoc more useful.

1.2.1 Code samples

All of the code samples in the documentation are tested, and most are complete programs that you can copy/paste and run yourself (given the right classpath etc.).

Most of the samples are given as tiny embedded Ratpack applications, under test. The following is the “Hello World” of Ratpack code samples.

import ratpack.test.embed.EmbeddedApp;
import static org.junit.jupiter.api.Assertions.assertEquals;
 
public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandler(ctx -> 
      ctx.render("Hello World!")
    ).test(httpClient -> 
      assertEquals("Hello World!", httpClient.getText())
    );
  }
}

The import statements are collapsed by default for clarity. Click them to show/hide them.

This example is a complete Ratpack application. However, the EmbeddedApp is not the entry point that is typically used for proper applications (see the Launching chapter for details on the typical entry point). EmbeddedApp is testing oriented. It makes it easy to start/stop very small (or fully fledged) apps during a larger application, and provides a convenient way to make HTTP requests against the app. It is used in the examples to keep the amount of bootstrapping to a minimum in order to focus on the API being demonstrated.

In this example we are starting a Ratpack server on an ephemeral port with default configuration that responds to all HTTP requests with the plain text string “Hello World”. The test() method being used here provides a TestHttpClient to the given function, that is configured to make requests of the server under test. This example and all others like it are making HTTP requests to a Ratpack server. EmbeddedApp and TestHttpClient are provided as part of Ratpack’s testing support.

Another key testing utility that is used in many examples is ExecHarness.

import com.google.common.io.Files;
import ratpack.test.exec.ExecHarness;
import ratpack.exec.Blocking;

import java.io.File;
import java.nio.charset.StandardCharsets;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    File tmpFile = File.createTempFile("ratpack", "test");
    Files.asCharSink(tmpFile, StandardCharsets.UTF_8).write("Hello World!");
    tmpFile.deleteOnExit();

    String content = ExecHarness.yieldSingle(e ->
        Blocking.get(() -> Files.asCharSource(tmpFile, StandardCharsets.UTF_8).read())
    ).getValueOrThrow();

    assertEquals("Hello World!", content);
  }
}

Where EmbeddedApp supports creating an entire Ratpack application, ExecHarness provides just the infrastructure for Ratpack’s execution model. It is typically used to unit test asynchronous code that uses Ratpack constructs like Promise (see the “Asynchronous & Non Blocking” chapter for more info on the execution model). ExecHarness is also provided as part of Ratpack’s testing support.

1.1.2.1 Java 8 style

Ratpack is built on, and requires, Java 8. The code samples extensively use Java 8 constructs such as lambda expressions and method references. If you are experienced with Java but not the new constructs in Java 8, you may find the examples “exotic”.

2 Quick Start

This chapter provides instructions on how to get a Ratpack application up and running to play with.

1.2 Using a Groovy script

A Ratpack application can be implemented as a single Groovy script. This is a useful way to experiment with Ratpack and Groovy.

First, install Groovy.

Create the file ratpack.groovy with the following content:

@Grapes([
  @Grab('io.ratpack:ratpack-groovy:2.0.0-rc-1'),
  @Grab('org.slf4j:slf4j-simple:1.7.36')
])
import static ratpack.groovy.Groovy.ratpack

ratpack {
    handlers {
        get {
            render "Hello World!"
        }
        get(":name") {
            render "Hello $pathTokens.name!"
        }
    }
}

You can now start the app by running the following on the command line:

groovy ratpack.groovy

The server will be available via http://localhost:5050/.

The handlers() method takes a closure that delegates to a GroovyChain object. The “Groovy Handler Chain DSL” is used to build the response handling strategy.

Changes to the file are live during development. You can edit the file, and the changes will take effect on the next request.

2.2 Using the Gradle plugin(s)

We recommend the use of the Gradle build system to build Ratpack applications. Ratpack does not require Gradle; any build system can be used.

The following instructions assume you have already installed Gradle. See the Gradle User Guide for installation instructions.

The Ratpack project provides two Gradle plugins:

  1. io.ratpack.ratpack-java - for Ratpack applications implemented in Java
  2. io.ratpack.ratpack-groovy - for Ratpack applications implemented in Groovy

For a more detailed explanation of the Gradle build support, please see the dedicated chapter.

1.2.2 Using the Gradle Java plugin

Create a build.gradle file with the following contents:

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
  }
}

apply plugin: "io.ratpack.ratpack-java"
apply plugin: "idea"

repositories {
  mavenCentral()
}

dependencies {
  runtimeOnly "org.slf4j:slf4j-simple:1.7.36"
}

mainClassName = "my.app.Main"

Create the file src/main/java/my/app/Main.java, with the following content:

package my.app;

import ratpack.core.server.RatpackServer;

public class Main {
  public static void main(String... args) throws Exception {
    RatpackServer.start(server -> server 
      .handlers(chain -> chain
        .get(ctx -> ctx.render("Hello World!"))
        .get(":name", ctx -> ctx.render("Hello " + ctx.getPathTokens().get("name") + "!"))     
      )
    );
  }
}

You can now start the application either by executing the run task with Gradle (i.e. gradle run on the command line), or by importing the project into your IDE and executing the my.app.Main class.

When run, the server will be available via http://localhost:5050/.

The handlers() method takes a function that receives a Chain object. The “Handler Chain API” is used to build the response handling strategy.

The Ratpack Gradle plugin supports Gradle’s Continuous Build feature. Use it to have changes to your source code be automatically applied to your running application.

For further information on using Ratpack with Groovy, please see the Gradle chapter.

2.2.2 Using the Gradle Groovy plugin

Create a build.gradle file with the following contents:

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
  }
}

apply plugin: "io.ratpack.ratpack-groovy"
apply plugin: "idea"

repositories {
  mavenCentral()
}

dependencies {
  runtimeOnly "org.slf4j:slf4j-simple:1.7.36"
}

Create the file src/ratpack/ratpack.groovy, with the following content:

import static ratpack.groovy.Groovy.ratpack

ratpack {
    handlers {
        get {
            render "Hello World!"
        }
        get(":name") {
            render "Hello $pathTokens.name!"
        }
    }
}

You can now start the application either by executing the run task with Gradle (i.e. gradle run on the command line), or by importing the project into your IDE and executing the ratpack.groovy.GroovyRatpackMain class.

When run, the server will be available via http://localhost:5050/.

The handlers() method takes a closure that delegates to a GroovyChain object. The “Groovy Handler Chain DSL” is used to build the response handling strategy.

The Ratpack Gradle plugin supports Gradle’s Continuous Build feature. Use it to have changes to your source code be automatically applied to your running application.

For further information on using Ratpack with Groovy, please see the Groovy chapter.

For further information on using Ratpack with Groovy, please see the Gradle chapter.

3 Architecture

This chapter describes Ratpack applications at a high level.

1.3 Strongly typed

Ratpack is strongly typed. Beyond being implemented in Java, a strongly typed language, its API embraces types. For example, the notion of a Registry is used extensively in Ratpack. A Registry can be thought of as a map that uses types as keys.

This may be of most interest to Ratpack users implementing their applications in Groovy. Ratpack’s Groovy adapter uses the latest Groovy features to fully support static typing, while maintaining an idiomatic, concise, Groovy API.

2.3 Non blocking

Ratpack at its core is an event based (i.e. non-blocking) HTTP IO engine, and an API that makes it easy to structure response logic. Being non blocking imposes a different style of API than “traditional” blocking Java APIs in that the API must be asynchronous.

Ratpack aims to significantly simplify this style of programming for HTTP applications. It provides support for structuring asynchronous code (see the “Asynchronous & Non Blocking” chapter), and uses an innovative approach for structuring request processing into a self building, asynchronously traversed, graph of functions (it’s nowhere near as complicated to use as that may sound).

3.3 The parts

In the following section, “quotes” are used to denote key Ratpack terms and concepts.

A Ratpack application begins with a “launch configuration”, which as you would assume provides the configuration that is needed to start the application. A Ratpack “server” can be built and started solely from a “launch configuration”. The “server” once started, starts listening for requests. See the “Launching” chapter for more detail on this aspect.

One key piece of configuration provided to the “launch configuration” is the “handler factory”, which creates a “handler”. The “handler” is asked to respond to each request. A handler can do one of three things:

  1. Respond to the request
  2. Delegate to the “next” handler
  3. “Insert” handlers and immediately delegate to them

All request processing logic is simply the composition of handlers (see the Handlers chapter for more detail on this aspect). Importantly, the processing is not bound to a thread and can be completed asynchronously. The “handler” API supports this asynchronous composition.

Handlers operate on a “context”. A “context” represents the state of request processing at that particular point in the handler graph. One of its key functions is to act as a “registry”, that can be used to retrieve objects by type. This allows handlers to retrieve strategy objects (typically just objects implementing key interfaces) from the “context” by public types. As handlers insert other handlers into the handler graph, they can contribute to the context registry. This allows handlers to contribute code (as strategy objects) to downstream handlers. See the “Context” chapter for more detail, and the following section for how this context registry is used in practice.

This has been a high level, abstract, description of a Ratpack application. It is likely unclear exactly how all this translates to real code. The rest of this manual, and the accompanying API reference will provide the detail.

4.3 Plugins and extensibility through the Registry

Ratpack has no notion of plugins. However, add-on integration with Google Guice facilitates a kind of plugin system through Guice modules. Guice is a dependency injection container. Guice modules define objects to be part of the dependency injection container. Guice modules can act as plugins by providing implementations of key Ratpack interfaces, that are used by handlers. When using the Guice integration, all of the objects known to Guice (typically through Guice modules) are obtainable via the “context registry”. That is, handlers can retrieve them by type.

To see why this is useful, we will use the requirement of rendering an object as JSON to the response. The “context” object given to a “handler” has a render(Object) method. The implementation of this method simply searches the context registry for an implementation of Renderer that can render objects of the given type. Because objects available to Guice are available through the registry, they may be used for rendering. Therefore, adding a Guice module with a Renderer implementation for the desired type will allow it to be integrated into request processing. This is no different in concept to plain dependency injection.

While we have used the Guice integration in the above example, this approach is not tied to Guice (Guice is not part of Ratpack’s core API). Another dependency injection container (such as Spring) could easily be used, or no container at all. Any source of objects can be adapted to Ratpack’s Registry interface (there is also a builder).

5.3 Services & business logic

Ratpack has no opinion on how you structure your code that isn’t related to request handling (i.e. business logic). We’ll use the term “service” as a catch all for an object that performs some kind of business logic.

Handlers can of course freely use whatever services they need to. There are two main patterns for access services from handlers:

  1. Provide the service to the handler when it is constructed
  2. Retrieve the service from the context registry

4 Launching

This chapter describes how Ratpack applications are started, effectively detailing the entry points to the Ratpack API.

1.4 RatpackServer

The RatpackServer type is the Ratpack entry point. You write your own main class that uses this API to launch the application.

package my.app;

import ratpack.core.server.RatpackServer;
import ratpack.core.server.ServerConfig;
import java.net.URI;

public class Main {
  public static void main(String... args) throws Exception {
    RatpackServer.start(server -> server
      .serverConfig(ServerConfig.embedded().publicAddress(new URI("http://company.org")))
      .registryOf(registry -> registry.add("World!"))
      .handlers(chain -> chain
        .get(ctx -> ctx.render("Hello " + ctx.get(String.class)))
        .get(":name", ctx -> ctx.render("Hello " + ctx.getPathTokens().get("name") + "!"))     
      )
    );
  }
}

Applications are defined as the function given to the of() or start() static methods of this interface. The function takes a RatpackServerSpec which can be used to specify the three fundamental aspects of Ratpack apps (i.e. server config, base registry, root handler).

Most examples in this manual and the API reference use EmbeddedApp instead of RatpackServer to create applications. This is due to the “testing” nature of the examples. Please see this section for more information regarding the code samples.

1.1.4 Server Config

The ServerConfig defines the configuration settings that are needed in order to start the server. The static methods of ServerConfig can be used to create instances.

1.1.1.4 Base dir

An important aspect of the server config is the base dir. The base dir is effectively the root of the file system for the application, providing a portable file system. All relative paths to be resolved to files during runtime will be resolved relative to the base dir. Static assets (e.g. images, scripts) are typically served via the base dir using relative paths.

The baseDir(Path) method allows setting the base dir to some known location. In order to achieve portability across environments, if necessary, the code that calls this is responsible for determining what the base dir should be for a given runtime.

It is more common to use BaseDir.find() that supports finding the base dir on the classpath, providing better portability across environments. This method searches for a resource on the classpath at the path "/.ratpack".

To use a different path than the /.ratpack default, use the BaseDir.find(String) method.

The contents of the marker file are entirely ignored. It is just used to find the enclosing directory, which will be used as the base dir. The file may be within a JAR that is on the classpath, or within a directory that is on the classpath.

The following example demonstrates using BaseDir.find() to discover the base dir from the classpath.

import ratpack.core.server.ServerConfig;
import ratpack.test.embed.EphemeralBaseDir;
import ratpack.test.embed.EmbeddedApp;

import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EphemeralBaseDir.tmpDir().use(baseDir -> {
      baseDir.write("mydir/.ratpack", "");
      baseDir.write("mydir/assets/message.txt", "Hello Ratpack!");
      Path mydir = baseDir.getRoot().resolve("mydir");

      ClassLoader classLoader = new URLClassLoader(new URL[]{mydir.toUri().toURL()});
      Thread.currentThread().setContextClassLoader(classLoader);

      EmbeddedApp.of(serverSpec -> serverSpec
        .serverConfig(c -> c.baseDir(mydir))
        .handlers(chain ->
          chain.files(f -> f.dir("assets"))
        )
      ).test(httpClient -> {
        String message = httpClient.getText("message.txt");
        assertEquals("Hello Ratpack!", message);
      });
    });
  }
}

The use of EphemeralBaseDir and the construction of a new context class loader are in the example above are purely to make the example self contained. A real main method would simply call BaseDir.find(), relying on whatever launched the Ratpack application JVM to have launched with the appropriate classpath.

Ratpack accesses the base dir via the Java 7 Path API, allowing transparent use of JAR contents as the file system.

2.1.1.4 Port

The port(int) method allows setting the port used to connect to the server. If not configured, the default value is 5050.

3.1.1.4 SSL

By default, the Ratpack server will listen for HTTP traffic on the configuration port. To enable HTTPS traffic, the ssl(SslContext) method allows for the SSL certificate and key.

As of v2.0, Ratpack also supports selecting SSL configurations based on the requested host using Server Name Indicators (SNI). The ssl(SslContext, Action) is used to specify the default SSL configuration and any additional domain mappings with alternative SSL configuration. The domains specified in the mapping support DNS Wildcard and will match at most one level deep in the domain hierarchy (e.g. *.ratpack.io will match api.ratpack.io but not docs.api.ratpack.io).

Configuring SSL settings via system properties or environment variables requires special handling to specify the domain names. The following table shows how to specify the default SSl configuration and the configuration for subdomains.

| System Property | Environment Variable | Description | |————————————————|————————————————–|———————————————————————————| | ratpack.server.ssl.keystoreFile | RATPACK_SERVER__SSL__KEYSTORE_FILE | Specifies the path to the JKS containing the server certificate and private key | | ratpack.server.ssl.keystorePassword | RATPACK_SERVER__SSL__KEYSTORE_PASSWORD | Specifies the password for the keystore JKS | | ratpack.server.ssl.truststoreFile | RATPACK_SERVER__SSL__TRUSTSTORE_FILE | Specifies the path to the JKS containing the trusted certificates | | ratpack.server.ssl.truststorePassword | RATPACK_SERVER__SSL__TRUSTSTORE_PASSWORD | Specifies the password for the truststore JKS | | ratpack.server.ssl.ratpack_io.keystoreFile | RATPACK_SERVER__SSL__RATPACK_IO__KEYSTORE_FILE | Specifies the path to the keystore for the domain ratpack.io | | ratpack.server.ssl.*_ratpack_io.kyestoreFile | RATPACK_SERVER__SSL___RATPACK_IO_KEYSTORE_FILE | Specifies the path to the keystore for the domain *.ratpack.io |

Note the following special rules: 1. In both system properties and environment variables, domain name separators (.) are converted into underscores (_) 2. In environment variables, the domain wildcard character (*) is specified using an underscore (_). This results in 3 underscores preceding the domain name (___RATPACK_IO).

2.1.4 Registry

A registry is a store of objects stored by type. There may be many different registries within an application, but all applications are backed by a “server registry”. A server registry is just the name given to the registry that backs the application and is defined at launch time.

3.1.4 Handler

The server handler receives all incoming HTTP requests. Handlers are composable, and very few applications are actually comprised of only one handler. The server handler for most applications is a composite handler, typically created by using the handlers(Action) method, that uses the Chain DSL to create the composite handler.

4.1.4 Start and stop actions

The Service interface allows hooking in to the application lifecycle. Before accepting any requests, Ratpack will notify all services and allow them to perform any initialization. Conversely, when the application stops, Ratpack will notify all services and allow them to perform any cleanup or termination.

5 Handlers

This chapter introduces handlers, which are the fundamental components of a Ratpack application.

1.5 What is a handler?

Conceptually, a handler (Handler) is just a function that acts on a handling context (Context).

The “hello world” handler looks like this…

import ratpack.core.handling.Handler;
import ratpack.core.handling.Context;

public class Example implements Handler {
  public void handle(Context context) {
      context.getResponse().send("Hello world!");
  }
}

As we saw in the previous chapter, one of the mandatory launch config properties is the HandlerFactory implementation that provides the primary handler. The handler that this factory creates is effectively the application.

This may seem limiting, until we recognise that a handler does not have to be an endpoint (i.e. it can do other things than generate a HTTP response). Handlers can also delegate to other handlers in a number of ways, serving more of a routing function. The fact that there is no framework level (i.e. type) distinction between a routing step and an endpoint offers much flexibility. The implication is that any kind of custom request processing pipeline can be built by composing handlers. This compositional approach is the canonical example of Ratpack’s philosophy of being a toolkit instead of a magical framework.

The rest of this chapter discusses aspects of handlers that are beyond HTTP level concerns (e.g. reading headers, sending responses etc.), which is addressed in the HTTP chapter.

2.5 Handler delegation

If a handler is not going to generate a response, it must delegate to another handler. It can either insert one or more handlers, or simply defer to the next handler.

Consider a handler that routes to one of two different handlers based on the request path. This can be implemented as…

import ratpack.core.handling.Handler;
import ratpack.core.handling.Context;

public class FooHandler implements Handler {
  public void handle(Context context) {
    context.getResponse().send("foo");
  }
}

public class BarHandler implements Handler {
  public void handle(Context context) {
    context.getResponse().send("bar");
  }
}

public class Router implements Handler {
  private final Handler fooHandler = new FooHandler();
  private final Handler barHandler = new BarHandler();

  public void handle(Context context) {
    String path = context.getRequest().getPath();
    if (path.equals("foo")) {
      context.insert(fooHandler);
    } else if (path.equals("bar")) {
      context.insert(barHandler);
    } else {
      context.next();
    }
  }
}

The key to delegation is the context.insert() method that passes control to one or more linked handlers. The context.next() method passes control to the next linked handler.

Consider the following…

import ratpack.core.handling.Handler;
import ratpack.core.handling.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PrintThenNextHandler implements Handler {
  private final String message;
  private final static Logger LOGGER = LoggerFactory.getLogger(PrintThenNextHandler.class);


  public PrintThenNextHandler(String message) {
    this.message = message;
  }

  public void handle(Context context) {
    LOGGER.info(message);
    context.next();
  }
}

public class Application implements Handler {
  public void handle(Context context) {
    context.insert(
      new PrintThenNextHandler("a"),
      new PrintThenNextHandler("b"),
      new PrintThenNextHandler("c")
    );
  }
}

Given that Application is the primary handler (i.e. the one returned by the launch config’s HandlerFactory), when this application receives a request the following will be written to System.out

a
b
c

And then what? What happens when the “c” handler delegates to its next? The last handler is always an internal handler that issues a HTTP 404 client error (via context.clientError(404) which is discussed later).

Consider that inserted handlers can themselves insert more handlers…

import ratpack.core.handling.Handler;
import ratpack.core.handling.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PrintThenInsertOrNextHandler implements Handler {
  private final String message;
  private final Handler[] handlers;
  private final static Logger LOGGER = LoggerFactory.getLogger(PrintThenInsertOrNextHandler.class);

  public PrintThenInsertOrNextHandler(String message, Handler... handlers) {
    this.message = message;
    this.handlers = handlers;
  }

  public void handle(Context context) {
    LOGGER.info(message);
    if (handlers.length == 0) {
      context.next();
    } else {
      context.insert(handlers);
    }
  }
}

public class Application implements Handler {
  public void handle(Context context) {
    context.insert(
      new PrintThenInsertOrNextHandler("a",
        new PrintThenInsertOrNextHandler("a.1"),
        new PrintThenInsertOrNextHandler("a.2"),
      ),
      new PrintThenInsertOrNextHandler("b",
        new PrintThenInsertOrNextHandler("b.1",
          new PrintThenInsertOrNextHandler("b.1.1")
        ),
      ),
      new PrintThenInsertOrNextHandler("c")
    );
  }
}

This would write the following to System.out

a
a.1
a.2
b
b.1
b.1.1
c

This demonstrates how the next handler of the handler that inserts the handlers becomes the next handler of the last of the inserted handlers. You might need to read that sentence more than once.

You should be able to see a certain nesting capability emerge. This is important for composibility, and also for scoping which will be important when considering the registry context later in the chapter.

It would be natural at this point to think that it looks like a lot of work to build a handler structure for a typical web application (i.e. one that dispatches requests matching certain request paths to endpoints). Read on.

3.5 Building handler chains

A chain (Chain) is a builder for composing (or chaining) handlers. The chain itself doesn’t respond to a request, but instead passes a request around to it’s attached handlers.

Consider again the Foo-Bar router example…

import ratpack.core.handling.Chain
import ratpack.core.handling.Handler;
import ratpack.core.handling.Context;
import ratpack.func.Action;

public class FooHandler implements Handler {
    public void handle(Context context) {
        context.getResponse().send("foo");
    }
}

public class BarHandler implements Handler {
    public void handle(Context context) {
        context.getResponse().send("bar");
    }
}

public class RouterChain implements Action<Chain> {
    private final Handler fooHandler = new FooHandler();
    private final Handler barHandler = new BarHandler();

    @Override
    void execute(Chain chain) throws Exception {
        chain.path("foo", fooHandler)
        chain.path("bar", barHandler)
    }
}

This time, we didn’t have to manually check the path and handle each code branch. The result, however, is the same. This chain will eventually be treated as handler. This handler will be setup to read the path from a request and first compare it with “foo”, then “bar”. If either of the matches, it will context.insert() the given handler. Otherwise, it will call context.next().

Like the handler, the context aims not to be a piece of magic. Instead it is a powerful tool built from the more flexible tool, the handler.

1.3.5 Adding Handlers and Chains

So the chain can most simply be thought of as a list of handlers. The most basic way to add a handler to the chain’s list is the all(Handler) method. The word “all” represents that all requests reaching this point in the chain will flow through the given handler.

If we stretch our minds a little and think of the chain as a handler (one that is just specialized in inserting handlers), then it also stands to reason that we can add additional chains to a chain. In fact, we can, and to match the all(Handler) method, you may use the insert(Action<Chain>) method. Likewise, this inserts a chain through which all requests are routed.

Now, the chain wouldn’t be very useful if it just handled a list of handlers, calling each in a row, so there are also several methods than can perform conditional inserts of handlers and chains:

2.3.5 Registry

TODO (A technical definition can be found on the Chain javadocs)

3.3.5 Path Bindings

(i.e. /player/:id )

TODO (A technical definition can be found on the Chain javadocs)

4.3.5 Path and Method Bindings

TODO (A technical definition can be found on the Chain javadocs)

6 Context

The Context type is at the core of Ratpack.

It provides:

For working directly with the request/response, see the HTTP chapter.

For information about delegation, see the Handlers chapter.

1.6 Contextual objects

The context is a registry. It provides access to via-type-lookup of objects that were made available upstream in the handler pipeline. This is the mechanism for inter-handler collaboration in Ratpack.

Consider the following example:

import ratpack.test.embed.EmbeddedApp;
import ratpack.exec.registry.Registry;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {

  public static interface Person {
    String getId();

    String getStatus();

    String getAge();
  }

  public static class PersonImpl implements Person {
    private final String id;
    private final String status;
    private final String age;

    public PersonImpl(String id, String status, String age) {
      this.id = id;
      this.status = status;
      this.age = age;
    }

    @Override
    public String getId() {
      return id;
    }

    @Override
    public String getStatus() {
      return status;
    }

    @Override
    public String getAge() {
      return age;
    }
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandlers(chain -> chain
          .prefix("person/:id", (personChain) -> personChain
            .all(ctx -> {
              String id = ctx.getPathTokens().get("id"); // (1)
              Person person = new PersonImpl(id, "example-status", "example-age");
              ctx.next(Registry.single(Person.class, person)); // (2)
            })
            .get("status", ctx -> {
              Person person = ctx.get(Person.class); // (3)
              ctx.render("person " + person.getId() + " status: " + person.getStatus());
            })
            .get("age", ctx -> {
              Person person = ctx.get(Person.class); // (4)
              ctx.render("person " + person.getId() + " age: " + person.getAge());
            }))
      )
      .test(httpClient -> {
        assertEquals("person 10 status: example-status", httpClient.get("person/10/status").getBody().getText());
        assertEquals("person 6 age: example-age", httpClient.get("person/6/age").getBody().getText());
      });
  }
}

At (2) we are pushing the Person instance into the registry for the downstream handlers to use and at (3) and (4) how they retrieve it. We are decoupling the creation detail from the usage, and avoiding duplicating the creation code in the status and age handlers. The benefit of avoiding duplication is obvious. What’s slightly more subtle is that the decoupling makes testing easier when the downstream handlers are not implemented as anonymous classes (see the Testing chapter for for information).

At (1) we are also using contextual objects. The prefix() chain method binds on a request path, potentially capturing tokens. If the binding is successful, a PathBinding object is registered with the context that describes the binding result. This includes any path tokens that were captured as part of the binding. In the case above, we are capturing the second path component as the id. The getPathTokens() method on a context is literally shorthand for get(PathBinding.class).getPathTokens() on the same context. This is another example of using the context object mechanism for inter-handler communication.

Another example of using contextual objects is the shorthand for accessing files from the file system. Consider the following script, which makes use of the context’s file method to retrieve a static asset from the file system:


import static ratpack.groovy.Groovy.ratpack ratpack { handlers { get { def f = file('../') render f ?: "null-value" } } }

In the above example, the context’s file() method is being called to retrieve a java.io.File instance for the provided path. The context’s file() method is a shorthand to retrieve the FileSystemBinding object from the registry, and literally is a shorthand to get(FileSystemBinding.class).file(path/to/file). The context will always resolve file assets relative to the application root, so in the case where an absolute path is provided, it should be noted that the path to the asset will be prefixed by the path in which the application exists. For example, if your application exists in /home/ratpack/app and your handler uses the file method to resolve /etc/passwd, then the actual path that is resolved will be /home/ratpack/app/etc/passwd. In the case where a file cannot be resolved from within the application’s root, the file() method may return a null value, which is demonstrated in the above example. The developer is responsible for handling scenarios where accessing a file may return a null object.

1.1.6 Partitioning

The context object mechanism supports partitioning application logic by providing different objects to different partitions. This is because objects registered with context are implicitly scoped, depending on how they were registered. Objects registered with the next() methods are available to all downstream handlers that were part of the same insertion (i.e. context.insert() including and nested insertions. Objects registered with the insert() methods are available to the inserted handlers and nested insertions.

A typical use for this is using different error handling strategies for different parts of your application.

import ratpack.core.error.ServerErrorHandler;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {

  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandlers(chain -> chain
        .prefix("api", api -> api
            .register(r -> r.add(ServerErrorHandler.class, (context, throwable) ->
                  context.render("api error: " + throwable.getMessage())
              )
            )
            .all(ctx -> {
              throw new Exception("in api - " + ctx.getRequest().getPath());
            })
        )
        .register(r -> r.add(ServerErrorHandler.class, (ctx, throwable) ->
              ctx.render("app error: " + throwable.getMessage())
          )
        )
        .all(ctx -> {
          throw new Exception("in app - " + ctx.getRequest().getPath());
        })
    ).test(httpClient -> {
      assertEquals("api error: in api - api/foo", httpClient.get("api/foo").getBody().getText());
      assertEquals("app error: in app - bar", httpClient.get("bar").getBody().getText());
    });
  }

}

7 Basic HTTP

This chapter introduces how to deal with basic HTTP concerns such as parsing requests, rendering responses, content negotiation, file uploads etc.

1.7 Request & Response

The context object that a handler operates on provides the getRequest() & getResponse() methods for accessing the Request and Response respectively. These objects provide more or less what you would expect.

For example, they both provide a getHeaders() method that returns a model of the HTTP headers sent with the request and a model of the HTTP headers that are to be sent with the response. The Request exposes other metadata attributes such as the HTTP method, the URI and a key/value model of the query string parameters among other things.

2.7 Redirecting

The redirect(int, Object) context method supports issuing redirects. This method obtains the Redirector from the context registry and forwards the arguments.

Ratpack provides a default implementation that supports:

  1. Literal URL values
  2. Protocol relative URL values
  3. Absolute paths within the current application
  4. Relative paths within the current application

Most applications do not need to provide a custom Redirector implementation, as the default behaviour is sufficient. One reason to provide a custom redirector implementation would be to interpret domain objects as locations to redirect to.

3.7 Reading the request

Several mechanisms are available for obtaining the body of a request. For simple use cases, Context.parse(Class<T>) will buffer the entire class into memory and yield an object of the specified type. When you just need a text or byte view of the entire request, you may use the lower-level Request.getBody() method. For advanced uses or for handling extra large requests, [Request.getBodyStream()] provides access to the individual byte chunks as they are recieved.

1.3.7 Parsers

The parser mechanism to turn the request body into an object representation. It works by selecting a Parser implementation from context registry. See Context.parse(Class<T>) for details and additional variants.

1.1.3.7 JSON

Support for dealing with JSON request bodies is provided out of the box, based on Jackson. See Jackson parsing for examples.

2.1.3.7 Forms

Ratpack provides a parser for Form objects in the core. This can be used for reading POST’d (or PUT’d etc. for that matter) forms, both URL encoded and multi part (including file uploads).

import ratpack.core.handling.Handler;
import ratpack.core.handling.Context;
import ratpack.core.form.Form;
import ratpack.core.form.UploadedFile;

public class MyHandler implements Handler {
  public void handle(Context context) {
    Promise<Form> form = context.parse(Form.class);

    form.then(f -> {
      // Get the first attribute sent with name “foo”
      String foo = form.get("foo");

      // Get all attributes sent with name “bar”
      List<String> bar = form.getAll("bar");

      // Get the file uploaded with name “myFile”
      UploadedFile myFile = form.file("myFile");

      // Send back a response …
    });
  }
}

See Form and UploadedFile for more information and examples.

2.3.7 Bytes and Text

Request.getBody() reads the entire request into memory, providing access to the data as either bytes or a string.

This method will default to rejecting requests which are larger than the server’s configured max content length. Additional flavors are available for configuring the rejection action and the maximum size.

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> {
         ctx.getRequest().getBody().then(data -> ctx.render("hello: "+data.getText()));
      })
      .test(httpClient -> {
        ReceivedResponse response = httpClient.request(req->{
          req.method("POST");
          req.getBody().text("world");
        });
        assertEquals("hello: world", response.getBody().getText());
      });
  }
}

3.3.7 Byte Chunk Stream

Request.getBodyStream() returns a stream of the individual chunks as they are received.

This method defaults to rejecting requests which are larger than the server’s configured max content length. Additional flavors are available for configuring the maximum size.

See the java docs for an example of how to stream the request body to a file.

4.7 Sending a response

Sending a HTTP response in Ratpack is easy, efficient, and flexible. Like most things in Ratpack, transmitting a response to a client is done in a non-blocking manner. Ratpack provides a few mechanisms for sending a response. The methods exposed to manipulating the response can be found in the Response and Context objects.

1.4.7 Setting the response status

Setting the status of a response is as easy as calling Response#status(int) or Response#status(ratpack.core.http.Status).

import ratpack.core.http.Response;
import ratpack.core.http.Status;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandlers(chain -> chain
      .all(ctx -> ctx.getResponse().status(202).send("foo"))
    )
    .test(httpClient ->
      assertEquals(202, httpClient.get().getStatusCode())
    );
  }
}

2.4.7 Sending the response

There are a few ways to send a response body to the client.

The shortest way to send a response is to simply call Response#send(). This will send a response with no response body.

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> ctx.getResponse().send())
      .test(httpClient -> {
        ReceivedResponse response = httpClient.get();
        assertEquals("", response.getBody().getText());
      });
  }
}

If you want to send a plain text response you can use Response#send(String).

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> ctx.getResponse().send("Ratpack is rad"))
      .test(httpClient -> {
        ReceivedResponse response = httpClient.get();
        assertTrue(response.getHeaders().get("Content-type").startsWith("text/plain;"));
        assertEquals("Ratpack is rad", response.getBody().getText());
      });
  }
}

There are additional send() methods that allow you send different the response body payloads, i.e. String, byte[], ByteBuf, as well as set the Content-type header. See Response for more on sending a response.

3.4.7 An alternative approach with Renderers

Sending empty or simple text responses may be fine but you may find yourself wanting to send a more complex response to the client. The Renderer is a mechanism that is able to render a given type to the client. More specifically, it’s the underlying mechanism that powers the render(Object) method, which can be found on the context object.

In the following example, we utilize the context’s render(Object) method to render an object of type String.

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> ctx.render("Sent using render(Object)!"))
      .test(httpClient -> {
        ReceivedResponse response = httpClient.get();
        assertEquals("Sent using render(Object)!", response.getBody().getText());
      });
  }
}

Because the String is of type CharSequence, Ratpack finds and uses the CharSequenceRenderer to render the String. Where did this CharSequenceRenderer come from? Ratpack provides a number of Renderers out of the box, including but not limited to: CharSequenceRenderer, RenderableRenderer, PromiseRenderer, DefaultFileRenderer.

If you attempt to render a type that is not registered, it will result in a server error.

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {

  static class Foo {
    public String value;
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> {
        Foo foo = new Foo();
        foo.value = "bar";
        ctx.render(foo);
      })
      .test(httpClient -> {
        ReceivedResponse response = httpClient.get();
        assertEquals(500, response.getStatusCode());
      });
  }
}

If you’d like to implement your own Renderer, Ratpack provides a RendererSupport that makes it easy to implement your own. You must also remember to register your Renderer so that Ratpack can use it.

import ratpack.core.handling.Context;
import ratpack.exec.registry.Registry;
import ratpack.core.http.client.ReceivedResponse;
import ratpack.core.render.RendererSupport;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {

  static class Foo {
    public String value;
  }

  static class FooRenderer extends RendererSupport<Foo> {
    @Override
    public void render(Context ctx, Foo foo) throws Exception {
      ctx.getResponse().send("Custom type: Foo, value=" + foo.value);
    }
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandlers(chain -> chain
        .register(Registry.single(new FooRenderer()))
        .all(ctx -> {
          Foo foo = new Foo();
          foo.value = "bar";
          ctx.render(foo);
        })
      )
      .test(httpClient -> {
        ReceivedResponse response = httpClient.get();
        assertEquals(200, response.getStatusCode());
        assertEquals("Custom type: Foo, value=bar", response.getBody().getText());
      });
  }
}

4.4.7 Sending JSON

Support for rendering arbitrary objects as JSON is based on Jackson. See Jackson rendering for examples.

5.4.7 Sending files

Sending static resources such as files can be done with sendFile(Path)

TODO introduce sendFile methods (pointing to use of render(file(«path»))) instead.

TODO introduce assets method

6.4.7 Before send

The Response object contains a method, beforeSend(Action<? super Response> responseFinalizer), that is invoked immediately before a Response is sent to the client.

You may not call any of the .send() methods on the Response within the responseFinalizer callback.

This method is particularly useful for modifying

at the last second.

A practical use-case for this would be modifying the status code or headers when using StreamedResponse.forwardTo().

For example:

import io.netty.handler.codec.http.HttpHeaderNames;
import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;
import ratpack.core.http.Headers;
import ratpack.core.http.Request;
import ratpack.core.http.Status;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> {
        ctx.getResponse()
          .contentType("application/json")
          .status(Status.OK)
          .beforeSend(response -> {
             response.getHeaders().remove(HttpHeaderNames.CONTENT_LENGTH);
             response.cookie("DNT", "1");
             response.status(Status.of(451, "Unavailable for Legal Reasons"));
             response.contentType("text/plain");
          }).send();
      })
      .test(httpClient -> {
        ReceivedResponse receivedResponse = httpClient.get();

        Headers headers = receivedResponse.getHeaders();
        assertEquals(451, receivedResponse.getStatusCode());
        assertEquals("text/plain", headers.get(HttpHeaderNames.CONTENT_TYPE));
        assertTrue(headers.get(HttpHeaderNames.SET_COOKIE).contains("DNT"));
      });
  }
}

5.7 Headers

HTTP Header information is available from an incoming request as it is for an outgoing response.

1.5.7 Request headers

The Headers interface allows you to retrieve header information associated with the incoming request.

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;
import ratpack.core.http.Headers;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> {
        Headers headers = ctx.getRequest().getHeaders();
        String clientHeader = headers.get("Client-Header");
        ctx.getResponse().send(clientHeader);
      })
      .test(httpClient -> {
        ReceivedResponse receivedResponse = httpClient
          .requestSpec(requestSpec ->
              requestSpec.getHeaders().set("Client-Header", "From Client")
          ).get();

        assertEquals("From Client", receivedResponse.getBody().getText());
      });
  }
}

2.5.7 Response headers

The MutableHeaders provides functionality that enables you to manipulate response headers via the response object Response#getHeaders().

import ratpack.core.http.MutableHeaders;
import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> {
        MutableHeaders headers = ctx.getResponse().getHeaders();
        headers.add("Custom-Header", "custom-header-value");
        ctx.getResponse().send("ok");
      })
      .test(httpClient -> {
        ReceivedResponse receivedResponse = httpClient.get();
        assertEquals("custom-header-value", receivedResponse.getHeaders().get("Custom-Header"));
      });
  }
}

Additionally you can set(CharSequence, Object), remove(CharSequence), clear() and more.

See MutableHeaders for more methods.

6.7 Cookies

As with HTTP headers, cookies are available for inspection from an inbound request as they are for manipulation for an outbound response.

1.6.7 Cookies from an inbound request

To retrieve the value of a cookie, you can use Request#oneCookie(String).

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandler(ctx -> {
      String username = ctx.getRequest().oneCookie("username");
      ctx.getResponse().send("Welcome to Ratpack, " + username + "!");
    }).test(httpClient -> {
      ReceivedResponse response = httpClient
        .requestSpec(requestSpec -> requestSpec
          .getHeaders()
          .set("Cookie", "username=hbogart1"))
        .get();

      assertEquals("Welcome to Ratpack, hbogart1!", response.getBody().getText());
    });
  }
}

You can also retrieve a set of cookies via Request#getCookies().

import io.netty.handler.codec.http.cookie.Cookie;
import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import java.util.Set;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandler(ctx -> {
      Set<Cookie> cookies = ctx.getRequest().getCookies();
      assertEquals(1, cookies.size());
      Cookie cookie = cookies.iterator().next();
      assertEquals("username", cookie.name());
      assertEquals("hbogart1", cookie.value());
      ctx.getResponse().send("Welcome to Ratpack, " + cookie.value() + "!");
    }).test(httpClient -> {
      ReceivedResponse response = httpClient
        .requestSpec(requestSpec -> requestSpec
          .getHeaders()
          .set("Cookie", "username=hbogart1"))
        .get();

      assertEquals("Welcome to Ratpack, hbogart1!", response.getBody().getText());
    });
  }
}

2.6.7 Setting cookies for an outbound response

You can set cookies to be sent with the response Response#cookie(String, String). To retrieve the set of cookies to be set with the response you may use Response#getCookies().

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandler(ctx -> {
      assertTrue(ctx.getResponse().getCookies().isEmpty());
      ctx.getResponse().cookie("whiskey", "make-it-rye");
      assertEquals(1, ctx.getResponse().getCookies().size());
      ctx.getResponse().send("ok");
    }).test(httpClient -> {
      ReceivedResponse response = httpClient.get();
      assertEquals("whiskey=make-it-rye", response.getHeaders().get("Set-Cookie"));
    });
  }
}

If you want to expire a cookie, you can do so with Response#expireCookie().

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertTrue;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandler(ctx -> {
      ctx.getResponse().expireCookie("username");
      ctx.getResponse().send("ok");
    }).test(httpClient -> {
      ReceivedResponse response = httpClient
        .requestSpec(requestSpec -> requestSpec
            .getHeaders().set("Cookie", "username=lbacall1")
        )
        .get();

      String setCookie = response.getHeaders().get("Set-Cookie");
      assertTrue(setCookie.startsWith("username=; Max-Age=0"));
    });
  }
}

7.7 Content Negotiation

Support for rendering different representations of a resource (JSON/XML/HTML, GIF/PNG, etc.) is provided via byContent(Action).

8.7 Sessions

Whenever you need to maintain consistency between multiple calls by the same client (in other words: stateful operations) you may want to use sessions. Ratpack offers a module to handle session handling for you. You can add the session module to your project and start using ratpack managed sessions.

1.8.7 Preparation

First off you need to add the required dependency to your project. Using gradle you can add the dependency by adding compile 'io.ratpack:ratpack-session:2.0.0-rc-1' to dependencies. When you just started out your gradle file will look like this:

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
  }
}

apply plugin: "io.ratpack.ratpack-groovy"

repositories {
  mavenCentral()
}

dependencies {
  runtimeOnly 'org.slf4j:slf4j-simple:1.7.36'
  implementation group: 'io.ratpack', name: 'ratpack-session', version: '2.0.0-rc-1'

  testImplementation "org.spockframework:spock-core:2.1-groovy-3.0"
}

Don’t forget to load the module in ratpack.

import static ratpack.groovy.Groovy.ratpack
import ratpack.session.SessionModule

ratpack {
	bindings {
		module(SessionModule)
	}
	/* ... */
}

2.8.7 Use session

You are now set for sessions. The following is a simple example of an application that uses sessions.

import ratpack.session.Session

import static ratpack.groovy.Groovy.ratpack
import ratpack.session.SessionModule

ratpack {
	bindings {
		module(SessionModule)
	}

	handlers {
		get('start') { Session session ->
			session.terminate().flatMap {
				session.set('keyForDataStoredInSession', 'Hello Session!').promise()
			}.then {
				response.send('I started a session for you.')
			}
		}
		get { Session session ->
			session.get('keyForDataStoredInSession').then {
				if (it.present) {
					response.send("Message: ${it.get()}")
				} else {
					response.send('I have nothing to say to you')
				}
			}
		}
		get('stop') { Session session ->
			session.terminate().then {
				response.send('The session is dead, dead, dead.')
			}
		}
	}
}

All session operations return either a Promise or an Operation. You can use those in your transformation flow as shown.

Make sure you terminate an old session before starting a new one (see get('start')-handler). This way you make sure that you actually get a new session and don’t just add data to an existing session.

3.8.7 Limitations

The ratpack session module uses in-memory sessions by default. It can hold up to 1000 sessions at a time and will drop the oldest session if a new session is opened. If you expect more than 1000 sessions you should consider to use a different session store than the default module. You could for example use the ratpack-session-redis module if you have a redis server handy. If your payload is small another option is to store some session data in the cookie itself by using the client side session module. Your payload should be small because all Cookies for a website (domain) combined cannot exceed 4K.

4.8.7 The ratpack-session-redis module

To use the redis session module add the dependency (compile 'io.ratpack:ratpack-session-redis:2.0.0-rc-1') to your project.

Then configure redis after loading the session module.

bindings {
  module(SessionModule)
  RedisSessionModule redisSessionModule = new RedisSessionModule()
  redisSessionModule.configure {
    it.host = 'localhost'
    it.port =  6379
    it.password = 'secret'
  }
  module(redisSessionModule)
}

You may want to inject the configuration for redis using Guice or some other mechanism. Apart from that you have successfully configured ratpack to use redis to store sessions. Make your your redis server is running and available and give you ratpack application a spin.

5.8.7 Comments & useful links

You now have a very rough idea of how to implement sessions in ratpack.

Ratpack handles session consistency with cookies, namely a JSESSIONID cookie that contains a UUID. Whenever you first add data to the session the id is generated and the cookie will be sent to the client using the Add-Cookie header in the response. Therefor you need to make sure that you a response is sent to the client or the session will be lost. Consecutive client requests contain the cookie in the Cookie-Header, so ratpack can determine the client’s session.

Here are some useful links to dive deeper into ratpack session handling: - more samples of how sessions work in the ratpack-session module tests - ratpack javadoc contains lots of examples and information - cookie behaviour can be customized (i.e. change name of cookie, use custom ids/expiration date) by providing a custom SessionCookieConfig to the session module

6.8.7 A final note when you use the default session store (in-memory):

The default session store probably isn’t useful in any production environment but it is useful for local testing with sessions. A call of session.terminate() sets the session cookie to an empty value. Consecutive calls therefor contain a cookie like this JSESSIONID=. At least the in-memory session store accepts the empty value as a valid session. Therefor if you don’t terminate your session before you intend to create a new one by adding values to it, you will add session data to the existing session with an empty uuid.

8 Asynchronous & Non Blocking

Ratpack is designed for “asynchronous” & “non blocking” request processing. Its internal IO (e.g. HTTP request and response transfer) is all performed in a non blocking fashion (thanks to Netty). This approach yields higher throughput, lower resource usage, and importantly, more predictable behaviour under load. This programming model has become increasingly popular of late due to the Node.js platform. Ratpack is built on the same non blocking, event driven, model as Node.js.

Asynchronous programming is notoriously tricky. One of Ratpack’s key value propositions is that it provides constructs and abstractions to tame the asynchronous beast, yielding better performance while keeping implementation simple.

1.8 Comparison to blocking frameworks & containers

The Java Servlet API, that underpins most JVM web frameworks and containers, along with the majority of the JDK is fundamentally based on a synchronous programming model. Most JVM programmers are very familiar and comfortable with this programming model. In this model, when IO needs to be performed the calling thread will simply sleep until the operation is complete and the result is available. This model requires a reasonably large pool of threads. In a web application context, this usually means that each request is bound to a thread from the large pool and that the application can process «X» number of parallel requests, where «X» is the size of the thread pool.

Version 3.0 of the Servlet API does facilitate asynchronous request processing. However, retrofitting asynchronous support as an opt-in option is a different proposition to a completely asynchronous approach. Ratpack is asynchronous from the ground up.

The benefit of this model is that synchronous programming is unarguably “simpler”. The drawback of this model, opposed to a non blocking model, is that it demands greater resource usage and yields lower throughput. In order to serve more requests in parallel, the size of the thread pool has to be increased. This creates more contention for compute resources and more cycles are lost to managing the scheduling of these threads, not to mention the increased memory consumption. Modern operating systems, and the JVM, are very good at managing this contention; however, it is still a scaling bottleneck. Moreover, it demands greater resource allocation, which is a serious consideration for modern pay-for-what-you-use deployment environments.

The asynchronous, non blocking, model does not require a large thread pool. This is possible because threads are never blocked waiting for IO. If IO needs to be performed, the calling thread registers a callback of some sort that will be invoked when the IO is done. This allows the thread to be used for other processing while the IO is occurring. Under this model, the thread pool is sized according to the number of processing cores available. Since the threads are always busy with computation, there is no point in having more threads.

Many Java APIs (InputStream, JDBC, etc.) are predicated on a blocking IO model. Ratpack provides a mechanism for using such API while minimizing the blocking cost (discussed below).

Ratpack is fundamentally asynchronous in two key ways…

  1. HTTP IO is event driven / non blocking (thanks to Netty)
  2. Request handling is organised as a pipeline of asynchronous functions

The HTTP IO being event driven is largely transparent when using Ratpack. Netty just does its thing.

The second point is the key characteristic of Ratpack. It does not expect your code to be synchronous. Many web frameworks that have opt in asynchronous support have serious constraints and gotchas that become apparent when trying to perform complex (i.e. real world) async operations. Ratpack is asynchronous from the ground up. Moreover, it provides constructs and abstractions that facilitate complex asynchronous processing.

2.8 Performing blocking operations (e.g. IO)

Most applications are going to have to perform some kind of blocking IO. Many Java APIs do not offer asynchronous options (e.g. JDBC). Ratpack provides a simple mechanism for executing blocking operations in a separate thread pool. This avoids blocking request processing (i.e. compute) threads (which is a good thing), but does incur some overhead due to thread contention. If you have to use blocking IO APIs there is unfortunately no other option.

Let’s consider a contrived data store API. It is conceivable that communication with the actual data store requires IO (or if it is in memory, then its access requires waiting on one or more locks which has the same blocking effect). The API methods cannot be called on a request processing thread because they will block. Instead, we need to use the “blocking” API…

import ratpack.core.handling.InjectionHandler;
import ratpack.core.handling.Context;
import ratpack.exec.Blocking;

import ratpack.test.handling.RequestFixture;
import ratpack.test.handling.HandlingResult;

import java.util.Collections;
import java.io.IOException;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {

  // Some API that performs blocking operations
  public static interface Datastore {
    int deleteOlderThan(int days) throws IOException;
  }

  // A handler that uses the API
  public static class DeletingHandler extends InjectionHandler {
    void handle(final Context context, final Datastore datastore) {
      final int days = context.getPathTokens().asInt("days");
      Blocking.get(() -> datastore.deleteOlderThan(days))
        .then(i -> context.render(i + " records deleted"));
    }
  }

  // Unit test
  public static void main(String... args) throws Exception {
    HandlingResult result = RequestFixture.handle(new DeletingHandler(), fixture -> fixture
        .pathBinding(Collections.singletonMap("days", "10"))
        .registry(r -> r.add(Datastore.class, days -> days))
    );

    assertEquals("10 records deleted", result.rendered(String.class));
  }
}

The function submitted as the blocking operation is executed asynchronously (i.e. the Blocking.get() method returns a promise instantly), in a separate thread pool. The result that it returns will processed back on a request processing (i.e. compute) thread.

See the Blocking#get() method for more details.

3.8 Performing async operations

The Promise#async(Upstream>) for integrating with async APIs. It is essentially a mechanism for adapting 3rd party APIs to Ratpack’s promise type.

import ratpack.test.embed.EmbeddedApp;
import ratpack.exec.Promise;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandler(ctx ->
        Promise.async((f) ->
            new Thread(() -> f.success("hello world")).start()
        ).then(ctx::render)
    ).test(httpClient -> {
      assertEquals("hello world", httpClient.getText());
    });
  }
}

4.8 Async composition and avoiding callback hell

One of the challenges of asynchronous programming lies in composition. Non trivial asynchronous programming can quickly descend into a phenomenon known as “callback hell”, which is the term used to describe the incomprehensibility of many layers of nested callbacks.

Elegantly and cleanly composing async operations together into complex workflows is an area of rapid innovation at this time. Ratpack does not attempt to provide a framework for asynchronous composition. Instead, it aims to integrate and provide adapters to specialised tools for this task. An example of this approach is Ratpack’s integration with RxJava.

In general, integration is a matter of adapting Ratpack’s Promise type with the composition primitive of the target framework.

9 Streams

Ratpack supports streaming data in a variety of ways. This chapter outlines the fundamentals of working with data streams in Ratpack and different ways to stream data.

1.9 The Reactive Streams API

Generally, streaming in Ratpack is based around the emerging Reactive Streams API standard.

From the Reactive Streams site:

Reactive Streams is an initiative to provide a standard for asynchronous stream processing with non-blocking back pressure on the JVM.

Ratpack uses the Reactive Streams API, opposed to a proprietary API, to allow users to choose their reactive toolkit of choice. Reactive toolkits such as RxJava and Reactor will support bridging to the Reactive Streams API in the near future. However, it is not required to use a specialist reactive library if your needs are modest. Ratpack provides some useful utilities for dealing with streams via its Streams class.

1.1.9 Back pressure

A key tenet of the Reactive Streams API is support for flow control via back pressure. This allows stream subscribers, which in the case of a HTTP server app is usually the HTTP client, to communicate to the publisher how much data they can handle. In extreme cases, without back pressure a slowly consuming client can exhaust resources on the server as the data producer produces data faster than it is being consumed, potentially filling up in memory buffers. Back pressure allows the data producer to match its rate of production with what the client can handle.

For more info on the importance of back pressure, please see the documentation from the Reactive Streams project.

Streaming a response always occurs via the Response.sendStream() method. See the documentation for this method for more precise semantics of what back pressure means when streaming data.

2.9 Chunked transfer encoding

Ratpack supports chunked transfer encoding for arbitrary data streams by way of the ResponseChunks renderable type.

3.9 Server-sent events

Ratpack supports server-sent events for streaming data to, primarily Javascript based, clients by way of the ServerSentEvents renderable type.

4.9 Websockets

Ratpack supports streaming data over websockets by way of the WebSockets.websocketBroadcast() method.

Ratpack also supports bidirectional websocket communication via the other websocket-opening methods of the WebSockets class.

10 Testing Ratpack applications

Testing is a first class citizen in Ratpack. The ratpack-test library contains the core support, and ratpack-groovy-test provides some Groovy sugar to these types.

The ratpack and ratpack-groovy Gradle plugins add these libraries implicitly to the test compile classpath.

The Ratpack test support is agnostic of the test framework in use. Any framework can potential be used.

Many Ratpack users use the Spock testing framework. While Spock requires writing tests in Groovy, it can effortlessly be used to effectively test Java code.

1.10 Unit testing

1.1.10 RequestFixture

The RequestFixture class facilitates creating a mocked request environment, ostensibly for testing Handler implementations. However, it is also common to use an ad-hoc handler with a request fixture that integrates with other components (e.g. Parser implementations).

Note: the GroovyRequestFixture class provides Groovy sugar for working with request fixtures.

2.1.10 ExecHarness

The ExecHarness fixture facilitates testing code that leverages Ratpack’s execution mechanisms outside of an application. If you need to unit test code that uses Promise, an exec harness is what you need.

2.10 Integration testing

Ratpack integration tests are tests that test a subset of application components, via the HTTP interface.

The EmbeddedApp fixture facilitates constructing an ad,hoc application that responds to real HTTP requests. In the context of integration testing, it is typically used to glue together a specific combination of application components to test.

As it constructs a real Ratpack application, it can also be used for testing implementations of Ratpack extension points such as Renderer, Parser and ConfigurableModule.

The EmbeddedApp fixture manages starting and stopping the application, as well as providing a TestHttpClient that makes requests of the embedded application.

Importantly, embedded apps must be closed when they are no longer needed in order to free resources. The EmbeddedApp type implements the java.io.AutoCloseable interface, who’s close() method can be used to stop the server. This can often be combined with the “after test” lifecycle event of the testing framework being used, such as JUnit’s @After methods.

Note: the EmbeddedApp fixture can also be used “standalone” for creating mocked HTTP services when testing other types of, non Ratpack, applications.

3.10 Functional testing

Ratpack functional tests are tests that test an entire application, via the HTTP interface.

For Ratpack apps that are defined as a Java main class, the MainClassApplicationUnderTest fixture can be used. For Ratpack app that are defined as a Groovy script, the GroovyRatpackMainApplicationUnderTest fixture can be used.

If you have a custom entry point, the ServerBackedApplicationUnderTest abstract super class can be extended for your needs.

These fixtures manage starting and stopping the application, as well as providing a TestHttpClient that makes requests of the embedded application.

Importantly, applications under test must be closed when they are no longer needed in order to free resources. The CloseableApplicationUnderTest type implements the java.io.AutoCloseable interface, who’s close() method can be used to stop the server. This can often be combined with the “after test” lifecycle event of the testing framework being used, such as JUnit’s @After methods.

1.3.10 Impositions

Ratpack provides a mechanism for augmenting applications under test for testability, known as impositions.

Typically, impositions are specified by sub-classing MainClassApplicationUnderTest or similar, and overriding the addImpositions(ImpositionsSpec) method.

2.3.10 Browser testing

Browser testing works similarly to what has been previously named as functional testing here, except that usage of Ratpack’s TestHttpClient is replaced with browser automation. This typically involves using MainClassApplicationUnderTest to start and stop the app, and to provide the application under test’s address via the getAddress() method.

Ratpack users commonly use Geb for browser tests as its expressive style and synergy with Spock suit well. An example of a Ratpack/Geb based test for the ratpack.io site is available for reference.

11 Http client

Ratpack provides its own HttpClient which can be used to make remote HTTP calls. The Ratpack provided HttpClient is fully non-blocking and is a part of the core Ratpack library. Just like the Ratpack server, the HttpClient also uses Netty under the hood and in fact shares the same EventLoopGroup as per Netty best practices.

1.11 Basic GET request

import ratpack.core.http.client.HttpClient;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    try (EmbeddedApp remoteApp = EmbeddedApp.fromHandler(ctx -> ctx.render("Hello from remoteApp"))) {
      EmbeddedApp.fromHandler(ctx -> ctx
          .render(
            ctx
              .get(HttpClient.class)
              .get(remoteApp.getAddress())
              .map(response -> response.getBody().getText())
          )
      ).test(httpClient -> 
        assertEquals("Hello from remoteApp", httpClient.getText())
      );
    }
  }
}

12 Static Assets

Ratpack provides support for serving static files as responses.

1.12 From a directory

Ratpack applications have a notion of a “base dir”, which is specified at launch time. This is effectively root of the filesystem as far as the application is concerned. Files from the base dir can be served, using the Chain.files() method.

import ratpack.test.embed.EmbeddedApp;
import ratpack.test.embed.EphemeralBaseDir;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EphemeralBaseDir.tmpDir().use(baseDir -> {
      baseDir.write("public/some.text", "foo");
      baseDir.write("public/index.html", "bar");

      EmbeddedApp.of(s -> s
        .serverConfig(c -> c.baseDir(baseDir.getRoot()))
        .handlers(c -> c
          .files(f -> f.dir("public").indexFiles("index.html"))
        )
      ).test(httpClient -> {
        assertEquals("foo", httpClient.getText("some.text"));
        assertEquals("bar", httpClient.getText());
        assertEquals(404, httpClient.get("no-file-here").getStatusCode());
      });

    });
  }
}

Files will be served with a Last-Modified header based on the file’s advertised last modified timestamp. If the client sends an If-Modified-Since header, Ratpack will respond with a 304 response if the file has not been modified since the given value. Served files do not include ETags.

By default, files will be GZIP compressed over the wire if the client asks for it. This can be disabled on a per request basis by calling the Response.noCompress() method. This is typically used by putting a handler in front of the file serving handler that inspects the request path (e.g. file extension) and disables compression.

2.12 Ad-hoc files

Individual files can be served by using the Context.file() and Context.render() methods.

import ratpack.test.embed.EmbeddedApp;
import ratpack.test.embed.EphemeralBaseDir;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EphemeralBaseDir.tmpDir().use(baseDir -> {
      baseDir.write("some.text", "foo");

      EmbeddedApp.of(s -> s
        .serverConfig(c -> c.baseDir(baseDir.getRoot()))
        .handlers(c -> c
          .get("f", ctx -> ctx.render(ctx.file("some.text")))
        )
      ).test(httpClient ->
        assertEquals("foo", httpClient.getText("f"))
      );

    });
  }
}

If the file returned by Context.file() doesn’t exist, a 404 will be issued.

Responses are timestamped and compressed in exactly the same manner as described for the Chain.files() method.

3.12 Advanced Asset serving with “Asset Pipeline”

The Asset Pipeline project offers an integration with Ratpack. This provides advanced asset bundling, compiling and serving.

13 Google Guice integration

The ratpack-guice extension provides integration with Google Guice. The primary feature of this extension is to allow the server registry to be built by Guice. That is, a Guice Injector can be presented as a Ratpack Registry. This allows the wiring of the application to be specified by Guice modules and bindings, but still allowing the registry to be the common integration layer between different Ratpack extensions at runtime.

The ratpack-guice module as of 2.0.0-rc-1 is built against (and depends on) Guice 5.1.0 (and the multibindings extension).

1.13 Modules

Guice provides the concept of a module, which is a kind of recipe for providing objects. See Guice’s “Getting Started” documentation for details.

The ratpack-guice library provides the BindingsSpec type for specifying the bindings for the application.

2.13 Dependency injected handlers

The Guice integration gives you a way to decouple the components of your application. You can factor out functionality into standalone (i.e. non Handler) objects and use these objects from your handlers. This makes your code more maintainable and more testable. This is the standard “Dependency Injection” or “Inversion of Control” pattern.

The Guice class provides static handler() factory methods for creating root handlers that are the basis of the application. These methods (the commonly used ones) expose Chain instance that can be used to build the application’s handler chain. The instance exposed by these methods provide a registry (via the getRegistry()) method that can be used to construct dependency injected handler instances.

See the documentation of the Guice class for example code.

3.13 Guice and the context registry

TODO guice backed registry impl

This offers an alternative to dependency injected handlers, as objects can just be retrieved on demand from the context.

More usefully, this means that Ratpack infrastructure can be integrated via Guice modules. For example, an implementation of the ServerErrorHandler can be provided by a Guice module. Because Guice bound objects are integrated into the context registry lookup mechanism, this implementation will participate in the error handling infrastructure.

This is true for all Ratpack infrastructure that works via context registry lookup, such as Renderer implementations for example.

14 Groovy

Groovy is an alternative JVM programming language. It has a strong synergy with Java and many language and library features that make it a compelling programming environment. Ratpack provides strong integration with Groovy via the ratpack-groovy and ratpack-groovy-test libraries. Writing Ratpack applications in Groovy generally leads to less code through Groovy’s concise syntax compared to Java and a more productive and enjoyable development experience. To be clear though, Ratpack applications do not need to be written in Groovy.

Groovy is commonly known as a dynamic language. However, Groovy 2.0 added full static typing and static compilation as an option. Ratpack’s Groovy support is strictly designed to fully support “static Groovy” and also leverages the newest features of Groovy to avoid introducing boilerplate code to achieve this goal. Said another way, Ratpack’s Groovy support does not use any dynamic language features and has a strongly typed API.

TODO: find decent links describing static Groovy and use above

1.14 Prerequisites

If you are new to Groovy, you may want to research the following foundational Groovy topics before proceeding:

  1. Closures
  2. The def keyword

TODO: what else should be in this list? Also, links needed

something else

2.14 Ratpack Groovy API

TODO: explain that Groovy API wraps Java API and mirrors it in corresponding ratpack.groovy.*

1.2.14 @DelegatesTo

@DelegatesTo is aimed at documenting code and providing the IDE and static type checker/compiler with more type information at compile-time. Applying the annotation is particularly interesting for DSL authors.

Let’s consider the following Ratpack code snippet:

ratpack {
  handlers {
    get {
      render "Hello world!"
    }
  }
}

This code is also known as being written in Ratpack’s “GroovyChain DSL”. It is essentially the same as

ratpack({
  handlers({
    get({
      render("Hello world!")
    })
  })
})

The methods ratpack, handlers, get and render are called. In fact there is no restriction to call each method only once, for example, the code could have had multiple method invocations of get, responding to different request URIs:

ratpack {
  handlers {
    get {
      render "Hello world!"
    }

    get('foo')  {
      render "bar"
    }
  }
}

Notice how the calls to ratpack, handlers and get build a hierarchy. But how is it actually transformed to calls to Groovy/Java objects?

That’s exactly where delegation comes into play. Groovy allows to change the target of method calls inside Closure code blocks. Let’s have a look at a very basic example:

class Settings {
   String host
   Integer port

   def host(String h) { this.host = h }
   def port(Integer p) { this.port = p }
}

Settings settings(Closure dsl)  {
    def p = new Settings()
    def code = dsl.clone() // better use: dsl.rehydrate(p, this, this)
    code.delegate = p
    code()
    return p
}

// our DSL starts here and returns a Settings instance
Settings config = settings {
  port 1234
  host 'localhost'
}

assert config.host == 'localhost'
assert config.port == 1234

The code in the settings DSL block calls methods which do not exist in the current lexical scope. At runtime, once the delegate property is set though, Groovy additionally resolves the method against the given delegate object, in our case, the Settings instance.

Delegation is commonly used in Groovy DSLs, as it is the case with the Ratpack DSL, to decouple DSL code from underlying objects.

This technique bears a problem for code completion in IDEs or the static type checker/compiler that has been added in Groovy 2. Running the following code in groovyConsole

class Settings {
   String host
   Integer port

   def host(String h) { this.host = h }
   def port(Integer p) { this.port = p }
}

Settings settings(Closure dsl)  {
    def p = new Settings()
    def code = dsl.clone() // better use: dsl.rehydrate(p, this, this)
    code.delegate = p
    code()
    return p
}

@groovy.transform.TypeChecked
void createConfig() {
	Settings config = settings {
	  port 1234
	  host 'localhost'
	}

	assert config.host == 'localhost'
	assert config.port == 1234
}

gives

[Static type checking] - Cannot find matching method ConsoleScript23#port(int). Please check if the declared type is right and if the method exists.
 at line: 20, column: 7

[Static type checking] - Cannot find matching method ConsoleScript23#host(java.lang.String). Please check if the declared type is right and if the method exists.
 at line: 21, column: 7

The type checker misses information about the delegate type Settings at compile-time.

This is the point where @DelegatesTo finally comes into play. It is exactly used in cases where this type information needs to be specified for Closure method parameters:

// ...

// let's tell the compiler we're delegating to the Settings class
Settings settings(@DelegatesTo(Settings) Closure dsl)  {
    def p = new Settings()
    def code = dsl.clone() // better use: dsl.rehydrate(p, this, this)
    code.delegate = p
    code()
    return p
}

// ...

Ratpack uses @DelegatesTo wherever Closure method parameters are used. This does not only serve better code completion or the static type checker, but also for documentation purposes.

3.14 ratpack.groovy script

TODO: introduce DSL used in this file, discuss reloading when in development mode

4.14 handlers {} DSL

TODO: introduce the GroovyChain DSL, and closures as handlers

5.14 Testing

Groovy comes with built-in support for writing tests. Besides integrated support for JUnit, the programming language comes with features proven to be very valuable for test-driven development. One of them is the extended assert keyword on which we’ll have a look at in the next section.

If you’re looking for Ratpack-specific test support documentation, there is a dedicated Ratpack testing guide section in this manual.

1.5.14 Power assertions

Writing tests means formulating assumptions by using assertions. In Java this can be done by using the assert keyword that has been added in J2SE 1.4. Assertion statements in Java are disabled by default.

Groovy comes with a powerful variant of assert also known as power assertion statement. Groovy’s power assert differs from the Java version in its output given once the boolean expression validates to false:

def x = 1
assert x == 2

// Output:
//
// Assertion failed:
// assert x == 2
//        | |
//        1 false

The java.lang.AssertionError contains an extended version of the original assertion error message. Its output shows all variable and expression values from the outer to the inner most expression.

Due to its expressional power, it has become common sense in the Groovy community to write test cases with the assert statement instead of any assert* methods provided by the testing library of choice.

The assert statement is only one of the many useful Groovy features for writing tests. If you’re looking for a comprehensive guide of all Groovy language testing features, please consult the Groovy testing guide.

2.5.14 JUnit 3 support

Groovy comes with embedded JUnit 3 support and a custom TestCase base class: groovy.util.GroovyTestCase. GroovyTestCase extends TestCase and adds useful utility methods.

One example is the shouldFail method family. Each shouldFail method takes a Closure and executes it. If an exception is thrown and the code block fails, shouldFail catches the exception and the test method does not fail:

import groovy.util.GroovyTestCase

class ListTest extends GroovyTestCase {

  void testInvalidIndexAccess1() {
    def numbers = [1,2,3,4]

    shouldFail {
      numbers.get(4)
    }
  }
}

shouldFail in fact returns the catched exception which allows for asserting on exception messages or types:

import groovy.util.GroovyTestCase

class ListTest extends GroovyTestCase {

  void testInvalidIndexAccess1() {
    def numbers = [1,2,3,4]

    def msg = shouldFail {
      numbers.get(4)
    }

    assert msg == 'Index: 4, Size: 4'
  }
}

In addition there is a variant of the shouldFail method that comes with a java.lang.Class parameter before the Closure parameter:

import groovy.util.GroovyTestCase

class ListTest extends GroovyTestCase {

  void testInvalidIndexAccess1() {
    def numbers = [1,2,3,4]

    def msg = shouldFail(IndexOutOfBoundsException) {
      numbers.get(4)
    }

    assert msg == 'Index: 4, Size: 4'
  }
}

If the thrown exception would be another type, shouldFail would rethrow the exception letting the test method fail

A complete overview of allGroovyTestCase methods can be found in the JavaDoc documentation.

3.5.14 JUnit 4 support

Groovy comes with embedded JUnit 4 support. As of Groovy 2.3.0, the groovy.test.GroovyAssert class can be seen as complement to Groovy’s GroovyTestCase JUnit 3 base class. GroovyAssert extends org.junit.Assert by adding static utility methods for writing JUnit 4 tests in Groovy.

import static groovy.test.GroovyAssert.shouldFail

class ListTest {
  void testInvalidIndexAccess1() {
    def numbers = [1,2,3,4]
    shouldFail {
      numbers.get(4)
    }
  }
}

A complete overview of all GroovyAssert methods can be found in the JavaDoc documentation.

4.5.14 Spock

Spock is a testing and specification framework for Java and Groovy applications. What makes it stand out from the crowd is its beautiful and highly expressive specification DSL. Spock specifications are written as Groovy classes.

Spock can be used for unit, integration or BDD (behavior-driven-development) testing, it doesn’t put itself into a specific category of testing frameworks or libraries.

In the following paragraphs we will have a first look at the anatomy of a Spock specification. It is not meant to be a full blown documentation, but rather give a pretty good feeling on what Spock is up to.

1.4.5.14 First steps

Spock lets you write specifications that describe features (properties, aspects) exhibited by a system of interest. The “system” can be anything between a single class and an entire application, a more advanced term for it is system under specification. The feature description starts from a specific snapshot of the system and its collaborators, this snapshot is called the feature’s fixture.

Spock specification classes are automatically derived from spock.lang.Specification. A concrete specification class might consist of fields, fixture methods, features methods and helper methods.

Let’s have a look at a Ratack unit test specification:

import ratpack.groovy.test.GroovyRatpackMainApplicationUnderTest
import ratpack.test.http.TestHttpClient
import ratpack.test.ServerBackedApplicationUnderTest

class SiteSpec {

  ServerBackedApplicationUnderTest aut = new GroovyRatpackMainApplicationUnderTest()
  @Delegate TestHttpClient client = TestHttpClient.testHttpClient(aut)

  def "Check Site Index"() {
    when:
      get("index.html")

    then:
      response.statusCode == 200
      response.body.text.contains('<title>Ratpack: A toolkit for JVM web applications</title>')
  }
}

Spock feature specifications are defined as methods inside a spock.lang.Specification class. They describe the feature by using a String literal instead of a method name. The specification above uses "Check Site Index" to test the outcome of a request to index.html.

The feature specification uses the when and then blocks. The when block creates a so-called stimulus and is a companion of the then block which describes the response to the stimulus. Notice how we can also leave out the assert statements in the then block. Spock will interpret Boolean expressions there correctly. The setup block could have been used to configure local variables visible inside the feature method only.

2.4.5.14 More on Spock

Spock provides more advanced features like data tables or mocking we didn’t have a look at in this section. Feel free to consult the Spock GitHub page for more documentation.

15 RxJava

The excellent RxJava can be used in Ratpack applications to elegantly compose asynchronous operations.

The ratpack-rx2 module provides the RxRatpack class that provides static methods for adapting Ratpack promises to RxJava’s Observable.

The ratpack-rx2 module as of 2.0.0-rc-1 is built against (and depends on) RxJava 2.2.21.

1.15 Initialization

The RxRatpack.initialize() must be called to fully enable the integration. This method only needs to be called once for the JVM’s lifetime.

2.15 Observing Ratpack

The integration is based on the RxRatpack.single() and RxRatpack.observe() static methods. These methods adapt Ratpack’s promise type into an observable, which can then be used with all of the observable operators that RxJava offers.

For example, blocking operations can be easily observed.

import ratpack.exec.Promise;
import ratpack.exec.Blocking;
import ratpack.test.handling.HandlingResult;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static ratpack.rx2.RxRatpack.single;
import static ratpack.test.handling.RequestFixture.requestFixture;

public class Example {
  public static void main(String... args) throws Exception {
    HandlingResult result = requestFixture().handle(context -> {
      Promise<String> promise = Blocking.get(() -> "hello world");
      single(promise).map(String::toUpperCase).subscribe(context::render);
    });

    assertEquals("HELLO WORLD", result.rendered(String.class));
  }
}

3.15 Implicit error handling

A key feature of the RxJava integration is the implicit error handling. All observable sequences have an implicit default error handling strategy of forwarding the exception to the execution context error handler. In practice, this means that error handlers rarely need to be defined for observable sequences.

import ratpack.core.error.ServerErrorHandler;
import ratpack.rx2.RxRatpack;
import ratpack.test.handling.RequestFixture;
import ratpack.test.handling.HandlingResult;
import io.reactivex.Observable;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    RxRatpack.initialize(); // must be called once per JVM

    HandlingResult result = RequestFixture.requestFixture().handleChain(chain -> {
      chain.register(registry ->
          registry.add(ServerErrorHandler.class, (context, throwable) ->
              context.render("caught by error handler: " + throwable.getMessage())
          )
      );

      chain.get(ctx -> Observable.<String>error(new Exception("!")).subscribe((s) -> {}));
    });

    assertEquals("caught by error handler: !", result.rendered(String.class));
  }
}

In this case, the throwable thrown during the blocking operation will be forwarded to the current ServerErrorHandler, which will probably render an error page to the response. If the subscriber does implement an error handling strategy, it will be used instead of the implicit error handler.

The implicit error handling applies to all observables that are created on Ratpack managed threads. It is not restricted to observables that are backed by Ratpack promises.

16 Jackson

Integration with the Jackson JSON marshalling library provides the ability to work with JSON. This is provided as part of ratpack-core.

As of Ratpack 2.0.0-rc-1 is built against (and depends on) Jackson Core 2.13.1.

The ratpack.core.jackson.Jackson class provides most of the Jackson related functionality.

1.16 Writing JSON responses

The Jackson integration adds a Renderer for rendering objects as JSON.

The Jackson.json() method can be used to wrap any object (serializable by Jackson) for use with the Context.render() method.

import ratpack.test.embed.EmbeddedApp;
import ratpack.core.http.client.ReceivedResponse;

import static ratpack.core.jackson.Jackson.json;
import static org.junit.jupiter.api.Assertions.*;

public class Example {

  public static class Person {
    private final String name;
    public Person(String name) {
      this.name = name;
    }
    public String getName() {
      return name;
    }
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp.of(s -> s
      .handlers(chain ->
        chain.get(ctx -> ctx.render(json(new Person("John"))))
      )
    ).test(httpClient -> {
      ReceivedResponse response = httpClient.get();
      assertEquals("{\"name\":\"John\"}", response.getBody().getText());
      assertEquals("application/json", response.getBody().getContentType().getType());
    });
  }
}

See the Jackson class documentation for more examples, including streaming and JSON events.

2.16 Reading JSON requests

The Jackson integration adds a Parser for converting JSON request bodies into objects.

The Jackson.jsonNode() and Jackson.fromJson() methods can be used to create objects to be used with the Context.parse() method.

import ratpack.guice.Guice;
import ratpack.test.embed.EmbeddedApp;
import ratpack.core.http.client.ReceivedResponse;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.reflect.TypeToken;

import java.util.List;

import static ratpack.func.Types.listOf;
import static ratpack.core.jackson.Jackson.jsonNode;
import static ratpack.core.jackson.Jackson.fromJson;
import static org.junit.jupiter.api.Assertions.*;

public class Example {

  public static class Person {
    private final String name;
    public Person(@JsonProperty("name") String name) {
      this.name = name;
    }
    public String getName() {
      return name;
    }
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp.of(s -> s
      .handlers(chain -> chain
        .post("asNode", ctx -> {
          ctx.render(ctx.parse(jsonNode()).map(n -> n.get("name").asText()));
        })
        .post("asPerson", ctx -> {
          ctx.render(ctx.parse(fromJson(Person.class)).map(p -> p.getName()));
        })
        .post("asPersonList", ctx -> {
          ctx.render(ctx.parse(fromJson(listOf(Person.class))).map(p -> p.get(0).getName()));
        })
      )
    ).test(httpClient -> {
      ReceivedResponse response = httpClient.requestSpec(s ->
        s.body(b -> b.type("application/json").text("{\"name\":\"John\"}"))
      ).post("asNode");
      assertEquals("John", response.getBody().getText());

      response = httpClient.requestSpec(s ->
        s.body(b -> b.type("application/json").text("{\"name\":\"John\"}"))
      ).post("asPerson");
      assertEquals("John", response.getBody().getText());

      response = httpClient.requestSpec(s ->
        s.body(b -> b.type("application/json").text("[{\"name\":\"John\"}]"))
      ).post("asPersonList");
      assertEquals("John", response.getBody().getText());
    });
  }
}

The integration adds a no opts parser, which makes it possible to use the Context.parse(Class) and Context.parse(TypeToken) methods.

import ratpack.test.embed.EmbeddedApp;
import ratpack.core.http.client.ReceivedResponse;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.reflect.TypeToken;

import java.util.List;

import static ratpack.func.Types.listOf;
import static org.junit.jupiter.api.Assertions.*;

public class Example {

  public static class Person {
    private final String name;
    public Person(@JsonProperty("name") String name) {
      this.name = name;
    }
    public String getName() {
      return name;
    }
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp.of(s -> s
      .handlers(chain -> chain
        .post("asPerson", ctx -> {
          ctx.parse(Person.class).then(person -> ctx.render(person.getName()));
        })
        .post("asPersonList", ctx -> {
          ctx.parse(listOf(Person.class)).then(person -> ctx.render(person.get(0).getName()));
        })
      )
    ).test(httpClient -> {
      ReceivedResponse response = httpClient.requestSpec(s ->
        s.body(b -> b.type("application/json").text("{\"name\":\"John\"}"))
      ).post("asPerson");
      assertEquals("John", response.getBody().getText());

      response = httpClient.requestSpec(s ->
        s.body(b -> b.type("application/json").text("[{\"name\":\"John\"}]"))
      ).post("asPersonList");
      assertEquals("John", response.getBody().getText());
    });
  }
}

3.16 Configuring Jackson

The Jackson API is based around the ObjectMapper. Ratpack adds a default instance to the base registry automatically. To configure Jackson behaviour, override this instance.

Jackson feature modules allow Jackson to be extended to support extra data types and capabilities. For example the JDK8 module adds support for JDK8 types like Optional.

To use such modules, simply add an appropriately configured ObjectMapper to the registry.

import ratpack.test.embed.EmbeddedApp;
import ratpack.core.http.client.ReceivedResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;

import java.util.Optional;

import static ratpack.core.jackson.Jackson.json;
import static org.junit.jupiter.api.Assertions.*;

public class Example {

  public static class Person {
    private final String name;
    public Person(String name) {
      this.name = name;
    }
    public String getName() {
      return name;
    }
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp.of(s -> s
      .registryOf(r -> r
        .add(ObjectMapper.class, new ObjectMapper().registerModule(new Jdk8Module())) 
      )
      .handlers(chain ->
        chain.get(ctx -> {
          Optional<Person> personOptional = Optional.of(new Person("John"));
          ctx.render(json(personOptional));
        })
      )
    ).test(httpClient -> {
      ReceivedResponse response = httpClient.get();
      assertEquals("{\"name\":\"John\"}", response.getBody().getText());
      assertEquals("application/json", response.getBody().getContentType().getType());
    });
  }
}

17 Resilience4j

Resilience4j is a lightweight fault tolerance library designed for Java 8. It is inspired by Netflix Hystrix which is in maintenance mode.

The resilience4j-ratpack module is maintained by the Resilience4j project. Refer to their Getting Started Guide for more information.

1.17 Demo Application

The Resilience4j team has published a Demo Application which shows the usage of Resilience4j in Ratpack.

18 Config

Most applications need some level of configuration. This can be used to specify the correct external resources to use (databases, other services, etc.), tune performance, or otherwise adjust to the requirements of a given environment. Ratpack provides an easy, flexible mechanism to access configuration information in your Ratpack application.

Configuration data is accessed via object binding using Jackson. Configuration objects provided by Ratpack are intended to be usable out of the box. Configuration data can be loaded from multiple sources, such as YAML files, JSON files, properties files, environment variables and system properties.

1.18 Quick-Start

To get started:

  1. Within your RatpackServer definition function, build a ConfigData instance (see class documentation for an example)
  2. Retrieve bound configuration objects from the config data

2.18 Config Sources

ConfigDataBuilder provides methods to easily load data from the most common sources.

Commonly used file formats can be used via the yaml, json and props methods. The provided signatures can be used to load data from local files (String or Path), over the network (URL), from the classpath (use Resources.getResource(String) to get a URL), or anywhere else you can treat as a ByteSource. Additionally, you can load data from non-file sources such as Maps/Properties objects (particularly useful for default values; see example), system properties, and environment variables. You can also choose to load configuration data from existing objects using the object method. If additional flexibility is needed, you can provide your own ConfigSource implementation.

1.2.18 Flat Config Sources

Environment variables, Properties, and Maps are flat data structures, whereas the binding model used by Ratpack is hierarchical. To bridge this gap, these config source implementations apply conventions to allow for the flat key-value pairs to be transformed into useful data.

1.1.2.18 Environment Variables

The default environment variable config source uses the following rules:

If custom environment parsing is needed, you can supply an EnvironmentParser implementation.

2.1.2.18 Properties/Maps

The default Properties/Map config source uses the following rules:

3.18 Usage

1.3.18 Ordering

If you have multiple config sources, add them to the builder from least important to most important. For example, if you had a configuration file that you wanted to be able to override via system properties, you would first add the configuration file source, followed by the system properties source. Likewise, if you have default settings that you wanted to be able to override via environment variables, you would first add the default settings source (perhaps via props), followed by the environment variables source.

2.3.18 Error Handling

As shown in the ConfigDataBuilder docs, onError can be used to customize the behavior when an error is encountered while loading data from a config source. Most commonly, this is used to make configuration sources optional by ignoring load exceptions.

3.3.18 Object Mapper

Ratpack uses Jackson for config object binding. The default ObjectMapper used is configured with commonly used Jackson modules pre-loaded, and set to allow unquoted field names, allow single quotes, and ignore unknown field names. This is intended to make it easy to use, out-of-the-box. However, there will sometimes be cases where you may want to change a Jackson configuration setting or add additional Jackson modules. If so, this can be accomplished via various signatures of ConfigData.of(...) or via ConfigDataBuilder.configureObjectMapper(...).

4.3.18 Binding

Once you’ve built your ConfigData instance, you can bind the data to configuration objects. The simplest option is to define a class that represents the entirety of your application’s configuration, and bind to it all at once using ConfigData.get(Class). Alternatively, you can bind objects one-at-a-time at specified paths within the data using ConfigData.get(String, Class). For the common case of binding to a ServerConfig object, ConfigData.getServerConfig(...) signatures are provided as a convenience.

19 Spring Boot

The ratpack-spring-boot extension provides integration with Spring Boot. There are two main features in this library: one allows a Ratpack server registry to be created from a Spring ApplicationContext, and the other allows Ratpack itself to be embedded in a Spring Boot application (making the ApplicationContext automatically part of the server registry).

1.19 The Spring convenience class

In a vanilla Ratpack application you can create a registry easily using the Spring convenience class. This is an alternative or a complement to Guice dependency injection because it works more or less the same way and Ratpack allows registries to be chained together quite conveniently.

Here’s an example of a main class that uses this API to configure the application.

package my.app;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import ratpack.core.server.RatpackServer;

import static ratpack.spring.Spring.spring;

public class Main {
  public static void main(String... args) throws Exception {
    RatpackServer.start(server -> server
      .registry(spring(MyConfiguration.class))
      .handlers(chain -> chain
        .get(ctx -> ctx
          .render("Hello " + ctx.get(Service.class).message()))
        .get(":message", ctx -> ctx
          .render("Hello " + ctx.getPathTokens().get("message") + "!")
        )
      )
    );
  }
}

@Configuration
class MyConfiguration {
  @Bean
  public Service service() {
    return () -> "World!";
  }
}

interface Service {
  String message();
}

The Spring.spring() method creates an ApplicationContext and adapts it to the Ratpack Registry interface.

NOTE: The Spring ListableBeanFactory API doesn’t current support looking up beans with parameterized types. The adapted Registry therefore instance doesn’t support this because of this limitation. There is a feature request to add the generics functionality to the Spring ListableBeanFactory API.

2.19 Embedding Ratpack in a Spring Boot application

As an alternative to embedding Spring (as a Registry) in a Ratpack application, you can do the opposite: embed Ratpack as a server in Spring Boot, providing a nice alternative to the Servlet containers that Spring Boot supports. The core of the feature set is an annotation @EnableRatpack which you add to a Spring configuration class in order to start Ratpack. Then you can declare handlers as @Beans of type Action<Chain>, for example:

package my.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import ratpack.func.Action;
import ratpack.core.handling.Chain;
import ratpack.spring.config.EnableRatpack;

@SpringBootApplication
@EnableRatpack
public class Main {

  @Bean
  public Action<Chain> home() {
    return chain -> chain
      .get(ctx -> ctx
        .render("Hello " + service().message())
      );
  }

  @Bean
  public Service service() {
    return () -> "World!";
  }

  public static void main(String... args) throws Exception {
    SpringApplication.run(Main.class, args);
  }

}

interface Service {
  String message();
}

TIP: Ratpack will register handlers automatically for static content in the classpath under “/public” or “/static” (just like a regular Spring Boot application).

1.2.19 Re-using existing Guice modules

If Ratpack is embedded in a Spring application it can be helpful to re-use existing Guice modules, e.g. for template rendering. To do this just include a @Bean of type Module. For example with the ThymeleafModule for Thymeleaf support:

package my.app;

import java.util.Collections;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import ratpack.func.Action;
import ratpack.core.handling.Chain;
import ratpack.thymeleaf.ThymeleafModule;
import ratpack.spring.config.EnableRatpack;

import static ratpack.thymeleaf3.Template.thymeleafTemplate;

@SpringBootApplication
@EnableRatpack
public class Main {

  @Bean
  public Action<Chain> home(Service service) {
    return chain -> chain.get(ctx -> ctx
      .render(thymeleafTemplate("myTemplate", 
        m -> m.put("key", "Hello " + service.message())))
    );
  }

  @Bean
  public ThymeleafModule thymeleafModule() {
    return new ThymeleafModule();
  }

  @Bean
  public Service service() {
    return () -> "World!";
  }

  public static void main(String... args) throws Exception {
    SpringApplication.run(Main.class, args);
  }

}

20 pac4j

The pac4j library is a security engine which abstracts over different authentication protocols such as OAuth, CAS, OpenID (Connect), SAML, Google App Engine and HTTP (form and basic auth) as well as custom authentication mechanisms (e.g. database backed). It also supports various authorization mechanisms: roles / permissions checks, CSRF token, security headers, etc. Integration with Ratpack is provided via the pac4j/ratpack-pac4j maintained by the pac4j community.

Gradle Dependency:

implementation 'org.pac4j:ratpack-pac4j:3.0.0'

1.20 Session Usage

As previously mentioned, using ratpack-pac4j requires session support via ratpack-session. When authenticated, the user’s profile is stored in the session. Therefore, terminating the session will effectively log the user out.

2.20 Demo application

Please see the ratpack-pac4j-demo application for a complete application that demonstrates how to use pac4j with Ratpack.

21 Retrofit Type Safe Clients

The ratpack-retrofit2 extension provides integration for declarative type-safe HTTP clients with the retrofit2 library (v2.9.0).

The retrofit library allows for representing HTTP APIs via a type-safe interface. This allows application code to remain agnostic about the underlying API design and implementation and focus solely on the behavioral aspects of interfacing with the API.

Retrofit clients generated using the RatpackRetrofit class are backed with Ratpack’s HttpClient and are capable of interfacing with Ratpack’s Promise construct as a return type.

By using the ratpack-retrofit2 integration, developers can gain the benefit of isolating the API constructs as class and method annotations, while still utilizing the non-blocking nature of the Netty backed HttpClient and Ratpack’s execution model.

1.21 Usage

The RatpackRetrofit.client(URI endpoint) and RatpackRetrofit.client(String endpoint) methods provide the entry point for creating API clients.

The provided URI or String specifies the base URL for all clients generated by the Builder.

The underlying Retrofit.Builder can be configured with the RatpackRetrofit.Builder.configure(Action<? super Retrofit.Builder> spec) method.

Once configured, the client is constructed by calling build(Class<T> api) with the api interface. This method returns a generated instance of the interface that will issue the HTTP requests and adapt the responses to the configured return type.

import ratpack.exec.Promise;
import ratpack.retrofit.RatpackRetrofit;
import ratpack.test.embed.EmbeddedApp;
import retrofit2.http.GET;

import static org.junit.jupiter.api.Assertions.*;

public class Example {

  public static interface HelloApi {
    @GET("hi") Promise<String> hello();
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp api = EmbeddedApp.of(s -> s
      .handlers(chain -> chain
        .get("hi", ctx -> ctx.render("hello"))
      )
    );
    EmbeddedApp.of(s -> s
      .registryOf(r -> 
        r.add(HelloApi.class,
          RatpackRetrofit
            .client(api.getAddress())
            .build(HelloApi.class))
      )
      .handlers(chain -> {
        chain.get(ctx -> {
          HelloApi helloApi = ctx.get(HelloApi.class);
            
          ctx.render(helloApi.hello());
        });
      })
    ).test(httpClient -> {
      assertEquals("hello", httpClient.getText());
      api.close();
    });
  }
}

2.21 Using Ratpack Promises in Retrofit APIs

The ratpack-retrofit2 integration provides support for utilizing Ratpack’s Promise as a return type for a client interface. It supports adapting to Promise when the promised value is the following types:

The following example shows the 3 variations configured for the same endpoint.

import ratpack.exec.Promise;
import ratpack.core.http.client.ReceivedResponse;
import retrofit2.Response;
import retrofit2.http.GET;

public interface Example {
  @GET("hi") Promise<String> hello();
  
  @GET("hi") Promise<Response<String>> helloResponse();
  
  @GET("hi") Promise<ReceivedResponse> helloRaw();
}

3.21 Creating multiple API implementations

Many APIs may be represented using distinct interfaces for different capabilities. To create multiple clients, the underlying Retrofit class can be obtained.

import ratpack.exec.Promise;
import ratpack.retrofit.RatpackRetrofit;
import ratpack.test.embed.EmbeddedApp;
import retrofit2.http.GET;
import retrofit2.Retrofit;

import static org.junit.jupiter.api.Assertions.*;

  
public class Example {

  public static interface HelloApi {
    @GET("hi") Promise<String> hello();
  }
  
  public static interface GoodbyeApi {
    @GET("bye") Promise<String> bye();
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp api = EmbeddedApp.of(s -> s
      .handlers(chain -> chain
        .get("hi", ctx -> ctx.render("hello"))
        .get("bye", ctx -> ctx.render("goodbye"))
      )
    );
    EmbeddedApp.of(s -> s
      .registryOf(r -> {
        Retrofit retrofit = RatpackRetrofit
          .client(api.getAddress())
          .retrofit();
        r.add(HelloApi.class, retrofit.create(HelloApi.class));
        r.add(GoodbyeApi.class, retrofit.create(GoodbyeApi.class));
      })
      .handlers(chain -> {
        chain.get(ctx -> {
            
          HelloApi hiApi = ctx.get(HelloApi.class);
          GoodbyeApi byeApi = ctx.get(GoodbyeApi.class);
            
          ctx.render(hiApi.hello().right(byeApi.bye()).map(p -> p.left() + " and " + p.right()));
        });
      })
    ).test(httpClient -> {
      assertEquals("hello and goodbye", httpClient.getText());
      api.close();
    });
  }
}

4.21 Using Retrofit Converters

By default, ratpack-retrofit2 registers the ScalarsConverterFactory. This allows for API responses to be converted into Java String, primitives and their box types.

If the remote API is responding in JSON, then the JacksonConverterFactory must be registered.

import com.fasterxml.jackson.databind.ObjectMapper;
import ratpack.exec.Promise;
import ratpack.retrofit.RatpackRetrofit;
import ratpack.exec.registry.Registry;
import ratpack.test.embed.EmbeddedApp;
import retrofit2.converter.jackson.JacksonConverterFactory;
import retrofit2.http.GET;
import retrofit2.Retrofit;

import java.util.List;

import static ratpack.core.jackson.Jackson.json;
import static org.junit.jupiter.api.Assertions.*;

  
public class Example {

  public static interface NameApi {
    @GET("names") Promise<List<String>> names();
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp api = EmbeddedApp.of(s -> s
      .handlers(chain -> chain
        .get("names", ctx -> ctx.render(json(new String[]{"John", "Jane"})))
      )
    );
    EmbeddedApp.of(s -> s
      .registry(r -> 
        Registry.single(NameApi.class, 
          RatpackRetrofit
            .client(api.getAddress())
            .configure(b -> 
              b.addConverterFactory(
                JacksonConverterFactory.create(
                  r.get(ObjectMapper.class)
                )
              )
            )
            .build(NameApi.class))
      )
      .handlers(chain -> {
        chain.get(ctx -> {
          ctx.get(NameApi.class).names().then(nameList -> ctx.render(json(nameList)));  
        });
      })
    ).test(httpClient -> {
      assertEquals("[\"John\",\"Jane\"]", httpClient.getText());
      api.close();
    });
  }
}

22 Dropwizard Metrics

The ratpack-dropwizard-metrics jar provides integration with Dropwizard Metrics library.

Dropwizard Metrics is one of the best metrics libraries out there for the JVM. It provides a toolkit of metric types and metric reporters that will give a deep insight into your application’s performance, whether that be at development time or real time in production. It allows you to easily capture statistics like the number of requests served or response times, and also more generic information like the state of your internal collections, queues or how many times some portion of code is being executed. By measuring your code you will know exactly what your code is doing when it runs and be able to make informed optimization decisions.

Ratpack’s integration with Dropwizard Metrics means that many of these key metrics are captured already for you, simply by registering the Guice module. Should you require further insight then Ratpack also makes it easy for you to capture additional metrics using the library’s many metric types and then report all of these metrics to your required output using the library’s metric reporters.

See DropwizardMetricsModule for detailed usage information.

1.22 Built-in metrics

Ratpack provides built-in metric collectors for key metrics. When metrics are enabled within your application using DropwizardMetricsModule, the built,in metric collectors will automatically be enabled too.

Ratpack has built-in metric collectors for:

Ratpack also has support for Dropwizard Metric’s JVM instrumentation. See DropwizardMetricsConfig.jvmMetrics(boolean) for usage information.

2.22 Custom metrics

Ratpack enables you to capture your own application metrics in two ways:

  1. Obtaining the MetricRegistry via dependency injection or context registry lookup and registering your own metrics with it.
  2. Add metrics annotations to your Guice injected classes.

See DropwizardMetricsModule for more details.

3.22 Reporting metrics

Ratpack supports metric reporters for the following outputs:

For an example of how to consume real-time metrics with websockets, see the example-books project.

23 Building with Gradle

The recommended way to build Ratpack applications is to use the Gradle Build System, by way of the Gradle plugins provided by the Ratpack project.

Ratpack is purely a runtime toolkit and not also a development time tool like Ruby on Rails and Grails. This means that you can use whatever you like to build a Ratpack app. The provided Gradle plugins merely provide convenience and are not fundamental to Ratpack development.

1.23 Setup

The first requirement is to apply the Gradle plugin to your Gradle project…

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
  }
}

apply plugin: "io.ratpack.ratpack-java"

repositories {
  mavenCentral()
}

Or for a Groovy based Ratpack project…

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
  }
}

apply plugin: "io.ratpack.ratpack-groovy"

repositories {
  mavenCentral()
}

The 'io.ratpack.ratpack-java' plugin applies the core Gradle 'java' plugin. The 'io.ratpack.ratpack-groovy' plugin applies the core Gradle 'groovy' plugin. This means that you can start adding code and dependencies to your app like a standard Gradle based project (e.g. putting source in src/main/[groovy|java]). Note that the 'io.ratpack.ratpack-groovy' plugin implicitly applies the 'io.ratpack.ratpack-java' plugin.

2.23 Ratpack dependencies

To depend on a Ratpack extension library, simply add it as a regular implementation dependency…

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
  }
}

apply plugin: "io.ratpack.ratpack-groovy"

repositories {
  mavenCentral()
}

dependencies {
  implementation ratpack.dependency("dropwizard-metrics")
}

Using ratpack.dependency("dropwizard-metrics") is equivalent to "io.ratpack:ratpack-dropwizard-metrics:«version of ratpack-gradle dependency»". This is the recommended way to add dependencies that are part of the core distribution.

The 'io.ratpack.ratpack-java' plugin adds the following implicit dependencies:

The 'io.ratpack.ratpack-groovy' plugin adds the following implicit dependencies:

The available libraries can be browsed via search.maven.org. All Ratpack jars are published to Maven Central.

3.23 The ‘application’ plugin

Both the 'ratpack-java' and 'ratpack-groovy' plugins also apply the core Gradle 'application' plugin. This plugin provides the ability to create a standalone executable distribution of your software. This is the preferred deployment format for Ratpack applications.

The 'application' plugin requires the main class (i.e. entry point) of your application to be specified. You must configure the 'mainClassName' attribute in your Gradle build file to be the fully qualified class name of class that contains a 'static void main(String[] args)' method which configures the Ratpack server. This is preconfigured by the 'ratpack-groovy' plugin to be the GroovyRatpackMain. This can be changed if you wish to use a custom entry point (consult the 'application' plugin documentation).

4.23 The ‘shadow’ plugin

Both the 'ratpack-java' and 'ratpack-groovy' plugins ship with integration support for the 3rd party 'shadow' plugin. This plugin provides the ability to create a self-contained “fat-jar” that includes your ratpack application and any compile and runtime dependencies.

The plugins react to the application of the 'shadow' plugin and configure additional task dependencies. They do not apply the 'shadow' plugin and, for compatibility reasons, do not ship with a version of the 'shadow' as a dependency.

To use the 'shadow' integration, you will need to include the dependency in your project and apply the plugin.

buildscript {
  repositories {
    mavenCentral()
    gradlePluginPortal()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
    classpath 'gradle.plugin.com.github.johnrengelman:shadow:7.1.2'
  }
}

apply plugin: "io.ratpack.ratpack-java"
apply plugin: 'com.github.johnrengelman.shadow'

repositories {
  mavenCentral()
}

The latest version of the 'shadow' plugin can be found on the project’s Github page.

You can now have the build generate the fat-jar, by running…

./gradlew shadowJar

5.23 The base dir

The base dir is effectively the root of the filesystem for the application. At build time, this is effectively the main set of resources for your application (i.e. src/main/resources). The Ratpack plugin adds a complimentary source of main resources, src/ratpack. You can choose not to use this dir, using src/main/resources instead, or changing its location via the ratpack.baseDir property in the Gradle build.

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
  }
}

apply plugin: "io.ratpack.ratpack-groovy"

repositories {
  mavenCentral()
}

ratpack.baseDir = file('ratpack')

When packaging as a distribution, the plugin will create a directory called app within the distribution that contains all the main resources of the project. This directory will be prepended to the classpath when the app is launched via the start scripts. This allows the application to read files from the base dir directly from disk instead of decompressing on the fly from the JAR. This is more efficient.

See Launching for more information.

1.5.23 The ‘ratpack.groovy’ script

The 'ratpack-groovy' plugin expects the main application definition to be located at either ratpack.groovy or Ratpack.groovy within the base dir. By default, it will effectively look in src/main/resources and src/ratpack. This file should not go into src/main/groovy as the application manages compilation of this file. Therefore, it needs to be on the classpath in source form (i.e. as a .groovy file) and not in compiled form.

See Groovy for more information about the contents of this file.

6.23 Running the application

The 'application' plugin provides the 'run' task for starting the Ratpack application. This is a task of the core Gradle JavaExec type. The 'ratpack-java' plugin configures this 'run' task to launch with the system property 'ratpack.development' set to true.

If you wish to set extra system properties for development time execution, you can configure this task…

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
  }
}

apply plugin: "io.ratpack.ratpack-java"

repositories {
  mavenCentral()
}

run {
  systemProperty "app.dbPassword", "secret"
}

1.6.23 Development time reloading

The Ratpack Gradle plugins integrate with Gradle’s Continuous Build feature. To leverage this, you can run the run task with the --continuous (or -t) argument.

Any changes made to source or resources will be compiled and processed and the application reloaded as a result.

2.6.23 Running with the ‘shadow’ plugin

If applied to the project, the 'shadow' plugin provides the 'runShadow' task for starting the Ratpack application from the fat-jar. Like the 'run' task, this is a task of the core Gradle JavaExec type. The 'shadow' plugin configure this 'runShadow' task to start the process using the java -jar <path/to/shadow-jar.jar> command.

Class reloading is not supported through the 'runShadow' task because the application is being run from the packaged jar file.

Extra system properties or JVM options can be configured on this task…

buildscript {
  repositories {
    mavenCentral()
    gradlePluginPortal()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
    classpath "gradle.plugin.com.github.johnrengelman:shadow:7.1.2"
  }
}

apply plugin: "io.ratpack.ratpack-java"
apply plugin: "com.github.johnrengelman.shadow"

repositories {
  mavenCentral()
}

runShadow {
  systemProperty "app.dbPassword", "secret"
}

24 Deploying to Heroku

Heroku is a scalable polyglot cloud application platform. It allows you to focus on writing applications in the language of your choice, and then easily deploy them to the cloud without having to manually manage servers, load balancing, log aggregation, etc. Heroku does not have, nor does it need, any special Ratpack support above its generic support for JVM applications. Heroku is a rich platform, with many elements such as Postgres, Redis, Memcache, RabbitMQ, New Relic, etc. It is a compelling option for serving Ratpack applications.

Deployments to Heroku are typically in source form. Deploying is as simple as performing a Git push at the end of your CI pipeline. Many popular cloud CI tools such as drone.io and Travis-CI (among others) have convenient support for pushing to Heroku.

It is recommended to read the Heroku Quickstart and Buildpack documentation if you are new to Heroku. The rest of this chapter outlines the requirements and necessary configuration for deploying Ratpack applications to Heroku.

1.24 Gradle based builds

Ratpack applications can be built by any build system, but the Ratpack team recommends Gradle. Heroku has native support for Gradle via the Gradle buildpack, which works well with the Ratpack Gradle plugins.

All Gradle projects should use the Gradle Wrapper. If the wrapper scripts are present in your project, Heroku will detect that your project is built with Gradle.

1.1.24 Building

The Gradle buildpack will invoke ./gradlew installDist -x test by default when it detects that Ratpack is being used. The installDist task is added by the Ratpack Gradle plugins (installApp prior to Gradle 2.3), and should work by default. This will build your application and install it into the directory build/install/«project name».

If you need to run a different task, you can add a stage task to your build.gradle. A typical stage task might look like this:

task stage {
  dependsOn clean, installDist
}

If a stage task is present, Heroku will run this instead of the default task.

1.1.1.24 Setting the project name

By default, Gradle uses the project’s directory name as the project’s name. In Heroku (and some CI servers), the project is built in a temporary directory with a randomly assigned name. To ensure that the project uses a consistent name, add a declaration to settings.gradle in the root of your project:

rootProject.name = "«project name»"

This is a good practice for any Gradle project.

2.1.24 Running (Procfile)

By default, Heroku will run the following script to start your app:

build/install/«project name»/bin/«project name»

You can customize this command by creating a Procfile in the root of your application and specifying the command that Heroku should use to start your application prefixed by web:.

3.1.24 Configuration

There are several ways to configure the environment for applications deployed to Heroku. You may want to use these mechanisms to set environment variables and/or JVM system properties to configure your application.

The application entry points that are used when using the ratpack and ratpack-groovy Gradle plugins support using JVM system properties to contribute to the ServerConfig (see the launching chapter chapter for more detail). The starter scripts created by the Ratpack Gradle plugins, support the standard JAVA_OPTS environment variable and an app specific «PROJECT_NAME»_OPTS environment variable. If your application name was foo-Bar, then the environment variable would be named FOO_BAR_OPTS.

One way to bring this all together is to launch your application via env:

web: env "FOO_BAR_OPTS=-Dapp.dbPassword=secret" build/install/«project name»/bin/«project name»

It is generally preferable to not use JAVA_OPTS as Heroku sets this to useful defaults for the platform.

Another approach is to use config vars. The benefit of setting the environment via the Procfile is that this information is in your versioned source tree. The benefit of using config vars is that they are only available to those with permissions to view/change them with Heroku. It is possible to combine both approaches by setting config vars for values that should be secret (like passwords) and referencing them in your Procfile.

web: env "FOO_BAR_OPTS=-Dapp.dbPassword=$SECRET_DB_PASSWORD" build/install/«project name»/bin/«project name»

Now it is easy to see which properties and environment variables are set in the source tree, but sensitive values are only visible via the Heroku management tools.

2.24 Other build tools and binary deployments

The Ratpack project does not provide any “official” integration with other build tools. However, it is quite possible to use whatever tool you like to build a Ratpack application for Heroku or even to deploy in binary form.

Once you have a compiled Ratpack application in the Heroku environment (either through building with another build tool or by binary deployment), you can simply start the application by using java directly.

web: java ratpack.groovy.GroovyRatpackMain

See the launching chapter for more detail on starting Ratpack applications.

3.24 General Considerations

1.3.24 Port

Heroku assigns each application an ephemeral port number, made available by the PORT environment variable. Ratpack honors this environment variable by default if the ratpack.port system property is not set.

25 Logging

Ratpack uses SLF4J for logging, which allows you to easily bind your favorite logging library at compile time.

Library options include:

Simply add one logging library as a dependency and use SLF4J syntax to log. If you are currently using another logging library, SLF4J provides a migration tool to automate the transition.
Examples for Java and Groovy are below and more details can be found in the SLF4J manual.

1.25 Java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogExample {
  private final static Logger LOGGER = LoggerFactory.getLogger(LogExample.class);
    
  public void log() {
    LOGGER.info("Start logging");
    LOGGER.warn("Logging with a {} or {}", "parameter", "two");
    LOGGER.error("Log an exception", new Exception("Example exception"));
    LOGGER.info("Stop logging");
  }
}

2.25 Groovy

import groovy.util.logging.Slf4j

@Slf4j
class LogExample {
  void log() {
    log.info "Start logging"
    log.warn "Logging with a {} or {}", "parameter", "two"
    log.debug "Detailed information"
    log.info "Stop logging"
  }
}

3.25 Request Logging

Ratpack provides a mechanism for logging information about each request, RequestLogger. The request logger is a handler. Each request that flows through it will be logged, when the request completes. Typically, it is placed early in the handler chain and added with the Chain.all(Handler) method so that all requests are logged.

Ratpack provides the RequestLogger.ncsa() method, that logs in the NCSA Common Log Format. This implementation logs to an slf4j logger named ratpack.requests (the RequestLogger.ncsa(Logger) method allows an alternative logger to be specified).

import ratpack.core.handling.RequestLogger;
import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;
import static org.junit.jupiter.api.Assertions.*;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandlers(c -> c
      .all(RequestLogger.ncsa())
      .all(ctx -> ctx.render("ok"))
    ).test(httpClient -> {
      ReceivedResponse response = httpClient.get();
      assertEquals("ok", response.getBody().getText());

      // Check log output: [ratpack-compute-1-1] INFO ratpack.requests - 127.0.0.1 - - [30/Jun/2015:11:01:18 -0500] "GET / HTTP/1.1" 200 2
    });
  }
}

See the documentation of RequestLogger for information on creating a logger with an alternative format.

26 Java 9 Support

This is a list of known issues, caveats, and workarounds to use early Java 9+ support. While Java 9 support is far from complete, core functionality appears to work with only a few warnings and messages here and there.

1.26 Known Library Caveats

The following are known version conflicts or requirements for Ratpack to run seamlessly on Java 9.

2.26 Known Issues

The following are issues caused by Java 9 and the underlying components to Ratpack at this time. Workarounds will be provided in each case

3.26 Known Java 9 Errors/Warning messages

The following are messages emitted by Java 9+ that are expected.

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.google.inject.internal.cglib.core.$ReflectUtils$1 (file:[User Gradle Cache Directory]/modules-2/files-2.1/com.google.inject/guice/[version hash]/[guice version jar]]
WARNING: Please consider reporting this to the maintainers of com.google.inject.internal.cglib.core.$ReflectUtils$1
WARNING: Use –illegal-access=warn to enable warnings on further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

11:04:44.477 [main] DEBUG i.n.u.i.PlatformDependent0 - -Dio.netty.noUnsafe: false
11:04:44.477 [main] DEBUG i.n.u.i.PlatformDependent0 - Java version: 11
11:04:44.479 [main] DEBUG i.n.u.i.PlatformDependent0 - sun.misc.Unsafe.theUnsafe: available
11:04:44.479 [main] DEBUG i.n.u.i.PlatformDependent0 - sun.misc.Unsafe.copyMemory: available
11:04:44.480 [main] DEBUG i.n.u.i.PlatformDependent0 - java.nio.Buffer.address: available
11:04:44.483 [main] DEBUG i.n.u.i.PlatformDependent0 - direct buffer constructor: unavailable
java.lang.UnsupportedOperationException: Reflective setAccessible(true) disabled
at io.netty.util.internal.ReflectionUtil.trySetAccessible(ReflectionUtil.java:31)
at io.netty.util.internal.PlatformDependent0$4.run(PlatformDependent0.java:224)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at io.netty.util.internal.PlatformDependent0.(PlatformDependent0.java:218)
at io.netty.util.internal.PlatformDependent.isAndroid(PlatformDependent.java:212)
at io.netty.util.internal.PlatformDependent.(PlatformDependent.java:80)
at io.netty.util.ConstantPool.(ConstantPool.java:32)
at io.netty.util.AttributeKey$1.(AttributeKey.java:27)
at io.netty.util.AttributeKey.(AttributeKey.java:27)
at ratpack.core.server.internal.DefaultRatpackServer.(DefaultRatpackServer.java:69)
at ratpack.core.server.RatpackServer.of(RatpackServer.java:81)
at ratpack.core.server.RatpackServer.start(RatpackServer.java:92)
at ratpack.groovy.GroovyRatpackMain.main(GroovyRatpackMain.java:38)
11:04:44.484 [main] DEBUG i.n.u.i.PlatformDependent0 - java.nio.Bits.unaligned: available, true
11:04:44.485 [main] DEBUG i.n.u.i.PlatformDependent0 - jdk.internal.misc.Unsafe.allocateUninitializedArray(int): unavailable
java.lang.IllegalAccessException: class io.netty.util.internal.PlatformDependent0$6 cannot access class jdk.internal.misc.Unsafe (in module java.base) because module java.base does not export jdk.internal.misc to unnamed module @366647c2
at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:361)
at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:591)
at java.base/java.lang.reflect.Method.invoke(Method.java:558)
at io.netty.util.internal.PlatformDependent0$6.run(PlatformDependent0.java:334)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at io.netty.util.internal.PlatformDependent0.(PlatformDependent0.java:325)
at io.netty.util.internal.PlatformDependent.isAndroid(PlatformDependent.java:212)
at io.netty.util.internal.PlatformDependent.(PlatformDependent.java:80)
at io.netty.util.ConstantPool.(ConstantPool.java:32)
at io.netty.util.AttributeKey$1.(AttributeKey.java:27)
at io.netty.util.AttributeKey.(AttributeKey.java:27)
at ratpack.core.server.internal.DefaultRatpackServer.(DefaultRatpackServer.java:69)
at ratpack.core.server.RatpackServer.of(RatpackServer.java:81)
at ratpack.core.server.RatpackServer.start(RatpackServer.java:92)
at ratpack.groovy.GroovyRatpackMain.main(GroovyRatpackMain.java:38)
11:04:44.485 [main] DEBUG i.n.u.i.PlatformDependent0 - java.nio.DirectByteBuffer.(long, int): unavailable
11:04:44.485 [main] DEBUG i.n.u.internal.PlatformDependent - sun.misc.Unsafe: available

27 Related Projects

This is a non-exhaustive list of 3rd party projects related to Ratpack.

1.27 Example Applications

The following projects are maintained by the Ratpack core team as simple examples.

2.27 Alternative Language Implementations

The following are projects and examples of Ratpack implemented using languages other than Java and Groovy.

3.27 3rd Party Modules

The following 3rd party projects provided additional functionality to Ratpack projects.

28 The Ratpack project

1.28 Credits

Ratpack has been made possible by the following people.

1.1.28 Active project members

The following people are currently actively contributing their time to Ratpack.

2.1.28 Contributors

The following people have provided significant contributions.

3.1.28 Past project members

The following people have contributed their time to Ratpack in the past, but are no longer actively working on it.

2.28 About this manual

1.2.28 Resources

1.1.2.28 Libraries

2.1.2.28 Fonts

Web fonts are served by Google Web Fonts.

3.1.2.28 Images