/*
* Copyright 2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ratpack.handlebars;
import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.cache.ConcurrentMapTemplateCache;
import com.github.jknack.handlebars.cache.TemplateCache;
import com.github.jknack.handlebars.io.TemplateLoader;
import com.google.common.reflect.TypeToken;
import com.google.inject.Injector;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import ratpack.file.FileSystemBinding;
import ratpack.guice.ConfigurableModule;
import ratpack.guice.internal.GuiceUtil;
import ratpack.handlebars.internal.FileSystemBindingTemplateLoader;
import ratpack.handlebars.internal.HandlebarsTemplateRenderer;
import ratpack.server.ServerConfig;
/**
* An extension module that provides support for Handlebars.java templating engine.
* <p>
* To use it one has to register the module and then render {@link ratpack.handlebars.Template} instances.
* Instances of {@code Template} can be created using one of the
* {@link ratpack.handlebars.Template#handlebarsTemplate(java.util.Map, String, String)}
* static methods.
* </p>
* <p>
* By default templates are looked up in the {@code handlebars} directory of the application root with a {@code .hbs} suffix.
* So {@code handlebarsTemplate("my/template/path")} maps to {@code handlebars/my/template/path.hbs} in the application root directory.
* This can be configured using {@link ratpack.handlebars.HandlebarsModule.Config#templatesPath(String)} and {@link ratpack.handlebars.HandlebarsModule.Config#templatesSuffix(String)}.
* </p>
* <p>
* The default template delimiters are {@code {{ }}} but can be configured using {@link ratpack.handlebars.HandlebarsModule.Config#delimiters(String, String)}.
* </p>
* <p>
* Response content type can be manually specified, i.e. {@code handlebarsTemplate("template", model, "text/html")} or can
* be detected based on the template extension. Mapping between file extensions and content types is performed using
* {@link ratpack.file.MimeTypes} contextual object so content type for {@code handlebarsTemplate("template.html")}
* would be {@code text/html} by default.
* </p>
* <p>Custom handlebars helpers can be registered by binding instances of {@link ratpack.handlebars.NamedHelper}.</p>
* <pre class="java">{@code
* import ratpack.guice.Guice;
* import ratpack.handlebars.HandlebarsModule;
* import ratpack.test.embed.EphemeralBaseDir;
* import ratpack.test.embed.EmbeddedApp;
* import java.nio.file.Path;
*
* import static ratpack.handlebars.Template.handlebarsTemplate;
* import static org.junit.Assert.*;
*
* public class Example {
* public static void main(String... args) throws Exception {
* EphemeralBaseDir.tmpDir().use(baseDir -> {
* baseDir.write("handlebars/myTemplate.html.hbs", "Hello {{name}}!");
* EmbeddedApp.of(s -> s
* .serverConfig(c -> c.baseDir(baseDir.getRoot()))
* .registry(Guice.registry(b -> b.module(HandlebarsModule.class)))
* .handlers(chain -> chain
* .get(ctx -> ctx.render(handlebarsTemplate("myTemplate.html", m -> m.put("name", "Ratpack"))))
* )
* ).test(httpClient -> {
* assertEquals("Hello Ratpack!", httpClient.getText());
* });
* });
* }
* }
* }</pre>
*
* @see <a href="http://jknack.github.io/handlebars.java/" target="_blank">Handlebars.java</a>
*/
public class HandlebarsModule extends ConfigurableModule<HandlebarsModule.Config> {
private static final TypeToken<NamedHelper<?>> NAMED_HELPER_TYPE = new TypeToken<NamedHelper<?>>() {
};
/**
* The configuration object for {@link HandlebarsModule}.
*/
public static class Config {
private String templatesPath = "handlebars";
private String templatesSuffix = ".hbs";
private String startDelimiter = Handlebars.DELIM_START;
private String endDelimiter = Handlebars.DELIM_END;
private int cacheSize = 100;
private Boolean reloadable;
public String getTemplatesPath() {
return templatesPath;
}
public Config templatesPath(String templatesPath) {
this.templatesPath = templatesPath;
return this;
}
public String getTemplatesSuffix() {
return templatesSuffix;
}
public Config templatesSuffix(String templatesSuffix) {
this.templatesSuffix = templatesSuffix;
return this;
}
public String getStartDelimiter() {
return startDelimiter;
}
public String getEndDelimiter() {
return endDelimiter;
}
public Config delimiters(String startDelimiter, String endDelimiter) {
this.startDelimiter = startDelimiter;
this.endDelimiter = endDelimiter;
return this;
}
public int getCacheSize() {
return cacheSize;
}
public Config cacheSize(int cacheSize) {
this.cacheSize = cacheSize;
return this;
}
public Boolean isReloadable() {
return reloadable;
}
public Config reloadable(boolean reloadable) {
this.reloadable = reloadable;
return this;
}
}
@Override
protected void configure() {
bind(HandlebarsTemplateRenderer.class).in(Singleton.class);
}
@SuppressWarnings("UnusedDeclaration")
@Provides
@Singleton
TemplateLoader provideTemplateLoader(Config config, FileSystemBinding fileSystemBinding) {
FileSystemBinding templatesBinding = fileSystemBinding.binding(config.getTemplatesPath());
return new FileSystemBindingTemplateLoader(templatesBinding, config.getTemplatesSuffix());
}
@SuppressWarnings("UnusedDeclaration")
@Provides
@Singleton
TemplateCache provideTemplateCache(Config config, ServerConfig serverConfig) {
boolean reloadable = config.isReloadable() == null ? serverConfig.isDevelopment() : config.isReloadable();
return new ConcurrentMapTemplateCache().setReload(reloadable);
}
@SuppressWarnings("UnusedDeclaration")
@Provides
@Singleton
Handlebars provideHandlebars(Config config, Injector injector, TemplateLoader templateLoader, TemplateCache templateCache) {
final Handlebars handlebars = new Handlebars()
.with(templateLoader)
.with(templateCache)
.startDelimiter(config.getStartDelimiter())
.endDelimiter(config.getEndDelimiter());
GuiceUtil.eachOfType(injector, NAMED_HELPER_TYPE, helper -> handlebars.registerHelper(helper.getName(), helper));
return handlebars;
}
}