1. What is Trimou?
Trimou is yet another Java templating engine. It’s a Mustache implementation but Helpers API (inspired by Handlebars.js) is also supported. The goal is to provide a simple to use and easy to extend templating engine for any Java SE or Java EE application. There are some ready-to-use extensions which provide integration with CDI, Servlets, PrettyTime, HtmlCompressor, google-gson, JSON Processing Object Model API (JSR 353), Spring MVC, Dropwizard and EL 3.0 (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
-
Template inheritance
-
Nested templates
-
Helpers API (inspired by Handlebars.js)
-
Basic i18n support
-
Extended processing of lambdas
-
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 - a key part ("foo" → ["foo"] , "foo.bar.name" → ["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 accessible public method with no params, non-void return type and name
keypart
,getKeypart
orisKeypart
defined on the context object’s class and its superclasses (except forjava.lang.Object
), -
find an accessible public field with name
keypart
and get its value,
-
-
java.util.List
and array elements can be accessed via index (the key must be an unsigned integer), -
an iteration metada object is available inside an iteration block, the default alias is
iter
:-
the alias can be configured, see Configuration,
-
this metadata has some useful properties:
iter.index
(the first element is at index1
),iter.position
(the first element has position0
),iter.hasNext
,iter.isFirst
anditer.isLast
, -
alternatively
iterIndex
,iterHasNext
,iterIsFirst
anditerIsLast
keywords can be used,
-
-
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}}
{{iter.index}} (5)
{{#iter.isFirst}} The is the first one! {{/iter.isFirst}} (6)
{{#iter.isLast}} This is the last one! {{/iter.isLast}} (7)
{{name}} (8)
{{#iter.hasNext}}, {{/iter.hasNext}} (9)
{{/items}}
{{#quxEnumClass.values}} (10)
{{this}}
{{/quxEnumClass.values}}
-
Try to get a value of key "foo" from the context object stack, e.g. if the supplied data context object is an instance of Map get the value of key "foo"
-
Try to get a value of key "bar" from the context object resolved in <1>, e.g. 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 iteration index (the first element is at index 1)
-
Render the text for the first iteration
-
Render the text for the last iteration
-
"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)
-
It’s also possible to invoke static methods; quxEnumClass is an enum class here and we iterate over the array returned from static method values()
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 (&
, "
, <
, >
).
{{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 valuetrue
, 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 valuefalse
, -
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
Alternatively, you can use predefined abstract classes like org.trimou.lambda.InputProcessingLambda
:
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.6.1. Simple lambdas
org.trimou.lambda.SimpleLambdas
utility class and its builder allow to create simple lambdas using JDK8 funcional interfaces:
MustacheEngine engine = MustacheEngineBuilder
.newBuilder()
.addGlobalData("toLowerCase",
SimpleLambdas.invoke((t) -> t.toLowerCase())
).build();
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
2.8. Built-in helpers
Since version 1.5.0 helpers API inspired by Handlebars.js is supported. There are five helpers registered automatically: if
, unless
, each
, with
(see http://handlebarsjs.com for examples :-) and is
(an inline version of if).
Tip
|
if and unless helpers also support multiple params evaluation. See the javadoc for more info.
|
Other helpers may be registered via MustacheEngineBuilder.registerHelper()
or MustacheEngineBuilder.registerHelpers()
methods. org.trimou.handlebars.HelpersBuilder
is useful when registering most built-in helpers with sensible default names.
See also Helper section to know how to create your own custom helpers.
Note
|
Handlebars support is enabled by default. See HANDLEBARS_SUPPORT_ENABLED in Configuration properties.
|
Trimou provides some useful helpers which are not registered automatically:
Class | Description | Default name |
---|---|---|
|
Works similarly as the JSP c:choose tag - it renders the content of the first |
choose |
|
Works similarly as the Java switch statement. |
switch |
|
Works similarly as WithHelper except the current hash is pushed on the context stack. |
set |
|
Allows to build the key dynamically and evaluate it afterwards. |
eval |
|
Works similarly as the partial tag except the name of the template to include may be obtained dynamically. |
include |
|
Logs debug messages. |
log |
|
Takes all the objects specified as the parameters and joins the |
join |
|
Embeds the template source (by default as a JavaScript snippet). |
embed |
|
Renders a block if the param is/isn’t null. |
isNull/isNotNull |
|
Renders a block if the first param does/doesn’t equal to the second param. |
isEq/isNotEq |
|
A simple numeric expression helper. |
numExpr |
|
Renders a block/text if the param is an even number. |
isEven |
|
Renders a block/text if the param is an odd number. |
isOdd |
|
Displays localized messages. |
N/A |
|
Displays localized times. |
N/A |
|
An alternative to |
N/A |
|
A helper whose content is rendered asynchronously. |
async |
|
Invokes public methods with parameters via reflection.
|
invoke |
|
Renders the first non-null/nonempty (default condition) parameter. It’s useful to specify default values:
|
alt |
|
Allows to cache template fragments in memory. It’s useful for resource-intensive parts of the template that rarely change. |
cache |
|
Allows to repeat a section multiple times or until the |
repeat |
2.8.1. Example of ResourceBundleHelper
Suppose we have the following resource bundle file:
my.message.key=My name is %s! hello.key.messageformat=Hello {0}!
We can use a ResourceBundleHelper
to render messages:
MustacheEngine engine = MustacheEngineBuilder
.newBuilder()
.registerHelper("msg", new ResourceBundleHelper("messages")) (1)
.build();
...
{{msg "my.message.key" "Martin"}}
{{msg "hello.key.messageformat" "world" format="message"}}
My name is Martin!
Hello world!
2.9. Nested templates
Any template may define any number of nested templates - use a section with +
to identify a nested template.
A nested template is only available within a defining template through the partial tag (or a special helper, e.g. IncludeHelper
).
If there is a regular template with the same name available the nested template has precedence.
It’s not possible to define a nested template within a nested template definition.
{{! This is the nested template definition }}
{{+item_detail}}
Name: {{name}}
Price: {{price}}
{{/item_detail}}
{{! Inject partial }}
{{#each activeItems}}
{{>item_detail}}
{{/each}}
{{! Inject partial again }}
{{#each allItems}}
{{>item_detail}}
{{/each}}
Note
|
The support for nested templates is enabled by default. See also NESTED_TEMPLATE_SUPPORT_ENABLED in Configuration properties.
|
3. How to use
3.1. Get started
First, get the trimou-core.jar
and slf4j-api
as its dependency.
<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
String data = "Hello world!";
String template = "{{this}}";
MustacheEngine engine = MustacheEngineBuilder.newBuilder().build(); (1)
Mustache mustache = engine.compileMustache("myTemplateName", template); (2)
String output = mustache.render(data); (3)
(1) (2) (3)
String output = MustacheEngineBuilder
.newBuilder()
.build()
.compileMustache("myTemplateName", template)
.render(data);
// Both snippets will render "Hello world!"
-
Build the engine
-
Compile the template
-
Render the template
Note
|
Instances of MustacheEngineBuilder are not reusable. The builder is considered immutable once the
build() method is called - subsequent invocations of any modifying method or build() methods result in IllegalStateException .
|
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:
-
Add template locators; see also Make use of template cache and template locators
-
Add thread-safe global data objects (available during execution of all templates)
-
Add custom resolvers; see also Resolver
-
Add template listeners; see also MustacheListener
-
Register additional helpers; see also Helper
-
Set custom
TextSupport
implementation; see TextSupport -
Set custom
LocaleSupport
implementation; see LocaleSupport -
Set custom
MissingValueHandler
; see MissingValueHandler -
Set custom
KeySplitter
; see KeySplitter -
Set configuration properties; see Configuration properties
Simply use appropriate MustacheEngineBuilder
methods, e.g.:
MustacheEngine engine = MustacheEngineBuilder
.newBuilder()
.addGlobalData("fooLambda", mySuperUsefulLambdaInstance)
.build();
3.2. Make use of template cache and template locators
Template locators automatically locate the template contents for the given template id (name, path, …). So that it’s not necessary to supply the template contents every time the template is compiled. Moreover if the template cache is enabled the compiled template is automatically put in the cache and no compilation happens the next time the template is requested.
Note
|
Template locators are required for partials! |
MustacheEngine engine = MustacheEngineBuilder
.newBuilder()
.addTemplateLocator(new FilesystemTemplateLocator(1, "/home/trimou/resources", "txt")) (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.2.1. Note about file encoding
Trimou does not perform any file encoding detection and conversion.
Instead, any template locator must provide a java.io.Reader
instance which is able to convert between Unicode and a other character encodings. Built-in locators don’t detect file encoding but use system file encoding by default. But it’s possible (and recommended) to define the default file encoding with configuration property EngineConfigurationKey.DEFAULT_FILE_ENCODING
(see also configuration properties).
Note
|
Applications are encouraged to always define a default file encoding per every MustacheEngine instance. |
3.3. Configuration properties
Trimou engine properties can be configured through system properties, trimou.properties
file or the property can be set manually with MustacheEngineBuilder.setProperty(String, Object)
method. Manually set properties have higher priority than system properties which have higher priority than properties from trimou.properties
file.
Note
|
Trimou logs all configuration properties and values during engine initialization |
Enum value / property key | Default value | Description |
---|---|---|
START_DELIMITER org.trimou.engine.config.startDelimiter |
{{ |
The default start delimiter. |
END_DELIMITER org.trimou.engine.config.endDelimiter |
}} |
The default end delimiter |
PRECOMPILE_ALL_TEMPLATES org.trimou.engine.config.precompileAllTemplates |
false |
If enabled, all available templates from all available template locators will be compiled during engine initialization. |
REMOVE_STANDALONE_LINES org.trimou.engine.config.removeStandaloneLines |
true |
Remove "standalone lines" from each template during compilation to fullfill the spec requirements (and get more readable output :-) |
REMOVE_UNNECESSARY_SEGMENTS org.trimou.engine.config.removeUnnecessarySegments |
true |
Remove unnecessary segments (e.g. comments and delimiters tags) from each template during compilation. Having this enabled results in spec not-compliant output, but may improve performance a little bit. |
NO_VALUE_INDICATES_PROBLEM org.trimou.engine.config.noValueIndicatesProblem |
false |
DEPRECATED - see MissingValueHandler SPI. By default a variable miss returns an empty string. If set to |
DEBUG_MODE org.trimou.engine.config.debugMode |
false |
Debug mode disables the template cache and provides some more logging during template rendering. |
CACHE_SECTION_LITERAL_BLOCK org.trimou.engine.config.cacheSectionLiteralBlock |
false |
The section-based literal blocks can be cached. This may be useful to optimize some lambdas processing scenarios, though it’s memory intensive. |
TEMPLATE_RECURSIVE_INVOCATION_LIMIT org.trimou.engine.config.templateRecursiveInvocationLimit |
10 |
The limit of recursive template invocation (partials, template inheritance); 0 - recursive invocation is forbidden. |
SKIP_VALUE_ESCAPING org.trimou.engine.config.skipValueEscaping |
false |
If |
DEFAULT_FILE_ENCODING org.trimou.engine.config.defaultFileEncoding |
System property "file.encoding" |
The encoding every template locator should use if reading template from a file. System file encoding by default. |
TEMPLATE_CACHE_ENABLED org.trimou.engine.config.templateCacheEnabled |
true |
The template cache is enabled by default. If set to false every |
TEMPLATE_CACHE_EXPIRATION_TIMEOUT org.trimou.engine.config.templateCacheExpirationTimeout |
0 |
The template cache expiration timeout in seconds. Zero and negative values mean no timeout. The template cache never expires by default. |
HANDLEBARS_SUPPORT_ENABLED org.trimou.engine.config.handlebarsSupportEnabled |
true |
Handlebars support is enabled by default. Right now only handlebars-like helpers are supported. |
REUSE_LINE_SEPARATOR_SEGMENTS org.trimou.engine.config.reuseLineSeparatorSegments |
true |
If set to |
ITERATION_METADATA_ALIAS org.trimou.engine.config.iterationMetadataAlias |
iter |
The alias for iteration metadata object available inside an iteration block. |
RESOLVER_HINTS_ENABLED org.trimou.engine.config.resolverHintsEnabled |
true |
If set to |
NESTED_TEMPLATE_SUPPORT_ENABLED org.trimou.engine.config.nestedTemplateSupportEnabled |
true |
If set to |
3.4. Basic i18n support
Trimou has a basic i18n support. There are some optional components provided to handle i18n requirements. All these components rely on org.trimou.engine.locale.LocaleSupport
implementation to get the current Locale
, see also LocaleSupport.
Type | Class | Description |
---|---|---|
Resolver |
|
Basic number formatting. |
Resolver |
|
Basic date and time formatting. |
Resolver |
|
Resolves localized messages. Unlike |
Helper |
|
This is an alternative to |
Helper |
|
The most flexible way of rendering localized messages. Supports message parameters and multiple resource bundles. |
Lambda |
|
Renders localized messages. Unlike |
3.4.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
3.5. Debugging
If you encounter a problem during template processing/rendering, try to:
-
configure Simple Logging Facade for Java (SLF4J) - increase the log level for
org.trimou
loggers -
enable debug mode - this disables the template cache and provides some more logging during template rendering (otherwise disabled due to performance)
-
implement your own MissingValueHandler - to handle variable miss during interpolation of a variable tag
-
use LogHelper - this might useful for production environments
4. How to extend
Basically, all the extension points are focused on MustacheEngine
configuration. Some components may be automatically added using the org.trimou.engine.config.ConfigurationExtension
and JDK service-provider loading facility. Others may be added manually via MustacheEngineBuilder
methods. See existing extensions to get acquainted with the basic principles.
Note
|
Automatic org.trimou.engine.config.ConfigurationExtension processing may be disabled per engine - see also MustacheEngineBuilder#omitServiceLoaderConfigurationExtensions() .
|
4.1. 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:
-
automatically via
org.trimou.engine.config.ConfigurationExtension
, -
you can also use
MustacheEngineBuilder.addResolver()
method.
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.1.2. EnhancedResolver
An enhanced resolver should be able to create a Hint
for a sucessfully resolved context object and name. A hint could be used to skip the resolver chain for a part of the key of a specific tag and improve the interpolation performance.
Note
|
Hints are enabled by default. See RESOLVER_HINTS_ENABLED in Configuration properties.
|
4.2. TemplateLocator
Template locators automatically locate the template contents for the given template identifier. The form of the template identifier is not defined, however in most cases the id will represent a template name, e.g. foo
and foo.html
, or virtual path like order/orderDetail
. The default virtual path separator is /
and can be configured via org.trimou.engine.locator.PathTemplateLocator.VIRTUAL_PATH_SEPARATOR_KEY
. Template locators may only be added with MustacheEngineBuilder.addTemplateLocator()
method.
There are three basic built-in implementations. org.trimou.engine.locator.FilesystemTemplateLocator
finds templates on the filesystem, within the given root directory (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
. See javadoc for more configuration info.
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
org.trimou.engine.text.TextSupport
is used to escape variable text if necessary (see also Escaping HTML). You can set the custom instance with org.trimou.engine.MustacheEngineBuilder.setTextSupport()
method. Implement your own logic to extend functionality or improve performance!
4.4. LocaleSupport
org.trimou.engine.locale.LocaleSupport
allows the engine and its components (e.g. resolvers) to get the current locale via getCurrentLocale()
. You can set the custom org.trimou.engine.locale.LocaleSupport
instance with org.trimou.engine.MustacheEngineBuilder.setLocaleSupport()
method.
4.5. MustacheListener
Any registered org.trimou.engine.listener.MustacheListener
receives notifications about template processing. In particular compilationFinished()
method is invoked when a template is compiled, renderingStarted()
and renderingFinished()
methods are invoked for each template rendering. parsingStarted()
is invoked right before a template is processed by the parser. Listeners are invoked in the order of their registration, except for renderingFinished()
method which is invoked in reverse order.
There are two ways to register a custom listener:
-
MustacheEngineBuilder.addMustacheListener()
method, -
automatically via
org.trimou.engine.config.ConfigurationExtension
(extension listeners are always registered after manually added listeners).
Note
|
Code inside a listener may throw an unchecked exception - this aborts further processing of template and no more listeners are invoked afterwards. |
4.6. Helper
org.trimou.handlebars.Helper
API is inspired by Handlebars but it’s not 100% compatible.
Mainly, it does not define the "inverse" section, so for example the built-in if
helper doesn’t support else
block.
On the other hand any helper is able to validate the tag definition (see Helper.validate()
) and fail fast if there’s invalid number of arguments etc.
A helper may be registered via MustacheEngineBuilder.registerHelper()
or MustacheEngineBuilder.registerHelpers()
methods.
Note that each helper must be registered with a unique name.
If there are more helpers registered with the same name an IllegalArgumentException
is thrown during engine build.
There is a special method MustacheEngineBuilder.registerHelpers(Map<String, Helper>, boolean)
which allows to overwrite the existing helper instance (e.g. to define a custom if
helper).
Some built-in helpers are registered automatically.
Note
|
The number of registered helpers should not affect the engine performance (unlike the number of registered resolvers). |
The main advantage of helpers is the ability to consume multiple parameters and optional hash map. A parameter or a hash map value may be a literal (see also LiteralSupport), a value placeholder (evaluated at runtime, during each execution of a helper) or a list of literals and value placeholders.
{{#if foo}}Foo is a value placeholder, is evaluated each time the helper is executed{{/if}}
{{#if true}}true is a literal{{/if}}
{{#each [foo, 1]}}
[foo, 1] is a list containing value placeholder and literal
{{/each}}
Check out org.trimou.handlebars.Options
and the source of built-in helpers to see what helpers can do.
4.6.1. Simple helpers
org.trimou.handlebars.SimpleHelpers
utility class and its builder allow to create simple helpers using JDK8 funcional interfaces. It’s even possible to validate the helper definition and provide configuration keys.
MustacheEngine engine = MustacheEngineBuilder
.newBuilder()
.registerHelper("toLowerCase", SimpleHelpers.execute(
(o, c) -> {
o.append(o.getParameters().get(0).toString().toLowerCase());
})
).build();
4.7. MissingValueHandler
org.trimou.engine.interpolation.MissingValueHandler
handles variable miss (no value found) during interpolation of a variable tag. By default org.trimou.engine.interpolation.NoOpMissingValueHandler
is used so that a miss does not result in any special operation. However you can set your own handler through the MustacheEngineBuilder.setMissingValueHandler()
method. There is also org.trimou.engine.interpolation.ThrowingExceptionMissingValueHandler
which throws an exception in case of a miss (actually it replaces deprecated configuration property EngineConfigurationKey#NO_VALUE_INDICATES_PROBLEM
).
4.8. KeySplitter
org.trimou.engine.interpolation.KeySplitter
is responsible for splitting a variable key.
org.trimou.engine.interpolation.DotKeySplitter
which follows the dot notation is used by default.
org.trimou.engine.interpolation.BracketDotKeySplitter
enables to use bracket notation and literals in variable keys.
E.g. {{messages["my.message.key"]}}
.
You can set your own splitter through the MustacheEngineBuilder.setKeySplitter()
method.
4.9. ComputingCacheFactory
org.trimou.engine.cache.ComputingCache
is a simple abstraction for thread-safe computing (lazy loading) cache.
It’s used in some internal components (e.g. ReflectionResolver
) and may also be used in custom components too. org.trimou.engine.cache.ComputingCacheFactory
component is responsible for creating new instances of ComputingCache
.
The default computing cache implementation is backed by java.util.concurrent.ConcurrentHashMap
.
4.10. IdentifierGenerator
org.trimou.engine.id.IdentifierGenerator
is used to generate identifiers for various components and use-cases (e.g. Mustache
, MustacheRenderingEvent
and one-off lambda names).
There are some restrictions on the uniqueness of the generated id - see also the javadoc.
4.11. LiteralSupport
org.trimou.engine.interpolation.LiteralSupport
allows to customize the way the helpers extract literals from params and hash values.
The default implementation currently supports String ("foo"
or 'foo'
), Integer, Long (10L
or 10l
), Boolean (true
or false
) literals.
4.12. ValueConverter
org.trimou.engine.convert.ValueConverter
is used to convert an object to a string representation.
Converters are mostly used in variable tags, e.g. {{foo}}
.
A converter has also a priority - converters with higher priority are called first.
If no converter is able to convert an object, Object#toString()
is used.
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 resolve a CDI bean with the given name (i.e. annotated with @Named
or with a @Named
stereotype).
5.1.2. Rendering context
The rendering scope is active during each rendering of a template, i.e. during Mustache.render()
invocation - there is exactly one bean instance per rendering which is destroyed after the rendering is finished. This could be useful in SE environments where usually only @ApplicationScoped
and @Dependent
built-in scopes are available. You can annotate your bean with org.trimou.cdi.context.RenderingScoped
to declare the rendering scope.
5.2. Servlets
At the moment only Servlet 3.x 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.
MustacheEngineBuilder
.newBuilder()
.addTemplateLocator(ServletContextTemplateLocator.builder().setRootPath("/WEB-INF/templates").build())
.build();
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.
Note
|
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()));
// Renders something similar:
// moments from now
5.3.2. PrettyTimeHelper
Developers are encouraged to use this helper instead of PrettyTimeResolver
to avoid the negative performance impact during interpolation. However, PrettyTimeResolver
is registered automatically through PrettyTimeConfigurationExtension
. So don’t forget to disable the resolver by means of org.trimou.prettytime.resolver.PrettyTimeResolver.ENABLED_KEY
, e.g. use org.trimou.prettytime.resolver.PrettyTimeResolver.enabled=false
in your properties file.
5.4. Minify
Minify extension allows you to minify your HTML and XML templates (or any other type of content if you provide your own org.trimou.minify.Minifier
implementation). Trimou integrates small and efficient HtmlCompressor library. There are two ways to minify the templates. It’s possible to register a special listener to minify templates before parsing/compilation or use a special lambda to minify some parts of the template contents.
Tip
|
org.trimou.minify.Minify helper methods are useful to create the default listeners and lambdas (i.e. if you don’t require some extra configuration).
|
Note
|
From the performance point of view: both listener and lambda decrease the size of the rendered template. However listeners may also improve the rendering performance (template is minified only once - before the compilation). Whereas lambdas will likely make rendering performance worse (part of the template is minified every time the lambda is invoked). |
<dependency>
<groupId>org.trimou</groupId>
<artifactId>trimou-extension-minify</artifactId>
<version>${version.trimou}</version>
</dependency>
5.4.1. MinifyListener
MustacheEngine engine = MustacheEngineBuilder
.newBuilder()
.addMustacheListener(Minify.htmlListener())
.build();
Mustache mustache = engine.compileMustache("minify_html","<html><body> <!-- My comment -->{{foo}} </body></html>");
String output = mustache.render(ImmutableMap.<String, Object> of("foo", "FOO"));
// Renders:
// <html><body> FOO </body></html>
Tip
|
It’s also possible to customize the underlying com.googlecode.htmlcompressor.compressor.HtmlCompressor instance - see also our MinifyListenerTest.
|
5.4.2. MinifyLambda
MustacheEngine engine = MustacheEngineBuilder
.newBuilder()
.build();
Mustache mustache = engine.compileMustache("minify_html_lambda","<html><body><!-- Remains -->{{#mini}}<!-- Will be removed --> FOO {{/mini}}</body></html>");
String output = mustache.render(ImmutableMap.<String, Object> of("mini", Minify.htmlLambda()));
// Renders:
// <html><body><!-- Remains --> FOO </body></html>
5.4.3. Minifier interface
You can also implement your own minifier and leverage existing infrastructure:
MustacheEngine engine = MustacheEngineBuilder
.newBuilder()
.addMustacheListener(Minify.customListener(new AbstractMinifier() {
@Override
public Reader minify(String mustacheName, Reader mustacheContents) {
return mustacheName.endsWith("html") ? mySuperMinification(Reader mustacheContents) : mustacheContents;
}
}))).build();
5.5. Gson
Gson extension brings some basic support for JSON format by means of google-gson APIs.
<dependency>
<groupId>org.trimou</groupId>
<artifactId>trimou-extension-gson</artifactId>
<version>${version.trimou}</version>
</dependency>
5.5.1. GsonValueConverter
Converts JsonPrimitive
to JsonPrimitive.getAsString()
and JsonNull
to an empty string.
This converter is enabled by default and could be disabled by setting org.trimou.gson.converter.GsonValueConverter.enabled
configuration property to false
.
5.5.2. JsonElementResolver
org.trimou.gson.resolver.JsonElementResolver
makes it easier to work with com.google.gson.JsonElement
instances.
It is automatically loaded if you place the extension jar on the classpath.
-
allows to access
JsonObject
properties via dot notation-
e.g. if foo is an instance of
JsonObject
thenfoo.bar
is translated tofoo.get("bar")
-
-
JsonArray
elements can be accessed via index-
e.g. if foo is an instance of
JsonArray
thenfoo.1
is translated tofoo.get(1)
-
JsonNull
and JsonPrimitive
might be automatically unwrapped if org.trimou.gson.resolver.JsonElementResolver.unwrapJsonPrimitive
configuration property is set to true
.
JsonNull
is resolved as a Placeholder#NULL
and JsonPrimitive
is unwrapped according to its type.
E.g. foo.bar
in JsonObject
example is translated to foo.get("bar").getAsNumber()
if bar is an instance of a java.lang.Number
.
Since 2.1 this automatic unwrapping is disabled by default - see JsonElementResolver
javadoc.
Note
|
Unwrapping only works if JsonElementResolver is involved! So for example if you iterate over ["Jim", true, 5] (and GsonValueConverter is not enabled), a special keyword unwrapThis must be used so that the primitives are unwrapped:
{{#jsonArray}}{{unwrapThis}}{{/jsonArray}} .
|
Example code
{
"firstName": "Jan",
"lastName": "Novy",
"age": 30,
"address": {
"street": "Nova",
"city": "Prague",
"state": "CZ",
"postalCode": "11000"
},
"phoneNumbers": [
{
"type": "home",
"number": "`42002012345"
},
{
"type": "mobile",
"number": "`420728000111"
}
]
}
Last name: {{lastName}}
Street: {{address.street}}
Phone numbers: {{#phoneNumbers}}{{number}}{{#iterHasNext}}, {{/iterHasNext}}{{/phoneNumbers}}
Type of the first phone number: {{phoneNumbers.0.type}}
Type of the second phone number: {{phoneNumbers.1.type}}
// Load the test data
JsonElement jsonElement = new JsonParser().parse(...);
// JsonElementResolver is loaded automatically
MustacheEngine engine = MustacheEngineBuilder
.newBuilder()
.build();
Mustache mustache = engine.getMustache("json_test.mustache");
String output = mustache.render(jsonElement);
Last name: Novy
Street: Nova
Phone numbers: `42002012345, `420728000111
Type of the first phone number: home
Type of the second phone number: mobile
5.6. JSON Processing (JSR 353)
This extension simplifies the usage of Object Model API along with Trimou templates.
<dependency>
<groupId>org.trimou</groupId>
<artifactId>trimou-extension-json-p</artifactId>
<version>${version.trimou}</version>
</dependency>
5.6.1. JsonProcessingValueConverter
Converts JsonString
to JsonString.getString()
and JsonValue.NULL
to an empty string.
This converter is enabled by default and could be disabled by setting org.trimou.jsonp.converter.JsonProcessingValueConverter.enabled
configuration property to false
.
5.6.2. JsonValueResolver
Warning
|
Since 2.1 this resolver is disabled by default. |
org.trimou.jsonp.resolver.JsonValueResolver
makes it easier to work with javax.json.JsonValue
instances.
It is automatically loaded if you place the extension jar on the classpath.
Since JsonObject
implements Map
and JsonArray
implements List
this resolver is only useful if automatic unwrapping is required.
Automatic unwrapping means resolving JsonString#getString()
for a JsonString
, JsonNumber#bigDecimalValue()
for a JsonNumber
, Boolean#TRUE
for a JsonValue#TRUE
, Boolean#FALSE
for JsonValue#FALSE
and Placeholder#NULL
for a JsonValue#NULL
.
However, unwrapping only works if JsonValueResolver
is involved!
So for example if you iterate over ["Jim", true, 5]
, a special keyword unwrapThis
must be used so that the primitives are unwrapped:
{{#jsonArray}}{{unwrapThis}}{{/jsonArray}}
Note
|
This resolver should always have higher priority than MapResolver to be able to process instances of JsonObject .
|
Example code
{
"firstName": "Jan",
"lastName": "Novy",
"age": 30,
"address": {
"street": "Nova",
"city": "Prague",
"state": "CZ",
"postalCode": "11000"
},
"phoneNumbers": [
{
"type": "home",
"number": "`42002012345"
},
{
"type": "mobile",
"number": "`420728000111"
}
]
}
Last name: {{lastName}}
Street: {{address.street}}
Phone numbers: {{#phoneNumbers}}{{number}}{{#iterHasNext}}, {{/iterHasNext}}{{/phoneNumbers}}
Type of the first phone number: {{phoneNumbers.0.type}}
Type of the second phone number: {{phoneNumbers.1.type}}
// Load the test data
JsonStructure jsonStructure = Json.createReader(...).read();
// JsonValueResolver is loaded automatically
MustacheEngine engine = MustacheEngineBuilder
.newBuilder()
.build();
Mustache mustache = engine.getMustache("json_test.mustache");
String output = mustache.render(jsonElement);
Last name: Novy
Street: Nova
Phone numbers: `42002012345, `420728000111
Type of the first phone number: home
Type of the second phone number: mobile
5.7. Spring MVC integration
This extension provides a basic Spring MVC integration.
5.7.1. Spring 3.x
<dependency>
<groupId>org.trimou</groupId>
<artifactId>trimou-extension-spring-mvc</artifactId>
<version>${version.trimou}</version>
</dependency>
5.7.2. Spring 4.x
<dependency>
<groupId>org.trimou</groupId>
<artifactId>trimou-extension-spring4-mvc</artifactId>
<version>${version.trimou}</version>
</dependency>
The TrimoView
implements the View
interface from the Spring MVC Framework and acts as the presentation layer. The
TrimouViewResolver
implements the ViewResolver
interface and is responsible to load the views from a desired
location. Per default the SpringResourceTemplateLocator
is used, that catches any templates prefixed with
classpath:/templates/
and the suffix .trimou
.
In order to use this extension it is your job to provide a recent version of spring-webmvc
as we do not provide any.
Note
|
The newly provided SpringResourceTemplateLocator does not respect the option TEMPLATE_CACHE_EXPIRATION_TIMEOUT .
|
i18n Support
In order to use Spring’s MessageSource
interface to localize message codes you can add the SpringMessageSourceHelper`during `MustacheEngine
initialization.
SpringMessageSourceHelper
MustacheEngineBuilder.newBuilder()
.setLocaleSupport(...)
.registerHelper("msg", new SpringMessageSourceHelper(messageSource))
.build();
This helper support two options locale
and defaultMessage
. If the locale
option is not given, the locale will be
read by the configured locale support of the engine.
label.greeting=Hello {0}!
{{msg 'label.greeting' 'world' locale='en'}}
Hello world!
{{msg 'label.missing' defaultMessage='My default message'}}
My default message
5.8. Spring Boot Starter
To make the Spring integration even more comfortable, we provide a Spring Boot starter artifact. This is the only artifact you have to include to use Trimou as template engine within your Spring MVC setup.
<dependency>
<groupId>org.trimou</groupId>
<artifactId>trimou-extension-spring-boot-starter</artifactId>
<version>${project.version}</version>
</dependency>
To customize the auto-configured MustacheEngine
you can create a trimou.properties
file in your classpath. In
addition to this default behaviour it’s also possible to set these settings via your application.properties
file. Any
configuration must be prefixed with trimou
(eg. org.trimou.engine.config.startDelimiter gets trimou.start-delimiter).
If you need to customize the MustacheEngine
even more, you can simply implement the interface TrimouConfigurer
and annotate this class as normal
Spring configuration
class.
@Configuration
class TrimouConfigurationDecorator implements TrimouConfigurer {
private final MessageSource messageSource;
public TrimouConfigurationDecorator(final MessageSource messageSource) {
this.messageSource = messageSource;
}
@Override
public void configure(final MustacheEngineBuilder engineBuilder) {
engineBuilder
.addGlobalData("footer", "(c) Trimou Team")
.registerHelper("msg", new SpringMessageSourceHelper(messageSource));
}
}
5.9. Dropwizard
This extension provides a basic Dropwizard integration.
<dependency>
<groupId>org.trimou</groupId>
<artifactId>trimou-extension-dropwizard</artifactId>
<version>${version.trimou}</version>
</dependency>
5.9.1. TrimouViewRenderer
org.trimou.dropwizard.views.TrimouViewRenderer
is a io.dropwizard.views.ViewRenderer
implementation backed by Trimou. There’s a simple builder for convenience: org.trimou.dropwizard.views.TrimouViewRenderer.Builder
.
5.10. EL 3.0
This extension provides a basic EL 3.0 integration.
<dependency>
<groupId>org.trimou</groupId>
<artifactId>trimou-extension-el</artifactId>
<version>${version.trimou}</version>
</dependency>
5.10.1. ELIfHelper
org.trimou.el.ELIfHelper
extends the built-in IfHelper
in the sense that a String
param is evaluated as EL expression:
{{#if "item.price gt 200"}}
{{item.name}}
{{/if}}
Note
|
The ELIfHelper is automatically registered if you place the extension on the class path.
|
5.10.2. ELHelper
org.trimou.el.ELHelper
evaluates the Object#toString()
of the first parameter.
If the helper represents a section and the value is not null
the value is pushed on the context stack and the section is rendered.
If the helper represents a variable and the value is null
, the current MissingValueHandler
is used.
If the helper represents a variable and the final value is not null
the the value’s Object#toString()
is rendered.
{{el 'item.active ? "active" : "inactive"'}}
Note
|
The ELHelper is automatically registered if you place the extension on the class path.
|