Handlebars engine is a tread-safe, so I can initialize it only once and then compile templates using the single instance (with a cache system). So I moved all the handlebars code into the module. There were a few changes: configuration has been extracted to the
handlebars.conf, handlebars cache has been used,
MessagesApi now injecting into the helpers, so they are not static, and result type of the handlebars compilation is a
play.twirl.api.Content.
Now I can easely inject HandlebarsApi in any component:
@Inject
private HandlebarsApi handlebarsApi;
And compile the template just with one line
Content page = handlebarsApi.html("page", data);
Configuration
First of all, I extracted the configuration. I like the
include statement, so I easily created the handlebars settings.
application.conf
# configure Handlebars API
handlebars{
include "handlebars.conf"
}
handlebars.conf in the same directory as the application.conf
directory: "/templates"
extension: ".hbs"
In the code I can read handlebars properties as easy as
configuration.getString("handlebars.directory")
Module
The creation of module in Play is simple.
Create a class. I used @Singleton because I want to initialize handlebars only ones and utilize the cache system.
package handlebars;
@Singleton
public class HandlebarsApi {
...
}
Extend the play.api.inject.Module class; bind your class.
package handlebars;
import play.api.Configuration;
import play.api.Environment;
import play.api.inject.Binding;
import scala.collection.Seq;
public class Module extends play.api.inject.Module {
@Override
public Seq<Binding<?>> bindings(final Environment environment, final Configuration configuration) {
return seq(bind(HandlebarsApi.class).toSelf());
}
}
The last step - enable the module in the application.conf
# Bind Handlebars API
play.modules.enabled += "handlebars.Module"
Cache
Handlebars has a good
cache system. There is no need to build your own. I selected the Guava cache.
Add dependency to the build.sbt
libraryDependencies += "com.github.jknack" % "handlebars-guava-cache" % "4.0.4"
Initialize handlebars with the cache
...
import java.util.concurrent.TimeUnit;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
...
// Initialize the cache. Could be builded from configuration as well
// For example: CacheBuilder.from(config.getString("hbs.cache")).build()
final Cache cache = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
// Initialize the engine with the cache
handlebars = new Handlebars(loader)
.with(new GuavaTemplateCache(cache));
...
MessagesApi
I use
MessagesApi in the handlebars helper. Helpers are registering only once a time, fortunately
MessagesApi is a singleton, so I put it in to the Helper's constructor.
Part of the Helpers class.
public final class Helpers {
final MessagesApi messagesApi;
public Helpers(final MessagesApi messagesApi){
this.messagesApi = messagesApi;
}
...
public CharSequence message(final String key, final Options options) {
// Get the current language.
final Lang lang = Context.current().lang();
// Retrieve the message, internally formatted by MessageFormat.
return messagesApi.get(lang, key, options.params);
}
}
Initialization of the Handlebars with the Helpers class.
...
@Inject
public HandlebarsApi(... final MessagesApi messagesApi) {
...
// Add helpers. MessagesApi is a singleton so we can use it in the helpers.
Helpers helpers = new Helpers(messagesApi);
handlebars.registerHelpers(helpers);
...
}
...
Content
Trivial result of the template processing in the Play is the object of
play.twirl.api.Content type. So I created a simple implementation of this class for the HTML template.
HTML Content wrapper:
package handlebars;
import play.twirl.api.Content;
class HtmlContent implements Content {
private String body;
HtmlContent(final String body){
this.body = body;
}
@Override
public String body() {
return body;
}
@Override
public String contentType() {
return "text/html";
}
}
Wrapping the handlebars result:
public String render(final String templateName, final Object data) throws Exception {
return handlebars
.compile(templateName)
.apply(data);
}
public Content html(final String templateName, final Object data) throws Exception {
return new HtmlContent(render(templateName, data));
}
Mocking the Http.Context
In the process of testing templates I spend a little time to beat the nasty error
java.lang.RuntimeException: There is no HTTP Context available from here.
So do not forget to mockup the
Http.Context. It's easy to do.
Here is an example from the project:
// Initialize application
Application application = new GuiceApplicationBuilder().build();
// Setup an HTTP Context
Http.Context context = mock(Http.Context.class);
// Setup the language and messages
Lang langRequest = Lang.forCode(requestLang);
Lang langSession = Lang.forCode(sessionLang);
MessagesApi messagesApi = application.injector().instanceOf(MessagesApi.class);
Messages messages = new Messages(langRequest, messagesApi);
// Train the Context
when(context.lang()).thenReturn(langSession);
when(context.messages()).thenReturn(messages);
//Http.Context.current.set(context);
// Get the handlebars API
HandlebarsApi handlebarsApi = application.injector().instanceOf(HandlebarsApi.class);
Project on the Github
You can find this simple project with the Handlebars module on the github:
https://github.com/andriykuba/playframework-handlebars-example