Dropwizard Views

The dropwizard-views-mustache & dropwizard-views-freemarker modules provide you with simple, fast HTML views using either FreeMarker or Mustache.

To enable views for your Application, add the ViewBundle in the initialize method of your Application class:

bootstrap.addBundle(new ViewBundle<>());

You can pass configuration through to view renderers by overriding getViewConfiguration:

bootstrap.addBundle(new ViewBundle<ViewsConfiguration>() {
    @Override
    public Map<String, Map<String, String>> getViewConfiguration(ViewsConfiguration config) {
        return config.getViewRendererConfiguration();
    }
});

The returned map should have, for each renderer (such as freemarker or mustache), a Map<String, String> describing how to configure the renderer. Specific keys and their meanings can be found in the FreeMarker and Mustache documentation:

---
# views: config->mustache
views:
  mustache:
    cache: false
    # views: config->mustache
  freemarker:
    strict_syntax: true

Then, in your resource method, add a View class:

package io.dropwizard.documentation;

import io.dropwizard.views.common.View;

public class PersonView extends View {
    private final Person person;

    public PersonView(Person person) {
        super("person.ftl");
        this.person = person;
    }

    public Person getPerson() {
        return person;
    }
}

person.ftl is the path of the template relative to the class name. If this class was com.example.service.PersonView, Dropwizard would then look for the file src/main/resources/com/example/service/person.ftl.

If your template path contains .ftl, .ftlh, or .ftlx, it’ll be interpreted as a FreeMarker template. If it contains .mustache, it’ll be interpreted as a Mustache template.

Tip

Dropwizard Freemarker Views also support localized template files. It picks up the client’s locale from their Accept-Language, so you can add a French template in person_fr.ftl or a Canadian template in person_en_CA.ftl.

Your template file might look something like this:

<#-- @ftlvariable name="" type="io.dropwizard.documentation.PersonView" -->
<html lang="en">
<body>
<!-- calls getPerson().getName() and sanitizes it -->
<h1>Hello, ${person.name?html}!</h1>
</body>
</html>

The @ftlvariable lets FreeMarker (and any FreeMarker IDE plugins you may be using) know that the root object is a com.example.views.PersonView instance. If you attempt to call a property which doesn’t exist on PersonViewgetConnectionPool(), for example – it will flag that line in your IDE.

Once you have your view and template, you can simply return an instance of your View subclass:

package io.dropwizard.documentation;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/people/{id}")
@Produces(MediaType.TEXT_HTML)
public class PersonResource {
    private final PersonDAO dao;

    public PersonResource(PersonDAO dao) {
        this.dao = dao;
    }

    @GET
    public PersonView getPerson(@PathParam("id") String id) {
        return new PersonView(dao.find(id));
    }
}

Tip

Jackson can also serialize your views, allowing you to serve both text/html and application/json with a single representation class.

For more information on how to use FreeMarker, see the FreeMarker documentation.

For more information on how to use Mustache, see the Mustache and Mustache.java documentation.

Template Errors

By default, if there is an error with the template (eg. the template file is not found or there is a compilation error with the template), the user will receive a 500 Internal Server Error with a generic HTML message. The exact error will logged under error mode.

To customize the behavior, create an exception mapper that will override the default one by looking for ViewRenderException:

environment.jersey().register(new ExtendedExceptionMapper<WebApplicationException>() {
    @Override
    public Response toResponse(WebApplicationException exception) {
        // Return a response here, for example HTTP 500 (Internal Server Error)
        return Response.serverError().build();
    }

    @Override
    public boolean isMappable(WebApplicationException e) {
        return ExceptionUtils.indexOfThrowable(e, ViewRenderException.class) != -1;
    }
});

As an example, to return a 404 instead of a internal server error when one’s mustache templates can’t be found:

environment.jersey().register(new ExtendedExceptionMapper<WebApplicationException>() {
    @Override
    public Response toResponse(WebApplicationException exception) {
        return Response.status(Response.Status.NOT_FOUND).build();
    }

    @Override
    public boolean isMappable(WebApplicationException e) {
        return ExceptionUtils.getRootCause(e).getClass() == MustacheNotFoundException.class;
    }
});

Caching

By default templates are cached to improve loading time. If you want to disable it during the development mode, set the cache property to false in the view configuration.

views:
  mustache:
    cache: false

Custom Error Pages

To get HTML error pages that fit in with your application, you can use a custom error view. Create a View that takes an ErrorMessage parameter in its constructor, and hook it up by registering a instance of ErrorEntityWriter.

environment.jersey().register(new ErrorEntityWriter<ErrorMessage, View>(MediaType.TEXT_HTML_TYPE, View.class) {
    @Override
    protected View getRepresentation(ErrorMessage errorMessage) {
        return new ErrorView(errorMessage);
    }
});

For validation error messages, you’ll need to register another ErrorEntityWriter that handles ValidationErrorMessage objects.

environment.jersey().register(new ErrorEntityWriter<ValidationErrorMessage, View>(MediaType.TEXT_HTML_TYPE, View.class) {
    @Override
    protected View getRepresentation(ValidationErrorMessage message) {
        return new ValidationErrorView(message);
    }
});