Templates
WebResponse.template(name, model) answers with a rendered page.
The name carries no extension: the engine appends its own.
app.get("/decks/{deckId}", req ->
WebResponse.template("deck", Model.of("deck", service.deck(req.pathParamLong("deckId")))));
Nothing has to be configured for that.
An App renders with jte over classpath:/jte and appends .jte, so the handler above renders classpath:/jte/deck.jte.
The model’s entries are passed to jte’s parameters by name, and ContentType.Html escapes ${} output for you.
A Model is immutable, so nothing the handler still holds changes the page.
A model with null values
Model.of(…) builds a template model in one expression and accepts a null value.
Map.of throws NullPointerException on a null value, and a page model holds one routinely: req.flashed(…), req.paramOrNull(…), and req.session().get(…) answer null when there is nothing to show.
app.get("/decks/{deckId}", req -> WebResponse.template("deck", Model.of(
"deck", service.deck(req.pathParamLong("deckId")),
"message", req.flashed("message"))));
Model.of takes up to ten keys and values, as Map.of does, and keeps the order the entries were given.
A null key throws NullPointerException, and a key given twice throws IllegalArgumentException, the same exceptions Map.of throws.
A Model is an immutable value, not a map.
A model that gains entries conditionally grows with with(key, value), which answers a new model:
Model model = Model.of("decks", service.decks());
if (req.session().get("user") instanceof User user) {
model = model.with("user", user);
}
return WebResponse.template("decks", model);
with throws IllegalArgumentException on a key the model already holds.
asMap() answers the entries as a read-only map, null values and order kept, for a renderer that hands them to an engine taking a map.
A root or a suffix of your own
app.templates(new JteTemplates("templates") // classpath:/templates
.suffix(".html")); // .../deck.html
The suffix is appended, never checked for, so a name that still carries its extension is looked up with the suffix twice over.
Another engine
TemplateRenderer is a single method, taking a template name, a Model, and the writer to render into.
Implement it for an engine that is not jte, and pass it to templates(renderer) to take the place of the default.
An engine that takes a map receives model.asMap().
app.templates((template, model, out) -> mustache.compile(template + ".mustache").execute(out, model.asMap()));
Three engines come as modules of their own, with the root, the suffix, and the escaping already settled: FreeMarker, Handlebars, and Thymeleaf.
app.templates(new FreeMarkerTemplates("freemarker")); // classpath:/freemarker/deck.ftlh
app.templates(new HandlebarsTemplates("hbs")); // classpath:/hbs/deck.hbs
app.templates(new ThymeleafTemplates("thymeleaf")); // classpath:/thymeleaf/deck.html
Any other engine plugs in with a one-liner like the Mustache one above.
A module only earns its place when there is something to settle beyond compile(name).render(model).
Rendering happens while the handler’s exception handling still applies.
A template that throws therefore reaches app.exception(…) like any other failure, and a HEAD of a rendered page reports the length the GET would have sent.
The handler returns the template’s name and model, and AppServlet renders them before the response is written:
Development and production
jte compiles templates at runtime by default, reading them from the classpath.
There is nothing to configure, but the deployment machine needs a JDK, and an edited template is not seen until the classpath copy is rebuilt.
The engine has a mode for each side of that trade, and JteTemplates has a constructor for each.
Development resolves templates from the source directory instead of the classpath:
app.templates(new JteTemplates(
new DirectoryCodeResolver(Path.of("src/main/resources/jte"))));
Before each render, jte checks the modification timestamp of the template file and everything it depends on, and recompiles what changed.
Saving a .jte file and refreshing the browser is the whole loop.
Production precompiles every template at build time with the jte Gradle plugin:
plugins {
id 'gg.jte.gradle' version '3.2.4'
}
jte {
sourceDirectory = file('src/main/resources/jte').toPath()
contentType = gg.jte.ContentType.Html
generate()
}
The spider-silk plugin's spiderSilk { jte() } writes this exact block for you, with jte’s native-image resources extension attached besides.
generate() turns each template into a Java class compiled along with the application, and the matching constructor hands the engine those classes:
app.templates(new JteTemplates(TemplateEngine.createPrecompiled(ContentType.Html)));
The jar then renders from compiled classes: no runtime compiler, no JDK on the machine, and a template that does not compile fails the build instead of the request.
The suffix applies either way, so no handler changes.
The example project switches between the two modes with a --dev flag.
A native image has no compiler at runtime at all, so precompiling is the only mode there.
Type-checked template calls with jte-models
The jte extension jte-models generates a Java method per template, so the compiler checks a handler’s call to a template.
A Model is matched to a template’s parameters by name only when the page renders: a misspelled key or a value of the wrong type fails the request, not the build.
The generated method takes the template’s @param declarations as its parameters instead.
The extension is two more lines beside spiderSilk { jte() }, which already applies the jte plugin and calls generate():
jte {
jteExtension('gg.jte.models.generator.ModelExtension')
}
dependencies {
jteGenerate 'gg.jte:jte-models:3.2.4'
implementation 'gg.jte:jte-models:3.2.4'
}
The build then generates the interface gg.jte.generated.precompiled.Templates, with one method per template, and two implementations of it.
StaticTemplates renders the precompiled classes, and DynamicTemplates renders through the TemplateEngine it is given.
Every @param is a parameter of the method, one with a default value included.
The method answers a JteModel, whose render() answers the page as a string, so WebResponse.html sends it:
Templates templates = new StaticTemplates();
// deck.jte declares @param Deck deck and @param String message = null
app.get("/decks/{deckId}", req -> WebResponse.html(
templates.deck(service.deck(req.pathParamLong("deckId")), req.flashed("message")).render()));
Core has no API of its own for jte-models, because html already sends a rendered string.
The call differs from template() in three ways:
-
The page renders inside the handler, so a template that throws reaches
app.exception(…)as any other exception of the handler does. jte wraps the failure in aTemplateException, as it does whenJteTemplatesrenders. -
The page renders before the after-route filters run, so a filter sees a
Textbody holding the page rather than aTemplatebody. -
App.templates(…)is not involved, so the renderer set there and its suffix do not apply.
Development hands DynamicTemplates an engine over the source directory, as the development mode above does:
Templates templates = dev
? new DynamicTemplates(TemplateEngine.create(
new DirectoryCodeResolver(Path.of("src/main/resources/jte")),
Path.of("build/jte-classes"), ContentType.Html))
: new StaticTemplates();
An edited template body shows on the next request.
An edited @param declaration changes the generated Templates interface, so it takes a rebuild.