Installation

Requirements

  • A Java 8 JDK
  • A build tool able to use Maven artifacts.

Quick Start application

The easiest way to try Spincast is to download the Quick Start application, which also can be used as a template to start a new application. This application already has in place most of the boilerplate code suggested to develop a solid and flexible Spincast application.

How to run :

  1. Download the Spincast Quick Start [.zip] application.
  2. Decompress the zip file, go inside the "spincast-quick-start" root directory using a command prompt and run :
    mvn clean package
    This will compile the application and produce an executable .jar file containing an embedded HTTP server.
  3. Start the application using :
    java -jar target/spincast-quickstart-1.0.0-SNAPSHOT.jar
  4. Once the application is running, open http://localhost:44419 in your browser!
  5. The next step would probably be to import the project in your favorite IDE and start debugging it to see how it works. The entry point of a standard Spincast application is the classic main(...) method. You'll find this method in class org.spincast.quickstart.App.

Note that the Quick Start application is not a simple "Hello World!" application. It contains some advanced (but recommended) features, such as a custom Request Context type and a custom WebSocket Context type! To learn Spincast from scratch, you may first want to read the three "Hello World!" tutorials before trying to understand the code of the Quick Start application.

Installing Spincast from scratch

If you want to start from scratch, without using the Quick Start application as a template, you first add the org.spincast:spincast-default:0.9.48 artifact to your pom.xml :

<dependency>
    <groupId>org.spincast</groupId>
    <artifactId>spincast-default</artifactId>
    <version>0.9.48</version>
</dependency>

This artifact installs a set of default plugins and provides all the required components for a Spincast application to be functional.

When this is done, you can follow the instructions of the Bootstrapping your app section to initialize your application. This process is very simple and simply requires you to use the Spincast.init(args) or Spincast.configure() bootstrapper in a main(...) method.

Here's a simple Spincast application :

public class App {

    public static void main(String[] args) {
        Spincast.init(args);
    }

    @Inject
    protected void init(DefaultRouter router, Server server) {
        router.GET("/").handle(context -> context.response().sendHtml("<h1>Hello World!</h1>"));
        server.start();
    }
}

There is a tutorial page for this simple "Hello World!" application. On that page you can download and try the application by yourself!

Spincast overview

Introduction

Spincast is based on the shoulders of a giant, Guice (from Google). Other Java web frameworks may claim they support Guice, and maybe even have a section of their documentation dedicated to the topic. Spincast is one of those that is totally built on Guice, from the ground up! If you already know Guice, Spincast will be really easy to grasp for you.

Guice is not only (in our opinion) the best dependency injection library of the Java ecosystem, but also a fantastic base to build modular applications. Everything is divided into modules which are swappable and overridable. Each module can declare which dependencies it requires from other modules. In fact, Guice is so flexible that you may even find ways of using Spincast we haven't think about!

If you know another dependency injection library, like Spring, it can also help but you'll probably have to learn one of two new tricks!

Here's what using Spincast looks like at a very high level:

Users make requests to your web application. This application can have an HTML interface, built using popular tools like jQuery, React, Ember, Angular, etc. That HTML interface can be generated by Spincast (using a built-in templating engine) or can be a Single Page Application where Spincast is used as a bootstrapper and a data provider (JSON or XML) for the SPA.

Spincast is also a good platform to build REST web services or microservices, without any user interface, since it "talks" JSON and XML natively.

Architecture

Spincast is composed of a core plugin and a set of default plugins. The core plugin is responsible for validating that an implementation for all the components required in a Spincast application has been bound by other plugins. The default plugins provide such default implementations.

You use the Bootstrapper to initialize your application. This bootstrapper will automatically bind the core plugin and the default plugins, but will also let you install extra plugin and custom modules for the components specific to your application :

Request handling

Here's how a request made by a user is handled by Spincast :

First, the embedded HTTP Server receives a request from a user. This server consists of a Server interface and an implementation. The default implementation is provided by the spincast-plugins-undertow plugin which uses the Undertow HTTP/Websocket server.

If the request is for a Static Resource, the server serves it directly without even reaching the framework. Note that it's also possible to generate the resource, using a standard route, so the request does enter the framework. There is even a third option which is what we call Dynamic Resources: if a request is made for a request which currently doesn't exist, the server will pass the request to the framework, the framework can then create the resource and return it... The following requests for the same resource will use the generated resource and won't reach the framework anymore!

If the request is not for a static resource, the server passes it to the core Spincast component : the Front Controller. The Front Controller is at the very center of Spincast! This is one of the very few components that is actually bound by the core plugin itself.

The job of the Front Controller is to:

  • Ask the Router for the appropriate route to use when a request arrives.
  • Call the Route Handlers of the matching route. A route can have many handlers : they are Filters which are run before the Main Handler, the Main Handler itself, and some Filters which are run after the Main Handler.
  • If the request is for a WebSocket connection, the handling of the request is made by a WebSocket Controller instead of a Route Handler. Also, the result is not a simple response but a permanent and full duplex connection where each side can send messages to the other side. Have a look at the WebSockets section for more information!
  • If no matching route is returned by the Router, the Front Controller will use a Not Found route. The Not Found route can be a custom one, or the default one provided by Spincast.
  • If any exception occures during any of those steps, the Front Controller will use an Exception route. The Exception route can be a custom one, or the default one provided by Spincast.

The job of the Router is to determine the appropriate route to use, given the URL of the request, its HTTP method, etc. It will also extract the value of the dynamic path tokens, if any. For example, a route path could be "/user/${userId}/tasks". If a "/user/42/tasks" request is made, the router will extract "42" as the value of the userId parameter and make this available to the rest of the framework.

Finally:

  • The Route Handlers receive a Request Context (which represents the request), and decide what to return as the response. This can be anything: Json, HTML, XML or even bytes.
    or:
  • A WebSocket Controller receives the request for a WebSocket connection, allows or denies it, and then receives and sends messages on that connection.

The main components

The main components are those without which a Spincast application can't even run. Any class, any plugin, can assume those components are available, so they can inject them and use them!

Those main components are all installed by the default plugins. Have a look at those default plugins for a quick overview of the big blocks constituting Spincast!

Spincast is very modular and you can replace any plugin, even the default. But, when you do this, you are responsible for providing any required components this plugin was binding.

Transitive dependencies

The spincast-core Maven artifact only has two direct dependencies which are external to Spincast:

  • Guice, which itself pulls some transitive dependencies, such as "Guava" and "jsr305".
  • SLF4J : The logging facade.

The versions used for those dependencies are defined in the spincast-parent Maven artifact's pom.xml.

Spincast core also uses some Apache commons libraries, but those are shaded, their classes have been relocated under Spincast's org.spincast.shaded package, so they won't conflit with your own dependencies.

That said, each Plugin also adds its own dependencies! If you start with the spincast-default Maven artifact, a bunch of transitive dependencies will be included. If you need full control over the transitives dependencies added to your application, start with the spincast-core Maven artifact and pick, one by one, the plugins and implementations you want to use.

Quick Tutorial

Here, we'll present a quick tutorial on how to develop a Spincast application : as a traditional website or as a SPA application. We won't go into too much details so, to dig deeper, have a look at :

  • The dedicated Demos/Tutorials section of the site.
  • The Quick Start application, which is a fully working Spincast application.
  • The others sections of this documentation!

1. Traditional website

A "traditional website" is a web application where the server generates HTML pages, using a Templating Engine. This is different from the more recent SPA approach, where the interface is generated client side (using javascript) and where the backend only provides REST services (by returning Json or, more rarely these days, XML).

1.1. Bootstrapping

Bootstrapping a Spincast application involves 3 main steps :

  • Using the Bootstrapper to initialize your application. This is where you specify the components to bind and the plugins to install in order to create the Guice context for your application.
  • Defining Routes and Route Handlers. We're going to see those in a minute.
  • Starting the HTTP Server.
Here's a quick example of using the bootstrapper :

Spincast.configure()
        .module(new AppModule())
        .init(args);

Please read the whole section dedicated to bootstrapping for more information about this topic.

The quickest way to start a Spincast application is to download the Quick Start application and to adapt it to your needs.

1.2. Defining Routes

You define some Routes and you specify which Route Handlers should handle them. The Route Handlers are often methods in a controller but can also be defined inline, directly in the Route definitions.

The Routes definitions can be all grouped together in a dedicated class or can be defined in controllers (have a look at The Router is dynamic for an example).

You can learn more about the various routing options in the Routing section, but here's a quick example of Route definitions :

// For a GET request. Uses a method reference
// to target a controller method as the Route Handler :
router.GET("/books/${bookId}").handle(bookController::booksGet);
       
// For any HTTP request. Uses an inine Route Handler :
router.ALL("/hello").handle(context -> context.response().sendPlainText("Hello!"));

1.3. Route Handlers

Most of the time, a Route Handler is implemented as a method in a controller. It receives a Request Context object as a parameter. This Request Context is extensible and is one of the most interesting parts of Spincast! In this quick example, we simply use the default Request Context implementation, "DefaultRequestContext" :

public class BookController {

    // Route Handler dedicated to handle GET requests
    // for a book : "/books/${bookId}"
    public void booksGet(DefaultRequestContext context) {
        // ...
    }
}

1.4. Getting information about the request

In your Route Handlers, you use the Request Context object and its various add-ons to get the information you need about the current request :

public void booksGet(DefaultRequestContext context) {

    // Path parameter
    // From "/books/${bookId}" for example
    String bookId = context.request().getPathParam("bookId");

    // QueryString parameter
    String page = context.request().getQueryStringParamFirst("page");
    
    // Field received from a POSTed form
    String newTitle = context.request().getFormData().getString("newTitle");

    // HTTP Header
    String authorizationHeader = context.request().getHeaderFirst("Authorization");

    // Cookie
    String localeCookieValue = context.request().getCookie("locale");

    //...
}

1.5. Building the response's model

You process the current request using any business logic you need, and you build the model for the response. This response model is a JsonObject accessible via "context.response().getModel()" : it is the object where you store all the information you want to return as the response.

You may add to this response model the variables you want your templates to have access to :

public void booksGet(DefaultRequestContext context) {
    
    //...
    
    JsonObject book = context.json().create();
    book.set("author", "Douglas Adams");
    book.set("title", "The Hitchhiker's Guide to the Galaxy");
    
    // Adds the book to the response model
    context.response().getModel().set("book", book);
    
    //...
}

1.6. Rendering the response model using a template

When you develop a traditional website, you usually want to render a template so HTML is going to be displayed. To do so, you use the integrated Templating Engine :

public void booksGet(DefaultRequestContext context) {
    
    //... builds the response model
    
    // Sends the response model as HTML, using a template
    context.response().sendTemplateHtml("/templates/book.html");
}

1.7. Writing the template

Here is a template example using the syntax of the default Templating Engine, Pebble. Notice that the variables we added to the response model are available.

{% if book is not empty %}
    <div class="book">
        <h2>{{book.title}}</h2>
        <p>Author : {{book.author}}</p>
    </div>
{% else %}
    <div>
        Book not found!
    </div>
{% endif %}

2. SPA / REST services

The main difference between a SPA application (or a set of plain REST services) and a traditional website, is that in a SPA you don't generate HTML server side. Instead, most of the logic is client-side, and your Spincast application only acts as a provider of REST services to which your client-side application talks using Json or, more rarely these days, XML.

2.1. Bootstrapping

Bootstrapping a Spincast application involves 3 main steps :

  • Using the Bootstrapper to initialize your application. This is where you specify the components to bind and the plugins to install in order to create the Guice context for your application.
  • Defining Routes and Route Handlers. We're going to see those in a minute.
  • Starting the HTTP Server.
Here's a quick example of using the bootstrapper :

Spincast.configure()
        .module(new AppModule())
        .init(args);

Please read the whole section dedicated to bootstrapping for more information about this topic.

The quickest way to start a Spincast application is to download the Quick Start application and to adapt it to your needs.

2.2. Defining Routes

You define some Routes and you specify which Route Handlers should handle them. The Route Handlers are often methods in a controller but can also be defined inline, directly on the Route definitions.

The Routes definitions can be all grouped together in a dedicated class or can be defined in controllers (have a look at The Router is dynamic for an example).

In general, if you are building a SPA, you want to return a single HTML page : that index page is going to load .js files and, using those, will bootstrap your client-side application. Using Spincast, you can return that index page as a Static Resource, or you can generate it using a template. Let's first see how you could return the index page as a Static Resource :

// The static "index.html" page that is going to bootstrap
// our SPA
router.file("/").classpath("/index.html").handle();

// The resources (.js, .css, images, etc.) will 
// be located under the "/public" path :
router.dir("/public").classpath("/myResources").handle();

// ... the REST endpoints routes

As you can see, Spincast will return the "index.html" file when a "/" request is made. In this HTML page, you are going to load all the required resources (mostly .js files first), and bootstrap your whole application.

You can also use a template to generate the first index page. This allows you to dynamically tweak it, to use variables. Here's an example :

// Inline Route Handler that evaluates
// a template to generate the HTML index page.
router.GET("/").handle(context -> {
    
    // Adds some variables to the response model so
    // the template has access to them.
    context.response().getModel().set("randomQuote", getRandomQuote());
    
    // Renders the template
    context.response().sendTemplateHtml("/index.html"); 
});

// The resources (.js, .css, images, etc.) will 
// be located under the "/public" path :
router.dir("/public").classpath("/public").handle();

// ... the REST endpoints routes

By using such template to send your index page, you have access to all the functionalities provided by the Templating Engine. Note that if your template is quite complexe, you're probably better creating a controller to define the Route Handler, instead of defining it inline like in our example!

Once the Route for the index page and those for the resources are in place, you add the ones required for your REST endpoints. For example :

// Endpoint to get a book
router.GET("/books/${bookId}").handle(bookController::booksGet);

// Endpoint to modify a book
router.POST("/books/${bookId}").handle(bookController::booksModify);
        
// ...

2.3. Route Handlers

Most of the time, a Route Handler is implemented as a method in a controller. It receives a Request Context object as a parameter. This Request Context is extensible and is one of the most interesting parts of Spincast! In this quick example, we simply use the default Request Context implementation, "DefaultRequestContext" :

public class BookController {

    // Route Handler dedicated to handle GET requests
    // for a book : "/books/${bookId}"
    public void booksGet(DefaultRequestContext context) {
        // ...
    }
}

2.4. Getting information about the request

In your Route Handlers, you use the Request Context object and its various add-ons to get the information you need about the current request (an AJAX request for example) :

public void booksGet(DefaultRequestContext context) {

    // The Json body of the request as a JsonObject
    JsonObject jsonObj = context.request().getJsonBody();

    // Path parameter
    // From "/books/${bookId}" for example
    String bookId = context.request().getPathParam("bookId");

    // HTTP Header
    String authorizationHeader = context.request().getHeaderFirst("Authorization");

    // Cookie
    String localeCookieValue = context.request().getCookie("locale");

    //...
}

Very often in a SPA application, or when you develop plain REST services, you are going to receive a Json object as the body of a request (with a "application/json" content-type). In the previous code snippet, context.request().getJsonBody() gets that Json from the request and creates a JsonObject from it so it is easy to manipulate.

2.5. Creating and sending a Json / XML response

When you receive a request, you process it using any required business logic, and you then build the Json (or XML) object to return as a response. There are two ways to achieve that. The prefered approach, is to create a typed object, a book created from a Book class for example, and explicitly send this entity as Json. For example :

public void booksGet(DefaultRequestContext context) {

    String bookId = context.request().getPathParam("bookId");
    Book someBook = getBook(bookId);
    
    context.response().sendJson(someBook);
}

The second option, probably more useful for traditional websites though, is to use the response model to dynamically create the Json object to send.

You get the response model as a JsonObject by calling the context.response().getModel() method, you add elements to it and you send it as Json :

public void booksGet(DefaultRequestContext context) {

    // Gets the response model
    JsonObject responseModel = context.response().getModel();

    // Gets a book
    String bookId = context.request().getPathParam("bookId");
    Book someBook = getBook(bookId);

    // Adds the book to the response model, using
    // the "data.book" key
    responseModel.set("data.book", book);

    // Adds a "code" element to the response model
    responseModel.set("code", AppCode.APP_CODE_ACCEPTED);

    // Adds a timestamp to the response model
    responseModel.set("timestamp", new Date());
    
    // This is going to send the response model as Json
    context.response().sendJson();
}

In this example, the generated Json response would have a "application/json" content-type and would look like this :

{
    "code" : 12345,
    "timestamp" : "2016-11-06T22:58+0000",
    "data" : {
        "book" : {
            "author" : "Douglas Adams",
            "title" : "The Hitchhiker's Guide to the Galaxy"
        }
    }
}

2.6. Consuming a Json / XML response

You consume the Json response from your client-side SPA application whatever it is built with : Angular, React, Vue.js, Ember, etc. Of course, we won't go into details here since there are so many client-side frameworks!

A Json response can also be consumed by a client which is not a SPA : it can be a response for a Ajax request made using Jquery or plain javascript. Such Json response can also be consumed by a backend application able to send HTTP requests.

Bootstrapping your app

Bootstrapping a Spincast application is very easy. Most of the time, you start with the spincast-default Maven artifact in your pom.xml (or build.gradle) :

<dependency>
    <groupId>org.spincast</groupId>
    <artifactId>spincast-default</artifactId>
    <version>0.9.48</version>
</dependency>

Then, in the main(...) method of your application, you use the Spincast class to initialize your application. You can do this the "quick way", or use the Bootstrapper to have more options. Let see both of those approaches...

Quick initialization

The quickest way to initialize a Spincast application is to call Spincast.init(args) :

public class App {

    public static void main(String[] args) {
        Spincast.init(args);
    }
    
    // ...
}

This will create a Guice context using all the default plugins, will bind the current App class itself in that context (as a singleton) and will load the App instance. You then simply have to add an init method to your App class to define Routes, add some logic, and start the HTTP Server :

public class App {

    public static void main(String[] args) {
        Spincast.init(args);
    }
    
    @Inject
    protected void init(DefaultRouter router, Server server) {
        router.GET("/").handle(context -> context.response().sendHtml("<h1>Hello World!</h1>"));
        server.start();
    }
}

This is a simple, but totally functional Spincast application! There is a demo page for this very example. On that page, you can download the sources and run the application by yourself.

Finally, note that Spincast.init(args) in fact creates a default Bootstrapper under the hood. We will now see how you can configure this bootstrapper explicitly to have more control over your application initialization...

The Bootstrapper

In most cases, you need more control than simply calling Spincast.init(args). You want to be able to add custom modules to the Guice context, to add extra plugins, etc.

You do so by using Spincast.configure() instead of Spincast.init(args). This starts a bootstrapper to help configure your application before it is started. Let's see an example :

public static void main(String[] args) {

    Spincast.configure()
            .module(new AppModule())
            .plugin(new SpincastHttpClientPlugin())
            .requestContextImplementationClass(AppRequestContextDefault.class)
            .init(args);
    //....
}

Explanation :

  • 3 : We start the bootstrapper by calling .configure() instead of .init(args)!
  • 4 : We add a custom Guice module so we can bind our application components.
  • 5 : We add an extra plugin.
  • 6 : We tell Spincast that we are using a custom Request Context type.
  • 7 : We finally call .init(args) so the Guice context is created, the current class is bound and then loaded. We also use this method to bind the arguments received in the main(...) method to the Guice context.

Bootstapper's options :
Let's now see the bootstrapper's options (Note that none of them is mandatory, except requestContextImplementationClass(...) if you are using a custom Request Context type and websocketContextImplementationClass(...) if you are using a custom WebSocket Context type).

  • module(...) : Adds a Guice module. It can be called multiple time to add more than one module. All the modules added using this method are going to be combined together.
  • plugin(...) : To register a plugin. You can add multiple plugins (in addition to the default ones). They will be applied in the order they are added to the bootstrapper.
  • disableAllDefaultPlugins() : Disables all the default plugins, including the core one. If you think about using this method, you should probably start with the spincast-core artifact instead of spincast-default.
  • disableDefaultXXXXXXXPlugin() : Disables a default plugin. There is a version of this method for every default plugin. If you disable a default plugin, you are responsible for binding the required components the plugin was in charge of!
  • requestContextImplementationClass(...) : Tells Spincast that you are using a custom Request Context type. You need to pass as a parameter the implementation class of your custom Request Context type. Calling this method is mandatory if you are using a custom Request Context type!
  • websocketContextImplementationClass(...) : Tells Spincast that you are using a custom WebSocket Context type. You need to pass as a parameter the implementation class of your custom WebSocket Context type. Calling this method is mandatory if you are using a custom WebSocket Context type!
  • bindCurrentClass(...) : By default, the class in which the bootstrapper is created is automatically bound in the Guice context (as a singleton) and its instance is loaded when the context is ready. To disable this, you can call bindCurrentClass(false).
  • appClass(...) : You can specify which class should be automatically bound and loaded when the Guice context is ready. Calling this method will disable the binding of the current class (as calling bindCurrentClass(false) would do).
  • getDefaultModule(...) : Allows you to get the Guice module resulting from all the default plugin. You can use this (in association with disableAllDefaultPlugins() and module(...)) to tweak the Guice module generated by the default plugins.

Various bootstrapping tips

  • Have a look at the code of the Quick Start application to see how it is bootstrapped. Also read the advanced version of the Hello world! tutorial.
  • The bootstrapping process is all about creating a regular Guice context, so make sure you read Guice documentation if you didn't already!
  • Be creative! For example, you could make the App class extend SpincastConfigDefault so you can override some default configurations right in that class! Everything in Spincast is based on dependency injection so you can easily replace/extend pretty much anything you want.
  • Split your application in controllers, services, repositories and utilities and inject the components you need using the standard @Inject annotation. Don't put everything in the App class, except if your application is very small.
  • Don't forget to register your implementation classes if you are using a custom Request Context type or a custom Request Context. You do this using the requestContextImplementationClass(...) method and the websocketContextImplementationClass(...) method on the Bootstrapper.
  • Remember that by using the Quick Start application as a template, pretty much everything discussed here has already been implemented for you! Simply load the code in your favorite IDE, and start adjusting the code to meet the needs of your application.

Using spincast-core directly

This is an advanced topic that most applications will never need.

If you need total control over how your application is built, you can decide to start without the default plugins and pick, one by one, which one to add.

By using "spincast-default" you add the default plugins as Maven artifact but also a lot of transitive dependencies. For example, dependencies for some Jackson artifacts are added by the default Spincast Jackson Json plugin. Those dependencies may conflict with other dependencies you use in your application. This is a situation where you may want to start without the default plugins.

To start a Spincast application from scratch, start with the "spincast-core" Maven artifact instead of "spincast-default":

<dependency>
    <groupId>org.spincast</groupId>
    <artifactId>spincast-core</artifactId>
    <version>0.9.48</version>
</dependency>

Doing so, you start with the core code but you need to provide an implementation for all the required components, by yourself. You generaly provide those implementations by choosing and installing some plugins by yourself.

For example, to provide an implementation for the Server and for the TemplatingEngine components, you could use:

<dependency>
    <groupId>org.spincast</groupId>
    <artifactId>spincast-plugins-undertow</artifactId>
    <version>0.9.48</version>
</dependency>

<dependency>
    <groupId>org.spincast</groupId>
    <artifactId>spincast-plugins-pebble</artifactId>
    <version>0.9.48</version>
</dependency>

// ...

Note that by starting without spincast-default, you don't have access to the Bootstrapper! You'll have to create the Guice context by yourself, using the modules provided by the different plugins.

If you fail to provide an implementation for a component that would be bound by a default plugin, you will get this kind of error when trying to start your application :

> ERROR - No implementation for org.spincast.server.Server was bound.

Configuration

Spincast doesn't force you to configure your application in a specific way, but does suggest a strategy. The only requirement is that in order to modify the configurations used by the internals of Spincast itself (for example the port the server is going to be started with), you need to bind a custom implementation for the SpincastConfig interface. Spincast retrieves the values to use for its configurations through this interface.

If you don't bind a custom implementation for that SpincastConfig interface, a default one will be used and will provide default values.

Configuration strategy - introduction

The strategy we suggest to configure your application allows you to both modify the default configurations and add specific configurations to your application in a single location. This strategy involves creating a standard Java class with getter methods for each configuration that is needed.

Compared to a simple .properties based configuration strategy, a class based one requires more work (since you do have to define a getter method for each configuration), but comes with three big advantages :

  • The configurations are typed, preventing many errors :

    // Doesn't compile! The "getServerHost()" getter returns a String.
    int port = configs.getServerHost();

    Compare this nice compile time error to a simple .properties based configuration that will fail at runtime :

    // Compiles... But boom at runtime!
    int port = (int)properties.get("server.host");

  • The creation of a configuration value can involve complex logic (caching the generated value is easy to implement too).

    // Some configuration getter...
    public int getHttpServerPort() {
    
    	// We use another configuration to create the value
    	// of this one! You can use any logic you need...
    	if("local".equals(getEnvironmentName())) {
    		return 12345;
    	} else {
    		return 80;
    	}
    }

  • A configuration can be of any type, not only String, Booleans and Numbers.

    // A configuration getter that returns a File object
    public File getSpincastWritableDir() {
    	// ...
    }

Configuration strategy - components

The first step is to create a custom AppConfig interface that extends SpincastConfig :


public interface AppConfig extends SpincastConfig {

    /**
     * An app specific configuration
     */
    public String getSomeAppConfiguration();

    /**
     * Another app specific configuration
     */
    public int getAnotherAppConfiguration();
}

And then create an implementation that implements your custom interface and extends the Spincast provided SpincastConfigDefault implementation :


public class AppConfigDefault extends SpincastConfigDefault implements AppConfig {

    /**
     * An app specific configuration
     */
    public String getSomeAppConfiguration() { ... }

    /**
     * Another app specific configuration
     */
    public int getAnotherAppConfiguration() { ... }
    
    /**
     * Overrides a default Spincast configuration too!
     */
    @Override
    public int getHttpServerPort() {
        return 12345;
    }
}

Finally, you add the associated bindings to your Guice module :

public class AppModule extends SpincastGuiceModuleBase {

    @Override
    protected void configure() {

        bind(AppConfig.class).to(AppConfigDefault.class).in(Scopes.SINGLETON);
        
        //...
    }
}

Note that Spincast will detect that a custom implementation of the SpincastConfig interface has been bound, and will automatically adjust the binding for this interface. You can bind SpincastConfig to AppConfigDefault by yourself if you want, but it is not required.

Have a look at the configuration of this very website for an example of how this strategy looks like!

Be careful with the dependencies you inject in your implementation class : the configurations are used by a lot of other components and it is therefore easy to create circular dependencies. One dependency that you can inject without any problem, and that is often useful in a configuration class, is the application arguments.

Configuration strategy - implementation

By using the strategy above, so by extending the SpincastConfigDefault base class, you also extend the ConfigFinder base class and get access to a lot of useful features to help you build your configuration. In particular, you gain access to an easy way to externalize the values of your configurations (ie : have different configurations depending on the environment the application runs on).

We'll see in the next section, Configuring the config plugin, that the way Spincast searches for external configurations is fully configurable.

Making configurations externalizable

To make configurations externalizable, the first thing to do in your implementation class is to remove any hardcoded values and, instead, use the provided getters. Those special getters are provided by the ConfigFinder class, from which SpincastConfigDefault extends. There are multiple getters, depending on the type of the configuration.

For example, in your implementation class, instead of this hardcoded value :

public class AppConfigDefault extends SpincastConfigDefault implements AppConfig {

    @Inject
    public AppConfigDefault(SpincastConfigPluginConfig spincastConfigPluginConfig) {
        super(spincastConfigPluginConfig);
    }

	/**
	 * The HTTP server port to use
	 */
    @Override
    public int getHttpServerPort() {
    	
    	// Hardcoded value!
        return 12345;
    }
    
    // ...
}

You would instead use the provided getInteger(...) method, so the "port" configuration is externalized :

public class AppConfigDefault extends SpincastConfigDefault implements AppConfig {

    @Inject
    public AppConfigDefault(SpincastConfigPluginConfig spincastConfigPluginConfig) {
        super(spincastConfigPluginConfig);
    }

	/**
	 * The HTTP server port to use
	 */
    @Override
    public int getHttpServerPort() {
    
    	// Makes this configuration externalizable
    	// and provides a default value in case no
    	// external value is found.
        return getInteger("server.port", 12345);
    }
    
    // ...
}

By using the special getters provided by the ConfigFinder base class, your configuration is now externalized. A getter is provided for all common types : String, Boolean, Integer, Long, BigDecimal and Date.

Note that date configuration values must be using a valid ISO-8601 format.

The sources of configuration values

Spincast will load externalized configurations from various sources, each source overriding the previous one, if a same configuration is found in both :

  1. If you don't override a Spincast configuration defined in SpincastConfig, the default value hardcoded in SpincastConfigDefault will be used.
  2. If you override a Spincast configuration, but you hardcode it in your implementation class, the configuration is not externalizable and the hardcoded value will be used.
  3. An app-config.yaml file is looked for on the classpath. This is where you generally will place the default values of your externalizable configurations.
  4. If your application is running from an executable .jar, Spincast will check if a app-config.yaml file exists next to it. If your application is not running from an executable .jar (for example it is launched in an IDE), Spincast will check if a app-config.yaml file exists at the root of the project.
  5. Environnement variables will be checked to see if some configurations are defined there. An environment variable must start with "app." to be considered as a configuration for a Spincast application. This prefix is configurable.
  6. System properties will be checked to see if some configurations have been passed to the application when launched. An system property must start with "app." to be considered as a configuration for a Spincast application. This prefix is configurable. System properties have the highest priority and overrides any existing configurations (except of course for hardcoded/non-externalized configurations).

Both environment variables and system properties can have multiple prefixes. In association with the feature that can strip those prefixes when getting the configurations (see next section), this allows you to define variables for more than one application on the same server. For example, you could have those environment variables :

  • app1.admin.email.address = user1@example.com
  • app2.admin.email.address = user2@example.com
  • common.admin.email.format = html
By configuring the environment variable prefixes of a first application as being "app1" and "common", and the prefixes of a second application as being "app2" and "common", you can have both application specific variables and common variables.

Configuration file example

Here's an example of a app-config.yaml file that could be used as a source of externalized configurations :

app:
	name: My Super app
	  
	api:
	  base: https://www.myApp.com
	  
	databases:
	  bd1:
	    host: dbHost.com
	    port: 12345
	    username: myDbUser
	    password: # Empty! Must be provided at runtime...

Then in your AppConfigDefault class, you could access the port for the "db1" database using :

@Override
public int getDb1Port() {
    return getInteger("app.databases.bd1.port");
}

In this example, the password for the "db1" database will have to be defined as an environment variable, or using any other mechanism that doesn't require the password to be defined as plain text and be committed to your version control system (which would be a really bad idea)! Since the configuration values are retrieved using standard Java methods, you can implement any mechanism you want in order to retrieve such "secret" configurations.

Configuring the config plugin

The steps described in the sources of configuration values section are configurable. You configure the way the Spincast Config plugin works by binding a custom implementation of the SpincastConfigPluginConfig interface. If you don't bind a custom implementation for this interface, a default implementation, SpincastConfigPluginConfigDefault, will be used.

Those are the methods you can tweak :

  • String getClasspathFilePath()
    The path to a configuration file to load from the classpath.

    Defaults to "app-config.yaml". This means you can simply create that file in your project's /src/main/resources/ folder and it will be used.

    @return the classpath path or null to disable this configuration source.
  • String getExternalFilePath()
    The path to a configuration file to load from the file system.

    The path can be relative or absolute. Spincast will check this using :

    File configFile = new File(thePath);
    if(configFile.isAbsolute()) {
        // ...
    }

    If the path is relative, it is from the executable .jar or, if not run from a .jar, from the root of the project.

    Defaults to "app-config.yaml".

    @return the path or null to disable this configuration source.
  • List<String> getEnvironmentVariablesPrefixes()
    The allowed prefixes an environment variable can have to be used as a configuration.

    Defaults to "app." only.

    @return the allowed prefixes or null to disable environment variables as a source for configurations.
  • boolean isEnvironmentVariablesStripPrefix()
    Should the prefix of an environment variable be stripped? For example, if environmentVariablesPrefixes() indicates that "app." is an environment variable prefix, then "app.admin.email" will result in a "admin.email" key.

    Note that each environment variable key must be unique once the prefixes are stripped, otherwise an exception will be thrown when the application starts!

    Defaults to false.

  • List<String> getSystemPropertiesPrefixes()
    The allowed prefixes a system property can have to be used as a configuration.

    Defaults to "app." only.

    @return the allowed prefixes or null to disable system properties as a source for configurations.
  • boolean isSystemPropertiesStripPrefix()
    Should the prefix of an system property be stripped? For example, if systemPropertiesPrefixes() indicates that "app." is an system property prefix, then "app.admin.email" will result in a "admin.email" key.

    Note that each system properties key must be unique once the prefixes are stripped, otherwise an exception will be thrown when the application starts!

    Defaults to false.

  • boolean isExternalFileConfigsOverrideEnvironmentVariables()
    If an external configuration file is used and environment variables too, should configurations from the file override those from environment variables?

    The default is false : environment variables have priority.

  • boolean isThrowExceptionIfSpecifiedClasspathConfigFileIsNotFound()
    Should an exception be thrown if a classpath config file is specified (is not null) but is not found.

    If set to false, a message will be logged but no exception will be thrown.

    Defaults to false.

  • boolean isThrowExceptionIfSpecifiedExternalConfigFileIsNotFound()
    Should an exception be thrown if an external config file is specified (is not null) but is not found.

    If set to false, a message will be logged but no exception will be thrown.

    Defaults to false.

Core Configurations

To know all the core configurations required by Spincast, have a look at the SpincastConfig javadoc. Here, we're simply going to introduce the most important ones, and their default value :

  • getPublicUrlBase() : This configuration is very important and you should override it in your application and adjust it from environment to environment! It tells Spincast what is the base public URL used to reach your application. For example, your application may be accessed using a URL such as http://www.example.com but can in fact be behind a reverse-router and actually started on the "localhost" host and on port "12345". The problem is that the public base URL ("http://www.example.com") can't be automatically found, but Spincast still requires it to :
    • Generate an absolute URL for a link to provide to the user.
    • Set a cookie using the appropriated domain.

    By default, the getPublicUrlBase() configuration will be "http://localhost:44419". This default can be used for development purposes, but very should be changed when releasing to another environment.

    It is so important to override this configuration that Spincast has a validation in place : when an application starts, an exception will be thrown if those conditions are all meet :

    • The environment name is not "local".
    • isDevelopmentMode() configuration returns false.
    • The public host is still "localhost".
    In other words, Spincast tries to catch the case where an application is running anywhere else than locally, without the default public base URL ajusted.

    Note that you can disable this startup validation using the isValidateLocalhostHost() configuration.

  • getServerHost() : The host/IP the HTTP Server will listen on. The default is 0.0.0.0, which means the Server will listen on any IP.
  • getHttpServerPort() : The port the Server will listen to for HTTP (unsecure) requests. If <= 0, the Server won't listen on HTTP requests.
  • getHttpsServerPort() : The port the Server will listen to for HTTPS (secure) requests. If <= 0, the Server won't listen on HTTPS requests. If you use HTTPS, you also have to provide some extra configurations related to the SSL certificate to use.
  • isDevelopmentMode() : If true, a development environment is taken for granted, and internal error messages may be displayed publicly, no cache will be used for the templates, etc. The default is true, so make sure you change this to false before deploying to production!

The Request Context

The Request Context is the object associated with the current request that Spincast passes to your matching Route Handlers. Its main purpose is to allow you to access information about the request, and to build the response to send.

Those functionalities are provided by simple methods, or by add-ons. What we call an "add-on" is an intermediate class containing a set of methods made available through the Request Context parameter. Here's an example of using the routing() add-on :

public void myHandler(DefaultRequestContext context) {

    if(context.routing().isNotFoundRoute()) {
        //...
    }
}

This routing() add-on is available to any Route Handler, via its Request Context parameter, and provides a set of utility methods.

Here are some add-ons and some standalone methods available by default on a Request Context object :

public void myHandler(DefaultRequestContext context) {

    // Accesses the request information
    String name = context.request().getPathParam("name");

    // Sets the response
    context.response().sendPlainText("Hello world");

    // Gets information about the routing process and the current route
    boolean isNotFoundRoute = context.routing().isNotFoundRoute();

    // Gets/Sets request-scoped variables
    String someVariable = context.variables().getAsString("someVariable");

    // Direct access to the Json manager
    JsonObject jsonObj = context.json().create();

    // Direct access to the XML manager
    JsonObject jsonObj2 = context.xml().fromXml("<someObj></someObj>");

    // Direct access the Guice context
    SpincastUtils spincastUtils = context.guice().getInstance(SpincastUtils.class);

    // Direct access to the Templating Engine
    Map<String, Object> params = new HashMap<String, Object>();
    params.set("name", "Stromgol");
    context.templating().evaluate("Hello {{name}}", params);
    
    // Gets the best Locale to use for the current request
    Locale localeToUse = context.getLocaleToUse();
    
    // Gets the best TimeZone to use for the current request
    TimeZone timeZoneToUse = context.getTimeZoneToUse();
    
    // Sends cache headers
    context.cacheHeaders().cache(3600);
    
    // ...
}

Again, the main job of the Request Context is to allow the Route Handlers to deal with the request and the response. But it's also an extensible object on which various functionalities can be added to help the Route Handlers do their job! Take the "templating()" add-on, for example:

public void myRouteHandler(DefaultRequestContext context) {

    Map<String, Object> params = new HashMap<String, Object>();
    params.set("name", "Stromgol");
    String content = context.templating().evaluate("Hi {{name}}!", params);

    // Do something with the evaluated content...
}

The templating() add-on does not directly manipulate the request or the response. But it still provides a useful set of methods for the Route Handlers.

If you have experience with Guice, or with dependency injection in general, you may be thinking that we could simply inject a TemplatingEngine instance in the controller and access it that way :

public class AppController {

    private final TemplatingEngine templatingEngine;

    @Inject
    public AppController(TemplatingEngine templatingEngine) {
        this.templatingEngine = templatingEngine;
    }

    protected TemplatingEngine getTemplatingEngine() {
        return this.templatingEngine;
    }

    public void myRouteHandler(DefaultRequestContext context) {

        Map<String, Object> params = new HashMap<String, Object>();
        params.set("name", "Stromgol");
        String content = getTemplatingEngine().evaluate("Hi {{name}}!", params);

        // Do something with the evaluated content...
    }
}

The two versions indeed lead to the exact same result. But, for functionalities that are often used inside Route Handlers, or for functionalities that should be request scoped, extending the Request Context can be very useful.

Imagine a plugin which job is to manage authentification and autorization. Wouldn't it be nice if this plugin could add some extra functionalities to the Request Context object? For example :

public void myHandler(ICustomRequestContext context) {

    if(context.auth().isAuthenticated()) {
        String username = context.auth().user().getUsername();
        // ...
    }
}

There is some boilerplate code involved to get such custom Request Context type but, when it's in place, it's pretty easy to tweak and extend. In fact, we highly recommend that you use a custom Request Context as soon as possible in your application. That way, you will be able to easily add add-ons when you need them.

If you use the Quick Start as a start for your application, a custom Request Context type is already provided. But if you start from scratch, an upcoming section will show you how to extend the default Request Context type, by yourself.

The default add-ons

There are add-ons which are always available on a Request Context object, in any Spincast application. Let's have a quick look at them :

  • RequestRequestContextAddon<R> request()

    The request() add-on allows access to information about the current request: its body, its headers, its URL, etc. The default implementation, SpincastRequestRequestContextAddon, is provided by the Spincast Request plugin. Check this plugin's documentation for all the available API.

    Examples :

    // Gets the request full URL
    String fullUrl = context.request().getFullUrl();
    
    // Gets the request body as a JsonObject
    JsonObject body = context.request().getJsonBody();
    
    // Gets a HTTP header
    String authorization = context.request().getHeaderFirst(HttpHeaders.AUTHORIZATION);
    
    // Gets a cookie value
    String sesionId = context.request().getCookie("sess");
    
    // Gets a queryString parameter
    String page = context.request().getQueryStringParamFirst("page");
    
    // Gets the value of a dynamic path token.
    // For example for the route "/users/${userId}"
    String userId = context.request().getPathParam("userId");

  • ResponseRequestContextAddon<R> response()

    The response() add-on allows you to build the response : its content, its content-type, its HTTP status, its headers. The default implementation, SpincastResponseRequestContextAddon, is provided by the Spincast Response plugin. Check this plugin's documentation for all the available API.

    Examples :

    // Sets the status code
    context.response().setStatusCode(HttpStatus.SC_FORBIDDEN);
    
    // Sets a HTTP header value
    context.response().setHeader(HttpHeaders.CONTENT_LANGUAGE, "en");
    
    // Sets the content-type
    context.response().setContentType(ContentTypeDefaults.JSON.getMainVariation());
    
    // Sets a cookie
    context.response().setCookie("locale", "en-US");
    
    // Permanently redirects to a new url (the new url
    // can be absolute or relative). A Flash message
    // can be provided.
    context.response().redirect("/new-url", true, myFlashMessage);
    
    // Adds an element to the response model
    context.response().getModel().set("name", "Stromgol");
    
    // Sends the response model as Json
    context.response().sendJson();
    
    // Sends some bytes
    context.response().sendBytes("Hello World".getBytes("UTF-8"));
    
    // Sends a specific object as Json
    context.response().sendJson(user);
    
    // Sends HTML evaluated from a template, using the response 
    // model to provide the required variables
    context.response().sendHtmlTemplate("/templates/user.html");

  • RoutingRequestContextAddon<R> routing()

    The routing() add-on allows you to get information about the current routing process.

    Examples :

    // Gets all the matches returned by the Router.
    IRoutingResult<DefaultRequestContext> routingResult = context.routing().getRoutingResult();
    
    // Gets the current match : the Route Handler, its position
    // and its parsed path parameters.
    RouteHandlerMatch<DefaultRequestContext> currentMatch =
            context.routing().getCurrentRouteHandlerMatch();
    
    // Is the current Route a "Not found" one?
    boolean isNotFoundRoute = context.routing().isNotFoundRoute();
    
    // Are we currently on a Route to handle an exception?
    boolean isExceptionHandling = context.routing().isExceptionRoute();

  • TemplatingRequestContextAddon<R> templating()

    The templating() add-on gives access to the Templating Engine functionalities.

    Examples :

    Map<String, Object> params = new HashMap<>();
    params.set("name", "Stromgol");
    
    // Evaluation of inline content 
    String html = context.templating().evaluate("Hello {{name}}", params);
    
    // Evaluation of a template file
    String html = context.templating().fromTemplate("/templates/user.html", params);

  • VariablesRequestContextAddon<R> variables()

    The variables() add-on allows you to add variables which are request scoped. They will only be available to the components accessing the current request. They are a good way to make a Route Handler communicate some informations to others.

    Examples :

    // Gets a request scoped variable as a JsonObject.
    JsonObject info = context.variables().getAsJsonObject("someObjectName");
    
    // Gets a request scoped variable as a String.
    String info = context.variables().getAsString("someKey");
    
    // Adds a new request scoped variable 
    context.variables().set("someKey", "someValue");
    

  • CacheHeadersRequestContextAddon<R> cacheHeaders()

    The cacheHeaders() add-on allows you to validate the HTTP cache headers sent by the client and to add such headers for the requested resource. Have a look at the HTTP Caching section for more information.

    Examples :

    // Tells the client to cache the resource for 3600 seconds
    context.cacheHeaders().cache(3600);
    
    // Tells the client to disable any cache for this resource
    context.cacheHeaders().noCache();
    
    // ETag and last modification date validation
    if(context.cacheHeaders().eTag(resourceEtag).lastModified(modifDate).validate(true)) {
        return;
    }

  • JsonManager json()

    Provides easy access to the JsonManager,
    for Json related methods.

  • XmlManager xml()

    Provides easy access to the XmlManager,
    for XML related methods.

  • Injector guice()

    Provides easy access to the Guice context of the application.

  • <T> T get(Class<T> clazz)

    Shortcut to get an instance from the Guice context. Will also cache the instance (as long as it is request scoped or is a singleton).

  • Locale getLocaleToUse()

    The best Locale to use for the current request, as found by the LocaleResolver.

  • TimeZone getTimeZoneToUse()

    The best TimeZone to use for the current request, as found by the TimeZoneResolver.

  • Object exchange()

    The underlying "exchange" object, as provided by the HTTP Server.
    If you know for sure what the implementation of this object is, you may cast it to access extra functionalities not provided by Spincast out of the box.

Extending the Request Context

Extending the Request Context is probably to most advanced thing to learn about Spincast. Once in place, a custom Request Context is quite easy to adjust and extend, but the required code to start may be somewhat challenging. This is why we recommend that you start your application with the Quick Start! This application already contains a custom Request Context type, so you don't have to write the bootstrapping code by yourself! But if you start from scratch or if you are curious about how a custom Request Context type is possible, keep reading.

First, let's quickly repeat why we could want to extend the default Request Context type... There may be a "translate(...)" method on some class and we frequently use it by our various Route Handlers. Let's say this is a method helping translate a sentence from one language to another.

Instead of injecting the class where this method is defined each time we need to use it, wouldn't it be nice if we would have access to it directly from a Request Context object? For example:

public class AppController {

    public void myRouteHandler(AppRequestContext context) {
        String translated = context.translate("Hello World!", Locale.ENGLISH, Locale.FRENCH);
        // ...
    }
}

Since this method doesn't exist on the default RequestContext interface, we'll have to create a custom type and add the method to it. In the previous snippet, this custom type is called "AppRequestContext".

Let's create this custom Request Context type...

public interface AppRequestContext extends RequestContext<AppRequestContext> {

    public void translate(String sentense, Locale from, Locale to);

    // Other custom methods and/or add-ons...
}

Note that we extend RequestContext, which is the base interface for any Request Context, but we parameterize it using our custom type. This is required because the base interface needs to know about it.

Then, the implementation:

public class AppRequestContextDefault extends RequestContextBase<AppRequestContext>
                                      implements AppRequestContext {

    @AssistedInject
    public AppRequestContextDefault(@Assisted Object exchange, 
                                    RequestContextBaseDeps<AppRequestContext> requestContextBaseDeps) {
        super(exchange, requestContextBaseDeps);
    }

    @Override
    public String translate(String sentense, Locale from, Locale to) {
        
        // More hardcoded than translated here!
        return "Salut, monde!";
    }
}

Explanation :

  • 1 : We extend RequestContextBase, to keep the default methods implementations and simply add our custom one. We also need to parameterize this base class with our custom AppRequestContext type.
  • 2 : We implement our custom interface.
  • 4-8 : The base class requires the server's exchange object and a RequestContextBaseDeps parameter, which are going to be injected using an assisted factory. Don't worry too much about this. Simply add this constructor, and things should be working.
  • 10-15 : We implement our new translate(...) method.

Last, but not the least, we need to tell Spincast about our new custom Request Context type! This is done by using the requestContextImplementationClass(...) of the Bootstrapper :

public static void main(String[] args) {

    Spincast.configure()
            .module(new AppModule())
            .requestContextImplementationClass(AppRequestContextDefault.class)
            .init(args);
    //....
}

Note that it is the implementation, "AppRequestContextDefault", that we have to specify, not the interface! This is to simplify your job : Spincast will automatically find the associated interface and will use it to parameterize the required components.

And that's it! From now on, when you are using a routing related component, which has to be parameterized with the Request Context type, you use your new custom type. For example:


Router<AppRequestContext, DefaultWebsocketContext> router = getRouter();

router.GET("/").handle(context -> {
    String translated = context.translate("Hello World!", Locale.ENGLISH, Locale.FRENCH);
    // do something with the translated sentence...
});

Or, using an inline Route Handler:


Router<AppRequestContext, DefaultWebsocketContext> router = getRouter();

router.GET("/").handle(new Handler<AppRequestContext>() {

    @Override
    public void handle(AppRequestContext context) {
        String translated = context.translate("Hello World!", Locale.ENGLISH, Locale.FRENCH);
        // do something with the translated sentence...
    }
});

(You may have motice that the parameterized version of the Router doesn't simply contain a Request Context type, but also a Websocket context type. This is because this type can also be extended.)

This may seem like a lot of boilerplate code! But it has to be done only one time and, once in place, it's easy to add new methods and add-ons to your Request Context objects! Also, using a unparameterized version of those generic components, it's way nicer. Let's see how to creat those unparameterized versions...

Using unparameterized components

You can do for your custom types what we already did for the default ones : to create an unparameterized version for each of them. For example, here's how the provided DefaultRouter is defined :

public interface DefaultRouter extends Router<DefaultRequestContext, DefaultWebsocketContext> {
    // nothing required
}

This interface has no other goal than to "hide" the parameterization, to be more visually appealing, more easy to use... Thanks to this definition, you can inject DefaultRouter instead of Router<DefaultRequestContext, DefaultWebsocketContext>, which is arguably nicer. Both types are interchangeable.

You can do the exact same thing with your custom Route Context type :

public interface AppRouter extends Router<AppRequestContext, DefaultWebsocketContext> {
    // nothing required
}

Now, you can inject AppRouter instead of Router<AppRequestContext, DefaultWebsocketContext> when you need an instance of your custom router! Here again, it's a matter of taste... Noth types are interchangeable.

For more details, have a look at the Quick Start application. It implements exactly this.

Sending the response

The kind of responses you send to incoming requests really depends on the type of application you're building! If you are building a traditional website, you will most of the time use the integrated Templating Engine to output HTML as the response to a request. But if you are building a SPA, or if you use Spincast for REST microservices/services, then your responses will probably be Json or XML objects.

The response model object

Inside a Route Handler, you can (but are not forced to) use the provided response model as an easy way to build the response. This can be useful to build a response to be sent as Json, but is mainly use to accumulate the various parameters required to render a template.

You get this model by using the getModel() method on the response() add-on :

public void myRouteHandler(DefaultRequestContext context) {
    
    JsonObject responseModel = context.response().getModel();
    
    // ... adds elements to this response model
    
    // ... then sends the response
}

The response model is a JsonObject so it can be manipulated as such! You can add any type of element on it. When the added object is not of a type native to JsonObjects, the object is converted to a JsonObject or to a JsonArray.

You can use the json() add-on to create new JsonObject and JsonArray elements to be added to the response model. For example, let's add to the response model : a simple String variable, a Book object and a JsonObject representing a user...

public void myRouteHandler(DefaultRequestContext context) {
   
    JsonObject responseModel = context.response().getModel();

    // Adds a simple String variable
    responseModel.set("simpleVar", "test");
    
    // Adds a Book : this object will automatically
    // be converted to a JsonObject
    Book book = getBook(42);
    responseModel.set("myBook", book);
        
    // Then adds a JsonObject representing a user
    JsonObject user = context.json().create();
    user.set("name", "Stromgol");
    user.set("age", 30);
    responseModel.set("user", user);

    // ...
}

At this point, the response model would be something like :

{
    "simpleVar": "test",
    "myBook": {
        "id": 42,
        "title": "The Hitchhiker's Guide to the Galaxy",
        "author": "Douglas Adams"
    },
    "user": {
        "name": "Stromgol",
        "age": 30
    }
}

To resume : you use any business logic required to process a request, you query some data sources if needed, then you build the response model. When the response model is ready, you decide how to send it. Let's see the different options...

Sending the response model as HTML, using a template

If you're building a traditional website, you will most of the time send HTML as the response for a request. To do so, you can use the Templating Engine, and specify which template to use to render the data contained in the response model :

public void myRouteHandler(DefaultRequestContext context) {
   
    JsonObject responseModel = context.response().getModel();
    
    // ... adds variables to the response model
    
    // Renders the response model using a template
    context.response().sendTemplateHtml("/templates/myTemplate.html");
}

The default templating engine is Pebble. The template files are found on the classpath by default, but there are overload methods to find them on the file system too. Learn more about that in the Templating Engine section.

Sending Json or XML

If you are using Spincast to build a Single Page Application or REST services, you will probably want to directly return a Json (or as XML) object instead of rendering an HTML template. Most of the time you are going to return that resource directly. Here's an example :

public void booksGet(DefaultRequestContext context) {

    String bookId = context.request().getPathParam("bookId");
    Book someBook = getBook(bookId);
    
    // Sends the book as Json
    context.response().sendJson(someBook);
    
    // ... or as XML
    context.response().sendXML(someBook);
}

By using the sendJson(someBook) method, the book object will automatically be serialized to Json and sent using the appropriated "application/json" content-type.

In some cases, it may be useful to build the object to return using the response model, exactly as you may do when developing a traditional website. This approach is discussed in the SPA Quick Tutorial. Here's an example :

public void myRouteHandler(DefaultRequestContext context) {
   
    JsonObject responseModel = context.response().getModel();
    
    JsonObject user = context.json().create();
    user.set("name", "Stromgol");
    user.set("age", 42);
    responseModel.set("user", user);
    
    // This will send the response model as "application/json" :
    // {"user":{"name":"Stromgol","age":42}}
    context.response().sendJson();
    
    // or, this will send the response model as "application/xml" :
    // <JsonObject><user><name>Stromgol</name><age>42</age></user></JsonObject>
    context.response().sendXml();
}

The sendJson() method, without any argument, takes the response model, converts it to a Json string and sends it with the appropriate "application/json" content-type.

Sending specific content

You can use the default response model to build the object which will be used for the response, but you can also send any object directly. We already saw that we can send an object using the sendJson(myObject) method, but Spincast provides other options. You can...

  • Send characters, using the content-type of your choice :

    public void myRouteHandler(DefaultRequestContext context) {
    
        // Sends as "text/plain"
        context.response().sendPlainText("This is plain text");
    
        // Sends as "application/json"
        context.response().sendJson("{\"name\":\"Stromgol\"}");
    
        // Sends as "application/xml"
        context.response().sendXml("<root><name>Stromgol</name></root>");
    
        // Sends as "text/html"
        context.response().sendHtml("<h1>Hi Stromgol!</h1>");
        
        // Sends using a specified content-type
        context.response().sendCharacters("<italic>Stromgol!</italic>", "text/richtext");
    }

  • Evaluate a template by yourself and send it as HTML, explicitly :

    public void myRouteHandler(DefaultRequestContext context) {
    
        Map<String, Object> params = getTemplateParams();
        String result = context.templating().evaluate("/templates/myTemplate.html", params);
        
        // Sends the evaluated template
        context.response().sendHtml(result);
    }

  • Send a specific object as Json or as XML :

    public void myRouteHandler(DefaultRequestContext context) {
    
        User user = getUserService().getUser(123);
        
        // Sends the user object as Json
        context.response().sendJson(user);
        
        // or, sends it as XML
        context.response().sendXml(user);
    }

  • Send binary data :

    public void myRouteHandler(DefaultRequestContext context) {
    
        byte[] imageBytes = loadImageBytes();
        
        // Sends as "application/octet-stream"
        context.response().sendBytes(imageBytes);
        
        // or sends using a specific content-type
        context.response().sendBytes(imageBytes, "image/png");
    }

Redirecting

Sometimes you need to redirect a request to a new page. There are multiple cases where that can be useful. For example when you decide to change a URL in your application, but don't want existing links pointing to the old URL to break. In that particular case you can use using redirection rules : the requests for the old URL won't even reach any route handler... A redirection header will be sent at the very beginning of the routing process.

Another case where a redirection is useful is when you are building a traditional website and a form is submitted via a POST method. In that case, it is seen as a good practice to redirect to a confirmation page once the form has been validated successfully. By doing so, the form won't be submitted again if the user decides to refresh the resulting page.

Other than redirection rules, there are two ways of redirecting a request to a new page :

  • By using the "redirect(...)" method on the response() add-on, in a Route Handler :

    public void myRouteHandler(DefaultRequestContext context) {
    
        context.response().redirect("/new-url"); 
    }

    Calling this redirect(...) method simply adds redirection headers to the response, it doesn't send anything. This means that any remaining Route Handlers/Filters will be ran as usual and could even, eventually, remove the redirection headers that the method added.

  • By throwing a RedirectException exception.

    public void myRouteHandler(DefaultRequestContext context) {
    
        // Any remaing filters will be skipped
        throw new RedirectException("/new-url");
    }

    Unlike the redirect(...) method approach, throwing a RedirectException will end the current routing process and immediately send the redirection headers. Only the remaining after Filters will be run, any other remaining handler will be skipped.

The URL parameter of a redirection can :

  • Be absolute or relative.
  • Be empty. In that case, the request will be redirected to the current URL.
  • Start with "?". In that case, the current URL will be used but with the specified queryString.
  • Start with "#". In that case, the current URL will be used but with the specified anchor.

Other redirections options :

  • You can specify if the redirection should be permanent (301) or temporary (302). The default is "temporary".
  • You can specify a Flash message :

    public void myRouteHandler(DefaultRequestContext context) {
    
        // Sends a permanent redirection (301) with
        // a Flash message to be displayed on the target page
        context.response().redirect("/new-url", 
                                    true, 
                                    FlashMessageLevel.WARNING, 
                                    "This is a warning message!");
    }

Forwarding

Forwarding the request doesn't send anything, it's only a way of changing the current route. By forwarding a request, you restart the routing process from scratch, this time using a new, specified route instead of the original one.

Forwarding is very different than Redirecting since the client can't know that the request endpoint has been changed... The process is entirely server side.

Since forwarding the request ends the current routing process and skips any remaining Route Handlers/Filters, it is done by throwing an exception, ForwardRouteException :

public void myRouteHandler(DefaultRequestContext context) {

    throw new ForwardRouteException("new-url");
}

Flushing the response

Flushing the response consists in sending the HTTP headers and any data already added to the response buffer. You only start to actually send something to the user when the response is flushed!

It is important to know that the first time the response is flushed, the HTTP headers are sent and therefore can't be modified anymore. Indeed, the HTTP headers are only sent once, during the first flush of the response.

Note that explicitly flushing the response is not required : this is automatically done when the routing process is over. In fact, you don't need to explicitly flush the response most of the time. But there are some few cases where you may need to do so, for example for the user starts receiving data even if you are still collecting more of it on the server.

So, how do you flush the response? The first option is by using the dedicated flush() method :

public void myRouteHandler(DefaultRequestContext context) {

    context.response().flush();
}

A second option is to use the "flush" parameter available on many sendXXX(...) methods of the response() add-on. For example...

public void myRouteHandler(DefaultRequestContext context) {

    // true => flushes the response
    context.response().sendPlainText("Hello world!", true);
}

Note that flushing the response doesn't prevent any remaining Route Handlers/Filters to be run, it simply send the response as it currently is.

Finally, note that you can also use the end() method of the response() add-on if you want the response to be flushed and be closed. In that case, remaining Route Handlers/Filters will still run, but they won't be able to send any more data :

public void myRouteHandler(DefaultRequestContext context) {

    context.response().end();
}

Skipping remaining handlers

Most of the time you want to allow the main Route Handler and all its associated filters to be run. A filter may modify some headers, may log some information, etc. But in the rare cases where you want to make sure the response is sent as is and that anything else is skipped, you can throw a SkipRemainingHandlersException.

public void myRouteHandler(DefaultRequestContext context) {

    context.response().sendPlainText("Hello world!");
        
    throw new SkipRemainingHandlersException();
}

Unlike simply flushing and ending the response (using the end() method), throwing a SkipRemainingHandlersException skips any remaining handlers : the response will be sent as is, and the routing process will be over. This means that even the after filters will be skipped!

Routing

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

The Routing Process

When an HTTP request arrives, the Router is asked to find what Route Handlers should be used to handle it. There can be more than one Route Handler for a single request because of Filters : Filters are standard Route Handlers, but that run before or after the main Route Handler.

There can be only one main Route Handler but multiple before and after Filters. Even if multiple Route Handlers would match a request, the router will only pick one as the main one (by default, it keeps the first one matching).

Those matching Routes Handlers are called in a specific order. As we'll see in the Filters section, a Filter can have a position which indicates when the Filter should run. So it is possible to specify that we want a Filter to run before or after another one.

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

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

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

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

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

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

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

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

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

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

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

Adding a Route

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

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

public class AppRouteDefinitions {

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

On that Router, there are methods to create a Route Builder. As its name indicates, this object uses a builder pattern to help you create a Route. Let's see that in details...

HTTP method and path

You create a Route Builder by choosing the HTTP methods and the path you want your Route to handle :


router.GET("/") ...

router.POST("/users") ...

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

// Handles all HTTP methods
router.ALL("/books") ...

// Handles POST and PUT requests only
router.methods("/books", HttpMethod.POST, HttpMethod.PUT) ...

Route id

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


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

Dynamic parameters

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

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

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

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

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

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

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

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

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

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


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

In this example, only requests starting with "/users/" and followed by a numeric value will match. In other words, "/users/1" and "/users/42" would match, but not "/users/abc".

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

Spincast has some built-in aliases :

// Matches only alpha characters (A to Z)
router.GET("/${param1:<A>}")

// Matches only numeric characters (0 to 9)
router.GET("/${param1:<N>}")

// Matches only alphanumeric characters (A to Z) and (0 to 9)
router.GET("/${param1:<AN>}")

// Matches only alpha characters (A to Z), "-" and "_"
router.GET("/${param1:<A+>}")

// Matches only numeric characters (0 to 9), "-" and "_"
router.GET("/${param1:<N+>}")

// Matches only alphanumeric characters (A to Z), (0 to 9), "-" and "_"
router.GET("/${param1:<AN+>}")

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

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

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

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

Routing Process Types

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

The three routing process types are:

  • Found
  • Not Found
  • Exception

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

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

// Only considered during a "Found" routing process
// (this is the default, so it is not required to specify it)
router.GET("/").found() ...

// Only considered during a "Not Found" routing process
router.GET("/").notFound() ...

// Only considered during an "Exception" routing process
router.GET("/").exception() ...

// Always considered!
router.GET("/").allRoutingTypes() ...

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

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

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

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

Content-Types

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

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

Let's see some examples :

// Only considered if the request accepts HTML
router.GET("/users").html() ...

// Only considered if the request accepts Json
router.GET("/users").json() ...

// Only considered if the request accepts XML
router.GET("/users").xml() ...

// Only considered if the request accepts HTML or plain text
router.GET("/users").accept(ContentTypeDefaults.HTML, ContentTypeDefaults.TEXT) ...

// Only considered if the request accepts PDF
router.GET("/users").acceptAsString("application/pdf") ...

HTTP Caching route options

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

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

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

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

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

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

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

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

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

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

Saving the route

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

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

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

Here's a complete example of a Route creation :

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

Filters

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

You can declare a Filter exactly like you would declare a standard Route, but using the extra "pos" ("position") property! The Filter's position indicate when the Filter should be run. The lower that position number is, the sooner the Filter will run. Note that the main Route Handlers are considered as having a position of "0", so Filters with a position below "0" are before Filters, and those with a position greater than "0" are after Filter.

An example :

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

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

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

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

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

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

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

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

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

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

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

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

The inline Filters don't have a position: they are run in the order they are declared! Also, they always run just before or just after the main Route Handler. In other words, they are always run closer to the main Route Handler than the global Filters.

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

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

public void myFilterHandler(AppRequestContext context) {

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

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

    // Actually run the Filter...
    
}

WebSockets

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

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

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

Static Resources

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

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

Static Resources can be on the classpath or on the file system. For example:

// Will serve all requests starting with the
// URL "/public" with files under the classpath
// directory "/public_files".
router.dir("/public").classpath("/public_files").handle();

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

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

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

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

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

Be aware that since requests for Static Resources don't reach the framework, Filters don't apply to them! Even a "catch all" Filter such as router.ALL().pos(-1).handle(handler) won't be applied...

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

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

Note that a file(...) resource has priority over of a dir(...) resource since it is more precise. For example, let's examine those two Route definitions :

router.dir("/public").cache(3600).classpath("/public_files").handle();
router.file("/public/test.txt").cache(60).classpath("/public_files/test.txt").handle();

Even if the "test.txt" file is under the "/public" directory, a cache of 60 seconds will be applied, not 3600.

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

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

Dynamic Resources

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

Here's an example of a Dynamic Resource definition :

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

          @Override
          public void handle(DefaultRequestContext context) {
    
              String tagId = context.request().getPathParam("tagId");
              byte[] tagBytes = generateTagImage(tagId);
    
              context.response().sendBytes(tagBytes, "image/png");
          }
      });

Or, for a single file :

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

          @Override
          public void handle(DefaultRequestContext context) {

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

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

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

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

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

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

          @Override
          public void handle(DefaultRequestContext context) {

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

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

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

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

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

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

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

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

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

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

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

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

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

CORS

CORS, Cross-Origin Resource Sharing, allows you to specify some resources in your application that can be accessed by a browser from another domain. For example, let's say your Spincast application runs on domain http://www.example1.com, and you want to allow another site, http://www.example2.com, to access some of your APIs, or some of your files. By default, browsers don't allow such cross domains requests. You have to enable CORS for them to work.

There is a provided Filter to enable CORS on regular Routes. Since Filters are not applied to Static Resources, there is also a special configuration to add CORS to those :

// Enable CORS for every Routes of the application,
// except for Static Resources.
getRouter().cors();

// Enable CORS for all Routes matching the specified path,
// except for Static Resources.
getRouter().cors("/api/*{path}");

// Enable CORS on a Static Resource directory.
getRouter().dir("/public").classpath("/public_files")
           .cors().handle();
           
// Enable CORS on a Static Resource file.
getRouter().file("/public/test.txt").classpath("/public_files/test.txt")
           .cors().handle();

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

// The default :
// - Allows any origins (domains)
// - Allows cookies
// - Allows any HTTP methods (except for Static Resources,
//   which allow GET, HEAD and OPTIONS only)
// - Allows any headers to be sent by the browser
// - A Max-Age of 24h is specified for caching purposes
//
// But :
// - Only basic headers are allowed to be read from the browser.
.cors()

// Like the default, but also allows some extra headers
// to be read from the browser. 
.cors(Sets.newHashSet("*"),
      Sets.newHashSet("extra-header-1", "extra-header-2"))

// Only allows the domains "http://example2.com" and "https://example3.com" to access 
// your APIs.
.cors(Sets.newHashSet("http://example2.com", "https://example3.com"))

// Like the default, but only allows the specified extra headers
// to be sent by the browser.
.cors(Sets.newHashSet("*"),
      null,
      Sets.newHashSet("extra-header-1", "extra-header-2"))

// Like the default, but doesn't allow cookies.
.cors(Sets.newHashSet("*"),
      null,
      Sets.newHashSet("*"),
      false)

// Like the default, but only allows the extra "PUT" method
// in addition to GET, POST, HEAD and OPTIONS 
// (which are always allowed). Not applicable to
// Static Resources.
.cors(Sets.newHashSet("*"),
      null,
      Sets.newHashSet("*"),
      true,
      Sets.newHashSet(HttpMethod.PUT))

// Like the default, but specifies a Max-Age of 60 seconds
// instead of 24 hours.
.cors(Sets.newHashSet("*"),
      null,
      Sets.newHashSet("*"),
      true,
      Sets.newHashSet(HttpMethod.values()),
      60)

HTTP Authentication

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

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

You enable HTTP Authentication in two steps :

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

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

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

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

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

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

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

Redirection rules

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

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

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

Here are the available options when creating such redirection rule:

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

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

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

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

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

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

Special exceptions

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

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

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

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

    public void myHandler(AppRequestContext context) {
    
        throw new ForwardRouteException("/new-url");
    }

  • SkipRemainingHandlersException : This exception stops the current routing process, but without starting any new one. In other words, the remaining Filters/Route Handlers won't be run, and the response will be sent as is, without any more modification. Learn more about this in the SkipRemainingHandlersException section. Here's an example :

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

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

    public void myHandler(AppRequestContext context) {
    
        throw new CustomStatusCodeException("Forbidden!", HttpStatus.SC_FORBIDDEN);
    }

  • PublicException : This exception allows you to specify a message that will be displayed to the user. This is mainly useful if you use the provided Exception Route Handler because in a custom handler you would have full control over what you send as a response anyway... Here's an example :

    public void myHandler(AppRequestContext context) {
    
        throw new PublicException("You don't have the right to access this page",
                                  HttpStatus.SC_FORBIDDEN);
    }

The Router is dynamic

Note that the Spincast Router is dynamic, which means you can always add new Routes to it. This also means you don't have to define all your Routes at the same place, you can let the controllers (or even the plugins) define their own Routes!

For example:

public class UserControllerDefault implements UserController {

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

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

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

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

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

Explanation :

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

Templating Engine

The Templating Engine (also called view engine, or template engine), is the component that you use to generate dynamic text content. It can be used for multiple purposes but its most frequent use is to generate HTML pages.

The default Templating Engine included with Spincast by default is Pebble.

Using the Templating Engine

To evaluate a template, you can inject the TemplatingEngine component anywhere you need it. But the preferred way to generate HTML pages is to use the sendTemplateXXX(...) methods on the response() add-on :

public void myRouteHandler(AppRequestContext context) {

    JsonObject model = context.response().getModel();
        
    // ... adds variables to the model
        
    // Renders the response model using a template
    // and sends the result as HTML
    context.response().sendTemplateHtml("/templates/myTemplate.html");
}

You can also evaluate a template without sending it as the response. The templating() add-on give you direct access to the Templating Engine. Here's an example where you manually evaluate a template to generate the content of an email :

public void myRouteHandler(AppRequestContext context) {

    User user = getUser();
    
    JsonObject params = context.json().create();
    params.set("user", user);
    
    String emailBody = context.templating().fromTemplate("/templates/email.html", params);
        
    // ... do something with the content
}

Note that, by default, the path to a template is a classpath path. To load a template from the file system instead, use false as the "isClasspathPath" parameter :

public void myRouteHandler(AppRequestContext context) {

    User user = getUser();
    
    JsonObject params = context.json().create();
    params.set("user", user);
    
    String emailBody = context.templating().fromTemplate("/templates/email.html", 
                                                         false, // From the file system!
                                                         params);
    
    // ... do something with the content
}

Finally you can evaluate an inline template :

public void myRouteHandler(AppRequestContext context) {

    // We can use a standard Map<String, Object> instead
    // of a JsonObject for the parameters
    Map<String, Object> params = new HashMap<String, Object>();
    params.set("name", "Stromgol");

    // This will be evaluated to "Hi Stromgol!"
    String result = context.templating().evaluate("Hi {{name}}!", params);
    
    // ... do something with the result
}

Templates basics (using Pebble)

The syntax to use for your templates depends on the Templating Engine implementation. Here, we'll show some examples using the default Templating Engine, Pebble. Make sure you read the Pebble documentation if you want to learn more...

Using the response model

If you are using the default way to render an HTML page, suing the response().sendTemplateHtml(...) method, you can use the response model as a container for the parameters your template needs. The response model becomes the root of all available variables when your template is rendered. For example, your Route Handler may look like :

public void myRouteHandler(AppRequestContext context) {

    // Gets the response model
    JsonObject model = context.response().getModel();
  
    // Creates a "user" on adds it to the
    // response model
    JsonObject user = context.json().create();
    user.set("name", "Stromgol");
    model.set("user", user);

    // Renders a template and sends it as HTML
    context.response().sendTemplateHtml("/templates/myTemplate.html");
}

The template, located on the classpath (at "src/main/resources/templates/myTemplate.html" in a Maven project) may look like this :

<!doctype html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>My application</title>
    </head>
    <body>
        <h1>Hello {{user.name}}!</h1> 
    </body>
</html>

Using JsonPaths

When accessing the variables in a template, you can use JsonPaths. Here are some examples :

  • {{user.name}} : The "name" attribute on the user object.
  • {{user.books[2].title}} : The "title" attribute of the third book of the user object.
  • {{user['some key']}} or {{user["some key"]}} : The "some key" attribute of the user object. Here brackets are required because of the space in the key.

Default templating variables

Spincast automatically provides some variables that can be used when rendering a template. Those variables will always be available to any template rendering (except if you are not in the scope of an HTTP request). Spincast adds those variables using a "before" Filter : addDefaultGlobalTemplateVariables(...)

The provided variables are :

  • "pathParams" : The parameters parsed from the path of the request. To be used like {{pathParams.myParam}}.
  • "qsParams" : The parameters parsed from the queryString of the request. Note that a single queryString variable may contain more than one values. To access the first value, use something like : {{qsParams.myParam[0]}}.
  • "cookies" : The current Cookies. To be used like {{cookies.myCookie.value}}.
  • "requestScopedVars" : The request scoped variables added by the various Route Handlers. To be used like {{requestScopedVars.myVar}}.
  • "langAbrv" : The abreviation of the current Locale to use. For example : "en".
  • "cacheBuster" : The current cache buster code.
  • "routeId" : The id of the current route (of its main Route Handler).
  • "fullUrl" : The full URL of the current request.
  • "isHttps" : Is the current URL secure (HTTPS)?
  • "alerts" : The Alert messages, if any. Those also include Flash messages (Spincast automatically converts Flash messages to Alert messages). They also contain Alert messages that you may have explictly added using the addAlert(...) method of the response() add-on. For example :

    public void myRouteHandler(AppRequestContext context) {
    
        context.response().addAlert(AlertLevel.ERROR, "Some message");
    }

Layout

If you are building a traditional website and use templates to render HTML, make sure you read the "Template Inheritance", "extends" and "include" sections of the Pebble documentation to learn how to create a layout for your website! This is an important foundation for a scalable website structure.

You can browse this Spincast website sources themselves to see how we use such layout using some {% block %}. The layout.html file is the root of our layout.

Provided functions and filters

Spincast provides some functions and filters for Pebble out of the box. They are defined in the SpincastPebbleExtensionDefault class.

Functions

  • get(String pathExpression)

    This function receives the path to an element as a string, evaluates it, and returns the element if it exists or null otherwise. In other words, it allows you to dynamically create the path to an element. For example :

    
    {% set user = get("myForm.users[" + generateRandomPosition() + "]") %}
    {% if user is not null %}
        <p>The name of the random user is {{user.name}}</p>
    {% endif %}

  • msg(String messageKey, ...params)

    This function displays a localized message taken from the Dictionary. Only the message key is required, but you can also pass some parameters to be evaluated.

    Example, without any parameters :

    
    <h1>{{ msg('app.home.title') }}</h1>
    

    With parameters :

    
    <div>{{ msg('welcome.user', 'firstName', user.firstName, 'lastName', user.lastName) }}</div>
    

    Note that each parameter's key must have an associated value or an exception will be thrown (the number of parameters must be even).

    Finally, if the first parameters is true, the evaluation of the message will be forced, even if no parameters are provided. Indeed, to improve performance, by default a message from the dictionary is only evaluated using the Templating Engine if at least one parameter is provided. Example of forcing the evaluation:

    
    <h1>{{ msg('app.display.date', true) }}</h1>
    

  • jsOneLine(String code)

    This function allows the output of javascript code inside quotes. It removes newlines and properly escapes the quotes in the code.

    
    let js="{{jsOneLine(code)}}";
    

    You can pass true as a second parameter if single quotes needs to be escaped instead of double quotes:

    
    let js='{{jsOneLine(code, true)}}';
    

  • querystring(String querystring)

    This function will add the specified querystring to the existing one. In other words, the querystring of the current request will be kept, but the specified one will be concatenated to it.

    If a parameter name already exist in the current querystring, it is overwritten.

    
    <a href="{{ querystring('?offset=' + newOffset) }}">link</a>
    

    If the previous example was evaluated part of a "https://example.com?limit=10" request, the resulting content would be something like "<a href="?limit=10&offset=10">link</a>"

    Finally, note that if this function is called without being inside a request context, the specified querystring will simply be used as is.

  • isRoute(String path, [boolean isRegEx, boolean allowSubPaths])

    This function returns true if the specified path matches the route of the current request.

    For example:

    
    <span class="menu {% if isRoute('/users') %}active{% endif %}"</span>
    

    If the second parameter is "true", the specified path will be considered as a regular expression:

    
    <span class="menu {% if isRoute('/(user|users)', true) %}active{% endif %}"</span>
    

    Finally, if the third parameter is "true", any subpath of the specified path will also match! If the specified path is a regular expression, then "(/?$|/.*)" will be concatenated to it. If the path is not a regular expression, Spincast will use "startWith(path)" instead of "equals(path)" to validate the current route:

    
    // Will match "/users", "users/123", "users/123/books/456"
    <span class="menu {% if isRoute('/users', false, true) %}active{% endif %}"</span>
    
    // Will match "/user", "/user/123/books/456", "/users/", "/users/123/books/456"
    <span class="menu {% if isRoute('/(user|users)', true, true) %}active{% endif %}"</span>
    
    

    If this function is evaluated outside of a request context (for example from a scheduled task), then false is returned.

  • isRouteId(String routeId)

    This function returns true if the specified id is the id of the current route.

    For example:

    
    <span class="menu {% if isRouteId('myUsersRouteId') %}active{% endif %}"</span>
    

    If this function is evaluated outside of a request context (for example from a scheduled task), then false is returned.

Filters

  • pathExpression | get()

    This filter does the same as the get() function : it receives the path to an element as a string, evaluates it, and returns the element if it exists or null otherwise.

    The difference with the get() function is that you can use undefined elements with this filter and no exception is going to be thrown, even if strictVariables is on.

    
    {% set user = "may.not.exist.users[" + generateRandomPosition() + "]" | get() %}
    {% if user is not null %}
        <p>The name of the random user is {{user.name}}</p>
    {% endif %}

  • someText | newline2br()

    This filter will replace the newlines of the text with <br />\n. This is useful when you want to display some text in an HTML template while respecting the newlines.

    
    {{ someText | newline2br }}
    

    By default, the rest of the text will be properly escaped. For example, "<em>a\nb</em>" will become "&lt;em&gt;a<br />\nb&lt;/em&gt;".

    To disable the escaping, pass false as a parameter:

    
    {{ someText | newline2br(false) }}
    

    This would result in "<em>a<br />\nb</em>".

  • someVar | boolean()

    This filter converts a "true" or "false" string to a proper boolean. This allows the string variable to be used in if statements.

    
    // Let's say val is "true" (a string) here...
    
    {% if val | boolean %}ok{% endif %}
    

    The main use case for this filter is when a form is submitted and contains a boolean field which is transformed to a string value. When redisplaying the form, you may need to interpret the value of the field as a true boolean to perform logic.

    If the variable is already a boolean, it will also work fine.

  • someElements | checked(String[] matchingValues)

    This filter outputs the string "checked" if at least one element from someElements matches one of the element from the matchingValues. Both sides can either be a single element or an array of elements. For example :

    <label for="drinkTea">
        <input type="radio" 
               id="drinkTea" 
               name="user.favDrink"
               {{user.favDrink | checked("tea")}}
               value="tea"/> Tea</label>

    Note that the elements are compared using equivalence, not using equality. So the String "true" matches the true boolean and "123.00" matches 123, for example.

  • someElements | selected(String[] matchingValues)

    This filter outputs the string "selected" if at least one element from someElements matches one of the element from the matchingValues. Both sides can either be a single element or an array of elements. For example :

    <select name="user.favDrink" class="form-control">
        <option value="tea" {{user.favDrink | selected("tea")}}>Tea</option>
        <option value="coffee" {{user.favDrink | selected("coffee")}}>Coffee</option>
        <option value="beer" {{user.favDrink | selected("beer")}}>WBeer</option>
    </select>

    Note that the elements elements are compared using equivalence, not using equality. So the String "true" matches the true boolean and "123.00" matches 123, for example.

The remaining filters are all about validation. Make sure you read the dedicated Validation Filters section to learn more about them and to see some examples!

  • ValidationMessages | validationMessages()

    This filter uses a template fragment to output the Validation Messages associated with a field.

  • ValidationMessages | validationGroupMessages()

    This filter is similar to validationMessages() but uses a different template. It is made to output the Validation Messages of a group of fields, instead of a single field.

  • ValidationMessages | validationClass()

    The validationClass(...) filter checks if there are Validation Messages and, if so, it outputs a class name.

  • ValidationMessages | validationFresh()
    ValidationMessages | validationSubmitted()

    Those two filters are used to determine if a form is displayed for the first time, or if it has been submitted and is currently redisplayed with potential Validation Messages.

  • ValidationMessages | validationHasErrors()
    ValidationMessages | validationHasWarnings()
    ValidationMessages | validationHasSuccesses()
    ValidationMessages | validationIsValid()

    Those four filters check if there are Validation Messages of a certain level and return true or false.

JsonObjects

JsonObject (and JsonArray) are components provided by Spincast to mimic real Json objects. You can think of JsonObjects as Map<String, Object> on steroids!

JsonObjects provide methods to get elements from them in a typed manner They also support JsonPaths, which is an easy way to navigate to a particular element in the JsonObject. JsonObjects are also very easy to validate.

Here's a quick example of using a JsonObject :

// Creates a new JsonObject
JsonObject jsonObj = getJsonManager().create();

// Adds an element
jsonObj.set("myElement", "42");

// Gets the element as a String
String asString = jsonObj.getString("myElement");

// Gets the same element as an Integer
Integer asInteger = jsonObj.getInteger("myElement");

JsonObject supports those types, natively :

  • String
  • Integer
  • Long
  • Float
  • Double
  • BigDecimal
  • Boolean
  • byte[] (serialized as a base 64 encoded String)
  • Date
  • Instant
  • Other JsonObjects and JsonArrays

Getters are provided for all of those native types :

  • String getString(String key)
  • Integer getInteger(String key)
  • JsonObject getJsonObject(String key)
  • ...
Every Getter has an overloaded version that you can use to provide a default value in case the requested element if not found (by default, null is returned if the element is not found). Let's see an example :

// Creates an empty JsonObject
JsonObject jsonObj = getJsonManager().create();

// Tries to get an inexistent element...
// "myElement" will be NULL
String myElement = jsonObj.getString("nope");

// Tries to get an inexistent element, but also specifies a default value...
// "myElement" will be "myDefaultValue"!
String myElement = jsonObj.getString("nope", "myDefaultValue");

When you add an object of a type that is not managed natively, the object is automatically converted to a JsonObject (or to a JsonArray, if the source is an array or a Collection). Spincast does this by using the SpincastJsonManager#convertToNativeType(...) method, which is based on Jackson by default. For example :

// Gets a typed user
User user = getUser(123);

// Adds this user to a JsonObject
JsonObject jsonObj = getJsonManager().create();
jsonObj.set("myUser", user);

// Gets back the user... It is now a JsonObject!
JsonObject userAsJsonObj = jsonObj.getJsonObject("myUser");

Note that you can have control over how an object is converted to a JsonObject by implementing the ToJsonObjectConvertible interface. This interface contains a convertToJsonObject() method that you implement to convert your object to a JsonObject the way you want. There is a similar ToJsonArrayConvertible interface to control how an object is converted to a JsonArray.

Creating a JsonObject

You can create an JsonObject (or an JsonArray) by using the JsonManager component. This JsonManager can be injected where you want, or it can be accessed through the json() add-on, when you are inside a Route Handler :

public void myHandler(AppRequestContext context) {

    // JsonObject creation
    JsonObject obj = context.json().create();
    obj.set("name", "Stromgol");
    obj.set("lastName", "Laroche");

    // JsonArray creation
    JsonArray array = context.json().createArray();
    array.add(111);
    array.add(222);

    // Or, using the JsonManager directly (if
    // injected in the current class) :
    JsonObject obj2 = getJsonManager().create();

    //...
}

Cloning and Immutability

By default, any JsonObject (or JsonArray) added to another JsonObject is added as is, without being cloned. This means that any external modification to the added element will affect the element inside the JsonObject, and vice-versa, since they both refere to the same instance. This allows you to do something like :

JsonArray colors = getJsonManager().createArray();
JsonObject obj = getJsonManager().create();

// Adds the array to the obj
obj.set("colors", colors);

// Only then do we add elements to the array
colors.add("red");
colors.add("blue");

// This returns "red" : the array inside the JsonObject
// is the same instance as the external one.
String firstColor = obj.getArrayFirstString("colors");

Sometimes this behavior is not wanted, though. You may need the external object and the added object to be two distinct instances so modifications to one don't affect the other! In those cases, you can call the "clone()" method on the original JsonObject object, or you can use "true" as the "clone" parameter when calling set(...)/add(...) methods. Both methods result in the original object being cloned. Let's see an example :

JsonArray colors = getJsonManager().createArray();
JsonObject obj = getJsonManager().create();

// Add a *clone* of the array to the object
obj.set("colors", colors, true);

// Or :
obj.set("colors", colors.clone());

// Then we add elements to the original array
colors.add("red");
colors.add("blue");

// This will now return NULL since a *new* instance of the 
// array has been added to the JsonObject!
String firstColor = obj.getArrayFirstString("colors");

Note that when you clone a JsonObject, a deep copy of the original object is made, which means the root object and all the children are cloned. The resulting JsonObject is guaranteed to share no element at all with the original object.

We also decided to make JsonObject and JsonArray objects mutable by default. This is a conscious decision to make those objects easy to work with : you can add and remove elements from them at any time.

But if you need more safety, if you work in a complex multi-threaded environment for example, you can get an immutable version of a JsonObject object by calling its .clone(false) method, using false as the "mutable" parameter :

JsonObject obj = getJsonManager().create();
obj.set("name", "Stromgol");

// "false" => make the clone not mutable!
JsonObject immutableClone = obj.clone(false);

// This will throw an exception!
immutableClone.set("nope", "doesn't work");

When you create an immutable clones, the root element and all the children are cloned as immutable. In fact, JsonObject objects are always fully mutable or fully immutable! Because of this, if you try to add an immutable JsonObject to a mutable one, a mutable clone will be created from the immutable object before being added. Same thing if you try to add an mutable JsonObject to an immutable one : an immutable clone will be created from the mutable object before being added.

At runtime, you can validate if a JsonObject is mutable or not using : if(myJsonObj.isMutable()).

JsonObject methods

Have a look at the JsonObject Javadoc for a complete list of available methods. Here we're simply going to introduce some interesting ones, other than set(...), getXXX(...) and clone(...) we already saw :

  • int size()
    The number of properties on the object.
  • boolean isElementExists(String jsonPath)
    Does the JsonObject contain an element at the specified JsonPath?
  • JsonObject merge(JsonObject jsonObj, boolean clone)
    JsonObject merge(Map<String, Object> map, boolean clone)
    Merges a external JsonObject or a plain Map<String, Object> into the JsonObject. You can specify if the added elements must be cloned or not (in case some are JsonObject or JsonArray).
  • JsonObject remove(String jsonPath)
    Removes an element using its jsonPath.
  • JsonObject getJsonObjectOrEmpty(String jsonPath)
    JsonArray getJsonArrayOrEmpty(String jsonPath)
    Returns the JsonObject or JsonArray at the specified JsonPath or returns an empty instance if it's not found. This allows you to try to get a deep element without any potential NullPointerException. For example :

    // This won't throw any NPE, even if the "myArrayKey"
    // array or its first element don't exist
    String value = obj.getJsonArrayOrEmpty("myArrayKey")
                      .getJsonObjectOrEmpty(0)
                      .getString("someKey", "defaultValue");
    

  • [TYPE] getArrayFirst[TYPE](String jsonPath, String defaultValue)
    For all types native to JsonObject, a getArrayFirst[TYPE](...) method exists. With those methods, you can get the first element of a JsonArray located at the specified JsonPath. This is useful in situations where you know the array only contains a single element :

    // This :
    String value = obj.getArrayFirstString("myArrayKey", "defaultValue")
    
    // ... is the same as :
    String value = obj.getJsonArrayOrEmpty("myArrayKey").getString(0, "defaultValue")
    

  • void transform(String jsonPath, ElementTransformer transformer)
    Applies an ElementTransformer to the element located at the specify JsonPath. This is used to modify an element without having to extract it first. For example, the provided JsonObject#trim(String jsonPath) method exactly does this : it internally calls transform(...) and pass it an ElementTransformer which trims the target element.
  • String toJsonString(boolean pretty)
    Converts the JsonObject object to a Json string. If pretty is true, the resulting Json will be formatted.
  • Map<String, Object> convertToPlainMap()

    If you need to use the elements of a JsonObject in some code that doesn't know how to handle JsonObjects, you can convert it to a plain Map<String, Object>. Spincast does this, for example, to pass the elements of the response model to the Template Engine when it's time to evaluate a template and send an HTML page. Pebble, the default templating Engine, knows nothing about JsonObjects but can easily deal with a plain Map<String, Object>.

    Note that all JsonObject children will be converted to Map<String, Object> too, and all JsonArray children to List<Object>.

JsonArray extra methods

Have a look at the JsonArray Javadoc for a complete list of available methods. Here are some interesting ones, other than the ones also available on JsonObjects :

  • List<Object> convertToPlainList()
    Converts the JsonArray to a plain List<Object>. All JsonObject children will be converted to Map<String, Object>, and all JsonArray children to List<Object>.
  • List<String> convertToStringList()
    Converts the JsonArray to a List<String>. To achieve this, the toString() method will be called on any non null element of the array.

JsonPaths

In Spincast, a JsonPath is a simple way of expressing the location of an element inside a JsonObject (or a JsonArray). For example :


String title = myJsonObj.getString("user.books[3].title");
In this example, "user.books[3].title" is a JsonPath targetting the title of the fourth book from a user element of the myJsonObj object.

Without using a JsonPath, you would need to write something like that to retrieve the same title :

JsonObject user = myJsonObj.getJsonObjectOrEmpty("user");
JsonArray books = user.getJsonArrayOrEmpty("books");
JsonObject book = books.getJsonObjectOrEmpty(3);
String title = book.getString("title");
As you can see, a JSonPath allows you to easily navigate a JsonObject without having to extract any intermediate elements.

Here is the syntax supported by JsonPaths :

  • To access a child you use a ".". For example : "user.car".
  • To access the element of a JsonArray you use "[X]", where X is the position of the element in the array (the first index is 0). For example : "books[3]".
  • If a key contains spaces, or a reserved character (".", "[" or "]"), you need to surround it with brackets. For example : "user['a key with spaces']".
  • That's it!

You can also use JsonPaths to insert elements at specific positions! For example :

// Creates a book object
JsonObject book = getJsonManager().create();
book.set("title", "The Hitchhiker's Guide to the Galaxy");

// Creates a "myJsonObj" object and adds the book to it
JsonObject myJsonObj = getJsonManager().create();
myJsonObj.set("user.books[2]", book);

The myJsonObj object is now :

{
    "user" : {
        "books" : [
            null,
            null,
            {
                "title" : "The Hitchhiker's Guide to the Galaxy"
            }
        ]
    }
}

Notice that, in that example, the user object didn't exist when we inserted the book! When you add an element using a JsonPath, all the parents are automatically created, if required.

If you really need to insert an element in a JsonObject using a key containing some of the JsonPaths special characters (which are ".", "[" and "]"), and without that key being parsed as a JsonPath, you can do so by using the setNoKeyParsing(...) method. For example :

// Creates a book object
JsonObject book = getJsonManager().create();
book.set("title", "The Hitchhiker's Guide to the Galaxy");

// Creates a "myJsonObj" object and adds the book to it
// using an unparsed key!
JsonObject myJsonObj = getJsonManager().create();
myJsonObj.setNoKeyParsing("user.books[2]", book);

The myJsonObj object is now :

{
    "user.books[2]" : {
        "title" : "The Hitchhiker's Guide to the Galaxy"
    }
}

As you can see, the "user.books[2]" key has been taken as is, without being parsed as a JsonPath.

Forms

This section is about HTML Forms, as used on traditional websites. If you use a SPA client-side, you in general don't use such POSTed forms, you rather use javascript to send and receive Json objects. Both approaches are supported out of the box by Spincast but this specific section is about traditional HTML forms and their validation!

We're going to learn :

  • How to populate a form and bind its fields to an underlying Form object.
  • How to validate a form that has been submitted.
  • How to redisplay a validated form with resulting validation messages.

A form always has a backing model to represent its data. This form model is sometimes called "form backing object", "form backing bean" or "command object". It's the object used to transfer the values of a form from the server to the client (to populate the form's fields) and vice versa.

On the server-side, this form model is represented using the Form class. A Form object is simply a JsonObject with extra validation features! You can manipulate a Form object exactly as a JsonObject and even cast it as one.

The validation pattern

The validation pattern shows how you create a form to be displayed, validate the form when it is submitted, and redisplay it again, with validation messages, if it is invalid...

First, let's start with the GET handler, which is the one called to display a form for the first time :


// GET handler
public void myHandlerGet(AppRequestContext context) {

    JsonObject form = context.response().getModel().getJsonObject("userForm");
    if (form == null) {
        form = context.json().create();
        context.response().getModel().set("userForm", form);
        
        User user = getUser(...);
        form.set("name", user.getName());
    }
    
    context.response().sendTemplateHtml("/templates/userEdit.html");
}

Explanation :

  • 5 : We check if the form already exist in the response model.
  • 7 : If the form doesn't exist yet, we create it as a simple JsonObject.
  • 8 : We add the form to the response model, so it is available to the templating engine.
  • 10-11 : We populate the form with the initial values, if required.
  • 14 : We send the response by evaluating a template which will display the form.

When the form is submitted, we retrieve its data inside a POST handler:


// POST handler
public void myHandlerPost(AppRequestContext context) {

    Form form = context.request().getFormWithRootKey("userForm");
    context.response().addForm(form);
    
    validateForm(form);
    
    if (!form.isValid()) {
        myHandlerGet(context);
        return;
    } else {

        processForm(form);
        
        context.response().redirect("/success",
                                    FlashMessageLevel.SUCCESS,
                                    "The user has been processed!");
    }
}

Explanation :

  • 5 : We retrieve the posted form from the request.
  • 6 : We immediately add the form to the response model. This will make the form available to the templating engine but will also provide a "validation" element containing any validation messages to display.
  • 8 : We validate the form and add error, warning or success validation messages to it.
  • 10 : Once the validation is done, we check if the form is valid.
  • 11-12 : if the form contains errors, we simply call the GET handler so the form is displayed again, with the validation messages we added to it.
  • 15 : if the form is valid, we process it. This may involve calling services, editing entities, etc.
  • 17-19 : we redirect the page with a Flash message to indicate that the form was processed successfully!

The important part to understand is how the GET handler first checks in the response model to see if the form already exists in it... Indeed, this handler may be called by the POST handler if a posted form is invalid... When it's the case, you do not want to populate the form with some default/initial values, you want to keep the submitted values!

Displaying the Form

By using a dynamic JsonObject/Form object as the form model, a benefit is that you don't have to create in advance all the elements required to match the fields of the HTML form. Simply by using a valid JsonPath as the "name" attribute of a field, the element will automatically be created on the form model.

As an example, let's again use a form dedicated to editing a user. This form will display two fields : one for a username and one for an email. Our initial form model doesn't have to specify those two elements when it is first created :


// GET handler
public void myHandlerGet(AppRequestContext context) {

    JsonObject userForm = context.response().getModel().getJsonObject("userForm");
    if (userForm == null) {
    
        // Empty model! 
        // No username and no email elements are specified.
        userForm = context.json().create();
        
        context.response().getModel().set("userForm", userForm);
    }
    
    context.response().sendTemplateHtml("/templates/userEdit.html");
}

Here's what the HTML for that form may look like (we are using the syntax for the default Templating Engine, Pebble):

<form method="post">
    <div class="form-group">
        <input type="text" 
               class="form-control" 
               name="userForm.username"
               value="{{userForm.username | default('')}}" />
    </div>
    <div class="form-group">
        <input type="text" 
               class="form-control" 
               name="userForm.email"
               value="{{userForm.email | default('')}}" />
    </div>
    <input type="submit" />
</form>

Notice that even if the form model doesn't contain any "username" or "email" elements, we still bind them to the HTML elements using their JsonPaths [6] and here [12]. This is possible in part because we use the default('') filter : this filter tells Pebble to use an empty string if the element doesn't exist.

The "name" attributes of the HTML elements are very important : they represent the JsonPaths that Spincast is going to use to dynamically create the Form object, when the page is submitted.

So let's say this form is submitted. You would then access the values of the fields like so, in your POST handler:


// POST handler
public void myHandlerPost(AppRequestContext context) {

    Form userForm = context.request().getFormWithRootKey("userForm");
    context.response().addForm(userForm);
    
    // The "username" and "email" elements have been
    // automatically created to represent the submitted
    // fields.
    String username = userForm.getString("username");
    String email = userForm.getString("email");  
    
    //...
}

As you can see, Spincast uses the "name" attribute of an HTML element as a JsonPath to dynamically create an associated model element. This gives you a lot of flexibility client-side since you can dynamically generate new fields or even entire forms, using javascript.

Text based fields

Text based fields, such as text, password, email and textarea are very easy to manipulate :

  • You use the JsonPath you want for their associated model element as their "name" attribute.
  • You use that same JsonPath to target the current value of the element on the model, and you output it in the "value" attribute.
  • You use the default('') filter to make sure not exception is thrown if the model element doesn't exist yet.
Quick example :

<input type="text" 
       name="userForm.email"
       value="{{userForm.email | default('')}}" />

Text based field groups

Sometimes we want multiple text fields to be grouped together. For example, let's say we want various "tags" to be associated with an "articleForm" object. Each of those "tags" will have its own dedicated field on the form, but we want all the "tags" to be available as a single array when they are submitted. To achieve that :

  • We use the same "name" attribute for every field, but we suffix this name with the position of the tag inside the final array. For example : "articleForm.tags[0]" or "articleForm.tags[1]"
  • We also use that same "[X]" suffixed name to get and display the "value" attributes.
What we are doing, again, is to use the JsonPath to target each element! For example :

<form method="post">
    <input type="text" class="form-control" name="articleForm.tags[0]"
           value="{{articleForm.tags[0] | default('')}}" />
    
    <input type="text" class="form-control" name="articleForm.tags[1]"
           value="{{articleForm.tags[1] | default('')}}">
    
    <input type="text" class="form-control" name="articleForm.tags[2]"
           value="{{articleForm.tags[2] | default('')}}">
    <input type="submit" />
</form>

When this form is submitted, you have access to the three "tags" as a single JsonArray :

public void manageArticle(AppRequestContext context) {

    Form form = context.request().getFormWithRootKey("articleForm");
    context.response().addForm(form);

    // Get all the tags of the article, as an array
    JsonArray tags = form.getJsonArray("tags");
    
    // You could also access one of the tag directly, using
    // its full JsonPath
    String thirdTag = form.getString("tags[2]");
    
    //...
}

Select fields

The select fields come in two flavors : single value or multiple values. To use them :

  • You specify the JsonPath of the associated element in the "name" attribute of the select HTML element.
  • For every option elements of the field you use the selected(...) filter to check if the option should be selected or not.
Here's an example for a single value select field :


<select name="userForm.favDrink" class="form-control">
    <option value="tea" {{userForm.favDrink | selected("tea")}}>Tea</option>
    <option value="coffee" {{userForm.favDrink | selected("coffee")}}>Coffee</option>
    <option value="beer" {{userForm.favDrink | selected("beer")}}>WBeer</option>
</select>

In this example, the values of the option elements are hardcoded, they were known in advance : "tea", "coffee" and "beer". Here's a version where the option elements are dynamically generated :

<select name="userForm.favDrink" class="form-control">
    {% for drink in allDrinks %}
        <option value="{{drink.id}}" {{userForm.favDrink | selected(drink.id)}}>{{drink.name}}</option>
    {% endfor %}
</select>

In this example, the selected(...) filter compares the current favorite drink of the user ("userForm.favDrink") to the value of every option element and outputs the "selected" attribute if there is a match.

Displaying a multiple values select field is similar, but :

  • You use "[]" after the "name" attribute of the select field. This tells Spincast that an array of values is expected when the form is submitted.
  • The left side of a selected(...) filter will be a list of values (since more than one option may have been selected). The filter will output the "seleted" attribute as long as the value of an option matches any of the values from the list.
For example :


<select multiple name="userForm.favDrinks[]" class="form-control">
    <option value="tea" {{userForm.favDrinks | selected("tea")}}>Tea</option>
    <option value="coffee" {{userForm.favDrinks | selected("coffee")}}>Coffee</option>
    <option value="beer" {{userForm.favDrinks | selected("beer")}}>WBeer</option>
</select>

Radio Buttons

To display a radio buttons group :

  • You use the JsonPath of the associated model element as the "name" attributes.
  • You output the "value" of each radio button. Those values can be hardcoded, or they can be dynamically generated inside a loop (we'll see an example of both).
  • You use the checked(...) filter provided by Spincast determine if a radio button should be checked or not.

Let's first have a look at an example where the values of the radio buttons are hardcoded :

<div class="form-group">
    <label for="drinkTea">
        <input type="radio" 
               id="drinkTea" 
               name="userForm.favDrink"
               {{userForm.favDrink | checked("tea")}}
               value="tea"/> Tea</label>
    
    <label for="drinkCoffee">
        <input type="radio" 
               id="drinkCoffee" 
               name="userForm.favDrink"
               {{userForm.favDrink | checked("coffee")}}
               value="coffee"> Coffee</label>
    
    <label for="drinkBeer">
        <input type="radio" 
               id="drinkBeer" 
               name="userForm.favDrink"
               {{userForm.favDrink | checked("beer")}}
               value="beer"> Beer</label>
</div>

Let's focus on the first radio button of that group. First, its "name" attribute :

<label for="drinkTea">
    <input type="radio" 
           id="drinkTea" 
           name="userForm.favDrink"
           {{userForm.favDrink | checked("tea")}}
           value="tea"/> Tea</label>

As we already said, the "name" attribute of a field is very important. Spincast uses it to create the element on the form model, when the form is submitted. This "name" will become the JsonPath of the element on the form model. In our example, the form model would contain a "favDrink" element.

Let's now have a look at the checked(...) filter :

<label for="drinkTea">
    <input type="radio" 
           id="drinkTea" 
           name="userForm.favDrink"
           {{userForm.favDrink | checked("tea")}}
           value="tea"/> Tea</label>

We don't know in advance if a radio button should be checked or not, this depends on the current value of the "userForm.favDrink" element. That's why we use "checked(...)". This filter will compare the current value of the "userForm.favDrink" model element to the value of the radio button ("tea" in our example). If there is a match, a "checked" attribute is printed!

Note that the parameter of the "checked(...)" filter can be an array. In that case, the filter will output "checked" if the current value matches any of the elements. For example :

<label for="drinkTea">
    <input type="radio" 
           id="drinkTea" 
           name="userForm.favDrink"
           {{userForm.favDrink | checked(["tea", "ice tea", chai"])}}
           value="tea"/> Tea</label>

This feature is mainly useful when the radio buttons are dynamically generated.

Speaking of dynamically generated radio buttons, let's see an example of those! The creation of the response model, in your Route Handler, may look like this :

public void myRouteHandler(AppRequestContext context) {

    //==========================================
    // Creates the available drink options and add them
    // to the reponse model directly. 
    // There is no need to add them to the form 
    // itself (but you can!).
    //==========================================
    JsonArray allDrinks = context.json().createArray();
    context.response().getModel().set("allDrinks", allDrinks);

    JsonObject drink = context.json().create();
    drink.set("id", 1);
    drink.set("name", "Tea");
    allDrinks.add(drink);

    drink = context.json().create();
    drink.set("id", 2);
    drink.set("name", "Coffee");
    allDrinks.add(drink);

    drink = context.json().create();
    drink.set("id", 3);
    drink.set("name", "Beer");
    allDrinks.add(drink);

    //==========================================
    // Creates the form, if it doesn't already exist.
    //==========================================
    JsonObject form = context.response().getModel().getJsonObject("userForm");
    if (userForm == null) {
        form = context.json().create();
        context.response().getModel().set("userForm", form);
        
        // Specifies the initial favorite drink of the user.
        User user = getUser(...);
        JsonObject user = context.json().create();
        form.set("favDrink", user.getFavDrink());
    }

    context.response().sendTemplateHtml("/templates/userTemplate.html");
}

With this response model in place, we can dynamically generate the radio buttons group and check the current favorite one of the user :

<div class="form-group">
    {% for drink in allDrinks %}
        <label for="drink_{{drink.id}}">
            <input type="radio" 
                   id="drink_{{drink.id}}" 
                   name="userForm.favDrink"
                   {{userForm.favDrink | checked(drink.id)}}
                   value="{{drink.id}}"/> {{drink.name}}</label> 
    {% endfor %}
</div>

Checkboxes

Checkboxes are often used in one of those two situations :

  • To allow the user to select a single boolean value. For example :

    [ ] Do you want to subscribe to our newsletter?

  • To allow the user to select multiple values for a single preference. For example :

    Which drinks do you like?
    [ ] Tea
    [ ] Coffee
    [ ] Beer

First, let's look at a single checkbox field :

<label for="tosAccepted">
    <input type="checkbox" 
           id="tosAccepted" 
           name="myForm.tosAccepted"
           {{myForm.tosAccepted | checked(true)}}
           value="true" /> I agree to the Terms of Service</label>

Note that, even if the value of the checkbox is "true" as a string, you can use true as a boolean as the filter parameter. This is possible because the checked(...) filter (and the selected(...) filter) compares elements using equivalence, not equality. So "true" would match true and "123.00" would match 123.

When this field is submitted, you would be able to access the boolean value associated with it using :

public void myRouteHandler(AppRequestContext context) {

    Form form = context.request().getFormWithRootKey("myForm");
    context.response().addForm(form);

    boolean tosAccepted = form.getBoolean("tosAccepted");
    
    //...
}

Now, let's see an example of a group of checkboxes :

<div class="form-group">
    <label for="drinkTea">
        <input type="checkbox" 
               id="drinkTea" 
               name="userForm.favDrinks[0]"
               {{userForm.favDrinks[0] | checked("tea")}}
               value="tea"/> Tea</label>
    
    <label for="drinkCoffee">
        <input type="checkbox" 
               id="drinkCoffee" 
               name="userForm.favDrinks[1]"
               {{userForm.favDrinks[1] | checked("coffee")}}
               value="coffee"> Coffee</label>
    
    <label for="drinkBeer">
        <input type="checkbox" 
               id="drinkBeer" 
               name="userForm.favDrinks[2]"
               {{userForm.favDrinks[2] | checked("beer")}}
               value="beer"> Beer</label>
</div>

Here, the checkboxes are grouped together since they share the same "name" attribute, name that is suffixed with the position of the element in the group. Again, their "name" is the JsonPath of their associated element on the form model.

With this in place, we can access all the checked "favorite drinks" as a single array, in our handler.

In the following example, we will retrieve such array without using a proper Form object, but by using request.getFormData() directly, to show this is also an option! But note that if you do it that way, you won't have access to the built-in validation features a Form provide... You are manipulating the form data as a raw JsonObject! :

public void myRouteHandler(AppRequestContext context) {

    JsonObject model = context.request().getFormData();
    
    // The checked favorite drinks, as an array!
    JsonArray favDrinks = model.getJsonArray("userForm.favDrinks");
    
    //...
}

Finally, note that the positions used in the "name" HTML attributes are kept when we receive the array! This means that if the user only checked "beer" for example (the last option), the array received in our handler will be [null, null, "beer"], not ["beer"]! This is a good thing because the JsonPath we use for an element always stays valid ("userForm.favDrinks[2]" here).

File upload

Uploading a file is very easy using Spincast. The main difference between a "file" element and the other types of elements is that the uploaded file will not be available as a form data when submitted. You'll have to use a dedicated method to retrieve it.

The HTML part is very standard :

<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" class="form-control" name="fileToUpload">
    <button type="submit">Submit</button>
</form>

To retrieve the uploaded file, you use one of the getUploadedFileXXX(...) methods on the request() add-on. For example :

public void myRouteHandler(AppRequestContext context) {

    File uploadedFile = context.request().getUploadedFileFirst("fileToUpload");
}

Note that even if the uploaded file is not part of the form data, you can still perform validation, as we'll see in the next section.

Form validation introduction

Validating a submitted form involves three main steps :

  • Retrieving the submitted form data.
  • Validating the form, and adding resulting validation messages to it.
  • Redisplaying the form with the validation messages resulting from the validation. If the form is valid, you may instead want to redirect the user to a confirmation page where a success Flash Message will be displayed.

Retrieving the submitted form

When an HTML form is submitted, Spincast treats the "name" attributes of the fields as JsonPaths in order to create a Form (a plain JsonObject with extra validation features) representing the form model. In other words, Spincast converts the submitted data to a Form instance so you can easily validate and manipulate it.

You access that Form representing the submitted data by using the getForm(...) method of the request() add-on:


// POST handler
public void myHandlerPost(AppRequestContext context) {

    Form userForm = context.request().getFormWithRootKey("userForm");
    context.response().addForm(userForm);
    
    //...
}

If you have more than one form on the same HTML page, you simply give them different names, and check which one has been submitted, by looking for the presence of a field which should always be submitted:


// POST handler
public void myHandlerPost(AppRequestContext context) {

    Form userForm = context.request().getFormWithRootKey("userForm");
    if(userForm.getString("userFormBtn") != null) {
        context.response().addForm(userForm);
        processUserForm(context, userForm);
        return;
    }
    
    Form bookForm = context.request().getFormWithRootKey("bookForm");
    if(bookForm.getString("bookFormBtn") != null) {
        context.response().addForm(bookForm);
        processBookForm(context, bookForm);
        return;
    }
    
    //...
}

Performing validations

Once you have the Form representing the submitted data, you can start validating it. Forms implement the ValidationSet interface and allow you to store validation results directly in them.

Here's an example where we validate that a submitted "email" is valid, and add an error to the form if it's not:


// POST handler
public void myHandlerPost(AppRequestContext context) {

    Form form = context.request().getFormWithRootKey("userForm");
    context.response().addForm(form);
    
    String email = form.getString("email");
    
    if (!form.validators().isEmailValid(email)) {
        form.addError("email",
                      "email_invalid",
                      "The email is invalid");
    }
    
    //... 
}

Explanation :

  • 5 : We retrieve the submitted form from the request.
  • 6 : We immediately add the form back to the response model.
  • 8 : We get the "email" from the form.
  • 10 : We validate the email using a validator provided on the Form object itself!
  • 11-13 : If the email is invalid, we add an error validation message to the form.
  • 11 : The first parameter, "email" is the JsonPath of the validated element.
  • 12 : The second parameter, "email_invalid" is a code representing the error. This can be used client-side to know what exact error occured.
  • 13 : The third parameter is the message to display to the user.

To validate an element of the form, you can use any method you need. Some validators, such as isEmailValid(...) are provided by the form.validators() method. But, most of the time, you're going to use custom code for your validations. For example:


Form form = context.request().getFormWithRootKey("userForm");

String name = form.getString("name");
if(StringUtils.isBlank(name)) {
    form.addError("name",
                  "name_empty",
                  "The name is required!");
}

Finally, note that there are "success" and "warning" validation messages too, in addition to the "error" ones.

Displaying Validation Messages

When you add the form to the response model, using context.response().addForm(form), you are in fact adding two elements :

  • The form itself, using its name as the key in the response model.
  • A Validation element, containing the validation messages added on the form.

By default, the Validation element containing the messages of a validated form is called "validation". You can choose a different name for this element when adding the form to the response model. For example:


// POST handler
public void myHandlerPost(AppRequestContext context) {

    Form userForm = context.request().getFormWithRootKey("userForm");
    
    // Uses "userFormValidation" as the name for the
    // validation element.
    context.response().addForm(userForm, "userFormValidation");
    
    // validation...
}

When it reaches the templating engine, the Validation element associated with a form will contain:

  • An object for every validation message added to the form, with the JsonPath of the validated element as the key and three fields, "level", "code" and "text":

    "userForm.name" : {
        "level" : "ERROR",
        "code" : "name_empty",
        "text" : "The name is required!" 
    }

  • A special "_" element that summarizes all the validations performed on the form:

    "userForm._" : {
        "hasErrors" : true,
        "hasWarnings" : false,
        "isValid" : false,
        "hasSuccesses" : false
    }

    This "_" element can be used in a template to display something if the form contains errors, for example.

Here's a bigger chunk of the model the templating engine will have access to to redisplay an invalid form :

{
    // The form itself
    "userForm" : {
        "name" : ""
        "email" : "abc"
        "books": [
            {
                "title" : "Dune",
                "author": "Frank Herbert"
            },
            {
                "title" : "The Hitchhiker's Guide to the Galaxy",
                "author" : ""
            }
        ]
    },
    
    // The "validation" element
    "validation" : {
        "userForm._" : {
            "hasErrors" : true,
            "hasWarnings" : false,
            "isValid" : false,
            "hasSuccesses" : false
        },
        "userForm.name" : {
            "level" : "ERROR",
            "code" : "name_empty",
            "text" : "The name is required!" 
        },
        "userForm.email" : {
            "level" : "ERROR",
            "code" : "email_invalid",
            "text" : "The email is invalid" 
        },
        "userForm.books[1].author" : {
            "level" : "ERROR",
            "code" : "author_empty",
            "text" : "The author is required!" 
        }
    }
    
    // ...
}

The important things to notice are :

  • In the form object, each element is positioned at its JsonPath. For example, the author of the second book is located at userForm.books[1].author.
  • In the "validation" element, each keys is the string representation of the JsonPath of the validated element! For example : validation['userForm.books[1].author'].
It is easy to find the validation messages associated with a specific element since the JsonPath of that element will be the key to use to retrieve them. For example:

<div class="form-group">
    <input type="text" 
           class="form-control" 
           name="userForm.email"
           value="{{userForm.email | default('')}}" />
    {{validation['userForm.email'] | validationMessages()}}
</div>

Validation Filters

Spincast provides utilities to display the validation messages with the default Templating Engine, Pebble. But, as we saw, the template model is a simple Map<String, Object> so no magic is involved and any other Templating Engine can be used.

Have a look at the Forms + Validation demos section to see the following validation filters in action!

  • ValidationMessages | validationMessages()

    This filter uses a HTML template fragment to output the Validation Messages associated with an element.

    Here's an example :

    <div class="form-group">
        <input type="text" 
               class="form-control" 
               name="myForm.email"
               value="{{myForm.email | default('')}}" />
        {{validation['myForm.email'] | validationMessages()}}
    </div>

    The path to the template fragment is configurable using the SpincastPebbleTemplatingEngineConfig#getValidationMessagesTemplatePath() method. The default path is "/spincast/spincast-plugins-pebble/spincastPebbleExtension/validationMessagesTemplate.html" which points to a template fragment provided by Spincast.

  • ValidationMessages | validationGroupMessages()

    This filter is similar to validationMessages() but uses a different template fragment. Its purpose is to output the Validation Messages of a group of elements.

    Here's an example :

    <div id="tagsGroup" class="form-group {{validation['demoForm.tags'] | validationClass()}}">
    
        <div class="col-sm-4">
            <label class="control-label">Tags *</label>
            {{validation['demoForm.tags'] | validationGroupMessages()}}
        </div>
        
        <div class="col-sm-8">
            <input type="text" name="demoForm.tags[0]" 
                   class="form-control {{validation['demoForm.tags[0]'] | validationClass()}}"
                   value="{{demoForm.tags[0] | default('')}}" />
            {{validation['demoForm.tags[0]'] | validationMessages()}}
            
            <input type="text" name="demoForm.tags[1]" 
                   class="form-control {{validation['demoForm.tags[1]'] | validationClass()}}"
                   value="{{demoForm.tags[1] | default('')}}">
            {{validation['demoForm.tags[1]'] | validationMessages()}}
        </div>
    </div>

    In this example, we ask the user to enter two tags. If one is invalid, we may want to display a "This tag is invalid" message below the invalid field, but we may also want to display a global "At least one tag is invalid" below the group title, "Tags *".

    This is exactly what the validationGroupMessages() filter is for. As you may notice, "demoForm.tags" is, in fact, the JsonPath to the tags array itself.

    The path to the template fragment used by this filter is configurable using the SpincastPebbleTemplatingEngineConfig#getValidationGroupMessagesTemplatePath() method. The default path is "/spincast/spincast-plugins-pebble/spincastPebbleExtension/validationGroupMessagesTemplate.html" which is a template fragment provided by Spincast.

  • ValidationMessages | validationClass()

    The validationClass(...) filter checks if there are Validation Messages and, if so, it outputs a class name.

    The default class names are :

    • "has-error" : when there is at least one Error Validation Message.
    • "has-warning" : when there is at least one Warning Validation Message.
    • "has-success" : when there is at least one Success Validation Message.
    • "has-no-message" : when there are no Validation Messages at all.

    For example :

    
    <div id="tagsGroup" class="form-group {{validation['demoForm.tags'] | validationClass()}}">
    
        <div class="col-sm-4">
            <label class="control-label">Tags *</label>
            {{validation['demoForm.tags'] | validationGroupMessages()}}
        </div>
        
        <div class="col-sm-8">
            <input type="text" name="demoForm.tags[0]" 
                   class="form-control {{validation['demoForm.tags[0]'] | validationClass()}}"
                   value="{{demoForm.tags[0] | default('')}}" />
            {{validation['demoForm.tags[0]'] | validationMessages()}}
            
            <input type="text" name="demoForm.tags[1]" 
                   class="form-control {{validation['demoForm.tags[1]'] | validationClass()}}"
                   value="{{demoForm.tags[1] | default('')}}">
            {{validation['demoForm.tags[1]'] | validationMessages()}}
        </div>
    </div>

    The validationClass() filter can be used both on single fields and on a group of fields. It is up to you to tweak the CSS of your application so the generated class are used properly.
  • ValidationMessages | validationFresh()
    ValidationMessages | validationSubmitted()

    Those two filters are used to determine if a form is displayed for the first time, or if it has been submitted and is currently redisplayed with potential Validation Messages. When one of those filters returns true, the other necessarily returns false.

    Most of the time, you are going to use the special "_" element, representing the validation as a whole, as the element passed to those filters. For example :

    
     {% if validation['myForm._'] | validationFresh() %}
         <div>This form is displayed for the first time!</div> 
     {% endif %}
    

    and :

    
     {% if validation['myForm._'] | validationSubmitted() %}
         <div>This form has been validated!</div> 
     {% endif %}
    

  • ValidationMessages | validationHasErrors()
    ValidationMessages | validationHasWarnings()
    ValidationMessages | validationHasSuccesses()
    ValidationMessages | validationIsValid()

    Those four filters check if there are Validation Messages of a particular level and return true or false.

    For example, you could use those filters to determine if you have to display an element or not, depending of the result of a validation.

    • validationHasErrors() : returns true if there is at least one Error Validation Message.
    • validationHasWarnings() : returns true if there is at least one Warning Validation Message.
    • validationHasSuccesses() :returns true if there is at least one Success Validation Message.
    • validationIsValid() : returnstrue if there is no Validation Message at all.
    For example :

    
     {% if validation['myForm.email'] | validationHasErrors() %}
         <div>There are errors associated with the email field.</div> 
     {% endif %}
    


    An important thing to know is that you can also use those filters to see if the form itself, as a whole, contains Validation Messages at a specific level. To do that, you use the special "_" element representing the form itself. For example :

    
     {% if validation['myForm._'] | validationHasErrors() %}
         <div>The form contains errors!</div> 
     {% endif %}
    


    It is also important to know that those filters will often be used in association with the validationSubmitted(...) filter. The reason is that when a form is displayed for the first time, it doesn't contain any Validation Messages, so the validationIsValid(...) filter will return true.

    But if you want to know if the form is valid after having been validated, then you need to use the validationSubmitted(...) filter too :

    
     {% if validation['myForm._'] | validationSubmitted() and validation['myForm.email'] | validationIsValid() %}
         <div>The email has been validated and is ok!</div> 
     {% endif %}
    

Forms are generic

You may have noticed that we are not using a dedicated class to represent the form model (a "UserForm" class, for example) : we use plain JsonObject objects (which Form object are based on).

Here's why:

  • You may be thinking about reusing an existing Entity class for the model of your form. For example, you may want to use an existing "User" Entity class for the model of a form dedicated to the creation of a new user. This seems logical at first since a lot of fields on the form would have a matching field on that User Entity class... But, in practice, it's very rare that an existing Entity class contains all the fields required to model the form.

    Let's say our form has a "name" field and a "email" field and uses those to create a new user : those fields would probably indeed have matching fields on a "User" Entity. But what about a captcha? Or an option to "subscribe to our newsletter"? Those two fields on the form have nothing to do with a "user" and there won't be matching fields for them on a "User" Entity class... So, what you do then? You have to create a new class that contains all the required fields. For that, you may be tempted to extend the "User" Entity and simply add the missing fields, but our opinion is that this is hackish at best and clearly not a good practice.

  • You may also feel that using a dedicated class for such form model is more robust, since that model is then typed. We understand this feeling since we're huge fans of statically typed code! But, for this particular component, for the model of a form, our opinion is that a dedicated class is not very beneficial...

    As soon as your form model leaves your controller, it is pretty much converted to a simple and dumb Map<String, Object>, so the Templating Engine can use it easily. At that moment, your typed form model is no more! And, at the end of the day, the model becomes plain HTML fields : nothing is typed there either.

    In other words, if you use a dedicated class for your form model, this model is going to be typed for a very short period, and we feel this doesn't worth the effort. That said, when your form has been validated and everything is fine, then you may want to convert the JsonObject/Form object to a dedicated Entity class and pass it to services, repositories, etc.

  • Last but not least : using an existing Entity class as a form model can lead to security vulnerabilities (PDF) if you are not careful.

In case you still want to use a dedicated class to back your forms, you are free to do so, and here's a quick example.... First, you would create a dedicated class for the model :

public class UserCreationForm {
    
    private String username;
    private String email;
    private String captcha;
    
    //... Getters
    //... Setters
}

You would then create a model instance like so :

public void displayUserForm(AppRequestContext context) {

    // A typed form model
    UserCreationForm userForm = new UserCreationForm();
    
    // ... that is quickly converted to a 
    // JsonObject anyway when added to the response model!
    context.response().getModel().set("userForm", userForm);
    
    sendMyTemplate();
}

When the form is submitted, you would then convert the form, which is a JsonObject under the hood, to an instance of your UserCreationForm class :

public void manageUserForm(AppRequestContext context) {

    // Back to a typed version of the form model!
    UserCreationForm userForm = context.request()
                                       .getFormWithRootKey("userForm")
                                       .convert(UserCreationForm.class);
                                       
   // ...
}

HTTP Caching

Spincast supports many HTTP caching features, as described in the HTTP 1.1 specification :

  • Cache-Control headers
  • Last modification dates
  • Etag headers
  • "No Cache" headers
Finally, Spincast also provides a mechanism for Cache Busting.

Cache-Control

The Cache-Control header (and the similar, but older, Expires header) is used to tell a client how much time (in seconds) it should use its cached copy of a resource before asking the server for a fresh copy.

This Cache-Control header can first be specified when you build a route (see HTTP Caching route options), using the cache(...) method.

For example :

router.GET("/test").cache(3600).handle(handler);

There are three options available when using this cache(...) method :

  • The number of seconds the client should uses its cached version without asking the server again.
  • Is the cache public (default) or private. The private option means an end client can cache the resource but not an intermediate proxy/CDN (more information).
  • The number of seconds a proxy/CDN should use its cached version without asking the server again. Only use this option if it must be different than the number of seconds for regular end clients.

This cache(...) method can also be used dynamically, in a route handler, using the cacheHeaders() add-on :

@Override
public void myHandler(AppRequestContext context) {
    context.cacheHeaders().cache(3600);
    context.response().sendPlainText("This will be cached for 3600 seconds!");
}

Default caching

When you do not explicitly set any caching options for a Static Resource, some defaults are automatically used. Those defaults are configurable using the SpincastConfig class.

There are two variations for those Static Resources default caching configurations :

  • One for plain Static Resources. The default is to send headers so the resource is cached for 86400 seconds (1 day).
  • One for Dynamic Resources. The default is to send headers so the resource is cached for 3600 seconds (1 hour). When a resource can be generated, it often means that the resource may change more frequently. This is why dynamic resources have their own default caching configuration.

When a Static/Dynamic Resource is served, a Last-Modified header is also automatically sent. This means that even when the client does ask for a fresh copy of a resource, it will often receive a "304 - Not Modified" response and will therefore again use its cached copy, without unnecessary data being transferred over the network.

Finally, note that no default caching headers are sent for regular Routes. You have to explictly use cache(...) to send some. But if you use cache() as is, without any parameter, then default values will be used (still configurable using the SpincastConfig class).

No Cache

Even when no explicit caching headers are sent, some clients (browsers, proxies, etc.) may use a default caching strategy for the resources they download. For example, if you press the "back" button, many browsers will display a cached version of the previous page, without requesting the server for a fresh copy... Even if no caching headers were sent for that page!

If you want to tell the client that it should disable any kind of caching for a resource, you can use the noCache(...) method. This can be done when building a Route :

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

Or can be used dynamically, in a Route Handler :

@Override
public void myHandler(AppRequestContext context) {
    context.cacheHeaders().noCache();
    context.response().sendPlainText("This will never be cached!");
}

Finally, note that you can not use the noCache() options on a Static Resource since this would defeat the notion of a "static" resource.

Cache Busting

Cache busting is the process of adding a special token to the URL of a resource in a way that simply by changing this token you invalidate any cache a client may have.

For example, let's say that in a HTML page you reference a .css file that you want the client to cache (since it won't frequently change) :

<link rel="stylesheet" href="/public/css/main.css">

The associated Route may be :

router.file("/public/css/main.css").cache(86400).classpath("/css/main.css").handle();

As you now know, when this resource is served a Cache-Control header will be sent to the client so it caches it for 24 hours. And this is great! But what happens if you release a new version of your application? You may then have changed "main.css" and you want all clients to use the new, fresh, version. How can you do that if many clients already have the old version in cache and you specified them that they can use that cached version for 24 hours? This is where cache busting become handy!

To enable cache busting for a particular resource, you add the "cacheBuster" template variable to its URL. This template variable is provided by Spincast, you simply need to add it in your HTML pages. For example, if you use Pebble, which is the default Templating Engine provided with Spincast, you need to use "{{cacheBuster}}" as the cache buster token. We recommend that you add this token right before the file name of the resource :


<link rel="stylesheet" href="/public/css/{{cacheBuster}}main.css">

When the HTML is rendered, the result will look something like this :


<link rel="stylesheet" href="/public/css/spincastcb_1469634250814_main.css">

When Spincast receives a request, it automatically removes any cache busters from the URL. So

  • "/public/css/spincastcb_1111111111111_main.css"
  • "/public/css/spincastcb_2222222222222_main.css"
will both result in the exact same URL, and will both target the same resource :
  • "/public/css/main.css"

But (and that's the trick!) the client will consider both URLs as different, targeting different resources, so it won't use any cached version when a cache buster is changed!

By default, the cache busting code provided by Spincast will change every time the application is restarted. You can modify this behavior and/or the format of the token by overriding the getCacheBusterCode() and removeCacheBusterCodes(...) methods from SpincastUtils.

Finally, note that the cache busting tokens are removed before the routing is done, so they don't affect it in any way.

Etag and Last Modification date

The Etag and Last modification date headers are two ways of validating if the cached version a client already has of a resource is still valid or not. We will call them "Freshness headers".

The client, when it wants to retrieve/modify/delete a resource it already has a copy of, sends a request for that resource by passing the Etags and/or Last modification date it currently has for that resource. The Server validates those values with the current information of the resource and decides if the current resource should be retrieved/modified/deleted. Some variations using those headers are even used to validate if a client can create a new resource.

Note that freshness headers management is not required on all endpoints. For example, an endpoint that would compute the sum of two numbers has no use for cache headers or for freshness validation! But when a REST endpoint deal with a resource, by Creating, Retrieving, Updating or Deleting it, then freshness validation is a must to respect the HTTP specification.

Proper use of the freshness headers is not trivial

The most popular use for freshness headers is to return a 304 - Not Modified response when a client asks for a fresh copy of a resource but that resource has not changed. Doing so, the response is very fast since no unnecessary data is actually transmitted over the network : the client simply continue to use its cached copy.

This "304" use case if often the only one managed by web frameworks. The reason is that it can be automated, to some extent. A popular way of automating it is to use an "after" filter to generate a hash from the body of a resource returned as the response to a GET request. This hash is used as an ETag header and compared with any existing If-None-Match header sent by the client. If the ETag matches, then the generated resource is not sent, a "304 - Not Modified" response is sent instead.

This approach may be attractive at first because it is very simple and doesn't require you to do anything except registering a filter. The problem is that this HTTP caching management is very, very, limited and only addresses one aspect of the caching mechanism described in the HTTP specification.

First, it only addresses GET requests. Its only purpose is to return a 304 - Not Modified response instead of the actual resource on a GET request. But, freshness headers should be used for a lot more than that. For example :

  • If a request is received with an "If-Match" (Etag) or an "If-Unmodified-Since" (Last modification date) header, and the resource has changed, then the request must fail and a 412 - Precondition Failed response must be returned. (specification)
  • If a request with an "If-None-Match: *" header is received on a PUT request, the resource must not be created if any version of it already exists. (specification)

Also, to hash the body of the resource to create an ETag may not always be appropriate. First, the resource must be generated for this hash to be computed. But maybe you shouldn't have let the request access the resource in the first place! Also, maybe there is a far better way to generate a unique ETag for your resource than to hash its body, using one of its field for example. Finally, what happens if you need to "stream" that resource? If you need to flush the response more than once when serving it? In that case, the "after" filter wouldn't be able to hash the body properly and send it as an ETag header.

All this to say that Etags and Last modification date may seem easy to manage at first, but in fact, they require some work from you. If you simply want to manage the GET use case where a 304 - Not Modified response can be returned instead of the resource itself, then creating your own "after" filter should be quite easy (we may even provide one in a future release). But if you want your REST endpoints to be more compliant with the HTTP specification, then keep reading to learn how to use the cacheHeaders() add-on and its validate() method!

Freshness management using the cacheHeaders() add-on

There are three methods on the cacheHeaders() add-on made to deal with freshness headers in a Route Handler :

  • .etag(...)
  • .lastModified(...)
  • .validate(...)

The first two are used to set the Etag and Last modification date headers for the current version of the resource. For example :

// Set the ETag
context.cacheHeaders().eTag(resourceEtag);

// Set the Last modification date
context.cacheHeaders().lastModified(resourceModificationDate);

But setting those freshness headers doesn't make sense if you do not validate them when they are sent back by a client! This is what the .validate(...) method is made for. It validates the current ETag and/or Last modification date of the resource to the ones sent by the client. Here's an example of what using this method looks like :

if(context.cacheHeaders().eTag(resourceEtag)
                         .lastModified(resourceModificationDate)
                         .validate(resource != null)) {
    return;
}

We will look in details how to use the validate() method, but the important thing to remember is that if this method returns true, then your route handler should return immediately, without returning/modifying/creating or deleting the associated resource. It also means that the response to return to the client has already been set and should be returned as is : you don't have to do anything more.

Note that, in general, you will use ETags or Last modification dates, not both. Since ETags are more generic (you can even use a modification date as an ETag!), our following example will only focus on ETags. But using the validate(...) method is the same, that you use ETags or Last modification dates.

Using the "validate(...)" method

Let's first repeat that, as we previously said, the Static Resources have their Last modification date automatically managed. The Server simply validates the modification date of the resource's file on disk and use this information to decide if a new copy of the resource should be returned or not. In other words, the validation pattern we are going to explain here only concerns regular Routes, where Route Handlers manage the target resources.

The freshness validation pattern looks like this, in a Route Handler :

public void myRouteHandler(AppRequestContext context) {

	// 1. Gets the current resource, if any
    MyResource resource = getMyResource();
    String resourceEtag = computeEtag(resource);

	// 2. Sets the current ETag and validates the freshness of the
	// headers sent by the client
	if(context.cacheHeaders().eTag(resourceEtag)
	                         .validate(resource != null)) {
	    return;
	}

	// 3. Validation done! 
	// Now the core of the handler can run :
	// it can create/return/update/delete the resource.
	
    // ...
};

Explanation :

  • 4 : We get the actual resource (from a service for example). Note that it can be null if it doesn't exist or if it has been deleted.
  • 5 : We compute the ETag for the resource. The ETag can be anything : it is specific to your application how to generate it. Note that the ETag can be null if the resource doesn't exist!
  • 9 : By using the cacheHeaders() add-on, we set the ETag of the current resource. This will add the appropriate headers to the response : those headers will be sent, whatever the result of the validate(...) method is.
  • 10 : We call the validate(...) method. This method takes one parameter : a boolean indicating if the resource currently exists or not. The method will validate the current ETag and/or the Last modification date with the ones received by the client. If any HTTP freshness rule is matched, some appropriate headers are set in the response ("304 - Not Modified" or "412 - Precondition Failed") and true is returned. If no freshness rule is matched, false is returned.
  • 11 : If the validate(...) method returns true, our Route Handler should return immediately, without further processing!
  • 14-18 : If the validate(...) method returns false, the main part of our Route Handler can run, as usual.

As you can see, the validation pattern consists in comparing the ETag and/or Last modification date of the actual resource to the headers sent by the client. A lot of validations are done in that validate(...) method, we try to follow as much as possible the full HTTP specification.

Note that if the resource doesn't currently exist, you should not create it before calling the validate(...) method! You should instead pass false to the validate(...) method. If the request is a PUT asking to create the resource, this creation can be done after the cache headers validation, and only if the validate(false) method returns false. In that case, the ETag and/or Last modification date will have to be added to the response by calling eTag(...) and/or lastModified(...) again :

public void createHandler(AppRequestContext context) {

	// Let's say the resource is "null" here.
	MyResource resource = getMyResource();
	
	// The ETag will be "null" too.
	String resourceEtag = computeEtag(resource);

	if(context.cacheHeaders().eTag(resourceEtag)
	                         .validate(resource != null)) {
	    return;
	}
	
	// The validation returned "false" so we can 
	// create the resource!
	resource = createResourceUsingInforFromTheRequest(context);
	
	// We add the new ETag to the response.
	resourceEtag = computeEtag(resource);
	context.cacheHeaders().eTag(resourceEtag);
}

If the resource doesn't exist and the request is a GET, you can return a 404 - Not Found after the freshness validation. In fact, once the validation is done, your handler can be processed as usual, as if there was no prior validation... For example :

public void getHandler(AppRequestContext context) {

	MyResource resource = getMyResource();
	String resourceEtag = computeEtag(resource);
	if(context.cacheHeaders().eTag(resourceEtag)
	                         .validate(resource != null)) {
	    return;
	}
	
    if(resource == null) {
        throw new NotFoundException();
    }
    
    return context.response().sendJson(resource);
}

To conclude, you may now see that proper management of HTTP freshness headers sadly can't be fully automated. A Filter is simply not enough! But, by using the cacheHeaders() add-on and its validate(...) method, a REST endpoint can follow the HTTP specification and be very performant. Remember that not all endpoints require that freshness validation, though! You can start your application without any freshness validation at all and add it on endpoints where it makes sense.

WebSockets

WebSockets allow you to establish a permanent connection between your application and your users. Doing so, you can receive messages from them, but you can also send messages to them, at any time. This is very different than standard HTTP which is: one request by the user => one response by the application.

WebSockets are mostly used when...

  • You want your application to be able to push messages to the connected users, without waiting for them to make requests.
  • You need your application to be the central point where multiple users can share real-time data. The classic example is a chat room: when a user sends a message, your application echoes that message back to the other Peers.

WebSockets's terminology is quite simple: an Endpoint is a group of Peers (users) connected together and that your application manages. A WebSocket Endpoint can receive and send text messages and binary messages from and to the Peers.

Your application can manage multiple Endpoints, each of them with its own set of Peers. Grouping Peers into separate Endpoints can be useful so you can easily send a specific message to a specific group of Peers only. Also, each Endpoint may have some different level of security associated with it: some users may be allowed to connect to some Endpoints, but not to some others.

Quick Example

Here's a quick example on how to use WebSockets. Each part of this example will be explained in more details in following sections. You can try this example live on the WebSockets demo page.

The source code for this example is:

First, we define a WebSocket Route:

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

The "chatWebsocketController" is an instance of a class that implements the WebsocketController interface. This component is responsible for handling all the WebSocket events:

public class ChatWebsocketController 
        implements WebsocketController<DefaultRequestContext, DefaultWebsocketContext> {

    private WebsocketEndpointManager endpointManager;

    protected WebsocketEndpointManager getEndpointManager() {
        return this.endpointManager;
    }

    @Override
    public WebsocketConnectionConfig onPeerPreConnect(DefaultRequestContext context) {
        
        return new WebsocketConnectionConfig() {

            @Override
            public String getEndpointId() {
                return "chatEndpoint";
            }

            @Override
            public String getPeerId() {
                return "peer_" + UUID.randomUUID().toString();
            }
        };
    }

    @Override
    public void onEndpointReady(WebsocketEndpointManager endpointManager) {
        this.endpointManager = endpointManager;
    }
    
    @Override
    public void onPeerConnected(DefaultWebsocketContext context) {
        context.sendMessageToCurrentPeer("Your peer id is " + context.getPeerId());
    }
    
    @Override
    public void onPeerMessage(DefaultWebsocketContext context, String message) {
        getEndpointManager().sendMessage("Peer '" + context.getPeerId() + 
                "' sent a message: " + message);
    }

    @Override
    public void onPeerMessage(DefaultWebsocketContext context, byte[] message) {
    }

    @Override
    public void onPeerClosed(DefaultWebsocketContext context) {
    }

    @Override
    public void onEndpointClosed(String endpointId) {
    }
}

Explanation :

  • 10-25 : Without going into too many details (we will do that in the following sections), onPeerPreConnect(...) is a method called before a new user is connected. In this example, we specify that this user should connect to the "chatEndpoint" Endpoint and that its Peer id will be "peer_" followed by a random String.
  • 27-30 : When a WebSocket Endpoint is ready to receive and send messages, the onEndpointReady(...) method is called and gives us access to an Endpoint Manager. We keep a reference to this manager since we are going to use it to send messages.
  • 32-35 : When the connection with a new Peer is established, the onPeerConnected(...) method is called. In this example, as soon as the Peer is connected, we send him a message containing his Peer id.
  • 37-40 : When a Peer sends a message, the onPeerMessage(...) method is called. In this example, we use the Endpoint Manager (which was received in the onEndpointReady(...) method [27-30]) and we broadcast this message to all the Peers of the Endpoint.

Here's a quick client-side HTML/javascript code example, for a user to connect to this Endpoint:

<script>
    var app = app || {};
    
    app.showcaseInit = function() {
        
        if(!window.WebSocket) {
            alert("Your browser does not support WebSockets.");
            return;
        }

        // Use "ws://" instead of "wss://" for an insecure 
        // connection, without SSL.
        app.showcaseWebsocket = new WebSocket("wss://" + location.host + "/chat");
        
        app.showcaseWebsocket.onopen = function(event) {
            console.log("WebSocket connection established!"); 
        };
        
        app.showcaseWebsocket.onclose = function(event) {
            console.log("WebSocket connection closed."); 
        };
        
        app.showcaseWebsocket.onmessage = function(event) {
            console.log(event.data); 
        };   
    };
    
    app.sendWebsocketMessage = function sendWebsocketMessage(message) {
        
        if(!window.WebSocket) {
            return;
        }
        if(app.showcaseWebsocket.readyState != WebSocket.OPEN) {
            console.log("The WebSocket connection is not open."); 
            return;
        }
        
        app.showcaseWebsocket.send(message);
    };
    
    app.showcaseInit();
    
</script>

<form onsubmit="return false;">
    <input type="text" name="message" value="hi!"/>
    <input type="button" value="send" 
           onclick="app.sendWebsocketMessage(this.form.message.value)"/>
</form>

WebSocket Routing

The WebSocketRroutes are defined similarly to regular Routes, using Spincast's Router. But, instead of beginning the creation of the Route with the HTTP method, like GET(...) or POST(...), you use websocket(...):

router.websocket("/chat") ...

There are fewer options available when creating a WebSocket Route compared to a regular HTTP Route. Here are the available ones...

You can set an id for the Route. This allows you to identify the Route so you can refer to it later on, delete it, etc:

router.websocket("/chat")
      .id("chat-endpoint") ...

You can also add "before" Filters, inline. Note that you can not add "after" Filters to a WebSocket Route because, as soon as the WebSocket connection is established, the HTTP request is over. But "before" Filters are perfectly fine since they applied to the HTTP request before it is upgraded to a WebSocket connection. For the same reason, global "before" Filters (defined using something like router.ALL(...).pos(-10)) will be applied during a WebSocket Route processing, but not the global "after" Filters (defined using a position greater than "0").

Here's an example of inline "before" Filters, on a WebSocket Route:

router.websocket("/chat")
      .id("chat-endpoint")
      .before(beforeFilter1) 
      .before(beforeFilter2) ...

Finally, like you do during the creating of a regular Route, you save the WebSocket Route. The handle(...) method for a WebSocket Route takes a WebSocket Controller, not a Route Handler as regular HTTP Routes do.

router.websocket("/chat")
      .id("chat-endpoint")
      .before(beforeFilter1) 
      .before(beforeFilter2)
      .handle(chatWebsocketController);

WebSocket Controllers

WebSocket Routes require a dedicated Controller as an handler. This Controller is responsible for receiving the various WebSocket events occurring during the connection.

You create a WebSocket Controller by implementing the WebsocketController interface.

The WebSocket events

Here are the methods a WebSocket Controller must implement, each of them associated with a specific WebSocket event:

  • WebsocketConnectionConfig onPeerPreConnect(R context)
    Called when a user requests a WebSocket connection. At this moment, the connection is not yet established and you can allow or deny the request. You can also decide on which Endpoint to connect the user to, and which Peer id to assign him.
    @param context the request context of the initial HTTP request. Remember that the WebSocket connection is not established yet!
  • void onEndpointReady(WebsocketEndpointManager endpointManager)
    Called when a new Endpoint is created within your application. The Endpoint Manager is passed as a parameter on your should keep a reference to it. You'll use this Manager to send messages, to close the connection with some Peers, etc.

    Note that this method should not block! More details below...
    @param endpointManager the Endpoint Manager.
  • void onPeerConnected(W context)
    Called when a new Peer is connected. At this point, the WebSocket connection is established with the Peer and you can send him messages.
    @param context the WebSocket context
  • void onPeerMessage(W context, String message)
    Called when a Peer sends a text message.
    @param context the WebSocket context
    @param message the text message sent by the Peer
  • void onPeerMessage(W context, byte[] message)
    Called when a Peer sends a binary message.
    @param context the WebSocket context
    @param message the binary message sent by the Peer
  • void onPeerClosed(W context)
    Called when the connection with a Peer is closed.
    @param context the WebSocket context
  • void onEndpointClosed(String endpointId)
    Called when the whole Endpoint is closed.
    @param endpointId the id of the closed Endpoint

The onPeerPreConnect(...) event

The onPeerPreConnect(...) is called before the WebSocket connection is actually established with the user. The request, here, is still the original HTTP one, so you receive a request context as regular Route Handlers do.

In that method, you have access to the user's cookies and to all the information about the initial HTTP request. This is a perfect place to decide if the requesting user should be allowed to connect to a WebSocket Endpoint or not. You may check if he is authenticated, if he has enough rights, etc.

If you return null from this method, the WebSocket connection process will be cancelled, and you are responsible for sending a response that makes sense to the user.

For example:

public WebsocketConnectionConfig onPeerPreConnect(DefaultRequestContext context) {

    String sessionId = context.request().getCookie("sessionId");
    if(sessionId == null || !canUserAccessWebsocketEndpoint(sessionId)) {
        context.response().setStatusCode(HttpStatus.SC_FORBIDDEN);
        return null;
    }

    return new WebsocketConnectionConfig() {

        @Override
        public String getEndpointId() {
            return "someEndpoint";
        }

        @Override
        public String getPeerId() {
            return "peer_" + encrypt(sessionIdCookie.getValue());
        }
    };
}

Explanation :

  • 1 : When a user requests a WebSocket connection, the onPeerPreConnect(...) method of the associated Controller is called. Note that here we receive the default DefaultRequestContext request context, but if you are using a custom request context type, you would receive an object of your custom type (AppRequestContext, for example).
  • 3 : We get the session id of the current user using a "sessionId" cookie (or any other way).
  • 4-7 : If the "sessionId" cookie is not found or if the user associated with this session doesn't have enough rights to access a WebSocket Endpoint, we set the response status as Forbidden and we return null. By returning null, the WebSocket connection process is cancelled and the HTTP response is sent as is.
  • 9-20 : If the user is allowed to access a WebSocket Endpoint, we return the information required for that connection. We'll look at that WebsocketConnectionConfig object in the next section.

The WebsocketConnectionConfig(...) object

Once you decided that a user can connect to a WebSocket Endpoint, you return an instance of WebsocketConnectionConfig from the onPeerPreConnect(...) method.

In this object, you have to specify two things:

  • The Endpoint id to which the user should be connected to. Note that you can't use the id of an Endpoint that is already managed by another Controller, otherwise an exception is thrown. If you use null here, a random Endpoint id will be generated.
  • The Peer id to assign to the user. Each Peer id must be unique inside a given Endpoint, otherwise an exception is thrown. If you return null here, a random id will be generated.

Multiple Endpoints

Note that a single WebSocket Controller can manage multiple Endpoints. The Endpoints are not hardcoded when the application starts, you dynamically create them, on demand. Simply by connecting a first Peer using a new Endpoint id, you create the required Endpoint. This allows your Controller to "group" some Peers together, for any reason you may find useful. For example, you may have a chat application with multiple "rooms": each room would be a specific Endpoint, with a set of Peers connected to it.

If the Endpoint id you return in the WebsocketConnectionConfig object is the one of an existing Endpoint, the user will be connected to it. Next time you send a message using the associated Manager, this new Peer will receive it.

If your Controller creates more than one Endpoint, you have to keep the Managers for each of those Endpoints!

For example:

public class MyWebsocketController 
        implements WebsocketController<DefaultRequestContext, DefaultWebsocketContext> {

    private final Map<String, WebsocketEndpointManager> 
            endpointManagers = new HashMap<String, WebsocketEndpointManager>();

    protected Map<String, WebsocketEndpointManager> getEndpointManagers() {
        return this.endpointManagers;
    }

    protected WebsocketEndpointManager getEndpointManager(String endpointId) {
        return getEndpointManagers().get(endpointId);
    }

    @Override
    public WebsocketConnectionConfig onPeerPreConnect(DefaultRequestContext context) {
        
        return new WebsocketConnectionConfig() {

            @Override
            public String getEndpointId() {
                return "endpoint_" + RandomUtils.nextInt(1, 11);
            }

            @Override
            public String getPeerId() {
                return null;
            }
        };
    }

    @Override
    public void onEndpointReady(WebsocketEndpointManager endpointManager) {
        getEndpointManagers().set(endpointManager.getEndpointId(), endpointManager);
    }

    @Override
    public void onPeerMessage(DefaultWebsocketContext context, String message) {
        getEndpointManager(context.getEndpointId()).sendMessage(message);
    }
    
    @Override
    public void onEndpointClosed(String endpointId) {
        getEndpointManagers().remove(endpointId);
    }

    @Override
    public void onPeerConnected(DefaultWebsocketContext context) {
    }

    @Override
    public void onPeerMessage(DefaultWebsocketContext context, byte[] message) {
    }

    @Override
    public void onPeerClosed(DefaultWebsocketContext context) {
    }
}

Explanation :

  • 4-5 : Here, our Controller will manage more than one Endpoints, so we create a Map to keep the association between each Endpoint and its WebSocket Manager.
  • 20-23 : As the Endpoint id to use, this example returns a random id between 10 different possibilities, randomly distributed to the connecting Peers. In other words, our Controller is going to manage up to 10 Endpoints, from "endpoint_1" to "endpoint_10".
  • 25-28 : By returning null as the Peer id, a random id will be generated.
  • 32-35 : When an Endpoint is created, we receive its Manager and we add it to our endpointManagers map, using the Endpoint id as the key. Our onEndpointReady method may be called up to 10 times, one time for each Endpoint our Controller may create.
  • 37-40 : Since we manage more than one Endpoints, we have to use the right Manager when sending a message! Here, we echo back any message received by a Peer, to all Peers connected to the same Endpoint.
  • 42-45 : When an Endpoint is closed, we don't need its Manager anymore so we remove it from our endpointManagers map.

Finally, note that a Controller can manage multiple WebSocket Endpoints, but only one Controller can create and manage a given WebSocket Endpoint! If a Controller tries to connect a Peer to an Endpoint that is already managed by another Controller, an exception is thrown.

The onEndpointReady(...) method should not block

It's important to know that the onEndpointReady(...) method is called synchronously by Spincast, when the connection with the very first Peer is being established. This means that this method should not block or the connection with the first Peer will never succeed!

Spincast calls onEndpointReady(...) synchronously to make sure you have access to the Endpoint Manager before the first Peer is connected and therefore before you start receiving events from him.

You may be tempted to start some kind of loop in this onEndpointReady(...) method, to send messages to the connected Peers, at some interval. Instead, start a new Thread to run the loop, and let the current thread continue.

In the following example, we will send the current time to all Peers connected to the Endpoint, every second. We do so without blocking the onEndpointReady(...) method :

public void onEndpointReady(WebsocketEndpointManager endpointManager) {

    getEndpointManagers().set(endpointManager.getEndpointId(), endpointManager);

    final String endpointId = endpointManager.getEndpointId();

    Thread sendMessageThread = new Thread(new Runnable() {

        @Override
        public void run() {

            while(true) {
            
                WebsocketEndpointManager manager = getEndpointManager(endpointId);
                if(manager == null) {
                    break;
                }

                manager.sendMessage("Time: " + new Date().toString());

                try {
                    Thread.sleep(1000);
                } catch(InterruptedException e) {
                    break;
                }
            }
        }
    });
    sendMessageThread.start();
    
}

Automatic pings and other configurations

By default, pings are automatically sent to each Peer every 20 seconds or so. This validates that the Peers are still connected. When those pings find that a connection has been closed, onPeerClosed(...) is called on the WebSocket Controller.

You can turn on/off those automatic pings and change other configurations, depending on the Server implementation you use. Here are the configurations available when using the default Server, Undertow.

The WebSocket context

Most methods of a WebSocket Controller receive a WebSocket context. This context object is similar to a Request Context received by a regular Route Handler : it gives access to information about the event (the Endpoint, the Peer, etc.) and also provides easy access to utility methods and add-ons.

WebSocket specific methods :

  • getEndpointId(): The id of the Endpoint the current Peer is connected to.
  • getPeerId(): The id of the current Peer.
  • sendMessageToCurrentPeer(String message): Sends a text message to the current Peer.
  • sendMessageToCurrentPeer(byte[] message): Sends a binary message to the current Peer.
  • closeConnectionWithCurrentPeer(): Closes the connection with the current Peer.

Utility methods and add-ons:

  • getLocaleToUse(): The best Locale to use for this Peer, as resolved during the initial HTTP request.
  • getTimeZoneToUse(): The best TimeZone to use for this Peer.
  • json(): Easy access to the JsonManager.
  • xml(): Easy access to the XMLManager.
  • templating(): Easy access to the TemplatingEngine.
  • guice(): Easy access to the application's Guice context.

Extending the WebSocket context

The same way you can extend the Request Context type, which is the object passed to your Route Handlers for regular HTTP requests, you can also extend the WebSocket Context type, passed to your WebSocket Controller, when an event occurs.

First, make sure you read the Extending the Request Context section : it contains more details and the process of extending the WebSocket Context is very similar!

The first thing to do is to create a custom interface for the new WebSocket Context type :

public interface AppWebsocketContext extends WebsocketContext<AppWebsocketContext> {
    public void customMethod(String message);
}

Explanation :

  • 1 : A custom WebSocket context type extends the base WebsocketContext interface and parameterizes it using its own type.

Then, we provide an implementation for that custom interface:

public class AppWebsocketContextDefault extends WebsocketContextBase<AppWebsocketContext>
                                                implements AppWebsocketContext {

    @AssistedInject
    public AppWebsocketContextDefault(@Assisted("endpointId") String endpointId,
                                      @Assisted("peerId") String peerId,
                                      @Assisted WebsocketPeerManager peerManager,
                                      WebsocketContextBaseDeps<AppWebsocketContext> deps) {
        super(endpointId,
              peerId,
              peerManager,
              deps);
    }

    @Override
    public void customMethod(String message) {
        sendMessageToCurrentPeer("customMethod: " + message);
    }
}

Explanation :

  • 1-2 : The implementation extends WebsocketContextBase so all the default methods/add-ons are kept. Of course, it also implements our custom AppWebsocketContext.
  • 4-13 : Don't worry about this scary constructor too much, just add it as such and it should work. For the curious, the annotations indicate that this object will be created using an assisted factory.
  • 15-18 : We implement our custom method. This dummy example simply sends a message to the current Peer, prefixed with "customMethod: ". Note that the sendMessageToCurrentPeer(...) method is inherited from WebsocketContextBase.

Finally, you must let Spincast know about your custom WebSocket Context type. This is done by using the websocketContextImplementationClass(...) of the Bootstrapper :

public static void main(String[] args) {

    Spincast.configure()
            .module(new AppModule())
            .requestContextImplementationClass(AppRequestContextDefault.class)
            .websocketContextImplementationClass(AppWebsocketContextDefault.class)
            .init(args);
    //....
}

If you both extended the Request Context type and the WebSocket Context type, the parameterized version of your Router would look like : Router<AppRequestContext, AppWebsocketContext>.

But you could also create an unparameterized version of it, for easier usage! :

public interface AppRouter extends Router<AppRequestContext, AppWebsocketContext> {
    // nothing required
}

Note that if you use the Quick Start to start your application, both the Request Context type and the WebSocket Context type have already been extended and the unparameterized routing components have already been created for you!

Testing

Spincast provides some nice testing utilities. You obviously don't have to use those to test your Spincast application, you may already have your favorite testing toolbox and be happy with it. But those utilities are heavily used to test Spincast itself, and we think they are an easy, fun, and very solid testing foundation.

First, Spincast comes with a custom JUnit runner which allows testing using a Guice context really easily. But, the biggest feature is to be able to test your real application itself, without even changing the way it is bootstrapped. This is possible because of the Guice Tweaker component which allows to indirectly mock or extend some components.

Installation

Add this Maven artifact to your project to get access to the Spincast testing utilities:

<dependency>
    <groupId>org.spincast</groupId>
    <artifactId>spincast-testing-default</artifactId>
    <version>0.9.48</version>
    <scope>test</scope>
</dependency>

Then, make your test classes extend SpincastTestBase or one of its children classes.

Most of the time, you'll want to extend AppBasedTestingBase, or AppBasedDefaultContextTypesTestingBase if your application uses the default request context types.

Demo

In this demo, we're going to test a simple application which only has one endpoint : "/sum". The Route Handler associated with this endpoint is going to receive two numbers, will add them up, and will return the result as a Json object. Here's the response we would be expecting from the "/sum" endpoint when sending the parameters "first" = "1" and "second" = "2" :

{
  "result": "3"
}

You can download that Sum application [.zip] if you want to try it by yourself or look at its code directly.

First, let's have a quick look at how the demo application is bootstrapped :

public class App {

    public static void main(String[] args) {
        Spincast.configure()
                .module(new AppModule())
                .init(args);
    }

    @Inject
    protected void init(DefaultRouter router,
                        AppController ctrl,
                        Server server) {

        router.POST("/sum").handle(ctrl::sumRoute);
        server.start();
    }
}

The interesting lines to note here are 4-6 : we use the standard Bootstrapper to start everything! We'll see that, without modifying this bootstrapping process, we'll still be able to tweak the Guice context, to mock some components.

Let's write a first test class :


public class SumTest extends AppBasedDefaultContextTypesTestingBase {

    @Override
    protected void callAppMainMethod() {
        App.main(null);
    }
    
    @Override
    protected AppTestingConfigs getAppTestingConfigs() {
        return new AppTestingConfigs() {

            @Override
            public boolean isBindAppClass() {
                return true;
            }

            @Override
            public Class<? extends SpincastConfig> getSpincastConfigTestingImplementationClass() {
                return SpincastConfigTestingDefault.class;
            }

            @Override
            public Class<?> getAppConfigTestingImplementationClass() {
                return null;
            }

            @Override
            public Class<?> getAppConfigInterface() {
                return null;
            }
        };
    }

    @Inject
    private JsonManager jsonManager;

    @Test
    public void validRequest() throws Exception {
        // TODO...
    }
}

Explanation :

  • 2 : Our test class extends AppBasedDefaultContextTypesTestingBase. This class is a child of SpincastTestBase and therefore allows us to use all the tools Spincast testing provides. Note there are other base classes you can extend, we're going to look at them soon.
  • 4-7 : The base class we are using requires that we implement the callAppMainMethod() method. In this method we have to initialize the application to test. This is easily done by calling its main(...) method.
  • 9-33 : We also have to implement the getAppTestingConfigs() method. This is to provide Spincast informations about the configurations we want to use when running this test class. Have a look at the Testing configurations section for more information!
  • 35-36 : Here we can see Spincast testing in action! Our test class has now full access to the Guice context of the application. Therefore, we can inject any component we need. In this test class, we are going to use the JsonManager.
  • 38-41 : a first test to implement.

As you can see, simply by extending AppBasedDefaultContextTypesTestingBase, and by starting our application using its main(...) method, we can write integration tests targeting our running application, and we can use any components from its Guice context. There is some boilerplate code to write though (you nee to implement the getAppTestingConfigs() method, for example), and this why you would in general create a base class to serve as a parent for all your test classes!

Let's implement our first test. We're going to validate that the "/sum" endpoint of the application works properly :

   //...

    @Test
    public void validRequest() throws Exception {
    
        HttpResponse response = POST("/sum").addFormBodyFieldValue("first", "1")
                                            .addFormBodyFieldValue("second", "2")
                                            .addJsonAcceptHeader()
                                            .send();
    
        assertEquals(HttpStatus.SC_OK, response.getStatus());
        assertEquals(ContentTypeDefaults.JSON.getMainVariationWithUtf8Charset(),
                     response.getContentType());
    
        String content = response.getContentAsString();
        assertNotNull(content);
    
        JsonObject resultObj = this.jsonManager.fromString(content);
        assertNotNull(resultObj);
    
        assertEquals(new Integer(3), resultObj.getInteger("result"));
        assertNull(resultObj.getString("error", null));
    }

Explanation :

  • 6-9 : the Spincast HTTP Client plugin is fully integrated into Spincast testing utilities. This allows us to very easily send requests to test our application. We don't even have to configure the host and port to use : Spincast will automatically find and use those of our application.
  • 11-13 : we validate that the response is a success ("200") and that the content-type is the expected "application/json".
  • 15-16 : we get the content of the response as a String and we validate that it is not null.
  • 18-19 : we use the JsonManager (injected previously) to convert the content to a JsonObject.
  • 21-22 : we finally validate the result of the sum and that no error occured.

Note that we could also have retrieved the content of the response as a JsonObject directly, by using response.getContentAsJsonObject() instead of response.getContentAsString(). But we wanted to demonstrate the use of an injected component, so bear with us!

If you look at the source of this demo, you'll see two more tests in that first test class : one that tests the endpoint when a parameter is missing, and one that tests the endpoint when the sum overflows the maximum Integer value.

Let's now write a second test class. In this one, we are going to show how easy it is to replace a binding, to mock a component.

Let's say we simply want to test that the responses returned by our application are gzipped. We may not care about the actual result of calling the "/sum" endpoint, so we are going to "mock" it. This is a simple example, but the process involved is similar if you need to mock a data source, for example.

Our second test class will look like this :


public class ResponseIsGzippedTest extends AppBasedDefaultContextTypesTestingBase {

    @Override
    protected void callAppMainMethod() {
        App.main(null);
    }
    
    @Override
    protected AppTestingConfigs getAppTestingConfigs() {
        return new AppTestingConfigs() {

            @Override
            public boolean isBindAppClass() {
                return true;
            }

            @Override
            public Class<? extends SpincastConfig> getSpincastConfigTestingImplementationClass() {
                return SpincastConfigTestingDefault.class;
            }

            @Override
            public Class<?> getAppConfigTestingImplementationClass() {
                return null;
            }

            @Override
            public Class<?> getAppConfigInterface() {
                return null;
            }
        };
    }

    public static class AppControllerTesting extends AppControllerDefault {

        @Override
        public void sumRoute(DefaultRequestContext context) {
            context.response().sendPlainText("42");
        }
    }

    @Override
    protected Module getExtraOverridingModule() {
        return new SpincastGuiceModuleBase() {

            @Override
            protected void configure() {
                bind(AppController.class).to(AppControllerTesting.class).in(Scopes.SINGLETON);
            }
        };
    }

    @Test
    public void isGzipped() throws Exception {
        // TODO...
    }
}

Explanation :

  • 2 : this test class also extends AppBasedDefaultContextTypesTestingBase.
  • 4-7 : we start our application.
  • 9-33 : if we had created a base class for our tests, we could have define the getAppTestingConfigs() there instead of having to repeat it in all test files!
  • 35-41 : we create a mock controller by extending the original one and replacing the sumRoute(...) Route Handler so it always returns "42".
  • 43-52 : We specify an overriding module to change the implementation that will be used for the AppController binding. Under the hood, this is done by the Guice Tweaker.

And let's write the test itself :

    //...

    @Test
    public void isGzipped() throws Exception {
     
        HttpResponse response = POST("/sum").addFormBodyFieldValue("toto", "titi")
                                            .addJsonAcceptHeader()
                                            .send();
    
        assertTrue(response.isGzipped());
    
        assertEquals(HttpStatus.SC_OK, response.getStatus());
        assertEquals(ContentTypeDefaults.TEXT.getMainVariationWithUtf8Charset(),
                     response.getContentType());
        assertEquals("42", response.getContentAsString());  
    }

Explanation :

  • 6-8 : We can send pretty much anything here as the parameters since the controller is mocked : they won't be validated.
  • 10 : We validate that the response was gzipped.
  • 12-15 : just to make sure our tweaking is working properly.

Being able to change bindings like this is very powerful : you are testing your real application, as it is bootstrapped, without even changing its code. All is done indirectly, using the Guice Tweaker.

Guice Tweaker

As we saw in the previous demo, we can tweak the Guice context of our application in order to test it. This is done by the GuiceTweaker, a component which is part of the Spincast testing machanism.

The Guice Tweaker is in fact a plugin. This plugin is special because it is applied even if it's not registered during the bootstrapping of the application.

It's important to know that the Guice Tweaker only works if you are using the standard Bootstrapper. It is implemented using a ThreadLocal that the bootstrapper will look for.

The Guice Tweaker is created in the SpincastTestBase class. By extending this class or one of its children, you have access to it.

By default, the Guice Tweaker automatically modifies the SpincastConfig binding of the application when tests are run. This allows you to use testing configurations very easily (for example to make sure the server starts on a free port). The implementation class used for those configurations are specified in the getAppTestingConfigs() method you have to implement. The Guice tweaker will use those informations and will create the required binding automatically. The default implementation for the SpincastConfig interface is SpincastConfigTestingDefault.

Those are the methods available, in a test file, to tweak your application :

  • getAppTestingConfigs(...) : a section dedicated to this method follows next.

  • getExtraOverridingModule(...) : to make the Guice Tweaker add an extra module to the created Guice context.
  • getExtraPlugins(...) : to make the Guice Tweaker add extra plugins to the created Guice context.
  • addExtraSystemProperties(...) : to add extra System properties before running the tests.

The testing configurations (getAppTestingConfigs())

When running integration tests, you don't want to use the same configurations then the ones you would when running the application directly. For example, you may want to provide a different connection string to use a mocked database instead of the real one.

As we saw in the previous section, the Guice Tweaker allows you to change some bindings when testing your application. But configurations is such an important component to modify, when running tests, that Spincast forces you to specify which implementations to use for those!

You specify the testing configurations by implementing the getAppTestingConfigs() method. This method must return an instance of AppTestingConfigs. This object tells Spincast :

  • getSpincastConfigTestingImplementationClass() : The implementation class to use for the SpincastConfig binding. In other words, this hook allows you to easily mock the configurations used by Spincast core components. The default testing implementation is the provided SpincastConfigTestingDefault class. You may want to have a look at this class when wrting your own since it shows how to implement some useful things, such as finding a free port to use when starting the HTTP server.
  • getAppConfigInterface() : The interface of your custom app configurations class. You can return null if you don't have a custom configurations class.
  • getAppConfigTestingImplementationClass() : The implementation class to use for your custom app configurations. You can return null if you don't have a custom configurations class.
  • isBindAppClass() : Should the App class itself (the class in which Spincast.init() or Spincast.configure() is called) be bound? In general, if you are running unit tests and don't need to start any HTTP server, you are going to return false... That way, your main class (in general named "App") won't be bound and therefore won't start the server.

Spincast will use the informations returned by this object and will add all the required bindings automatically. You don't need to do anything by yourself, for example by using the Guice Tweaker, to change the bindings for the configurations when running integration tests. You just need to implement the getAppTestingConfigs() method.

In most applications, the testing implementation to use for the SpincastConfig interface and the one for your custom configurations interface will be the same! Indeed, if you follow the suggested way of configuring your application, then your custom configurations interface AppConfig extends SpincastConfig.

Your testing configurations can often be shared between multiple tests classes. It is therefore a good idea to create an abstract base class, named "AppTestingsBase" or something similar, to implement the getAppTestingConfigs() method there, and use this base class as the parent for all your integration test classes. Have a look at this base class for an example.

While mocking some configurations is often required, it's still a good idea to make testing configurations as close as possible as the ones that are going to be used in production. For example, returning false for the isDevelopmentMode() method is suggested. That way, you can be confident that once your tests pass, your application will do well in production.

You can mock some Environment Variables used as configurations, by overriding the getEnvironmentVariables() method in your configurations implementation class.

Testing base classes

Multiple base classes are provided, depending on the needs of your test class. They all ultimately extend SpincastTestBase, they all use the Spincast JUnit runner and all give access to Guice Tweaker.

Those test base classes are split into two main categories : those based on your actual application and those that are not. Most of the time, you do want to test using the Guice context of your application! But you may sometimes have components that can be unit tested without the full Guice context of your application.

Those are the main testing base classes provided by Spincast. All of them can be modify using the Guice Tweaker :

App based

Not based on an app

  • NoAppTestingBase : base class to use to test components using the default Guice context (the default plugins only). No application class is involved.
  • NoAppStartHttpServerTestingBase : as NoAppTestingBase, but if you also need the HTTP server to be started! This base class will be responsible to start and stop the server.

Spincast JUnit runner

Spincast's testing base classes all use a custom JUnit runner: SpincastJUnitRunner.

This custom runner has a couple of differences as compared with the default JUnit runner, but the most important one is that instead of creating a new instance of the test class before each test, this runner only creates one instance.

This way of running the tests works very well when a Guice context is involved. The Guice context is created when the test class is initialized, and then this context is used to run all the tests of the class. If Integration testing is used, then the HTTP Server is started when the test class is initialized and it is used to run all the tests of the class.

Let's see in more details how the Spincast JUnit runner works :

  • First, a beforeClass() method is called. As opposed to a classic JUnit's @BeforeClass annotated method, Spincast's beforeClass() method is not static. It is called when the test class is initialized.
  • The createInjector() method is called in the beforeClass() method. This is where the Guice context will be created, by starting an application or explictly.
  • The dependencies are automatically injected from the Guice context into the instance of the test class. All your @Inject annotated fields and methods are fulfilled.
  • If an exception occures during the execution of the beforeClass() method, the beforeClassException(...) method will be called, the process will be stop and the tests won't be run.
  • If no exception occures, the tests are then run.
  • The afterClass() method is called. Like the beforeClass() method, this method is not static. Note that the afterClass() method won't be called if an exception occurred in the beforeClass() method.

Since the Guice context is shared by all the tests of a test class, you have to make sure you reset everything required before running a test. To do this, use JUnit's @Before annotation, or the beforeTest() and afterTest() method.

Spincast JUnit runner features

  • If your test class is annotated with @ExpectingBeforeClassException then the beforeClass() method is expected to throw an exception! In other words, the test class will be shown by JUnit as a "success" only of the beforeClass() method throws an exception. This is useful, in integration testing, to validate that your application refuses some invalid configuration when it starts, for example.
  • If your test class (or a parent) implements TestFailureListener then the testFailure(...) method will be called each time a test fails. This allows you to add a breakpoint or some logs, and to inspect the context of the failure.
  • You can use the @Repeat annotation. When added to the class itself, this annotation makes your test class loop X number of times. Note that the beforeClass() and afterClass() methods will also be called X number of time, so the Guice context will be recreated each time. You can specify an amount of milliseconds to sleep between two loops, using the sleep parameter.
  • You can also use the @Repeat annotation on a test. This will make the test run X number of time during the execution of a test class loop.
  • If your test class (or a parent) implements RepeatedClassAfterMethodProvider then the afterClassLoops() method will be called when all the loops of the test class have been run.

A quick note about the @Repeat annotation : this annotation should probably only be used for debugging purpose! A test should always be reproducible and should probably not have to be run multiple times. But this annotation, in association with the testFailure(...) method, can be a great help to debug a test which sometimes fails and you don't know why!

Managing cookies

A frequent need during integration testing is to be able to keep cookies across multiple requests... By doing so, the behavior of a real browser is simulated.

To keep the cookies sent by a response, simple call saveResponseCookies(response) when a valid response is received. Then, you can add back those cookies to a new request using .setCookies(getPreviousResponseCookies()):

// First request, we save the cookies from the response...
HttpResponse response = GET("/one").send();
assertEquals(HttpStatus.SC_OK, response.getStatus());
saveResponseCookies(response);

// Second request, we resend the cookies!
response = GET("/two").setCookies(getPreviousResponseCookies()).send();
assertEquals(HttpStatus.SC_OK, response.getStatus());
saveResponseCookies(response);

H2 test database

An integration with the H2 database is provided as a testing tool. It allows you to run your tests using an in-memory SQL database.

You enable the Spincast Testing H2 database simply by binding SpincastTestingH2 as a Provider for your DataSource:

@Override
protected Module getExtraOverridingModule() {
    return Modules.override(super.getExtraOverridingModule()).with(new SpincastGuiceModuleBase() {

        @Override
        protected void configure() {
            bind(DataSource.class).toProvider(SpincastTestingH2.class).in(Scopes.SINGLETON);
            // ...
        }
    });
}

You then inject SpincastTestingH2 and the DataSource in your test file (or a base class):

@Inject
protected SpincastTestingH2 spincastTestingH2;

@Inject
private DataSource testDataSource;

In beforeClass(), you can make sure the database starts in a clean state:

@Override
public void beforeClass() {
    super.beforeClass();
    spincastTestingH2.clearDatabase();
}

... you can also do this before each test, if required:

@Override
public void beforeTest() {
    super.beforeTest();
    spincastTestingH2.clearDatabase();
}

When the tests are over, you stop the server:

@Override
public void afterClass() {
    super.afterClass();
    spincastTestingH2.stopServer();
}

The way the H2 server is started, you are able to connect to your database using an external tool. For example, you can set a breakpoint and open the database using DBeaver (or another tool) using the proper connection string ("jdbc:h2:tcp://localhost:9092/mem:test;MODE=PostgreSQL;DATABASE_TO_UPPER=false" for example). This allows you to easily debug your tests.

You can change some configurations used by Spincast Testing H2 (the server port for example) by binding a custom implementation of the SpincastTestingH2Config interface. If you don't, the default configurations will be used.

Plugins

What is a Spincast plugin?

A plugin can be a simple library, like any other Maven artifacts you add to your project. It can provide components and utilities to be used in your application.

But a plugin can also contribute to the Guice context of your application. They can add some bindings to that context and can even modify/remove some! All plugins are applied, in the order they are registered, during the bootstrapping phase.

Some plugins may also suggest an add-on to install.

Installing a plugin

You first need to add the Maven artifact of the plugin to your pom.xml (or build.gradle). For example :

<dependency>
    <groupId>com.example</groupId>
    <artifactId>some-wonderful-plugin</artifactId>
    <version>1.2.3</version>
</dependency>

Most of the plugins need to bind some components to the Guice context of the application. For them to be able to do so, you need to register them using the plugin(...) method of the Bootstrapper. For example :

public static void main(String[] args) {

    Spincast.configure()
            .module(new AppModule())
            .plugin(new SomeWonderfulPlugin())
            .requestContextImplementationClass(AppRequestContextDefault.class)
            .init(args);
   //...
}

Here, SomeWonderfulPlugin is the main class of the plugin, the one implementing the SpincastPlugin interface. To know what class to use to register a plugin, you have to read its documentation.

Installing a Request Context add-on

One of the coolest features of Spincast is the ability to extend the Request Context type. The Request Context are objects, associated with a request, that Spincast automatically creates and passes to your Route Handlers. You can extend the type of those object by adding add-ons. Some plugins may suggest that you use one of their components as such add-on.

To do so, you first add the add-on entry point to your Request Context interface. This entry point is simply a method, with a meaningful name, and that returns the add-on's main component :

public interface AppRequestContext extends RequestContext {

    public WonderfulComponent wonderful();
    
    //... other add-ons and methods
}

Here, the add-on is named "wonderful()" and its main component is "WonderfulComponent".

Then, you inject a Provider for the main component in your Request Context implementation, and you use it to return component instances :

public class AppRequestContext extends RequestContextBase<AppRequestContext>
                               implements AppRequestContext {

    private final Provider<WonderfulComponent> wonderfulComponentProvider;

    @AssistedInject
    public AppRequestContext(@Assisted Object exchange,
                             RequestContextBaseDeps<AppRequestContext> requestContextBaseDeps,
                             Provider<WonderfulComponent> wonderfulComponentProvider) {
        super(exchange, requestContextBaseDeps);
        this.wonderfulComponentProvider = wonderfulComponentProvider;
    }
    
    @Override
    public WonderfulComponent wonderful() {
        return this.wonderfulComponentProvider.get();
    }
    
    //...
}

It's a good practice to always use a Provider for the add-on's component, because it is often not a singleton and may even be request scoped.

You can now use the newly installed add-on, directly in your Route Handlers! For example :

public void myRouteHandler(AppRequestContext context) {

    context.wonderful().doSomething();
    
    //...
}

Default plugins

By using the spincast-default Maven artifact and the Bootstrapper, some plugins are installed by default. Those plugins provide implementations for the main components required in any Spincast application. If you disable one of those plugins, you have to bind by yourself implementations for the components that this plugin was binding.

  • SpincastCorePlugin : this is the only plugin which is not listed in the plugins section because it is the very core of Spincast. But it's interesting to know that even this core is a plugin!
  • Spincast Routing : binds all the components related to routing, allows the creation of the Routes and provides the "routing()" add-on.
  • Spincast Request : provides the "request()" add-on which allows Route Handlers to get information about the current request.
  • Spincast Response : provides the "response()" add-on which allows Route Handlers to build the response to send.
  • Spincast Undertow : provides an implementation for the required HTTP/Websocket Server component, using Undertow.
  • Spincast Locale Resolver : provides an implementation for the required LocaleResolver component.
  • Spincast TimeZone Resolver : provides an implementation for the required TimeZoneResolver component.
  • Spincast Variables add-on : as its name suggests, this plugin simply provides a "variables()" add-on to write and read information in the request scope. This can be used to pass information from a Route Handler to another.
  • Spincast Templating add-on : provides a "templating()" add-on giving access to utilities to render some text based templates.
  • Spincast Pebble : provides an implementation for the required TemplatingEngine component, using Pebble.
  • Spincast Jackson Json : provides an implementation for the required JsonManager component, using Jackson.
  • Spincast Jackson XML : provides an implementation for the required XmlManager component, using Jackson.
  • Spincast Config : provides everything that is required to configure a Spincast application. It allows you to tweak the default configurations used by the Spincast core components, and to create configurations that are specific to your application.
  • Spincast Dictionary : provides an implementation of the Dictionary interface, allowing internationalization ("i18n"). It is used to specify labels in a multilingual application.
  • Spincast HTTP Caching Addon : provides a "cacheHeaders()" add-on to help dealing with HTTP caching.

JDBC / SQL

Spincast provides utilities to perform SQL queries:

  • Scopes - automatic connections management and support for transactions.
  • Statements - allow the creation of SQL queries in a safe and easy manner, with named parameters support.
  • Result Sets - with goodies and better null support compared to plain JDBC.

To access those JDBC features, you first have to install the Spincast JDBC plugin. This plugin is not part of the spincast-default artifact.

Scopes

A JDBC scope is a block of code that provides a connection to a data source and automatically returns it to the connection pool when it is not needed anymore.

There is three kinds of scopes: autoCommit, transactional and specificConnection. You start a JDBC scope by calling the JdbcUtils's scopes() method.

Here's how to start an autoCommit scope :


getJdbcUtils().scopes().autoCommit(getMainDataSource(), new JdbcQueries<Void>() {
    @Override
    public Void run(Connection connection) {

        // SQL query #1...

        // SQL query #2...
        
        return null;
    }
});

You call getJdbcUtils().scopes().autoCommit(...) by passing the DataSource to use, and a JdbcQueries instance. In the run(...) method you receive a connection that is ready to be used to run your SQL queries.

When the run(...) method exits, the connection is automatically returned to the connection pool.

A transactional scope adds an extra functionality: all the SQL queries performed inside that scope, directly or indirectly, will be part of a single transaction. In other words, all the queries will be committed only when the scope exits, or will all be rollbacked if an exception occurres.

There are two ways to create a transactional scope. It can be created the same way an autoCommit one is, by passing a DataSource and a JdbcQueries instance:


getJdbcUtils().scopes().transactional(getMainDataSource(), new JdbcQueries<Void>() {

    @Override
    public Void run(Connection connection) throws Exception {

        // SQL query #1...

        // SQL query #2...
        
        return null;
    };
});

Or it can be created only to start a transaction, without the immediate need for a connection :


getJdbcUtils().scopes().transactional(new TransactionalScope<Void>() {

    @Override
    public Void run() throws Exception {

        // Use component #1...

        // Use component #2...
        
        return null;
    };
});

In both situations, any SQL queries performed in the scope, directly or indirectly, and targetting a common DataSource, will be part of the same transaction.

The final type of scope is specificConnection. In such scope, all queries (directly or indirectly) are going to be ran using the same connection, the one provided when creating the scope.


getJdbcUtils().scopes().specificConnection(connection, getMainDataSource(), new JdbcQueries<Void>() {
    @Override
    public Void run(Connection connection) {

        // SQL query #1...

        // SQL query #2...
        
        return null;
    }
});

Statements

By using the JdbcStatementFactory, by injecting it or from the JdbcUtils's statements() utility method, you start the creation of SQL statements :


getJdbcUtils().scopes().transactional(getMainDataSource(), new JdbcQueries<Void>() {

    @Override
    public Void run(Connection connection) throws Exception {

        SelectStatement stm1 = getJdbcUtils().statements().createSelectStatement(connection);
        
        InsertStatement stm2 = getJdbcUtils().statements().createInsertStatement(connection);
        
        BatchInsertStatement stm3 = getJdbcUtils().statements().createBatchInsertStatement(connection);
        
        UpdateStatement stm4 = getJdbcUtils().statements().createUpdateStatement(connection);
        
        DeleteStatement stm5 = getJdbcUtils().statements().createDeleteStatement(connection);
        
        // ...
        
        return null;
    };
});

From such statements, you can build your SQL query and bind named parameters.

For example :


SelectStatement stm = getJdbcUtils().statements().createSelectStatement(connection);

stm.sql("SELECT name, level " +
        "FROM users " +
        "WHERE name = :name ");

stm.setString("name", "Stromgol");

if (minLevel != null) {
    stm.sql("AND level >= :minLevel ");
    stm.setInteger("minLevel", minLevel);
}

There are utility methods for IN conditions:


SelectStatement stm = getJdbcUtils().statements().createSelectStatement(connection);

stm.sql("SELECT name, level " +
        "FROM users " +
        "WHERE name IN(:names) ");

stm.setInString("names", Sets.newHashSet("Stromgol", "Patof", "Bozo"));   

A setInstant(...) method is provided : it converts the Instant object to a Timestamp at the UTC timezone. In association with the result set's getInstant(), and a "timestamp with time zone" or "timestamptz" column type, it is an easy and efficient way of dealing with dates and timezones.

You can retrieve the current SQL and clears it if you need to :


stm.sql("SELECT * FROM users ");

// "true" => human friendly formatted
String currentSql = stm.getSql(true);

// clears the current query
stm.clearSql();

Finally, when your SQL query is ready, you execute it. The method to call to execute the query depends on the type of statement you are using:


// SelectStatement
SelectStatement stm1 = getJdbcUtils().statements().createSelectStatement(connection);
stm.selectOne(...);
// or
stm.selectList(...);
 // or
stm.selectListAndTotal(...);

// InsertStatement
InsertStatement stm2 = getJdbcUtils().statements().createInsertStatement(connection);
stm2.insert();
// or
stm2.insertGetGeneratedKeys();

// BatchInsertStatement
BatchInsertStatement stm3 = getJdbcUtils().statements().createBatchInsertStatement(connection);  
stm3.batchInsert();
// or
stm3.batchInsertGetGeneratedKeys();

// UpdateStatement
UpdateStatement stm4 = getJdbcUtils().statements().createUpdateStatement(connection);
stm4.update();

// DeleteStatement
DeleteStatement stm5 = getJdbcUtils().statements().createDeleteStatement(connection);
stm5.delete(); 

Result Sets

When you execute a SelectStatement, you have to pass a ResultSetHandler in order to use the values returned by the database.

For example :


SelectStatement stm = getJdbcUtils().statements().createSelectStatement(connection);

stm.sql("SELECT name, level " +
        "FROM users " +
        "WHERE name = :name ");

stm.setString("name", "Stromgol");

User user = stm.selectOne(new ResultSetHandler<User>() {

    @Override
    public User handle(SpincastResultSet rs) throws Exception {
        User user = new User(rs.getString("name"),
                             rs.getIntegerOrNull("level"));
        return user;
    }
});     

In this example, you can see that you receive a SpincastResultSet to deal with the data returned from the database. This SpincastResultSet object implements the default Java's java.sql.ResultSet, but also provide additional features.

In SpincastResultSet, those methods are deprecated: getBoolean(...), getByte(...), getShort(...), getInteger(...), getLong(...), getFloat(...), getDouble(...).

They are replaced by :

  • getBooleanOrNull(...)
  • getByteOrNull(...)
  • getShortOrNull(...)
  • getIntegerOrNull(...)
  • getLongOrNull(...)
  • getFloatOrNull(...)
  • getDoubleOrNull(...)

Or, to get 0 like the original JDBC's getters would return when the actual value in the database is null:

  • getBooleanOrZero(...)
  • getByteOrZero(...)
  • getShortOrZero(...)
  • getIntegerOrZero(...)
  • getLongOrZero(...)
  • getFloatOrZero(...)
  • getDoubleOrZero(...)

We do this to deal with the bad decision JDBC's creators did by using primitive types as return types for those getters and therefore preventing null from being returned properly. Using plain JDBC, when a null value is returned by the database, is it transformed to the default value of the primitive type : false for booleans and 0 for number types. Using SpincastResultSet, you receive a proper null value when this is what the database returns.

Finally, a getInstant() method is provided to easily convert a column of type "timestamp with time zone" or "timestamptz" to an Instant object.

Miscellaneous

Dictionary (i18n / internationalization)

Usage

The Dictionary interface represents the object in which you store and from which you get localized messages for a multilingual application.

You get a localized message by injecting the Dictionary in a class and by specifying the key of the message to get :

public class MyClass { 

    private final Dictionary dictionary;
    
    @Inject
    public MyClass(Dictionary dictionary) {
        this.dictionary = dictionary;
    }
    
    protected Dictionary getDictionary() {
        return this.dictionary;
    }
    
    public void someMethod() {
        String localizedMessage = getDictionary().get("some.message.key");
        System.out.println(localizedMessage);
    }
}

In this example, the message key is "some.message.key"

By default, the Locale used to pick the right version of the message is the one returned by the Locale Resolver. But you can also specify the Locale as a parameter:


String localizedMessage = getDictionary().get("some.message.key", Locale.JAPANESE);
System.out.println(localizedMessage);

The default Dictionary implementation, SpincastDictionaryDefault, uses the Templating Engine to evaluate messages. This means that you can pass parameters when getting a message. For example :


// Let's say the "some.message.key" message in the dictionary is :
// "Hi {{userName}}! My name is {{authorName}}."

String localizedMessage = getDictionary().get("some.message.key",
                                              Pair.of("userName", user.name),
                                              Pair.of("authorName", admin.name));

Note that, to improve performance, a message from the dictionary is only evaluated using the Templating Engine if at least one parameter is passed when getting it! Otherwise, the message is going to be returned as is, without any evaluation. If you have a message that doesn't require any parameter but still needs to be evaluated, you can force the evaluation using the "forceEvaluation" parameter :


String localizedMessage = getDictionary().get("some.message.key", true);

Finally, note that Spincast also provides a msg(...) function to get a localized message from a template (a HTML template, for example).

Adding messages to the dictionary

By default, only some Spincast core messages and some plugins messages will be added to the Dictionary. To add your own messages, the ones required in your application, you extend the SpincastDictionaryDefault base class and you override the addMessages() method. By using the key() and msg() helpers, you then specify the localized messages of your application. For example :


public class AppDictionary extends SpincastDictionaryDefault {

    @Inject
    public AppDictionary(LocaleResolver localeResolver, 
                         TemplatingEngine templatingEngine, 
                         AppConfigs appConfig,
                         Set<DictionaryEntries> dictionaryEntries) {
        super(localeResolver, templatingEngine, appConfig, dictionaryEntries);
    }

    @Override
    protected void addMessages() {

        key("users.home.welcome",
            msg("", "Hi {{name}}!"),
            msg("fr", "Salut {{name}}!"));

        key("users.profile.title",
            msg("", "Your profile"),
            msg("fr", "Votre profil"),
            msg("ja", "あなたのプロフィール"));
            
        // ...      
    }
}

The first parameter of the msg() helper is the language of the message. The empty language ("") is called the fallback language or default language. It is english in our example. If a message is requested using a specific Locale but is not found, Spincast will return the message using the fallback language, if it exists.

The default Dictionary implementation, SpincastDictionaryDefault, uses the Templating Engine to evaluate messages. This means that you can perform logic inside your messages ("if", "for loops"... anything supported by Pebble) and you can use parameters, as we can see with the "{{name}}" part in the previous example.

Finally, don't forget to register your custom implementation of the Dictionary interface (in our example: "AppDictionary") in your Guice context! For example :

public class AppModule extends SpincastGuiceModuleBase {

    @Override
    protected void configure() {

        bind(Dictionary.class).to(AppDictionary.class).in(Scopes.SINGLETON);
        
        //...
    }
}

Overriding core messages and plugins messages

Spincast core messages and plugins's messages are added to the Dictionary before you add your own messages using the addMessage() method. This means that you can override them and even translate them in a new language, if required.

The plugins should provide public constants representing the keys of the messages they use. This way, you can easily know how to override them. For example, the keys of Spincast's core messages are provided as constants in the SpincastCoreDictionaryEntriesDefault class. Each plugins should similarly list the keys of their messages in their respective documentation.

Here's an example of overriding a core Spincast message:


public class AppDictionary extends SpincastDictionaryDefault {

    @Inject
    public AppDictionary(LocaleResolver localeResolver, 
                         TemplatingEngine templatingEngine, 
                         AppConfigs appConfig,
                         Set<DictionaryEntries> dictionaryEntries) {
        super(localeResolver, templatingEngine, appConfig, dictionaryEntries);
    }

    @Override
    protected void addMessages() {

        // Override a core message!
        key(SpincastCoreDictionaryEntriesDefault.MESSAGE_KEY_ROUTE_NOT_FOUND_DEFAULTMESSAGE,
            msg("", "my custom message!"));

        // Then, add your custom messages...
    }
}

Configuration

A configuration for the dictionary is available through SpincastConfig:

Note that if a version of the message exists with the fallback language (""), it will always be found.

Flash messages

A Flash message is a message that is displayed to the user only once. It is most of the time used to display a confirmation message to the user when a form is submitted and the page redirected.

A good practice on a website is indeed to redirect the user to a new page when a form has been POSTed and is valid : that way, even if the user refreshes the resulting page, the form won't be resubmitted. But this pattern leads to a question : how to display a confirmation message on the page the user is redirected to? Flash messages are the answer to this question.

(All the Forms & Validation demos use Flash messages to display a confirmation message when the Form is submitted and is valid... Try them!)

A Flash message is most of the time used to display a confirmation (success) message to the user, but it, in fact, supports three "levels" :

  • Success
  • Warning
  • Error
A Flash message can also have some variables associated with it (in the form of an JsonObject), and those variables can be used when the Flash message is retrieved.

You can specify a Flash message :

  • By using the .response().redirect(...) method in a Route Handler

    public void myHandler(AppRequestContext context) {
        context.response().redirect("/some-url/",
                                    FlashMessageLevel.SUCCESS,
                                    "The form has been processed successfully.");
    }
  • By throwing a RedirectException

    public void myHandler(AppRequestContext context) {
        throw new RedirectException("/some-url/",
                                    FlashMessageLevel.SUCCESS,
                                    "The form has been processed successfully.");
    }

Of course, it doesn't make sense to specify a Flash message when redirecting the user to an external website!

Flash messages, when retrieved, are automatically added as a global variable for the Templating Engine. It is, in fact, converted to an Alert message.

How are Flash messages actually implemented? If Spincast has validated that the user supports cookies, it uses one to store the "id" of the Flash message and to retrieve it on the page the user is redirected to. If cookies are disabled, or if their support has not been validated yet, the "id" of the Flash messageis added as a queryString parameter to the URL of the page the user is redirected to.

Alert messages

An Alert message is a message that has a Success, Warning or Error level and that is displayed to the user, usually at the top of the page or of a section. It aims to inform the user about the result of an action, for example.

There are multiple ways to display an Alert message on a website. The Spincast website uses Toastr.js when javascript is enabled , and a plain <div> when javascript is disabled ( Try it!)

An Alert message is simply some text and a level associated with it, both added as a templating variable. You can easily implement your own way to pass such messages to be displayed to the user, but Spincast suggests a convention and some utilities :

  • The Alert messages are provided to the Templating Engine as a variable associated with the "spincast.alerts" key.
  • There is a dedicated "addAlert(...)" method on the response() add-on to easily add one from a Route handler :

    public void myHandler(AppRequestContext context) {
        context.response().addAlert(AlertLevel.ERROR, 
                                    "Your submission is not valid!");
    }
Note that Flash messages will be automatically converted to Alert messages when it's time to render a template! This means that as long as you add code to display Alert messages in your interface, Flash messages will also be displayed properly.

Using a SSL certificate (HTTPS)

It is recommended that you serve your application over HTTPS and not HTTP, which is not secure. To achieve that, you need to install a SSL certificate.

If you download the Quick Start application, you will find two files explaining the required procedure :

  • /varia/ssl_certificate_howto/self-signed.txt
    Shows how to use a self-signed certificate, for development purpose.
  • /varia/ssl_certificate_howto/lets-encrypt.txt
    Shows how to use a Let's Encrypt certificate. Let's Encrypt is a provider of free, but totally valid, SSL certificates. Instructions in this file will probably work for certificates obtained from other providers, but we haven't tested it yet.

Spincast Utilities

Spincast provides some generic utilities, accessible via the SpincastUtils interface :

  • void zipDirectory(File directoryToZip, File targetZipFile, boolean includeDirItself)
    Zips a directory.
    @param targetZipFile the target .zip file. If the parent directories don't exist, tries to create them.
    @param If true, the directory itself will be included in the zip file, otherwise only its content will be.
  • void zipExtract(File zipFile, File targetDir)
    Extracts a .zip file to the specified directory.
    @param targetDir The target directory. If it doesn't exist, tried to create it (and its parents, if required).
  • String getMimeTypeFromMultipleSources(String responseContentTypeHeader, String resourcePath, String requestPath)
    Gets the mime type using multiple sources of information.
    @param contentTypeHeader an already existing Content-Type header on the response. Can be null.
    @param resourcePath the path (absolute or relative) to the target resource. Can be null.
    @param requestPath the path of the current request. Can be null.
    @return the mime type or null if it can't be decided.
  • String getMimeTypeFromPath(String path)
    Gets the mime type from a path, using its extension.
    @return the mime type or null if it can't be decided.
  • String getMimeTypeFromExtension(String extension)
    Gets the mime type from the extension.
    @return the mime type or null if it can't be decided.
  • Locale getLocaleBestMatchFromAcceptLanguageHeader(String acceptLanguageHeader)
    Gets the best Locale to use given a "Accept-Language" HTTP header.
    @return the best Locale to use or null if the given header can't be parsed.
  • boolean isContentTypeToSkipGziping(String contentType)
    Should the specified Content-Type be gzipped?
  • File getAppJarDirectory()
    Returns the working directory: the directory in which the executable .jar is located.
    @return the working directory or null if the application is running inside an IDE.
  • String getSpincastCurrentVersion()
    Gets the current Spincast version.
  • String getCacheBusterCode()
    The cache buster to use.

    This should probably change each time the application is restarted or at least redeployed.

    It should also be in such a format that it's possible to remove it from a given text.

    This must be kept in sync with removeCacheBusterCode!

  • String removeCacheBusterCodes(String text)
    Removes the cache buster code occurences from the given text.

    Note that this won't simply remove the current cache busting code, it will remove any valid cache busting code... This is what we want since we don't want a client sending a request containing an old cache busting code to break!

    This must be kept in sync with getCacheBusterCode!

  • String readClasspathFile(String path)

    Reads a file on the classpath and returns it as a String.

    Paths are always considered from the root at the classpath. You can start the path with a "/" or not, it makes no difference.

    Uses UTF-8 by default.

    @return the content of the file or null if not found.

@MainArgs

The init(...) method of the Bootstrapper allows you to bind the arguments received in your main(...) method. For example :


public static void main(String[] args) {

    Spincast.configure()
            .module(new AppModule())
            .init(args);
    //....
}

By doing so, those arguments will be available for injection, using the @MainArgs annotation :

public class AppConfig extends SpincastConfig implements AppConfig {

    private final String[] mainArgs;

    @Inject
    public AppConfig(@MainArgs String[] mainArgs) {
        this.mainArgs = mainArgs;
    }

    protected String[] getMainArgs() {
        return this.mainArgs;
    }

    @Override
    public int getHttpServerPort() {

        int port = super.getHttpServerPort();
        if(getMainArgs().length > 0) {
            port = Integer.parseInt(getMainArgs()[0]);
        }
        return port;
    }
}

Using an init() method

This is more about standard Guice development than about Spincast, but we feel it's a useful thing to know.

Guice doesn't provide support for a @PostConstruct annotation out of the box. And since it is often seen as a bad practice to do too much work directly in a constructor, what we want is an init() method to be called once the object it fully constructed, and do the initialization work there.

The trick is that Guice calls any @Inject annotated methods once the object is created, so let's use this to our advantage :

public class UserService implements UserService {

    private final SpincastConfig spincastConfig;

    @Inject
    public UserService(SpincastConfig spincastConfig) {
        this.spincastConfig = spincastConfig;
    }

    @Inject
    protected void init() {
        doSomeValidation();
        doSomeInitialization();
    }

    //...
}

Explanation :

  • 5-8 : The constructor's job is only to receive the dependencies.
  • 10-14 : An init() method is also annotated with @Inject. This method will be called once the object is fully constructed. This is a good place to do some initialization work!

What we recommend is constructor injection + one (and only one) @Inject annotated method. The problem with multiple @Inject annotated methods (other than constructors) is that it's hard to know in which order they will be called.

Finally, if the init() method must be called as soon as the application starts, make sure you bind the object using asEagerSingleton()!

Server started listeners

Spincast provides a hook so you can be informed when your application's HTTP server has been successfully started. For a class to be called:

1. The class must implement the ServerStartedListener interface. When the server is started, the serverStartedSuccessfully() method will be called:

public class MyClass implements ServerStartedListener { 

    @Override
    public void serverStartedSuccessfully() {
        System.out.println("Server started hook!");
    }
    
    //...
}

Note that each listener is called in a new Thread.

2. The class must be registered on the Multibinder<ServerStartedListener> multibinder, in your application's Guice module:

Multibinder<ServerStartedListener> serverStartedListenersMultibinder =
        Multibinder.newSetBinder(binder(), ServerStartedListener.class);
serverStartedListenersMultibinder.addBinding().to(MyClass.class).in(Scopes.SINGLETON);