1. What is Trimou?
Trimou is yet another Mustache implementation written in Java. It’s goal is to provide a simple to use and easy to extend templating engine for any Java SE or Java EE application. So far there are three ready-to-use extensions which provide integration with CDI, Servlets and PrettyTime (see Extensions section for more info).
Note
|
Trimou is available under the Apache License 2.0. |
2. Features
The basic features of every Mustache implementation are defined in the Mustache spec. Trimou passes all the tests (version 1.1.2), except "Section - Alternate Delimiters" from lambdas optional module.
The most notable enhancements are:
-
Template caching
-
Extended processing of lambdas
-
Template inheritance
-
Very basic i18n support
-
Extension points (template locators, resolvers, text support, …)
2.1. Variables, interpolation tags
Spec description: https://github.com/mustache/spec/blob/master/specs/interpolation.yml
Note
|
The interpolation tag’s content is called a key hereafter. The key consists of one or more dot-separated parts ("foo", ".", "foo.bar.name"). The current object the key part is resolved against is called a context object hereafter. |
In general Trimou interpolation works very similar to the spec description - walk the context object stack from top to bottom, and resolve the value for the given part of the key (first part againts the context stack, other parts against the result from the former resolution).
However there are some differences:
-
it’s possible to use this keyword to reference the object atop the context object stack; the spec only allows .,
-
if the context object is an instance of a java.util.Map, value of the entry with the given key is resolved,
-
for any context object Trimou tries to:
-
find and invoke an accesible method (public, with no params and non-void return type) with name key, getKey or isKey defined on the context object’s class and its superclasses (except for java.lang.Object),
-
find an accesible field (public) with name key and get its value,
-
-
java.util.List and array elements can be accessed via index (the key must be unsigned integer),
-
iterIndex and iterHasNext keywords can be used inside iteration block,
-
Trimou allows you to define a resolver that does not handle the context object stack at all (e.g. looks up a CDI bean).
{{foo}} <1> {{foo.bar}} <2> {{list.0}} <3> {{array.5}} <4> {{#items}} {{iterIndex}} <5> {{name}} <6> {{iterHasNext}}, {{/iterHasNext}} <7> {{/items}}
-
Try to get a value of key "foo" from supplied data map
-
If "foo" is an instance of Map, get the value of key "bar"; otherwise try to invoke bar(), getBar() or isBar() on the "foo" instance or get the value of the field with name "bar"
-
Get the first element
-
Get the sixth element
-
The current index of the iteration block
-
"name" is resolved against the context object stack (iteration element, supplied data map)
-
render a comma if the iteration has more elements (iterHasNext is true)
Note
|
The set of resolvers may be extended - so in fact the above mentioned applies to the default set of resolvers only. |
2.1.1. Escaping HTML
The interpolated value is escaped unless & is used. The spec only tests the basic escaping (&, ", <, >). Trimou also escapes all ISO-8859-1 characters by default.
{{foo}} <1> {{& foo}} <2> {{{foo}}} <3>
-
Escape foo
-
Do not escape foo
-
Do not escape foo; works only for default delimiters!
Tip
|
You can implement your own escaping logic, e.g. to improve escaping performance - see Configure the engine and TextSupport sections. |
2.2. Sections
Spec description: https://github.com/mustache/spec/blob/master/specs/sections.yml
The section content is rendered one or more times if there is an object found for the given key. If the found object is:
-
non-empty Iterable or array, the content is rendered for each element,
-
a Boolean of value true, the content is rendered once,
-
an instance of Lambda, the content is processed according to the lambda’s specification,
-
any other non-null object represents a nested context.
The section content is not rendered if there is no object found, or the found object is:
-
a Boolean of value false,
-
an Iterable with no elements,
-
an empty array.
{{#boolean}} This line will be rendered only if "boolean" key resolves to java.lang.Boolean#TRUE, or true {{/boolean}} {{#iterable_or_array}} This line will be rendered for each element, and the element is pushed on the context object stack {{/iterable_or_array}}
2.3. Inverted sections
Spec description: https://github.com/mustache/spec/blob/master/specs/inverted.yml
The content is rendered if there is no object found in the context, or is a Boolean of value false, or is an Iterable with no elements, or is an empty array.
{{#iterable}} This line will be rendered if the resolved iterable has no elements {{/iterable}}
2.4. Partials
Spec description: https://github.com/mustache/spec/blob/master/specs/partials.yml
Partials only work if at least one template locator is in action. Otherwise the template cache is not used and there is no way to locate the required partial (template). See Configure the engine and Template locator sections for more info.
{{#items}} {{>item_detail}} - process the template with name "item_detail" for each iteration element {{/items}}
2.5. Delimiters
Spec description: https://github.com/mustache/spec/blob/master/specs/delimiters.yml
{{=%% %%=}} - from now on use custom delimiters %%foo.name%% - interpolate "foo.name" %%={{ }}=%% - switch back to default values
Tip
|
It’s also possible to change the delimiters globally, see Configuration. |
2.6. Lambdas
Spec description: https://github.com/mustache/spec/blob/master/specs/lambdas.yml
You can implement org.trimou.lambda.Lambda interface in order to define a lambda/callable object. Predefined abstract org.trimou.lambda.SpecCompliantLambda follows the behaviour defined by the spec:
Lambda makeMeBold = new SpecCompliantLambda() {
@Override
public String invoke(String text) {
return "<b>" + text + "</b>";
}
}
and template
{{#makeMeBold}} Any text...{{name}} {{/makeMeBold}}
results in:
<b>Any text...{{name}}</b> -> the variable is not interpolated
However this might be a little bit more useful:
Lambda makeMeUppercase = new InputProcessingLambda() {
@Override
public String invoke(String text) {
return text.toUpperCase();
}
@Override
public boolean isReturnValueInterpolated() {
return false;
}
}
and template
{{#makeMeUppercase}} Any text...{{name}} {{/makeMeUppercase}}
results in:
ANY TEXT...EDGAR -> the variable is interpolated before the lambda invoke() method is invoked
See org.trimou.lambda.Lambda API javadoc for more info.
2.7. Extending templates
This feature is not supported in the spec. Trimou basically follows the way mustache.java implements the template inheritance. In the extended template, the sections to extend are defined - use $ to identify such sections. In extending templates, the extending sections are defined - again, use $ to identify such sections. Sections to extend may define the default content.
Following template with name "super":
This a template to extend {{$header}} -> section to extend The default header {{/header}} In between... {{$content}} -> section to extend The default content {{/content}} © 2013
can be extended in this way:
Hello world! {{<super}} {{$header}} -> extending section My own header {{/header}} Only extending sections are considered... {{/super}} Lalala...
and the result is:
Hello world! This a template to extend <1> My own header <2> In between... The default content <3> © 2013 <4> Lalala...
-
"super start
-
section "header" is extended
-
section "content" has the default content
-
"super" end
3. How to use
3.1. Get started
First, get the trimou-core.jar and its dependencies (guava,slf4j-api and commons-lang3 at the moment).
<dependency>
<groupId>org.trimou</groupId>
<artifactId>trimou-core</artifactId>
<version>${version.trimou}</version>
</dependency>
And now for something completely different…
3.1.1. The simplest possible scenario
MustacheEngine engine = MustacheEngineBuilder.newBuilder().build(); <1>
Mustache mustache = engine.compileMustache("myTemplateName", "{{! empty template}}"); <2>
String output = mustache.render(null); <3>
<1> <2> <3>
String output = MustacheEngineBuilder
.newBuilder()
.build()
.compileMustache("myTemplateName", "{{! empty template}}")
.render(null);
-
Build the engine
-
Compile the template
-
Render the text
3.1.2. Provide your own Appendable
MustacheEngine engine = MustacheEngineBuilder.newBuilder().build();
Mustache mustache = engine.compileMustache("fooTemplate", "{{foo}}");
// It's possible to pass a java.lang.Appendable impl, e.g. any java.io.Writer
StringWriter writer = new StringWriter();
mustache.render(writer, ImmutableMap.<String, Object> of("foo", "bar"));
// writer.toString() -> "bar"
3.1.3. Configure the engine
You may want to:
Simply use appropriate MustacheEngineBuilder methods, e.g.:
MustacheEngine engine = MustacheEngineBuilder
.newBuilder()
.addGlobalData("fooLambda", mySuperUsefulLambdaInstance)
.build();
3.1.4. Configuration properties
Trimou engine properties can be configured through system properties, trimou.properties file or the property may be manually set via: MustacheEngineBuilder.setProperty(String, Object). Manually set properties have hihger priority than system properties which have higher priority than properties from trimou.properties file. Global configuration properties are defined and described in EngineConfigurationKey enum.
3.2. Make use of template cache and template locators
Template locators automatically load the template contents for the given template name. So that it’s not necessary to supply the template contents every time the template is compiled. Moreover the compiled template is automatically put in the template cache (no compilation happens the next time the template is requested).
Note
|
Template cache is required for partials! |
MustacheEngine engine = MustacheEngineBuilder
.newBuilder()
.addTemplateLocator(new FilesystemTemplateLocator(1, "txt", "/home/trimou/resources")) <1>
.build();
Mustache mustache = engine.getMustache("foo"); <2>
String output = mustache.render(null);
-
Add a filesystem-based template locator with priority 1, root path "/home/trim/resources", template files have suffix "txt"
-
Get the template with name "foo" from the template cache, compile it if not compiled before
There may be more than one template locators registered with the engine. Locators with higher priority are called first.
Tip
|
Use MustacheEngine#invalidateTemplateCache() to invalidate all template cache entries and force recompilation. |
See also TemplateLocator SPI.
3.3. Basic i18n support
Trimou has a very basic i18n support. Basically it provides three optional resolvers: org.trimou.engine.resolver.i18n.NumberFormatResolver, org.trimou.engine.resolver.i18n.DateTimeFormatResolver, org.trimou.engine.resolver.i18n.ResourceBundleResolver and one optional lambda: org.trimou.lambda.i18n.ResourceBundleLambda. All these components rely on org.trimou.engine.locale.LocaleSupport implementation to get the current Locale. See javadoc for more info.
3.3.1. DateTimeFormatResolver example
MustacheEngine engine = MustacheEngineBuilder
.newBuilder()
.setProperty(DateTimeFormatResolver.CUSTOM_PATTERN_KEY, "DD-MM-YYYY HH:mm") <1>
.addResolver(new DateTimeFormatResolver()) <2>
.build();
Mustache mustache = engine.getMustache("foo");
String output = mustache.render(ImmutableMap.<String, Object> of("now", new Date()));
-
DateTimeFormatResolver also supports custom formatting pattern
-
Manually add resolver
Now: {{now.formatCustom}}
results in something similar:
Now: 03-05-2013 22:05
4. How to extend
4.1. Resolver
SPI: org.trimou.engine.resolver.Resolver
Resolvers define the set of resolvable objects for your templates. The built-in set of resolvers should satisfy most of the basic requirements.
4.1.1. Custom resolvers
Warning
|
Implementing/adding a custom resolver may have serious impact on the engine functionality and performance. |
All resolvers have a priority and resolvers with higher priority are called first. Keep in mind that all resolvers must be thread-safe. There are two ways to extend the basic set of resolvers. Trimou utilizes JDK service-provider loading facility. Simply create a file with name org.trimou.engine.resolver.Resolver, write down FCQN of your resolver (one per line) and place the file into META-INF/services folder and the listed resolvers are loaded automatically. This way of loading resolvers may be disabled per engine, see also MustacheEngineBuilder.omitServiceLoaderResolvers(). You can also use MustacheEngineBuilder.addResolver() method - this approach is suitable if you need to customize the way the resolver instance is created.
Tip
|
trimou-extension-cdi extension provides CDIBeanResolver to lookup normal-scoped CDI beans with name. trimou-extension-servlet extension provides HttpServletRequestResolver to get the current Servlet request wrapper. |
4.2. TemplateLocator
SPI: org.trimou.engine.locator.TemplateLocator
Template locators automatically load the template contents for the given template name. There are three built-in implementations. org.trimou.engine.locator.FilesystemTemplateLocator loads templates from the given root directory on the filesystem (watch out, this wouldn’t be likely portable across various operating systems). org.trimou.engine.locator.ClassPathTemplateLocator makes use of ClassLoader, either thread context class loader (TCCL) or custom CL set via constructor. org.trimou.engine.locator.MapTemplateLocator is backed by a Map.
Tip
|
Locators with higher priority are called first. |
Tip
|
trimou-extension-servlet extension provides org.trimou.servlet.locator.ServletContextTemplateLocator to be used in web apps deployed to a servlet container. |
4.3. TextSupport
SPI: org.trimou.engine.text.TextSupport
So far there’s only one method to implement - escapeHtml(String) (see also [escaping_hml]). Implement your own logic to extend functionality or improve performance.
4.4. LocaleSupport
SPI: org.trimou.engine.locale.LocaleSupport
Allows the engine and its components (e.g. resolvers) to get the current locale via getCurrentLocale().
5. Extensions
5.1. CDI
<dependency>
<groupId>org.trimou</groupId>
<artifactId>trimou-extension-cdi</artifactId>
<version>${version.trimou}</version>
</dependency>
5.1.1. CDIBeanResolver
Tries to lookup a normal-scoped CDI bean with the given name (key).
Note
|
At the moment only built-in normal scopes are supported. |
5.2. Servlets
At the moment only Servlet 3.0 API is supported.
<dependency>
<groupId>org.trimou</groupId>
<artifactId>trimou-extension-servlet</artifactId>
<version>${version.trimou}</version>
</dependency>
5.2.1. ServletContextTemplateLocator
Locates the template anywhere in the web app. The root path must begin with a / and is interpreted as relative to the current context root, or relative to the /META-INF/resources directory of a JAR file inside the web application’s /WEB-INF/lib directory.
5.2.2. HttpServletRequestResolver
Resolves a key of value request to HttpServletRequestWrapper. Why the wrapper? Well, we just don’t think it’s the right thing to call the request object directly.
5.2.3. RequestLocaleSupport
Obtains the current locate from the current servlet request.
5.3. PrettyTime
<dependency>
<groupId>org.trimou</groupId>
<artifactId>trimou-extension-prettytime</artifactId>
<version>${version.trimou}</version>
</dependency>
5.3.1. PrettyTimeResolver
This resolver allows you to use PrettyTime date-formatting in your templates.
// The PrettyTimeResolver is automatically loaded if you place the extension jar on the classpath
MustacheEngine engine = MustacheEngineBuilder
.newBuilder()
.build();
Mustache mustache = engine.compileMustache("prettyTime","{{now.prettyTime}}");
String output = mustache.render(ImmutableMap.<String, Object> of("now", new Date()));
// Renderds something similar:
// moments from now