Water Template Engine
Water Template Engine is an open-source modern Java 8 template engine that simplifies the way you interact with templates. With no external dependencies, it is very lightweight and robust.
Just like mustache, Water is a logic-less template engine, but it takes advantage of statically typed languages features to increase reliability and prevent errors.
- Why to use Water?
- Maven
- Quick start
- Documentation
- Nested templates
- Adding arguments
- Commands
- Conventions
- i18n
- Developer mode
- JAX-RS
- Try it yourself!
Why to use Water?
Water is a not only logic-less, but also transparent. It means you see everything you do. It means there are no complex under-the-hood features which try to abstract the problems you are trying to solve.
Everything you do is explicit, and while the other template engines try to help you with reflection solutions or thousands of features which give you flexibility, Water restricts its use to its porpourse.
1 to 1 complex
Every template class describes one and one only template file. Each of your .html or whatever you're templating are described by an specific class. It gives you a coupling-free hierarchy. Every template is independent. The relationships between templates are made inside your classes, not in your template files.
Transparent
Transparency should be more often present in software artifacts. It is so easy to hide undesired things from its users that many people do it unconsciously. Water hides you nothing. Not even a simple toString()
method is called without you calling it explicitly.
Logic-less
Its obvious that no logic should be placed in your template files. But aren't include tags, nesting or parameterization logic? In Water templates, every these things are not possible inside template files. And even though Water provides the handy if command, it makes sure that every logic is still computed in your template classes by accepting only Booleans as conditions.
No function calls inside templates
Why enabling function calls inside your template files if you can do it in your Java classes? It may seem a feature less than the other engine templates. But it ensures your template files are actually not becoming programs.
No reflection
It's straight forward to say that reflection is either slow and dangerous. Even if it promisses that you're writing less code, it creates a complex environment which hides things from the developers. You end up not knowing exactly wheter functions are used or not.
No configuration
Water relies in very tiny amount of conventions instead of providing non-obvious configuration. Adding the dependency to your project and extending Template
give you full power to start building your templates.
No dynamic i18n
Water provides no dynamic i18n solution. There's no point in querying a .properties file millions of times during the lifecycle of your application. The i18n project allows you to build your internationalized templates during build time. However, there are values which are locale sensitive, such as dates or currency. Water provides an elegant solution for such cases.
Maven
Add the maven dependency to your project.
Read this if you use RestEasy, Jersey or any JAX-RS implementation.
Quick start
Imagine a template:
<h1>Months of ~year~</h1>
<ul>
~for month in months:
<li>
<span> ~month.lowerName~ </span>
<span> with ~month.daysCount~ days </span>
</li>
:~
</ul>
Save it to classpath:templates/en_US/months_grid.html
. Read the list of conventions to know why to save in this specific path.
Represent it in a Java class:
class MonthsGrid extends Template {
private static final Collection<Month> months = Arrays.asList(Month.values());
MonthsGrid(final Year year) {
add("year", year.toString());
addCollection("months", months, (month, map) -> {
map.add("lowerName", month.name().toLowerCase());
map.add("daysCount", month.length(year.isLeap()) + "");
});
}
@Override
protected String getFilePath() {
return "months_grid.html";
}
}
Render it:
public static void main(String[] args) {
MonthsGrid monthsGrid = new MonthsGrid(Year.of(2015));
System.out.println(monthsGrid.render());
}
See the result:
<h1>Months of 2015</h1>
<ul>
<li>
<span> january </span>
<span> with 31 days </span>
</li>
<li>
<span> february </span>
<span> with 28 days </span>
</li>
<li>
<span> march </span>
<span> with 31 days </span>
</li>
<li>
<span> april </span>
<span> with 30 days </span>
</li>
... and so on
</ul>
#Documentation
Adding arguments
Water works with a different approach to arguments. Unlike many other template engines, Water uses no reflection at any time and doesn't make it possible to call functions within your template files. Everything you add as an argument must have a key associated with it and can be formatted or manipulated through the mapping mechanism. There are five basic methods which let you add arguments:
add("email", user.getEmail()); // takes a String
// Will match with ~email~
add("user_is_popular", user.isPopular()); // takes a Boolean
// Will match with ~user_is_popular~
addMappedObject("user", user, (userMap) -> {
userMap.add("email", user.getEmail());
});
// Will match with ~user.email~
addCollection("users", users, (user, userMap) -> {
userMap.add("email", user.getEmail());
});
// Will match with ~for user in users: ~user.email~ :~
addLocaleSensitiveObject("now", new Date(), (now, locale) -> {
return DateFormat.getDateInstance(DateFormat.FULL, locale).format(now); // returns a String
});
// Will match with ~now~
You can also nest MappedObjects
and LocaleSensitiveObjects
or add them inside a collection mapping:
addCollection("users", users, (user, userMap) -> {
userMap.addMappedObject("name", user.getName(), (name, nameMap) -> {
nameMap.add("upper", name.toUpperCase());
});
userMap.addLocaleSensitiveObject("birth_date", user.getBirthDate(), (birthDate, locale) -> {
return DateFormat.getDateInstance(DateFormat.FULL, locale).format(birthDate);
});
});
// Will match with
// ~for user in users: ~user.name~ was born in ~user.birth_date~ :~
// or also with
// ~for user in users: ~user.name.upper~ was born in ~user.birth_date~ :~
It is only possible to add Strings and Booleans. Collections and MappedObjects are special types which should never be evaluated. The toString()
method is never implicitly called.
Nested templates
Water gives you the possibility to nest templates in many levels. Each Template
can have one MasterTemplate
and many SubTemplates
. When creating a Template
, you can override the getMasterTemplate
and getSubTemplates
methods to specify how is your tree going to be.
Also, each Template
has one, and one only, template file associated with it. This 1 to 1 relationship ensures that you cannot access other template files within your Template
and you cannot access other Templates
within your template files.
See an example.
Commands
Water provides if and for commands.
-
If: The if condition must be a boolean. Null objects are not a valid condition.
-
For: The for collection must be added by the
addCollection
method. The else is triggered when the collection is empty or null.
Full syntax
~for user in users:
<span> ~user.name~ </span>
~if user.is_already_followed:
<input type="button" value="Unfollow"/>
:else:
<input type="button" value="Follow"/>
:~
:else:
<span> No users to display </span>
:~
List of conventions
-
~content~
is a reserved identifier. It's where your Template goes inside its Master template. -
Every template file must be placed in
classpath:templates/[locale]/
. The i18n project helps you with that. -
The default locale is
Locale.US
. However, you can change it easily. See how.
How to change the default locale?
Every Template
has a method called getDefaultLocale
which you can override. If you want to change the default locale for every template it's recommended that you create a class in the middle of Template
and your Templates
which overrides this method and propagates the change to its child classes.
i18n
Water provides an i18n solution too. See the i18n project to know how to use it and why it works so good together with the engine.
Developer mode
During development you'll want to reload your template files several times. To save time, you can run your server in developer mode by setting the "dev-mode" system property. If you're using maven to start your development server you can add the -Ddev-mode parameter to use it. For exemple: mvn jetty:run -Ddev-mode
JAX-RS
If you want to provide your webpages as resources, JAX-RS is a good way to do that. Adding this dependency to your project lets you return a Template
object directly. The locale will be injected during the rendering of each call, so your i18n is safe.
Run an example following the information below.
@GET
@Path("/home")
public Template homePage() {
return new HomePage();
}
@GET
@Path("/months/{year}")
public Template monthsGrid(@PathParam("year") Integer year) {
return new MonthsGrid(Year.of(year));
}
Try it yourself!
Go to the examples project and follow the instructions.