/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.jooby.hbs; import static java.util.Objects.requireNonNull; import java.util.Deque; import java.util.HashSet; import java.util.LinkedList; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Consumer; import org.jooby.Env; import org.jooby.Jooby; import org.jooby.Renderer; import org.jooby.internal.hbs.ConfigValueResolver; import org.jooby.internal.hbs.HbsEngine; import org.jooby.internal.hbs.HbsHelpers; import org.jooby.internal.hbs.RequestValueResolver; import org.jooby.internal.hbs.SessionValueResolver; import com.github.jknack.handlebars.Handlebars; import com.github.jknack.handlebars.ValueResolver; import com.github.jknack.handlebars.cache.GuavaTemplateCache; import com.github.jknack.handlebars.cache.NullTemplateCache; import com.github.jknack.handlebars.context.FieldValueResolver; import com.github.jknack.handlebars.context.JavaBeanValueResolver; import com.github.jknack.handlebars.context.MapValueResolver; import com.github.jknack.handlebars.context.MethodValueResolver; import com.github.jknack.handlebars.io.ClassPathTemplateLoader; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheBuilderSpec; import com.google.inject.Binder; import com.google.inject.multibindings.Multibinder; import com.google.inject.name.Names; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import com.typesafe.config.ConfigValueFactory; /** * <h1>handlebars</h1> * <p> * Logic-less and semantic Mustache templates via * <a href="https://github.com/jknack/handlebars.java">handlebars.java</a>. * </p> * * <h2>exports</h2> * <ul> * <li>{@link Handlebars} object.</li> * <li>{@link Renderer} object.</li> * </ul> * * <h2>usage</h2> * <p> * It is pretty straightforward: * </p> * * <pre> * { * use(new Hbs()); * * get("/", req {@literal ->} Results.html("index").put("model", new MyModel()); * } * </pre> * <p> * public/index.html: * </p> * * <pre> * {{model}} * </pre> * * <p> * Templates are loaded from root of classpath: <code>/</code> and must end with: <code>.html</code> * file extension. * </p> * * <h2>options</h2> * <h3>helpers</h3> * <p> * Simple/basic helpers are add it at startup time: * </p> * * <pre> * { * use(new Hbs().doWith((hbs, config) {@literal ->} { * hbs.registerHelper("myhelper", (ctx, options) {@literal ->} { * return ...; * }); * hbs.registerHelpers(Helpers.class); * }); * } * </pre> * <p> * Now, if the helper depends on a service and require injection: * </p> * * <pre> * { * use(new Hbs().with(Helpers.class)); * } * </pre> * * <p> * The <code>Helpers</code> will be injected by Guice and Handlebars will scan and discover any * helper method. * </p> * * <h3>template loader</h3> * <p> * Templates are loaded from the root of classpath and must end with <code>.html</code>. You can * change the default template location and extensions too: * </p> * * <pre> * { * use(new Hbs("/", ".hbs")); * } * </pre> * * <h3>cache</h3> * <p> * Cache is OFF when <code>env=dev</code> (useful for template reloading), otherwise is ON. * </p> * <p> * Cache is backed by Guava and the default cache will expire after <code>100</code> entries. * </p> * <p> * If <code>100</code> entries is not enough or you need a more advanced cache setting, just set the * <code>hbs.cache</code> option: * </p> * * <pre> * hbs.cache = "expireAfterWrite=1h" * </pre> * * <p> * See {@link CacheBuilderSpec}. * </p> * * <p> * That's all folks! Enjoy it!!! * </p> * * @author edgar * @since 0.5.0 */ public class Hbs implements Jooby.Module { private final Handlebars hbs; private BiConsumer<Handlebars, Config> callback; private Set<Class<?>> helpers = new HashSet<>(); private Deque<ValueResolver> resolvers = new LinkedList<>(); /** * Creates a new {@link Hbs} module. * * @param prefix Template prefix. * @param suffix Template suffix. * @param helpers Optional list of helpers. */ public Hbs(final String prefix, final String suffix, final Class<?>... helpers) { this.hbs = new Handlebars(new ClassPathTemplateLoader(prefix, suffix)); with(helpers); // default value resolvers. this.resolvers.add(MapValueResolver.INSTANCE); this.resolvers.add(JavaBeanValueResolver.INSTANCE); this.resolvers.add(MethodValueResolver.INSTANCE); this.resolvers.add(new RequestValueResolver()); this.resolvers.add(new SessionValueResolver()); this.resolvers.add(new ConfigValueResolver()); this.resolvers.add(FieldValueResolver.INSTANCE); } /** * Creates a new {@link Hbs} module. * * @param prefix Template prefix. * @param helpers Optional list of helpers. */ public Hbs(final String prefix, final Class<?>... helpers) { this(prefix, ".html", helpers); } /** * Creates a new {@link Hbs} module. * * @param helpers Optional list of helpers. */ public Hbs(final Class<?>... helpers) { this("/", helpers); } /** * Set a handlebars callback. Usage: * * <pre>{@code * { * use(new Hbs().doWith((hbs, conf) -> { * ... * }); * } * }</pre> * * @param callback Configurer callback. * @return This module. */ public Hbs doWith(final BiConsumer<Handlebars, Config> callback) { this.callback = requireNonNull(callback, "Configurer is required."); return this; } /** * Set a handlebars callback. * * <pre>{@code * { * use(new Hbs().doWith((hbs, conf) -> { * ... * }); * } * }</pre> * @param callback Configurer callback. * @return This module. */ public Hbs doWith(final Consumer<Handlebars> callback) { requireNonNull(callback, "Configurer is required."); return doWith((hbs, conf) -> callback.accept(hbs)); } /** * Append one or more helper classes. * * @param helper Helper class. * @return This module. */ public Hbs with(final Class<?>... helper) { for (Class<?> h : helper) { helpers.add(h); } return this; } /** * Append a {@link ValueResolver}. * * @param resolver Resolver. * @return This module. */ public Hbs with(final ValueResolver resolver) { requireNonNull(resolver, "Value resolver is required."); this.resolvers.addFirst(resolver); return this; } @Override public void configure(final Env env, final Config config, final Binder binder) { // cache if ("dev".equals(env.name()) || config.getString("hbs.cache").isEmpty()) { // noop cache hbs.with(NullTemplateCache.INSTANCE); } else { hbs.with(new GuavaTemplateCache( CacheBuilder .from(config.getString("hbs.cache")) .build())); } if (callback != null) { callback.accept(hbs, config); } /** XSS */ hbs.registerHelper("xss", (value, opts) -> { String[] xss = new String[opts.params.length]; System.arraycopy(opts.params, 0, xss, 0, opts.params.length); return new Handlebars.SafeString(env.xss(xss).apply(value.toString())); }); binder.bind(Handlebars.class).toInstance(hbs); Multibinder<Object> helpersBinding = Multibinder .newSetBinder(binder, Object.class, Names.named("hbs.helpers")); helpers.forEach(h -> helpersBinding.addBinding().to(h)); HbsEngine engine = new HbsEngine(hbs, resolvers.toArray(new ValueResolver[resolvers.size()])); Multibinder.newSetBinder(binder, Renderer.class).addBinding() .toInstance(engine); // helper bootstrap binder.bind(HbsHelpers.class).asEagerSingleton(); } @Override public Config config() { return ConfigFactory.empty(Hbs.class.getName()) .withValue("hbs.cache", ConfigValueFactory.fromAnyRef("maximumSize=100")); } }