jte is a simple, yet powerful templating engine for Java. All jte templates are compiled to Java class files, meaning jte adds essentially zero overhead to your application. All template files use the .jte file extension.
- Rendering a template
- Displaying data
- Control structures
- Comments
- Tags
- Layouts
- Content
- Variables
- HTML Rendering
- HTML Escaping
- Hot Reloading
- Precompiling Templates
- Binary rendering for max throughput
To render any template, an instance of TemplateEngine
is required. Typically you create it once per application (it is safe to share the engine between threads):
CodeResolver codeResolver = new DirectoryCodeResolver(Path.of("jte")); // This is the directory where your .jte files are located.
TemplateEngine templateEngine = TemplateEngine.create(codeResolver, ContentType.Html);
With the TemplateEngine ready, templates are rendered like this:
TemplateOutput output = new StringOutput();
templateEngine.render("example.jte", model, output);
Where output
specifies where the template is rendered to and model
is the data passed to this template, which can be an instance of any class. Ideally, root templates have exactly one data parameter passed to them. If multiple parameters are required, there is a render
method overload that takes a Map<String, Object>
.
Besides
StringOutput
, there are several otherTemplateOutput
implementations you can use, or create your own if required. Currently the following implementations are available:
StringOutput
- writes to a stringFileOutput
- writes to the given filePrintWriterOutput
- writes to aPrintWriter
, for instance the writer provided byHttpServletRequest
.
A minimal template would look like this.
Hello world!
Rendering it with templateEngine.render("example.jte", null, output);
will return Hello world!
.
Data passed to the template can be displayed by wrapping it in ${}
.
@import my.Model
@param Model model
Hello ${model.name}!
If your model class would look like this:
package my;
public class Model {
public String name = "jte";
}
The output of the above template would be Hello jte!
.
jte provides convenient shortcuts for common Java control structures, such as conditional statements and loops. These shortcuts provide a very clean, terse way of working with control structures, while also remaining familiar to their Java counterparts.
You may construct if statements using the @if
, @elseif
, @else
and @endif
keywords. These translate directly to their Java counterparts:
@if(model.entries.isEmpty())
I have no entries!
@elseif(model.entries.size() == 1)
I have one entry!
@else
I have ${model.entries.size()} entries!
@endif
In addition to if statements, jte provides the @for
and @endfor
keywords to loop over iterable data. Again, for
translates directly to its Java counterpart:
@for(Entry entry : model.entries)
<li>${entry.title}</li>
@endfor
@for(var entry : model.entries)
<li>${entry.title}</li>
@endfor
@for(int i = 0; i < 10; ++i)
<li>i is ${i}</li>
@endfor
When looping, you may use the ForSupport
class to gain information about the loop, such as whether you are in the first or last iteration through the loop.
@import gg.jte.support.ForSupport
<%-- ... --%>
@for(var entryLoop : ForSupport.of(model.entries))
<tr class="${(entryLoop.getIndex() + 1) % 2 == 0 ? "even" : "odd"}">
${entryLoop.get().title}
</tr>
@endfor
jte allows you to define comments in your templates. jte comments are not included in the output of your template:
<%-- This comment will not be present in the rendered output --%>
To share common functionality between templates, you can extract it into tags. All tags must be located within the tag
directory in the jte root directory.
Here is an example tag, located in tag/drawEntry.jte
@import my.Entry
@param Entry entry
@param boolean verbose
<h2>${entry.title}</h2>
@if(verbose)
<h3>${entry.subtitle}</h3>
@endif
Tags can be called like regular Java methods.
@tag.drawEntry(model.entry1, true)
@tag.drawEntry(model.entry2, false)
Subdirectories in the tag
directory act like packages in java. For instance, if the drawEntry tag was located in tag/entry/drawEntry.jte
, you would call it like this:
@tag.entry.drawEntry(model.entry1, true)
@tag.entry.drawEntry(model.entry2, false)
If you don't want to depend on the parameter order, you can explicitly name parameters when calling the template (this is what the IntelliJ plugin suggests by default).
@tag.entry.drawEntry(entry = model.entry1, verbose = true)
@tag.entry.drawEntry(entry = model.entry2, verbose = false)
You can also define default values for all parameters, so that they only need to be passed when needed.
@import my.Entry
@param Entry entry
@param boolean verbose = false
<h2>${entry.title}</h2>
@if(verbose)
<h3>${entry.subtitle}</h3>
@endif
The second call could then be simplified to this:
@tag.entry.drawEntry(entry = model.entry1, verbose = true)
@tag.entry.drawEntry(entry = model.entry2)
The last parameter of a tag can be a varargs parameter. For instance, if you created a tag to wrap elements in a list you could create something like tag/list.jte
:
@param String title
@param String ... elements
<h2>${title}</h2>
<ul>
@for(var element : elements)
<li>${element}</li>
@endfor
</ul>
And call it like this:
@tag.list(title = "Things to do", "Cook dinner", "Eat", "Netflix and Chill")
Layouts have the same features as tags. All layouts must be located within the layout
directory in the jte root directory.
You can use layouts to better distinguish page layouts from regular tags, but you don't have to.
Layouts usually contain of one or several content blocks.
gg.jte.Content
is a special parameter type to pass template code to tags, much like lambdas in Java. They are particularly useful to share structure between different templates.
Here is an example layout with a content block:
@import org.example.Page
@import gg.jte.Content
@param Page page
@param Content content
@param Content footer = null
<head>
@if(page.getDescription() != null)
<meta name="description" content="${page.getDescription()}">
@endif
<title>${page.getTitle()}</title>
</head>
<body>
<h1>${page.getTitle()}</h1>
<div class="content">
${content}
</div>
@if (footer != null)
<div class="footer">
${footer}
</div>
@endif
</body>
The shorthand to create content blocks within jte templates is an @
followed by two backticks. Let's call the layout we just created and pass a a page content and footer:
@import org.example.WelcomePage
@param WelcomePage welcomePage
@layout.page(
page = welcomePage,
content = @`
<p>Welcome, ${welcomePage.getUserName()}.</p>
`,
footer = @`
<p>Thanks for visiting, come again soon!</p>
`
)
Local variables can be declared like this:
!{var innerObject = someObject.get().very().deeply().located().internal().object();}
${innerObject.a()}
${innerObject.b()}
Local variables translate 1:1 to Java code.
For rendering HTML documents, ContentType.Html
is highly recommended for security but also for convenience.
Expressions in HTML attributes are evaluated, so that optimal output is generated. This means attributes with a single output that evaluates to an empty string, null, or false, are not rendered. For instance:
<span data-title="${null}">Info</span>
Will be rendered as:
<span>Info</span>
If an HTML attribute is boolean, jte requires you to provide a boolean expression and it will omit the attribute if that expression evaluates to false
. For example:
<select id="cars">
<option value="volvo" selected="${false}">Volvo</option>
<option value="saab" selected="${true}">Saab</option>
<option value="opel" selected="${false}">Opel</option>
<option value="audi" selected="${false}">Audi</option>
</select>
Will render this HTML:
<select id="cars">
<option value="volvo">Volvo</option>
<option value="saab" selected>Saab</option>
<option value="opel">Opel</option>
<option value="audi">Audi</option>
</select>
All HTML, CSS and JavaScript comments are not rendered. You can use the natural comment syntax without worrying to leak too much information/data to the outside.
Output escaping depends on the ContentType
the engine is created with:
- With
ContentType.Plain
there is no output escaping. - With
ContentType.Html
, the OwaspHtmlTemplateOutput is used for context sensitive output escaping.
In Html
mode, user content ${}
is automatically escaped, depending what part of the template it is placed into:
- HTML tag bodies
- HMTL attributes
- JavaScript attributes, e.g.
onclick
<script>
blocks
User output in HTML tag bodies is escaped with org.owasp.encoder.Encode#forHtmlContent
(org.owasp.encoder.Encode#forHtml
before jte 1.5.0).
<div>${userName}</div>
With userName
being <script>alert('xss');</script>
,
the output would be <div><script>alert('xss');</script></div>
.
User output in HTML attributes is escaped with org.owasp.encoder.Encode#forHtmlAttribute
. It ensures that all quotes are escaped, so that an attacker cannot escape the attribute.
<div data-title="Hello ${userName}"></div>
With userName
being "><script>alert('xss')</script>
,
the output would be <div data-title="Hello "><script>alert('xss')</script>"></div>
. The quote "
is escaped with "
and the attacker cannot escape the attribute.
User output in HTML attributes is escaped with org.owasp.encoder.Encode#forJavaScriptAttribute
. Those are all HTML attributes starting with on
.
<span onclick="showName('${userName}')">Click me</span>
With userName
being '); alert('xss
,
the output would be <span onclick="showName('\x27); alert(\x27xss')">Click me</span>
.
In case you run a strict content security policy without unsafe-inline
, you could configure jte to run with gg.jte.html.policy.PreventInlineEventHandlers
. This would cause errors at compile time, if inline event handlers are used. See this issue for additional context.
public class MyHtmlPolicy extends OwaspHtmlPolicy {
public MyHtmlPolicy() {
addPolicy(new PreventInlineEventHandlers());
}
}
Then, you set it with templateEngine.setHtmlPolicy(new MyHtmlPolicy());
.
For more examples, you may want to check out the TemplateEngine_HtmlOutputEscapingTest.
In rare cases you may want to skip output escaping for a certain element. You can do this by using $unsafe{}
instead of ${}
. For instance, to trust the user name, you would write:
<div>$unsafe{userName}</div>
The syntax $unsafe{}
was picked on purpose. Whenever you use it, you're risking XSS attacks and you should carefully consider if it really is okay to trust the data you're outputting.
It is possible to provide your own implementation of HtmlTemplateOutput
. Maybe you want to extend the default OwaspHtmlTemplateOutput, or use your own implementation.
Before rendering, you'd simply wrap the actual TemplateOutput
you are using:
TemplateOutput output = new MySecureHtmlOutput(new StringOutput());
When using the DirectoryCodeResolver
, hot reloading is supported out of the box. Before a template is resolved, the modification timestamp of the template file and all of its dependencies is checked. If there is any modification detected, the template is recompiled and the old one discarded to GC.
It makes sense to do this on your local development environment only. When running in production, for maximum performance and security precompiled templates are recommended instead.
If you clone this repository, you can launch the SimpleWebServer example's main method. It will fire up a tiny webserver with one page to play with at http://localhost:8080.
In case you're using jte to pre-render static websites as HTML files, you can also listen to template file changes during development and re-render affected static files. Add the jte-watcher module to your project:
<dependency>
<groupId>gg.jte</groupId>
<artifactId>jte-watcher</artifactId>
<version>${jte.version}</version>
</dependency>
DirectoryWatcher::start()
starts a daemon thread listening to file changes within the jte template directory. Once file changes are detected, a listener is called with a list of changed templates.
if (isDeveloperEnvironment()) {
DirectoryWatcher watcher = new DirectoryWatcher(templateEngine, codeResolver);
watcher.start(templates -> {
for (String template : templates) {
// Re-render the static HTML file
}
});
}
By default generated sources and classes are outputted into the subdirectory jte-classes
under the current directory.
It is possible to customize this directory when creating template engine. But in order to have the hot reload feature
working, a custom class directory must not be on the classpath. If it is on the classpath, generated classes
will be visible to the default class loader and once a generated class is loaded, it will not be possible to reload it
after recompiling a template, thus making the hot reload effectively non-functional.
To speed up startup and rendering your production server, it is possible to precompile all templates during the build. This way, the template engine can load the .class file for each template directly, without first compiling it. For security reasons you may not want to run a JDK on production - with precompiled templates this is not needed. The recommended way to setup jte, is to instantiate the engine differently, depending on when you are developing or running on a server.
if (isDeveloperMachine()) {
// create template engine with hot reloading (a bit slower)
} else {
// create template engine with precompiled templates (fastest)
}
To do this, you need to create a TemplateEngine
with the createPrecompiled
factory method and specify where compiled template classes are located. Currently there are two options available to do this.
When using this method you need to deploy the precompiled templates to your server.
Path targetDirectory = Path.of("jte-classes"); // This is the directoy where compiled templates are located.
TemplateEngine templateEngine = TemplateEngine.createPrecompiled(targetDirectory, ContentType.Html);
There is a Maven plugin you can use to precompile all templates during the Maven build. You would need to put this in build / plugins of your projects' pom.xml
. Please note that paths specified in Java need to match those specified in Maven.
It is recommended to create a variable like
${jte.version}
in Maven, to ensure that the jte maven plugin always matches your jte dependency.
<plugin>
<groupId>gg.jte</groupId>
<artifactId>jte-maven-plugin</artifactId>
<version>${jte.version}</version>
<configuration>
<sourceDirectory>src/main/jte</sourceDirectory> <!-- This is the directory where your .jte files are located. -->
<targetDirectory>jte-classes</targetDirectory> <!-- This is the directoy where compiled templates are located. -->
<contentType>Html</contentType>
</configuration>
<executions>
<execution>
<phase>process-classes</phase>
<goals>
<goal>precompile</goal>
</goals>
</execution>
</executions>
</plugin>
Since 1.6.0 there is a Gradle plugin you can use to precompile all templates during the Gradle build. Please note that paths specified in Java need to match those specified in Gradle.
Make sure that the jte gradle plugin version always matches the jte dependency version.
Groovy
plugins {
id 'java'
id 'gg.jte.gradle' version '${jte.version}'
}
dependencies {
implementation('gg.jte:jte:${jte.version}')
}
jte {
precompile()
}
Kotlin
plugins {
java
id("gg.jte.gradle") version("${jte.version}")
}
dependencies {
implementation("gg.jte:jte:${jte.version}")
}
jte {
precompile()
}
In case you would like to build a self-contained JAR, you can add this to your build.gradle:
from fileTree("jte-classes") {
include "**/*.class"
include "**/*.bin" // Only required if you use binary templates
}
And init the template engine like this for production builds:
TemplateEngine templateEngine = TemplateEngine.createPrecompiled(ContentType.Html);
This way the templates are loaded from the application class loader. See this issue for additional information.
When using this method the precompiled templates are bundled within your application jar file. The plugin generates *.java
files for all jte templates during Maven's GENERATE_SOURCES
phase. Compilation of the templates is left to the Maven Compiler plugin.
While this provides you with a nice self-containing jar, it has some limitations:
- Once the sources are generated, IntelliJ will put them on the classpath and hot reloading will not work unless the generated sources are deleted.
- Some plugin settings are not supported, like configuring a custom HtmlPolicy class (this is because project classes are not yet compiled at the
GENERATE_SOURCES
phase).
TemplateEngine templateEngine = TemplateEngine.createPrecompiled(ContentType.Html);
There is a Maven plugin you can use to generate all templates during the Maven build. You would need to put this in build / plugins of your projects' pom.xml
. Please note that paths specified in Java need to match those specified in Maven.
It is recommended to create a variable like
${jte.version}
in Maven, to ensure that the jte maven plugin always matches your jte dependency.
<plugin>
<groupId>gg.jte</groupId>
<artifactId>jte-maven-plugin</artifactId>
<version>${jte.version}</version>
<configuration>
<sourceDirectory>${basedir}/src/main/jte</sourceDirectory> <!-- This is the directory where your .jte files are located. -->
<contentType>Html</contentType>
</configuration>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
Since 1.6.0 there is a Gradle plugin you can use to generate all templates during the Gradle build. Please note that paths specified in Java need to match those specified in Gradle.
Make sure that the jte gradle plugin version always matches the jte dependency version.
Groovy
plugins {
id 'java'
id 'gg.jte.gradle' version '${jte.version}'
}
dependencies {
implementation('gg.jte:jte:${jte.version}')
}
jte {
generate()
}
Kotlin
plugins {
java
id("gg.jte.gradle") version("${jte.version}")
}
dependencies {
implementation("gg.jte:jte:${jte.version}")
}
jte {
generate()
}
An application jar with generated classes can be built into a native binary using GraalVM native-image. To support this, jte can generate the necessary configuration files to tell native-image about classes loaded by reflection.
To use this feature, set generateNativeImageResources = true
in your Gradle jte
block. (Docs for Maven TBD)
There's an example gradle test project using native-image compilation.
Most template parts are static content and only few parts of a template are dynamic. It is wasteful to convert those static parts over and over on every request, if your web-framework sends binary UTF-8 content to the user. Since jte 1.7.0 it is possible to encode those static parts at compile time:
templateEngine.setBinaryStaticContent(true);
This generates a binary content resource for each template at compile time. Those pre-encoded UTF-8 byte[] arrays are loaded in memory from the resource file together with the template class. This also implies, that the constant pool is released of holding template strings.
To fully utilize binary templates you need to use a binary template output, like Utf8ByteOutput
. This output is heavily optimized to consume as little CPU and memory as possible when using binary templates.
Hint: You will only see a performance increase if you use binaryStaticContent in tandem with a binary output. Other outputs convert the pre-encoded byte[] arrays back to Java Strings and defeat this optimization.
Example usage with HttpServletResponse
:
Utf8ByteOutput output = new Utf8ByteOutput();
templateEngine.render(template, page, output);
response.setContentLength(output.getContentLength());
response.setContentType("text/html");
response.setCharacterEncoding("UTF-8");
response.setStatus(page.getStatusCode());
try (OutputStream os = response.getOutputStream()) {
output.writeTo(os);
}
There are a few pretty cool things going on here:
- We know about the binary content-length directly after rendering, at no additional cost
- All static parts are streamed directly to the output stream, without any copying / encoding overhead
- Dynamic parts are usually small - and written very efficiently to internal chunks during rendering
With binary content you will be able to render millions of pages per second (in case there's no DB or other external service interaction, heh) - with very little CPU, memory and GC usage.