Installation

Requirements

  • A Java 7 JDK or newer
  • A build tool able to use Maven artifacts.

Most of the examples and demos on this site required Java 8 (for lambdas and method references). But it is certainly possible to use Spincast with Java 7 : there are no Java 8 specific features in Spincast's core, only in the examples and demos.

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.31 artifact to your pom.xml :

<dependency>
    <groupId>org.spincast</groupId>
    <artifactId>spincast-default</artifactId>
    <version>0.9.31</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() 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();
    }

    @Inject
    protected void init(DefaultRouter router, Server server) {
        router.GET("/").save(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 3 direct dependencies which are external to Spincast:

  • com.google.inject:* (Guice), which pulls three transitive dependencies :
    • com.google.guava:guava-parent (Guava)
    • javax.inject:javax.inject
    • aopalliance:aopalliance
  • com.google.code.findbugs:jsr305 : For the @Nullable annotation.
  • org.slf4j:slf4j-api : The SLF4J 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, 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();

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 directly (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}").save(bookController::booksGet);
       
// For any HTTP request. Uses an inine Route Handler :
router.ALL("/hello").save(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
    Cookie localeCookie = context.cookies().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.put("author", "Douglas Adams");
    book.put("title", "The Hitchhiker's Guide to the Galaxy");
    
    // Adds the book to the response model
    context.response().getModel().put("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, 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 application builder :

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

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 directly (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").save();

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

// ... 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("/").save(context -> {
    
    // Adds some variables to the response model so
    // the template has access to them.
    context.response().getModel().put("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").save();

// ... 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}").save(bookController::booksGet);

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

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
    Cookie localeCookie = context.cookies().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.put("data.book", book);

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

    // Adds a timestamp to the response model
    responseModel.put("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.31</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() :

public class App {

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

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();
    }
    
    @Inject
    protected void init(DefaultRouter router, Server server) {
        router.GET("/").save(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() 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(). 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(). 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)
            .mainArgs(args)
            .init();
    //....
}

Explanation :

  • 3 : We start the bootstrapper by calling .configure() instead of .init()!
  • 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 bind the arguments received in the main(...) method to the Guice context.
  • 8 : We finally call .init() so the Guice context is created, the current class is bound and then loaded.

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!
  • mainArgs(...) : To bind the arguments received in the main(...) method. They will then be available for injection using the @MainArgs String[] key.
  • 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.31</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.31</version>
</dependency>

<dependency>
    <groupId>org.spincast</groupId>
    <artifactId>spincast-plugins-pebble</artifactId>
    <version>0.9.31</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.

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.put("name", "Stromgol");
    context.templating().evaluate("Hello {{name}}", params);
    
    // Gets the best Locale to use for the current request
    Locale localeToUse = context.getLocaleToUse();
    
    // 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.put("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.put("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 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);
    
    // Adds a HTTP header value
    context.response().addHeaderValue(HttpHeaders.CONTENT_LANGUAGE, "en");
    
    // Sets the content-type
    context.response().setContentType(ContentTypeDefaults.JSON.getMainVariation());
    
    // 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().put("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");

  • CookiesRequestContextAddon<R> cookies()

    The cookies() add-on allows you to read and write cookies.

    Examples :

    // Gets a cookie
    Cookie cookie = context.cookies().getCookie("someCookie");
    
    // Adds a cookie (multiple options are provided)
    context.cookies().addCookie("someCookie", "someValue");
    
    // Deletes a cookie
    context.cookies().deleteCookie("someCookie");

  • 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.put("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().add("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.

  • 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)
            .mainArgs(args)
            .init();
    //....
}

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("/").save(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("/").save(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.put("simpleVar", "test");
    
    // Adds a Book : this object will automatically
    // be converted to a JsonObject
    Book book = getBook(42);
    responseModel.put("myBook", book);
        
    // Then adds a JsonObject representing a user
    JsonObject user = context.json().create();
    user.put("name", "Stromgol");
    user.put("age", 30);
    responseModel.put("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.put("name", "Stromgol");
    user.put("age", 42);
    responseModel.put("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. Any remaining Route Handlers/Filters 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.

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 as dedicted to 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. But that Not Found routing process can also be 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.SOME("/books", HttpMethod.POST, HttpMethod.PUT) ...

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().save(handler)
router.notFound(handler);

// Synonym of : 
// router.ALL("/*{path}").exception().save(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().save(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 save(...) 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("/").save(controller::indexHandler);

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

Using Java 7, you have to declare an inline Route Handler :

router.GET("/").save(new AppHandler() {
    @Override
    public void handle(AppRequestContext 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().save(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 "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).save(ctl::filter1);

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

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

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

There are some shortcuts that you can use if you don't need to specify fine grain information and just want to add a quick "before" or "after" Filter :

// Will be applied to any request, at position "-10".
// Synonym of : router.ALL("/*{path}").pos(-10).save(ctl::filter)
router.before().save(ctl::filter);

// Will be applied to any request starting with "/users", 
// at position "-10".
// Synonym of : router.ALL("/users").pos(-10).save(ctl::filter)
router.before("/users").save(ctl::filter);

// Will be applied to any request, at position "10".
// Synonym of : router.ALL("/*{path}").pos(10).save(ctl::filter)
router.after().save(ctl::filter);

// This actually generates *two* Filters.
// They will be applied to any request, both at
// position "-10" and at position "10".
router.beforeAndAfter().save(ctl::filter);

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 Route by default
router.before().id("myFilter").save(ctl::filter);

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

You can also add inline Filters that are run only on on a specific Route :

// 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)
      .save(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.cookies().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).save(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").save();

// 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").save();

// 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").save();

// 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").save();

// 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").save();

// 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").save();

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("/*{path}").pos(-1).save(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").save();
router.file("/public/test.txt").cache(60).classpath("/public_files/test.txt").save();

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")
      .save(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")
      .save(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")
      .save(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}").save(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").save(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}").save(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}").save(generator);
    

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().save();
           
// Enable CORS on a Static Resource file.
getRouter().file("/public/test.txt").classpath("/public_files/test.txt")
           .cors().save();

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}").save(this::getUser);
        router.POST("/users").save(this::addUser);
        router.DELETE("/users/${userId}").save(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.put("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.put("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.put("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.put("name", "Stromgol");
    model.put("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 %}

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 %}

  • 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.put("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
  • 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.put("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.put("name", "Stromgol");
    obj.put("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.put("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 put(...)/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.put("colors", colors, true);

// Or :
obj.put("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.put("name", "Stromgol");

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

// This will throw an exception!
immutableClone.put("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 put(...), 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.put("title", "The Hitchhiker's Guide to the Galaxy");

// Creates a "myJsonObj" object and adds the book to it
JsonObject myJsonObj = getJsonManager().create();
myJsonObj.put("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 putNoKeyParsing(...) method. For example :

// Creates a book object
JsonObject book = getJsonManager().create();
book.put("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.putNoKeyParsing("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.

Validation

Spincast provides a good set of utilities to validate objects. We decided to base those utilities on plain Java code and not on annotations. Validation annotations may seem convenient at first, but, in a real project, you quickly realize that you can't express all the details you need : you can't use complex logic, you can't validate an element based on the result of the validation of another element, etc. In general, you end up writing plain Java code anyway in addition to those annotations... But then your validation logic is scattered in many places and this is not the ideal.

In Spincast, you validate an object using three main steps :

  1. You create a Validation Set instance. This is a container where the results of the validation will be saved.
  2. You use Predefined Validations or custom validation code to actually validate the object. Those validations may result in Validation Messages. Most of the time those are Error Validation Messages resulting from failed validations ("This email is invalid", for example).

  3. When the validation is all done, you use the resulting Validation Messages by retrieving them from the Validation Set. In general, you want to display them to a user, or you generate some kind of report.

Here's a quick example :

// Creation of a Validation Set
ValidationSet myValidationSet = getValidationFactory().createValidationSet();

// The object we're going to validate
User user = getUser();

// Validates the email of the user using the 
// "validationEmail()" predefined validation
myValidationSet.validationEmail()
               .key("email")
               .element(user.getEmail())
               .validate();
                                           
// Validates the username of the user, using
// custom validation code
String username = user.getUsername();
if(username == null || username.length() < 3 || username.length() > 42) {
    myValidationSet.addError("username",
                             "USERNAME_INVALID",
                             "Must contain between 3 and 42 characters.");  
}

// Uses/Displays the resulting Validation Messages
if(!myValidationSet.isValid()) {
    
    Map<String, List<ValidationMessage>> validationMessages = myValidationSet.getMessages();
    
    // ... do something with the messages
}

In the JsonObject validation section, we'll see that validation is even easier when done using JsonObjects...

The Validation Set

At the core of a validation process, is a ValidationSet. A Validation Set is a set of Validation Messages and of utility methods. It acts as a container to save the results of the various validations run on an object.

For example, let's say we validate a User object. At the end of this validation, the resulting Validation Set may contain two Validation Messages :

  • An Error Validation Message : "The email is invalid."
  • A Success Validation Message : "This username is available!"

There are two ways of creating a Validation Set instance :

  • By injecting and using the ValidationFactory :

    ValidationFactory validationFactory = getValidationFactory();
    ValidationSet validationSet = validationFactory.createValidationSet();

  • By using the validationSet(...) method on a JsonObject :

    JsonObject responseModel = context.response().getModel();
    JsonObjectValidationSet validationSet = responseModel.validationSet();

    When you get a Validation Set this way, it is bound to the JsonObject it originates from. A JsonObjectValidationSet is returned instead of a plain Validation Set, and you have access to extra methods dedicated to validate the associated JsonObject. We'll see that in details in the JsonObject validation section.

A Validation Set is more than a simple container to store the validation results. It also provides Predefined Validations and a bunch of other utility methods. Those utility methods are :

  • ValidationSet addMessage(String validationKey, ValidationMessage validationMessage)
    Adds a new Validation Message to this set, using the specified validation key.
    @return itself (fluent style)
  • ValidationSet addMessage(String validationKey, ValidationLevel messageLevel, String code, String text)
    Creates and adds a new Validation Message.
    @return itself (fluent style)
  • ValidationSet addError(String validationKey, String code, String text)
    Creates and adds a new Error Validation Message.
    @return itself (fluent style)
  • ValidationSet addWarning(String validationKey, String code, String text)
    Creates and adds a new Warning Validation Message.
    @return itself (fluent style)
  • ValidationSet addSuccess(String validationKey, String code, String text)
    Creates and adds a new Success Validation Message.
    @return itself (fluent style)
  • ValidationSet mergeValidationSet(ValidationSet set)
    Merges an existing Validation Set.
    @return itself (fluent style)
  • ValidationSet mergeValidationSet(String validationKeyPrefix, ValidationSet result)

    Merges an existing Validation Set but prefixes all the validation keys with the specified validationKeyPrefix.

    The validationKeyPrefix parameter allows you to merge a Validation Set resulting from a validation not performed directly on your current Validation Set (you may be reusing an external Validator for example) and still have the keys of the merged Validation Messages to properly represent JsonPaths on your current Set. We'll talk about that in a next section...

    @return itself (fluent style)
  • boolean hasMessages()
    Does this Validation Set contain any Validation Messages?
  • boolean hasMessages(String validationKey)
    Are there some Validation Messages associated with the specified validation key?
  • boolean isWarning()
    Returns true if the Validation Set contains at least one Warning Validation Message, but no Error Validation Messages.
  • boolean isWarning(String validationKey)
    Returns true if there is at least one Warning Validation Message, but no Error Validation Messages associated with the specified validation key.
  • boolean isSuccess()
    Returns true if the validation set only contains Success Validation Messages (or contains no messages at all).
  • boolean isSuccess(String validationKey)
    Returns true if there are only Success Validation Messagesassociated with the specified validation key (or contains no messages at all).
  • boolean isError()
    Returns true if the validation set contains at least one Error Validation Message.
  • boolean isError(String validationKey)
    Returns true if there is at least one Error Validation Message associated with the specified validation key.
  • boolean isValid()

    Returns true if the Validation Set does not contain any Error Validation Messages. The set can contain Warning Validation Message and Success Validation Message.

    This is a synonym of !isError().

  • boolean isValid(String validationKey)

    Returns true if there are no Error Validation Messages associated with the specified validation key. There can be Warning Validation Message and Success Validation Message.

    This is a synonym of !isError(validationKey).

  • Map<String, List<ValidationMessage>> getMessages()

    Gets the Validation Messages, with their associated validation keys as the leys of the Map.

    The Map and the List values are immutable.

  • List<ValidationMessage> getMessages(String validationKey)

    Gets the Validation Messages associated with the specified validation key.

    The Map and the List values are immutable.

  • String getMessagesFormatted(ValidationMessageFormatType formatType)
    Quick way to get a formatted version of the Validation Messages.

    @param formatType The type of output for the messages (Text, HTML, Json or XML).
    @return the formatted messages or null if there are no validation messages.
  • String getMessagesFormatted(String validationKey, ValidationMessageFormatType formatType)
    Quick way to get a formatted version of the Validation Messages associated with the specified validation key.

    @param formatType The type of output for the messages (Text, HTML, Json or XML).
    @return the formatted messages or null if there are no validation messages.
  • public JsonObject convertToJsonObject()

    Converts the Validation Set to a JsonObject object. Read the Converting to a JsonObject section to see what the resulting JsonObject object looks like!

    The resulting JsonObject object is immutable.

Validation Messages

A ValidationMessage is composed of three things :

  • A Validation Level. This level can be :
    • Error
    • Warning
    • Success
  • A text, which is the description of the validation result. For example : "This email is invalid".
  • A code, which in general is a constant representing the type of validation this Validation Message is associated with. For example : "VALIDATION_TYPE_EMAIL".

When you add a Validation Message to a Validation Set, you do so using a validation key. The validation key allows you to retrieve the Validation Messages associated with a specific element from the Validation Set.

Here's a quick example where we perform a validation and create a Validation Message :


ValidationSet validatorSet = validationFactory.createValidationSet();
JsonObject user = getUser();
        
String thirdBookTitle = user.getString("user.books[2].title");
if(containsNsfwWords(thirdBookTitle)) {
    
    ValidationMessage message = 
            getValidationFactory().createMessage(ValidationLevel.WARNING, 
                                                 "NSFW_MATERIAL", 
                                                 "This book contains NSFW material.");
                                                 
    validatorSet.addMessage("user.books[2].title", message);
}

Explanation :

  • 2 : We create a Validation Set.
  • 3 : The user (as a JsonObject) that we're going to validate.
  • 5 : We get the title of the third book of the user. We use a JsonPath to target it!
  • 6 : We perform a manual validation on that title. We could use a Predefined Validation too.
  • 8 : We create a Validation Message to represent the validation result.
  • 9 : The level of this Validation Message. Here, it's going to be a Warning Validation Message. The two other possible levels are Error and Success.
  • 10 : The code (or "type") of the validation the Validation Message is associated with.
  • 11 : The text for the Validation Message.
  • 13 : We add the Validation Message to the Validation Set using the "user.books[2].title" validation key. We'll see in the next section that using the JsonPath of the validated element as its validation key is often a good idea!

Validation Sets also provides some methods to quickly add Validation Messages, without having to create the messages manually. For example :

// Creates and adds an Error Validation Message
validatorSet.addError("user.books[2].title", "LENGTH", "The title is too long");

// Creates and adds a Warning Validation Message
validatorSet.addWarning("user.books[2].title", "LENGTH", "The title is rather long");

// Creates and adds a Success Validation Message
validatorSet.addSuccess("user.books[2].title", "LENGTH", "The title length is perfect");

Validation Keys

A validation key is used to associate Validation Messages to the validated element they have been created for. You can think of a Validation Set as a big Map<String, List<ValidationMessage>> where the keys are validation keys and where the values are the Validation Messages.

If you validate a simple email element for example, you could use "email" or "email-validation" as the validation key. It's up to you to choose a meaningful name for the key : you will later use it to retrieve all the Validation Messages associated with this element so you can display them, create a report from them, etc.

Even if you can use any string as a validation key, we suggest that you stick to some conventions since a validation key must be unique and should well represent the element it is associated with.

The convention we suggest is that, when possible, you use the JsonPath of the validated element as the validation key.

Let's say we validate this user object :

{
    "name": "Stromgol",
    "email": "test@example.com"
    "books": [
        {
            "title" : "Dune",
            "author": "Frank Herbert"
        },
        {
            "title" : "The Hitchhiker's Guide to the Galaxy",
            "author" : "Douglas Adams"
        }
    ]
}

First, we will validate the email :

JsonObject user = getUser();

// We create a Validation Set
ValidationSet validationSet = getValidationFactory().createValidationSet();

if(!isEmailValid(user.getString("email"))) {

    // We add a Validation Message using "email" as the validation key
    validationSet.addError("email", "EMAIL_VALIDATION", "Invalid email");  
}

We can later retrieve the Validation Messages associated with the email element using that "email" validation key :


List<ValidationMessage> validationMessages = validationSet.getMessages("email");

Very straightforward! But now let's try to do the thing with the title of the first book :

// Gets the title of the first book, using its JsonPath
String title = user.getString("books[0].title");

if(!isTitleValid(title) {

    // We add a Validation Message using "title" as the validation key...
    // This may not be a good idea!
    validationSet.addError("title", "TITLE_VALIDATION", "Invalid title");  
}
 
List<ValidationMessage> validationMessages = validationSet.getMessages("title");

We may be tempted to use a very short name as the validation key, "title" for example. It would work, but it's not a very good idea... The problem is that now the validation key doesn't well represent the validated element it is associated with. Remember that each validation key must be unique. What happens if we also want to validate the title of the second book? Or if there are more than one user with books to validate? What validation keys will we use then? We can't use the same "title" validation key for more than one element!

Since each element of a JsonObject already has a unique identifier, which is its JsonPath, why not use this identifier as its validation key?

// Gets the title of the first book, using its JsonPath
String title = user.getString("books[0].title");

if(!isTitleValid(title) {

    // We add a Validation Message using the JsonPath
    // of the validated element as the validation key : 
    // much better!
    validationSet.addError("books[0].title", "TITLE_VALIDATION", "Invalid title");  
}
 
List<ValidationMessage> validationMessages = validationSet.getMessages("books[0].title");

Now there won't be any conflict, even if there are a lot of validation keys in our Validation Set, and every key will be clearly indicate which validated element it is associated with!

Note that there are situations where you can't use a JsonPath for a validation key. You don't always validate elements individually, for instance. Sometimes, a combination of elements is invalid, and therefore no JsonPath is available.

In those situations, we suggest that you scope the validation key as much as possible, and that you use creativity in order to make the key unique and meaningful!

For example, let's say that an HTML form has been submitted and you need to validate that two passwords from it match. If they don't match, you could had an Error Validation Message for both of those elements, or only for the second one (you could say that only the second one is invalid since it doesn't match the first one). But you could also want to add a Validation Message to represent that invalid combination, not the fields taken individually. You may want to display a "The passwords don't match" error in the section where both fields are, for example.

In such situations, you can't use a JsonPath as the validation key, since you are not referring to a single element. But we suggest that you still try to scope the validation key, so it is as unique and meaningful as possible. For example, that key could be "myForm.user.passwordsMatch" :

ValidationSet validationSet = getValidationFactory().createValidationSet();

JsonObject formData = context.request().getFormData();

// Gets the passwords using their JsonPath
String password1 = formData.getString("myForm.user.password1", "");
String password2 = formData.getString("myForm.user.password2", "");
if(!password1.equals(password2)) {

    // Use a scoped and meaningful
    // validation key, even if it's not 
    // a true JsonPath!
    validationSet.addError("myForm.user.passwordsMatch", 
                           "PASSWORDS_MATCH", 
                           "The password don't match!");  

A question remains : what happens to this "Use the JsonPath of a validated element as its validation key" convention when we validate an element using an external Validator? For example, we may have a TitleValidator that can be reused in multiple places in our application. For example :

ValidationSet validationSet = getValidationFactory().createValidationSet();

String title = getUser().getString("books[0].title");

TitleValidator titleValidator = getTitleValidator();

ValidationSet titleValidationSet = titleValidator.validate(title);

validationSet.mergeValidationSet(titleValidationSet);

// Oups! 
// The validation key is "title" here, not "books[0].title" as we
// would like...
List<ValidationMessage> messages = validationSet.getMessages("title");

External validators return a Validation Set and you merge this set to your own local set [9] (you can learn more about merging Validation Sets in the Sharing / Merging Validation Sets section).

The problem here is that TitleValidator has no idea of the position of the "title" element inside our local user object! It will see the "title" element as being a root element and therefore it won't generate a validation key respecting our local JsonPath... It will probably simply use "title" as the validation key.

For this reason, the mergeValidationSet(...) method accepts a "validationKeyPrefix" parameter. If this parameter is specified, all the validation keys will be prefixed with it when merged :

ValidationSet validationSet = getValidationFactory().createValidationSet();

String title = getUser().getString("books[0].title");

TitleValidator titleValidator = getTitleValidator();

// We use the external validator
ValidationSet titleValidationSet = titleValidator.validate(title);

// We prefixe the merged validation keys
validationSet.mergeValidationSet("books[0].", titleValidationSet);

// The Validation Messages can now be retrieved using
// JsonPaths thast are valid on our validated root object!
List<ValidationMessage> messages = validationSet.getMessages("books[0].title");

There is also a prefixValidationKeys(...) method which can be used to prefix the validation keys of a Validation Set directly, without having to merge this set into another one. This can be useful, for example if you want to prefix the validation keys before adding a Validation Set to a JsonObject :

// Uses an external validator
ValidationSet validationSet = getExternalValidator().validate(someObject);
        
// Prefixes the resulting validation keys
// *directly on the Validation Set*
validationSet.prefixValidationKeys("someObject.");

// Adds the Validation Set (with the validation keys
// now modified) to the response model
context.response().getModel().put("validation", validationSet);

Predefined Validations

Spincast provides some predefined validations to help validate an object. They are defined on the ValidationSet interface so there are easily accessible during a validation process. Let's have a look at an example :

// We create an inital Validation Set
ValidationSet myValidatorSet = getValidationFactory().createValidationSet();

// This is a username we're going to validate!
String username = getSomeUsername();

// Validates that the username is not blank and, only if 
// it's not, validates that it contains at least 3 characters.
ValidationSet lastResult = myValidatorSet.validationNotBlank()
                                         .key("username")
                                         .element(username)
                                         .validate();
if(lastResult.isValid()) {
    lastResult = myValidatorSet.validationMinLength(3)
                               .key("username")
                               .element(username)
                               .validate();
}

Notice that a predefined validation returns it's own Validation Set [9]! This allows you to conditionally apply a validation depending on the result of a previous one [13].

To use a predefined validation :

  • You call the associated validateXXXXXX(...) method on your Validation Set. For example : myValidatorSet.validationNotBlank(). This starts a builder to create the validation.
  • You specify a validation key using the "key(...)" method on the builder. This key associate the resulting Validation Messages to the validated elements. For example : .key("username").
  • You specify the element to validate. For example : .element(username).
  • You finally call .validate() to perform the actual validation.

When the validate() method is called, the validation is perform and :

  • The resulting Validation Messages are automatically added to the root Validation Set.
  • A new Validation Set, representing this particular validation only, is returned. This allows you to conditionally apply a validation depending on the result of a previous one.

Here's the list the predefined validations available on a Validation Set :

  • ValidationBuilderKey validationNotBlank()
    Starts the creation of a "not blank" validation (null, empty or spaces only).
  • ValidationBuilderKey validationBlank()
    Starts the creation of a "must be blank" validation.
  • ValidationBuilderKey validationEmail()
    Starts the creation of an email validation.
  • ValidationBuilderKey validationNotNull()
    Starts the creation of a "not null" validation.
  • ValidationBuilderKey validationNull()
    Starts the creation of a "must be null" validation.
  • ValidationBuilderKey validationPattern(String pattern)
    Starts the creation of a "pattern must match" validation.
  • ValidationBuilderKey validationNotPattern(String pattern)
    Starts the creation of a "pattern must not match" validation.
  • ValidationBuilderKey validationSize(int size, boolean ignoreNullValues)
    Starts the creation of a "size" validation.
    @param ignoreNullValues If true, null values will be ignored in the total count. For example, if the validation is run on a JsonArray which is [null, null "abc"], then the size would be : "3" if ignoreNullValues is false, "1" if it's true.
  • ValidationBuilderKey validationMinSize(int minSize, boolean ignoreNullValues)
    Starts the creation of a "minimum size" validation.
    @param ignoreNullValues If true, null values will be ignored in the total count. For example, if the validation is run on a JsonArray which is [null, null "abc"], then the size would be : "3" if ignoreNullValues is false, "1" if it's true.
  • ValidationBuilderKey validationMaxSize(int maxSize, boolean ignoreNullValues)
    Starts the creation of a "maximum size" validation.
    @param ignoreNullValues If true, null values will be ignored in the total count. For example, if the validation is run on a JsonArray which is [null, null "abc"], then the size would be : "3" if ignoreNullValues is false, "1" if it's true.
  • ValidationBuilderKey validationLength(int length)
    Starts the creation of a "length" validation.
  • ValidationBuilderKey validationMinLength(int minLength)
    Starts the creation of a "minimum length" validation.

    A null value makes this validation fail, otherwise toString() is called on the object to check its "length".

  • ValidationBuilderKey validationMaxLength(int maxLength)
    Starts the creation of a "maximum length" validation.

    A null value is a success, otherwise toString() is called on the object to check its "length".

  • ValidationBuilderKey validationEquivalent(Object reference)

    Starts the creation of a "equivalent" validation.

    To validate if an element is equivalent to another, one is converted to the type of the other (when this is required). If this conversion fails, the elements are not equivalent. When both elements are of the same type, they are compared using their equals(...) method. This process is done in the ObjectConverter#isEquivalent(...) method.

  • ValidationBuilderKey validationNotEquivalent(Object reference)

    Starts the creation of a "not equivalent" validation.

    To validate if an element is equivalent to another, one is converted to the type of the other (when this is required). If this conversion fails, the elements are not equivalent. When both elements are of the same type, they are compared using their equals(...) method. This process is done in the ObjectConverter#isEquivalent(...) method.

  • ValidationBuilderKey validationLess(Object reference) throws CantCompareException

    Starts the creation of a "less than" validation.

    To validate if an element is less than another, one is converted to the type of the other (when this is required). If this conversion fails, the elements can't be compared. The resulting type of the elements must implement the Comparable interface, so the compareTo(...) method is used to compare the two elements.

    An null element is always less than a non-null element.

    @throws CantCompareException if the two elements can't be compared together.
  • ValidationBuilderKey validationGreater(Object reference) throws CantCompareException

    Starts the creation of a "greater than" validation.

    To validate if an element is greater than another, one is converted to the type of the other (when this is required). If this conversion fails, the elements can't be compared. The resulting type of the elements must implement the Comparable interface, so the compareTo(...) method is used to compare the two elements.

    An null element is always less than a non-null element.

    @throws CantCompareException if the two elements can't be compared together.
  • ValidationBuilderKey validationEquivalentOrLess(Object reference) throws CantCompareException

    Starts the creation of a "equivalent or less" validation.

    To validate if an element is equivalent or less than another, one is converted to the type of the other (when this is required). If this conversion fails, the elements can't be compared. The resulting type of the elements must implement the Comparable interface, so the compareTo(...) method is used to compare the two elements.

    An null element is always less than a non-null element.

    @throws CantCompareException if the two elements can't be compared together.
  • ValidationBuilderKey validationEquivalentOrGreater(Object reference) throws CantCompareException

    Starts the creation of a "equivalent or greater" validation.

    To validate if an element is equivalent or greater than another, one is converted to the type of the other (when this is required). If this conversion fails, the elements can't be compared. The resulting type of the elements must implement the Comparable interface, so the compareTo(...) method is used to compare the two elements.

    An null element is always less than a non-null element.

    @throws CantCompareException if the two elements can't be compared together.

Predefine Validation options

Some options are available during the process of using a predefine validation :

  • Instead of validating a single element, using the "element(...)" method of the builder, you can validate a JsonArray of elements by using the "all(...)" method. Doing so, all the elements of the array will be validated and some Validation Messages will potentially be added for each of them!

    Note that, when such array is validated, the generated validation keys are going to be the key of the array + the position of the elements in the array. For example :

    ValidationSet myValidatorSet = validationFactory.createValidationSet();
    
    JsonArray titles = getJsonManager().createArray();
    titles.add("A valid title");
    titles.add("");
    titles.add("  ");
    titles.add(null);
    
    // Using ".all(titles)", this validates all the elements
    // of the array
    ValidationSet lastResult = myValidatorSet.validationNotBlank()
                                             .key("titles")
                                             .all(titles)
                                             .validate();

    In this example, the resulting Validation Set will contain three Error Validation Messages : one with the key "titles[1]", one with the key "titles[2]", and one with the key "titles[3]". The first title ("titles[0]") is valid so no Validation Message will be added for it.

  • By default a Validation Message is added only when the validation fails. If you want to add a Success Validation Message, when the validation is sucessful, you can use the "addMessageOnSuccess(...)" method. For example :

    ValidationSet myValidatorSet = validationFactory.createValidationSet();
    
    JsonArray titles = getJsonManager().createArray();
    titles.add("A valid title");
    titles.add("");
    
    ValidationSet lastResult = myValidatorSet.validationNotBlank()
                                             .key("titles")
                                             .all(titles)
                                             .addMessageOnSuccess()
                                             .validate();

    In this example, the resulting Validation Set will contain two Validation Messages : a Success Validation Message with the key "titles[0]" and an Error Validation Message with the key "titles[1]".
  • When using the "addMessageOnSuccess(...)" method, you can specify the text that is going to be used for the message (instead of the default one). For example :

    ValidationSet myValidatorSet = validationFactory.createValidationSet();
    
    JsonArray titles = getJsonManager().createArray();
    titles.add("A valid title");
    titles.add("");
    
    ValidationSet lastResult = myValidatorSet.validationNotBlank()
                                             .key("titles")
                                             .all(titles)
                                             .addMessageOnSuccess("A custom Success message!")
                                             .validate();

  • When a validation fails, an Error Validation Message is generated by default. If you want the failure of a validation to generate a Warning Validation Message instead, use the "treatErrorAsWarning()" method. But remember that a Validation Set containing Warning Validation Message is still considered as being valid, only Error Validation Messages make it invalid! For example :

    ValidationSet myValidatorSet = validationFactory.createValidationSet();
    
    String username = "";
    
    ValidationSet lastResult = myValidatorSet.validationNotBlank()
                                             .key("username")
                                             .element(username)
                                             .treatErrorAsWarning()
                                             .validate();
                                             
    // There is now one *Warning* Validation Message
    // in our Validation Set
    myValidatorSet.getMessages().size(); // == 1
    
    // The level of the Validation Set is "Warning"
    myValidatorSet.isWarning(); // true
    myValidatorSet.isError(); // false
    myValidatorSet.isSuccess(); // false
    
    // The Validation Set is still valid!
    myValidatorSet.isValid(); // true

  • You can specify the text to use for a Validation Message when a validation fails, using the "failMessageText(...)" method. The specified text will then be used for the Error Validation Message if one is generated, but also for a Warning Validation Message if "treatErrorAsWarning()" is used.
  • You can specify that a validation must not be run if some Validation Messages at a specific level already exist in the Validation Set.

    Let's say for example that you want to validate that a username contains at least 3 characters but you only want to perform this validation if the username is not blank in the first place! You could do this programmatically, but you can also pass a special parameter to the validate(...) method :

    ValidationSet myValidatorSet = validationFactory.createValidationSet();
    
    String username = "";
    
    ValidationSet lastResult = myValidatorSet.validationNotBlank()
                                             .key("username")
                                             .element(username)
                                             .validate();
    
    // This validation is only going to be performed if
    // the Validation Set doesn't contain any errors yet                            
    lastResult = myValidatorSet.validationMinLength(3)
                               .key("username")
                               .element(username)
                               .validate(ValidationLevel.ERROR);
    

    In this example, the resulting Validation Set will only contain one Validation Message : the one for the "Not Blank" validation. The "Minimum Length" validation is not performed since the Validation Set already contains a Validation Message at the Error level!

    Also note that "validate(true)" is a shortcut for "validate(ValidationLevel.ERROR)". It means "Only run this validation is the Validation Set is still valid!".

If you are validating a JsonArray, by using the "all(...)" method, then a couple of extra methods are also available :

  • arrayItselfAddFailMessage() : if you call this method, a Validation Message will be added for the array itself if the validation for at least one of its elements fails. The validation key for this message will be the specified validation key (remember that the keys for the elements of the array are going to be the specified validation key + the position of the elements in the array).

    You can also specify the text to use for that message (instead of the default one) : arrayItselfAddFailMessage("Some elements are invalid!")

  • arrayItselfAddSuccessMessage() : if you call this method, a Validation Message will be added for the array itself if the validation is successful for all its elements. The validation key for this message will be the specified validation key (remember that the keys for the elements of the array are the specified validation key + the position of the elements in the array).

    You can also specify the text to use for the message (instead of the default one) : arrayItselfAddSuccessMessage("All good!")

JsonObject validation

Validating a JsonObject is very easy : you simply use its "validationSet()" method to get a Validation Set specifically made to validate it. For example :

JsonObject user = getUser();
JsonObjectValidationSet userValidationSet = user.validationSet();
ValidationSet lastResult = userValidationSet.validationEmail()
                                            .jsonPath("email")
                                            .validate();

Explanation :

  • 2 : When you get a Validation Set from a JsonObject, the type of this set is JsonObjectValidationSet. This type extends the ValidationSet base type and adds some extra functionalities.
  • 4 : Because this Validation Set is bound to a specific JsonObject, a new "jsonPath(...)" method is available! It allows you to specify the JsonPath to the element to validate on the current object, and will also be used as the validation key.

By calling the "validationSet()" method on a JsonObject, you get a JsonObjectValidationSet instance. This type extends the ValidationSet base type and adds some extra functionalities.

With a JsonObjectValidationSet, you don't need to specify a "key(...)" to use or an "element(...)" to validate! You simply specify the jsonPath(...) to the element to validate. The resulting Validation Messages, if any, will have that JsonPath as their validation keys. For example :

JsonObject user = getJsonManager().create();
user.put("email", "nope");

JsonObjectValidationSet userValidationSet = user.validationSet();

// The Validation Set is bound to the "user" object here,
// so we use a JsonPath starting from this object as the root
// to target the "email" element to validate 
ValidationSet lastResult = userValidationSet.validationEmail()
                                            .jsonPath("email")
                                            .validate();

// The generated validation key is the JsonPath of
// the validated element!
List<:ValidationMessage> messages = userValidationSet.getMessages("email");
System.out.println(messages.size()); // prints "1"

ValidationMessage message = messages.get(0);
System.out.println(message.getValidationLevel()); // prints "ERROR"
System.out.println(message.getCode()); // prints "VALIDATION_TYPE_EMAIL"
System.out.println(message.getText()); // prints "Invalid email address"

Finally, note that there is also a "jsonPathAll(...)" method to validate all the elements of a JsonArray.

Sharing / Merging Validation Sets

You can merge a Validation Set into another one using the mergeValidationSet() method. This allows you to make some elements being validated by an external validator and then merge the resulting Validation Messages in your local Validation Set.

Let's say you have a CompanyValidator object that provides a validateCompany(...) method able to validate a "company" JsonObject :

// The user object we're going to validate
JsonObject user = getUser();

// The local Validation Set bound to our "user" object
JsonObjectValidationSet userValidationSet = user.validationSet();

// We validate the email of the user directly
ValidationSet lastResult = userValidationSet.validationEmail()
                                            .jsonPath("email")
                                            .validate();
                                            
// We use an *external validator* to validate the company
// of the user
ValidationSet companyValidationSet = 
        getCompanyValidator().validateCompany(user.getJsonObject("company"));

// We merge the Validation Set of the company validation
// to our local set!
userValidationSet.mergeValidationSet("company.", companyValidationSet);

Explanation :

  • 2 : Let's say this is a user object containing an email and a company field, and we want to validate it.
  • 5 : We get the Validation Set of the user : this set will contain all the Validation Messages at the end of the validation process.
  • 8-10 : We validate the email of the user using a predefined validation on our local Validation Set.
  • 14-15 : We use an external validator to validate the "company" element of the user! This validation returns a Validation Set containing the Validation Messages resulting from this company validation.
  • 19 : We merge the Validation Set for the company element into our user Validation Set, and we scope its validation keys using the "company." prefix.

    At this point, the user Validation Set contain both the Validation Messages for the email field, and the Validation Messages for the company field. If printed, the set would look something like this, given that both the email and the company were invalid :

    messages = 
    {
        email = [Invalid email address - VALIDATION_TYPE_EMAIL] java.util.ArrayList
        company.name = [Can't be empty - VALIDATION_TYPE_NOT_BLANK] java.util.ArrayList
    }

When you merge two Validation Sets, don't forget to scope the merged validation keys (if required)... Learn more about this in the Validation Keys section!

Being able to share and merge Validation Sets allows you to be creative and structure your application as you wish. You can have standalone validators that are used in multiple places and situations.

Converting to a JsonObject

When your validation is done, you will want to use the Validation Messages saved in the Validation Set. In some cases, you need to serialize them to Json, for example to send them as a response to an Ajax call.

To do that, you simply call the convertToJsonObject() method on the Validation Set. This will convert the set to a JsonObject which, in turn, can easily be serialized to a plain Json string.

When you convert a Validation Set to a JsonObject, an extra root element is added (in addition to the Validation Messages) : "_". This special "_" element summarizes the validation performed using the Validation Set. It contains those fields :

  • isValid : Is the whole set valid? It is if it contains no Error Validation Messages.
  • hasErrors : Does the set contain Error Validation Messages?
  • hasWarnings : Does the set contain Warning Validation Messages?
  • hasSuccesses : Does the set contain Success Validation Messages?

Have a look at the Displaying validation messages section to see the use of this "_" element in action!

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 approach 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 model.
  • How to validate a form that has been submitted.
  • How to redisplay a validated form with Validation Messages.

The form model

You need to prepare a model to back the form you are going to display. This model is sometimes called "form backing object", "form backing bean" or "command object". It's the object used to transfert the values of a form from the server to the client (to populate the form's fields) and vice versa.

You create that form model as a JsonObject and you add it to the response model, the root object the Templating Engine has access to. For example :

public void displayUserForm(AppRequestContext context) {

    // Creates a JsonObject as the the model for the form
    JsonObject userForm = context.json().create();
    
    // ... populates it with inital values, 
    // if required
    userForm.put("email", "test@example.com");

    // Adds the form model to the response model
    context.response().getModel().put("userForm", userForm);
    
    // Displays the form using a HTML template
    context.response().sendTemplateHtml("/templates/userCreationTemplate.html");
}

You can then use this form model to populate fields, in the template. For example :

<form method="post">
    <input type="text" 
           name="userForm.email"
           value="{{userForm.email}}" />
    //...
</form>

When the form is submitted, you get back its model using the context.request().getFormData() method, in your Route Handler. We will see this in details in the Getting the submitted form data section, but here's a quick example :

public void manageUserForm(AppRequestContext context) {

    // Gets the submitted form model
    JsonObject userForm = context.request()
                                 .getFormData()
                                 .getJsonObject("userForm");
                                 
   // ...
}

You may have noticed that we are not using a dedicated class to represent the form model (a "UserForm" class, for example) : we use a dynamic JsonObject. Spincast supports both approaches, but we think a dynamic JsonObject is better for the model of a form. 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 model 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 form, 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().put("userForm", userForm);
    
    sendMyTemplate();
}

When the form is submitted, you would then convert context.request().getFormData(), which is a JsonObject, 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()
                                       .getFormData()
                                       .getJsonObject("userForm")
                                       .convert(UserCreationForm.class);
                                       
   // ...
}

Displaying the Form

By using a dynamic JsonObject as the form model, a benefit is that we don't have to create in advance all the elements required to match the fields of our 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 create a user. This form will display two fields : one for a username and one for an email. Our inital form model doesn't have to specify those two elements when it is first created :

public void myRouteHandler(AppRequestContext context) {

    // Empty model! No username and no email 
    // element specified...
    JsonObject userForm = context.json().create();
    
    // Adds the form model to the response model
    context.response().getModel().put("userForm", userForm);

    // Renders a template containing the user creation
    // form
    context.response().sendTemplateHtml("/templates/userCreationTemplate.html");
}

Here's what that HTML form may looks 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 fields 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 yet (this is required if strictVariables is on).

The "name" attributes of the fields are very important : they represent the JsonPaths that Spincast is going to use to dynamically create the required elements on the model, when the form is submitted.

Let's say this form is submitted. You would then access the values of the fields like so, in your Route Handler :

public void myRouteHandler(AppRequestContext context) {

    // Gets the form model
    JsonObject userForm = context.request()
                                 .getFormData()
                                 .getJsonObject("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");                                
}

Without extracting the model of the form first, you could also reference those elements directly using their full JsonPaths :

public void myRouteHandler(AppRequestContext context) {

    String username = context.request().getFormData().getString("userForm.username");
    String email = context.request().getFormData().getString("userForm.email");
}

As you can see, Spincast uses the "name" attribute of a field as a JsonPath to dynamically create an element for the field. This gives you a lot of flexibility client-side since you can dynamically generate new fields or even entire new 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="user.email"
       value="{{user.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 "article" 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 : "article.tags[0]" or "article.tags[1]"
  • We also use that same "[X]" suffixed name to get and display the "value" attributes.
What we are doing, in fact, is to use the JsonPath to target each element! For example :

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

    JsonObject model = context.request().getFormData();
    
    // Get all the tags of the article, as an array
    JsonArray tags = model.getJsonArray("article.tags");
    
    // You could also access one of the tag directly, using
    // its full JsonPath
    String thirdTag = model.getString("article.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 field.
  • 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="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>

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

In this example, the selected(...) filter compares the current favorite drink of the user ("user.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="user.favDrinks[]" class="form-control">
    <option value="tea" {{user.favDrinks | selected("tea")}}>Tea</option>
    <option value="coffee" {{user.favDrinks | selected("coffee")}}>Coffee</option>
    <option value="beer" {{user.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="user.favDrink"
               {{user.favDrink | checked("tea")}}
               value="tea"/> Tea</label>
    
    <label for="drinkCoffee">
        <input type="radio" 
               id="drinkCoffee" 
               name="user.favDrink"
               {{user.favDrink | checked("coffee")}}
               value="coffee"> Coffee</label>
    
    <label for="drinkBeer">
        <input type="radio" 
               id="drinkBeer" 
               name="user.favDrink"
               {{user.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="user.favDrink"
           {{user.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 model would contain a "user" root element with a "favDrink" element under it.

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

<label for="drinkTea">
    <input type="radio" 
           id="drinkTea" 
           name="user.favDrink"
           {{user.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 "user.favDrink" element. That's why we use "checked(...)". This filter will compare the current value of the "user.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="user.favDrink"
           {{user.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) {

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

    // Creates the available drink options
    JsonArray allDrinks = context.json().createArray();

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

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

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

    // Creates a "user" object and specifies 
    // his favorite drink
    JsonObject user = context.json().create();
    user.put("favDrink", 2);

    // Adds the drinks options and the user object 
    // to the response model
    model.put("allDrinks", allDrinks);
    model.put("user", user);

    // Renders an HTML template containing the form
    // to display
    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="user.favDrink"
                   {{user.favDrink | checked(drink.id)}}
                   value="{{drink.id}}"/> {{drink.name}}</label> 
    {% endfor %}
</div>

You may notice that, in this example, we haven't scoped the elements of the form under a dedicated "userForm" parent element as we did in other examples. We added the "allDrinks" and "user" objects as root elements (instead of naming them "userForm.allDrinks" and "userForm.user"). You don't have to scope the elements of a form inside a parent element when you are working with a simple template... But as soon as your template is somewhat complex, for example if it contains more than one form, then scoping elements is a good idea.

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) {

    JsonObject model = context.request().getFormData();
    
    boolean tosAccepted = model.getBoolean("myForm.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="user.favDrinks[0]"
               {{user.favDrinks[0] | checked("tea")}}
               value="tea"/> Tea</label>
    
    <label for="drinkCoffee">
        <input type="checkbox" 
               id="drinkCoffee" 
               name="user.favDrinks[1]"
               {{user.favDrinks[1] | checked("coffee")}}
               value="coffee"> Coffee</label>
    
    <label for="drinkBeer">
        <input type="checkbox" 
               id="drinkBeer" 
               name="user.favDrinks[2]"
               {{user.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. In fact, 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 Route Handler :

public void myRouteHandler(AppRequestContext context) {

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

Note that the positions used in the "name" 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 Route handler would be [null, null, "beer"], not ["beer"]! This is a good thing because the JsonPath we use for an element always stays valid ("user.favDrinks[2]" here).

File upload

Uploading a file is very easy using Spincast. The main difference between a "file" field and the other types of fields is that the uploaded file will not be available on the model when the form is submitted. You'll have to use a dedicated method to retrieve it in your Route handler.

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 add Validation Messages for it, if the file is not valid.

Form validation introduction

Validating a submitted form involves three main steps :

  • Getting the submitted form data.
  • Using everything explained in the Validation section to validate the form model.
  • Redisplaying the form with the Validation Messages resulting from the validation, if any. If the form is valid, you may instead want to redirect the user to a confirmation page where a Flash Message will be displayed.

Make sure you read the dedicated Validation section! Here, we are going to take for granted that you already know how to create a Validation Set from a JsonObject and how to validate that JsonObject.

Getting the submitted form data

When a HTML form is submitted, Spincast threats the "name" attributes of the fields as JsonPaths in order to create a JsonObject representing the form model. In other words, Spincast converts the submitted form to a JsonObject so you can easily validate and manipulate it.

You access that JsonObject representing the submitted form using the getFormData() method of the request() add-on. For example :

public void myRouteHandler(AppRequestContext context) {

    JsonObject myForm = context.request().getFormData();
    
    //... validates the form
}

If you have more than one form on the same HTML page, you will probably want to scope the names of every field. For example, a text field of a form named "myFirstForm" could be :

<input type="text" 
       class="form-control" 
       name="myFirstForm.email"
       value="{{myFirstForm.email | default('')}}" />

And you can then retrieve the JsonObject representing this particular "myFirstForm" form model using :

public void myRouteHandler(AppRequestContext context) {

    // Gets the "myFirstForm" form model
    JsonObject myFirstForm = context.request()
                                    .getFormData()
                                    .getJsonObject("myFirstForm");
                               
    // Gets the value of the "email" field from 
    // the "myFirstForm" form model
    String email = myFirstForm.getString("email");
}

Instead of "extracting" the form model first, you can also select the value of a field directly from the root of the form data :

public void myRouteHandler(AppRequestContext context) {

    String email = context.request().getFormData().getString("myFirstForm.email");
}

Spincast supports arrays to group a bunch of fields together. When multiple fields have the same "name" attribute, they are automatically grouped together when a form is submitted. For example :

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

When submitted, this form would result in the two "user.favDrinks" fields to be grouped in a single array. We could access this array with :

public void myRouteHandler(AppRequestContext context) {

    JsonArray tags = context.request().getFormData().getJsonArray("user.favDrinks");
    
    //...
}

There are two problems with this approach though :
  • If only one of those "user.favDrinks" field is submitted, Spincast has no way of knowing it should be part of an array, so it won't create one. In other words, your code must be prepare to receive those "favDrinks" in an array or as a single element. Not cool!
  • When a form is submitted, the order in which fields are received is not garanteed by the HTTP specification. This means that you can potentially receive those "favDrinks" elements in a different order, from requests to requests. Not cool either!

The first improvement we can make, is to make sure we always receive an array, never a single element alone! To achieve that, we can use a "name" attribute ending with "[]". For example :

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

This guarantees that an array will always be created to group the "user.favDrinks[]" fields, even if only one of those fields is actually submitted. But this approach still doesn't guarantee the order of the elements in the resulting array, when multiple fields are submitted!

The third and best method, is to suffix the "names" attribute not only with "[]", but with "[X]", where "X" is the position of the element in the group! For example :

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

Now, the order is also guaranteed : the "user.favDrinks[0]" field will always be at the first position of the resulting array and "user.favDrinks[1]" always be at the second position. Note that since the positions are now guaranteed, Spincast will automatically adds null elements if some indexes are not associated with a submitted field.

In this third solution, notice that the "name" attribute is the actual JsonPath of the associated element. We like that!

Validating the form

Once you we the JsonObject representing the model of the submitted form, we can start validating it. To do so, we create a Validation Set from this object. For example :

public void myRouteHandler(AppRequestContext context) {

    // Gets the form model
    JsonObject myForm = context.request().getFormData().getJsonObject("myForm");
    
    // Gets a Validation Set for the form model
    JsonObjectValidationSet formValidationSet = myForm.validationSet();
    // ...
}

We are going to use this Validation Set object to store the Validation Messages resulting from the validation we're about to perform.

Here's a quick example where we make sure that an "email" field is valid and, if it is, that the email is not already used by another user in our system :

public void myRouteHandler(AppRequestContext context) {

    JsonObject myForm = context.request().getFormData().getJsonObject("myForm");
    JsonObjectValidationSet formValidationSet = myForm.validationSet();

    ValidationSet lastResult = formValidationSet.validationEmail().jsonPath("email").validate();
    if(lastResult.isValid()) {

        boolean emailIsAvailable = isEmailAvailable(myForm.getString("email"));
        if(!emailIsAvailable) {
            formValidationSet.addError("email",
                                       "EMAIL_ALREADY_EXISTS",
                                       "This email is already used.");
        }
    }
    // ...
}

Explanation :

  • 6 : Using the validationEmail() predefined validation, we validate the submitted email.
  • 7 : Only if the last validation doesn't generate any error (so only if the email is valid) do we perform the next validation...
  • 9 : Here we use some custom code to validate in our system if the specified email is available or not.
  • 10-14 : If the email is not available, we manually add an Error Validation Message to our Validation Set, using the addError(...) method.

When we are done validating a form, we usually have two choices :

  • If the form is valid, we can process it. This often involves creating Entities objects from the form data, calling business logic in services and making modifications to data sources.

    When the submitted (and valid) form has been processed, it's a good practice to redirect the user to a confirmation page, with a Flash message to be displayed. Doing do, you prevent the form to be submitted again, in case the user tries to refresh the resulting page.

  • If the form is not valid (i.e. if some Error Validation Messages have been generated by the validation), then we need to redisplay the form to the user with the Validation Messages. We're going to see how to display those messages in the following Displaying validation messages section but, for now, let's see what first needs to be done in the Route Handler...

In order to prepare a form to be redisplayed with the Validation Messages resulting from its validation, we need to add the form model and the resulting Validation Set back to the response model! This is required so the Templating Engine can redisplay the fields with their submitted (but potentially invalid values) and the various Validation Messages. For example :

public void myRouteHandler(AppRequestContext context) {

    JsonObject myForm = context.request().getFormData().getJsonObject("myForm");
    JsonObjectValidationSet formValidationSet = myForm.validationSet();
    
    // ... Perfoms validation and saves Validation Messages
    // on the Validation Set
    
    if(formValidationSet.isValid()) {
        
        // ... Calls services / business logic / data sources
        // to process the submitted data
        
        // Redirects to a confirmation page
        context.response().redirect(FlashMessageLevel.SUCCESS,
                "The form has been processed successfully.");
    } else {
        
        // Adds the form model back to the response model
        context.response().getModel().put("myForm", myForm);
        
        // Adds the Validation Set to the response model
        // using a key of our choice
        context.response().getModel().put("validation", formValidationSet);
        
        // Redisplays the form using the Templating Engine
        context.response().sendTemplateHtml("/templates/myTemplate.html");
    }
}

Explanation :

  • 6-7 : Performs the validations on the form.
  • 9 : We check if the form is valid.
  • 11-12 : Since the form is valid we can process it! This part is specific to each application.
  • 15-16 : We redirect the user to a confirmation page with a Flash message to be displayed.
  • 20 : The form is invalid so we need to redisplay it with the Validation Messages... So we add it back to the response model.
  • 24 : We add the Validation Set to the response model too.
  • 27 : We redisplay our form using an HTML template.

In this example, we added the Validation Set to the response model using a "validation" key. This would result in the Validation Messages to be accessible in the template using validation['some.validation.key']. For example :


<div>{{validation['myForm.email']}}</div>

Now, everything required by our Templating Engine to be able to redisplay the form with the Validation Messages has been made available.

Displaying Validation Messages

At the end of the previous section, we showed that two main elements have to be added to the response model in order to redisplay an invalid form :

  • The submitted form model itself
  • The Validation Set resulting from the validation
If you followed the "Use the JsonPath of a validated element as its validation key" convention as suggested in the Validation Keys section, the final model your templates will have access to will look like this :

{
    "myForm" : {
        "name" : "Stromgol"
        "email" : "abc"
        "books": [
            {
                "title" : "Dune",
                "author": "Frank Herbert"
            },
            {
                "title" : "The Hitchhiker's Guide to the Galaxy",
                "author" : ""
            }
        ]
    },
    
    "validation" : {
        "_" : {
            "hasErrors" : true,
            "hasWarnings" : false,
            "isValid" : false,
            "hasSuccesses" : false
        },
        "myForm.email" : {
            "level" : "ERROR",
            "code" : "VALIDATION_TYPE_EMAIL",
            "text" : "Invalid email address" 
        },
        "myForm.books[1].author" : {
            "level" : "ERROR",
            "code" : "VALIDATION_TYPE_NOT_BLANK",
            "text" : "The author can't be empty" 
        }
    }
    
    // ...
}

  • At the top is the serialized form model : notice that the JsonPath of every element is respected.
  • Below is the serialized Validation Set resulting from the validation. Here, the validation keys have been scoped in a "validation" object so they don't clash with any other element on the model.

    Notice that each validation key is the string representation of the JsonPath of the element it is associated with!

    You can also see the special "_" element that is automatically added when a Validation Set is converted to a JsonObject. This element summarizes the validation.

  • In real life, there are always more elements than this in a template model, for example the Default templating variables... We didn't list them here for the sake of simplicity.

This structure of the templating model makes the Validation Messages very easy to be retrieved and displayed. For example :

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

Here, validationMessages() is a Pebble filter provided by Spincast that can be used to display the Validation Messages associated with a field. Since our validation keys are in fact the string representation of the JsonPaths of the validated elements, it's very clear how to retrieve the Validation Messages for a particular field : it is, in most cases, the same as the "name" attribute of that field [4].

Validation Filters

Spincast provides utilities to display the Validation Messages when the default Templating Engine is used, Pebble. But, as we saw, the template model is pretty much a simple Map<String, Object> so no magic is involved and any other Templating Engine could 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 a field.

    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 fields.

    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. In the Validation section, you can learn that Spincast can automatically add such Validation Messages for an array itself, when the validation of its elements is done.

    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 (more info), 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 %}
    

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).save(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().save(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").save();

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").save(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 router.before(...)) will be applied during a WebSocket Route processing, but not the global "after" Filters (defined using router.after(...)).

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 save(...) 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)
      .save(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) {

    Cookie sessionIdCookie = context.cookies().getCookie("sessionId");
    if(sessionIdCookie == null || !canUserAccessWebsocketEndpoint(sessionIdCookie.getValue())) {
        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().put(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().put(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.
  • 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();
    //....
}

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.31</version>
    <scope>test</scope>
</dependency>

Then, make your test classes extend SpincastTestBase or one of its children classes.

Demo

In this demo, we're going to test a simple application which only has one route : "/sum". The Route Handler associated with this Route 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 by 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();
    }

    @Inject
    protected void init(DefaultRouter router,
                        AppController ctrl,
                        Server server) {

        router.POST("/sum").save(ctrl::sumRoute);
        server.start();
    }
}

The interesting lines to note here are 4-6 : we use the bootstrapper to start everything! We'll see that, without touching this bootstrapping, we'll still be able to modify the Guice context, to mock some components.

Let's write a first test class :


public class SumTest extends IntegrationTestAppDefaultContextsBase {

    @Override
    protected void initApp() {
        App.main(null);
    }

    @Inject
    private JsonManager jsonManager;

    @Test
    public void validRequest() throws Exception {
        // TODO...
    }
}

Explanation :

  • 2 : Our test class extends IntegrationTestAppDefaultContextsBase. 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 initApp() method. In this method we have to start the application to test. This is easily done by calling its main(...) method.
  • 9-10 : 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.
  • 12-15 : a first test to implement.

As you can see, simply by extending IntegrationTestAppDefaultContextsBase, 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.

Let's implement that first test. We're going to validate that the "/sum" endpoint of our application works properly :

@Test
public void validRequest() throws Exception {

    HttpResponse response = POST("/sum").addEntityFormDataValue("first", "1")
                                        .addEntityFormDataValue("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 :

  • 4-7 : 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.
  • 9-11 : we validate that the response is a success ("200") and that the content-type is the expected "application/json".
  • 13-14 : we get the content of the response as a String and we validate that it is not null.
  • 16-17 : we use the JsonManager (injected previously) to convert the content to a JsonObject.
  • 19-20 : 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 IntegrationTestAppDefaultContextsBase {

    @Override
    protected void initApp() {
        App.main(null);
    }

    public static class AppControllerTesting extends AppControllerDefault {

        @Override
        public void sumRoute(DefaultRequestContext context) {
            context.response().sendPlainText("42");
        }
    }

    @Override
    protected SpincastPluginThreadLocal createGuiceTweaker() {

        SpincastPluginThreadLocal guiceTweaker = super.createGuiceTweaker();

        guiceTweaker.module(new SpincastGuiceModuleBase() {

            @Override
            protected void configure() {
                bind(AppController.class).to(AppControllerTesting.class).in(Scopes.SINGLETON);
            }
        });

        return guiceTweaker;
    }

    @Test
    public void isGzipped() throws Exception {
        // TODO...
    }
}

Explanation :

  • 2 : this test class also extends IntegrationTestAppDefaultContextsBase.
  • 4-7 : we start our application.
  • 9-15 : we create a mock controller by extending the original one and replacing the sumRoute(...) Route Handler so it always returns "42".
  • 17-31 : we override the createGuiceTweaker() method to add a custom Guice module to the Guice Tweaker . As we will see in the next section, the Guice Tweaker allows us to modify the Guice context of our application, without touching its code directly. Here, we change the AppController binding so it uses our mock controller implementation instead of the default one.

And let's write the test itself :

@Test
public void isGzipped() throws Exception {
 
    HttpResponse response = POST("/sum").addEntityFormDataValue("toto", "titi")
                                        .addJsonAcceptHeader()
                                        .send();

    assertTrue(response.isGzipped());

    assertEquals(HttpStatus.SC_OK, response.getStatus());
    assertEquals(ContentTypeDefaults.TEXT.getMainVariationWithUtf8Charset(),
                 response.getContentType());
    assertEquals("42", response.getContentAsString());
        
}

Explanation :

  • 4-6 : We can send pretty much anything here as the parameters since the controller is mocked : they won't be validated.
  • 8 : We validate that the response was gzipped.
  • 10-13 : 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 configuring the GuiceTweaker.

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. To configure it, you override the createGuiceTweaker() method and modify it as you need. You can see an example of this in the previous demo.

By default, the Guice Tweaker automatically modifies the SpincastConfig binding of the application. 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 can be changed by overriding the getSpincastConfigTestingImplementation() method. The Guice tweaker will use this implementation for the binding. The default implementation is SpincastConfigTestingDefault. You can disable that automatic configurations tweaking by overriding the isEnableGuiceTweakerTestingConfigMecanism() method and making it return false.

For integration testing, when a test class extends IntegrationTestBase or one of its children, the Spincast HTTP Client with WebSockets plugin is also registered automatically by the Guice Tweaker. The features provided by this plugin are used intensively to perform requests.

Finally, the Guice Tweaker provides three main methods to help tweak the Guice context of your application :

  • plugin(...) : to apply an extra plugin.
  • pluginToIgnore(...) : to ignore a plugin which would be applied otherwise.
  • module(...) : to install an extra Guice module.

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 made for integration testing and those made for unit testing. We use the expression "integration testing" when the HTTP Server is started to run the tests and "unit testing" otherwise.

Integration testing base classes :

  • IntegrationTestAppBase : base class to use to test an application that you are going to start using its main(...) method. This class needs to be parameterized with the Request Context and WebSocket Context types to use.
  • IntegrationTestAppDefaultContextsBase : base class similar to the previous one, but to use if your application uses the default Request Context and WebSocket Context types. There is no need to parameterize this class.
  • IntegrationTestNoAppBase : base class to use when you are not starting an application using its main(...) method. In that case, the base class will start the HTTP Server by itself. Also, all routes are going to be cleared before each test. This class needs to be parameterized with the Request Context and WebSocket Context types to use.
  • IntegrationTestNoAppDefaultContextsBase : base class similar to the previous one, but if your application uses the default Request Context and WebSocket Context types. There is no need to parameterize this class.
  • WebsocketIntegrationTestNoAppBase : base class made for test classes involving WebSocket requests. This class needs to be parameterized with the Request Context and WebSocket Context types to use.
  • WebsocketIntegrationTestNoAppDefaultContextsBase : base class similar to the previous one, but to use if your application uses the default Request Context and WebSocket Context types. There is no need to parameterize this class.

Unit testing base classes :

  • UnitTestBase : base class to use when the HTTP server doesn't need to be started. A default Guice context is still created and you can easily tweak it (for example by using the getExtraOverridingModule() method). Using this class requires you to specify the Request Context and WebSocket Context types to use.
  • UnitTestDefaultContextsBase : base class similar to the previous one, but if your components use the default Request Context and WebSocket Context types.

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 occurs in the beforeClass() method, the process stops and the tests are not run.
  • The tests are then run. Note that since the @FixMethodOrder(MethodSorters.NAME_ASCENDING) annotation is used on the SpincastTestBase parent class, the tests will be sorted alphanumerically before they are run. Without this annotation, JUnit doesn't guarantee the order in which your tests are 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!

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)
            .mainArgs(args)
            .init();
   //...
}

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.

Miscellaneous

Default Configurations

To know what are the configurations required by Spincast, have a look at the SpincastConfig javadoc. Here, we're simply going to introduce the most important ones :

  • 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.
  • isDebugEnabled() : 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!
  • getPublicServerSchemeHostPort() : This configuration is quite 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 getPublicServerSchemeHostPort() configuration will be "http://localhost:44419". This default can be used for development purposes, but must 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".
    • isDebugEnabled() 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.

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 mainArgs(...) 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())
            .mainArgs(args)
            .init();
    //....
}

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()!