Skip to content

Commit 54fd803

Browse files
committed
injection for handlebars helpers fix #50
Mustache/Handlebars templates for Jooby. Exposes a [Handlebars](https://github.com/jknack/handlebars.java) and [Body.Formatter]. ```xml <dependency> <groupId>org.jooby</groupId> <artifactId>jooby-hbs</artifactId> <version>{{version}}</version> </dependency> ``` It is pretty straightforward: ```java { use(new Hbs()); get("/", req {@literal ->} View.of("index", "model", new MyModel()); } ``` public/index.html: ``` {{model}} ``` Templates are loaded from root of classpath: ```/``` and must end with: ```.html``` file extension. Simple/basic helpers are add it at startup time: ```java { use(new Hbs().doWith((hbs, config) {@literal ->} { hbs.registerHelper("myhelper", (ctx, options) {@literal ->} { return ...; }); hbs.registerHelpers(Helpers.class); }); } ``` Now, if the helper depends on a service and require injection: ```java { use(new Hbs().with(Helpers.class)); } ``` The ```Helpers``` will be injected by Guice and Handlebars will scan and discover any helper method. Templates are loaded from the root of classpath and must end with ```.html```. You can change the default template location and extensions too: ```java { use(new Hbs("/", ".hbs")); } ``` Cache is OFF when ```env=dev``` (useful for template reloading), otherwise is ON. Cache is backed by Guava and the default cache will expire after ```100``` entries. If ```100``` entries is not enough or you need a more advanced cache setting, just set the ```hbs.cache``` option: ``` hbs.cache = "expireAfterWrite=1h" ``` See [CacheBuilderSpec] for more detailed expressions. That's all folks! Enjoy it!!!
1 parent 147429e commit 54fd803

15 files changed

Lines changed: 470 additions & 62 deletions

File tree

coverage-report/src/test/java/org/jooby/hbs/HbsCustomFeature.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
package org.jooby.hbs;
22

3-
import static org.junit.Assert.assertSame;
4-
53
import org.jooby.View;
64
import org.jooby.test.ServerFeature;
75
import org.junit.Test;
86

9-
import com.github.jknack.handlebars.Handlebars;
107
import com.github.jknack.handlebars.io.ClassPathTemplateLoader;
118

129
public class HbsCustomFeature extends ServerFeature {
1310

1411
{
15-
Handlebars handlebars = new Handlebars(new ClassPathTemplateLoader("/org/jooby/hbs", ".html"));
16-
use(new Hbs(handlebars).doWith((h, c) -> assertSame(handlebars, h)));
12+
use(new Hbs().doWith((h, c) -> {
13+
h.with(new ClassPathTemplateLoader("/org/jooby/hbs", ".html"));
14+
}));
1715

1816
get("/", req -> View.of("index", "model", req.param("model").value()));
1917
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package org.jooby.hbs;
2+
3+
import java.util.Collections;
4+
import java.util.Map.Entry;
5+
import java.util.Set;
6+
7+
import org.jooby.View;
8+
import org.jooby.test.ServerFeature;
9+
import org.junit.Test;
10+
11+
import com.github.jknack.handlebars.ValueResolver;
12+
13+
public class HbsCustomValueResolverFeature extends ServerFeature {
14+
15+
public static class VR implements ValueResolver {
16+
17+
@Override
18+
public Object resolve(final Object context, final String name) {
19+
return "VR";
20+
}
21+
22+
@Override
23+
public Object resolve(final Object context) {
24+
return "VR";
25+
}
26+
27+
@Override
28+
public Set<Entry<String, Object>> propertySet(final Object context) {
29+
return Collections.emptySet();
30+
}
31+
}
32+
33+
{
34+
use(new Hbs().with(new VR()));
35+
36+
get("/", req -> View.of("org/jooby/hbs/index"));
37+
}
38+
39+
@Test
40+
public void shouldInjectHelpers() throws Exception {
41+
request()
42+
.get("/")
43+
.expect("<html><body>VR</body></html>");
44+
}
45+
46+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package org.jooby.hbs;
2+
3+
import org.jooby.View;
4+
import org.jooby.test.ServerFeature;
5+
import org.junit.Test;
6+
7+
public class HbsHelpersFeature extends ServerFeature {
8+
9+
public static class Helpers {
10+
11+
public String itWorks() {
12+
return "oh yea";
13+
}
14+
}
15+
16+
{
17+
use(new Hbs(Helpers.class));
18+
19+
get("/", req -> View.of("org/jooby/hbs/helpers"));
20+
}
21+
22+
@Test
23+
public void shouldInjectHelpers() throws Exception {
24+
request()
25+
.get("/")
26+
.expect("oh yea!");
27+
}
28+
29+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{itWorks}}!

jooby-hbs/src/main/java/org/jooby/hbs/Hbs.java

Lines changed: 150 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,23 @@
2020

2121
import static java.util.Objects.requireNonNull;
2222

23+
import java.util.Deque;
24+
import java.util.HashSet;
25+
import java.util.LinkedList;
26+
import java.util.Set;
2327
import java.util.function.BiConsumer;
2428

2529
import org.jooby.Body;
2630
import org.jooby.Env;
2731
import org.jooby.Jooby;
2832
import org.jooby.View;
33+
import org.jooby.internal.hbs.ConfigValueResolver;
34+
import org.jooby.internal.hbs.HbsEngine;
35+
import org.jooby.internal.hbs.HbsHelpers;
36+
import org.jooby.internal.hbs.LocalsValueResolver;
2937

30-
import com.github.jknack.handlebars.Context;
3138
import com.github.jknack.handlebars.Handlebars;
32-
import com.github.jknack.handlebars.Template;
39+
import com.github.jknack.handlebars.ValueResolver;
3340
import com.github.jknack.handlebars.cache.GuavaTemplateCache;
3441
import com.github.jknack.handlebars.cache.NullTemplateCache;
3542
import com.github.jknack.handlebars.context.FieldValueResolver;
@@ -38,72 +45,159 @@
3845
import com.github.jknack.handlebars.context.MethodValueResolver;
3946
import com.github.jknack.handlebars.io.ClassPathTemplateLoader;
4047
import com.google.common.cache.CacheBuilder;
48+
import com.google.common.cache.CacheBuilderSpec;
4149
import com.google.inject.Binder;
4250
import com.google.inject.Key;
4351
import com.google.inject.multibindings.Multibinder;
4452
import com.google.inject.name.Names;
4553
import com.typesafe.config.Config;
4654
import com.typesafe.config.ConfigFactory;
55+
import com.typesafe.config.ConfigValueFactory;
4756

57+
/**
58+
* Exposes a {@link Handlebars} and a {@link Body.Formatter}.
59+
*
60+
* <h1>usage</h1>
61+
* <p>
62+
* It is pretty straightforward:
63+
* </p>
64+
*
65+
* <pre>
66+
* {
67+
* use(new Hbs());
68+
*
69+
* get("/", req {@literal ->} View.of("index", "model", new MyModel());
70+
* }
71+
* </pre>
72+
* <p>
73+
* public/index.html:
74+
* </p>
75+
*
76+
* <pre>
77+
* {{model}}
78+
* </pre>
79+
*
80+
* <p>
81+
* Templates are loaded from root of classpath: <code>/</code> and must end with: <code>.html</code>
82+
* file extension.
83+
* </p>
84+
*
85+
* <h1>helpers</h1>
86+
* <p>
87+
* Simple/basic helpers are add it at startup time:
88+
* </p>
89+
*
90+
* <pre>
91+
* {
92+
* use(new Hbs().doWith((hbs, config) {@literal ->} {
93+
* hbs.registerHelper("myhelper", (ctx, options) {@literal ->} {
94+
* return ...;
95+
* });
96+
* hbs.registerHelpers(Helpers.class);
97+
* });
98+
* }
99+
* </pre>
100+
* <p>
101+
* Now, if the helper depends on a service and require injection:
102+
* </p>
103+
*
104+
* <pre>
105+
* {
106+
* use(new Hbs().with(Helpers.class));
107+
* }
108+
* </pre>
109+
*
110+
* <p>
111+
* The <code>Helpers</code> will be injected by Guice and Handlebars will scan and discover any
112+
* helper method.
113+
* </p>
114+
*
115+
* <h1>template loader</h1>
116+
* <p>
117+
* Templates are loaded from the root of classpath and must end with <code>.html</code>. You can
118+
* change the default template location and extensions too:
119+
* </p>
120+
*
121+
* <pre>
122+
* {
123+
* use(new Hbs("/", ".hbs"));
124+
* }
125+
* </pre>
126+
*
127+
* <h1>cache</h1>
128+
* <p>
129+
* Cache is OFF when <code>env=dev</code> (useful for template reloading), otherwise is ON.
130+
* </p>
131+
* <p>
132+
* Cache is backed by Guava and the default cache will expire after <code>100</code> entries.
133+
* </p>
134+
* <p>
135+
* If <code>100</code> entries is not enough or you need a more advanced cache setting, just set the
136+
* <code>hbs.cache</code> option:
137+
* </p>
138+
*
139+
* <pre>
140+
* hbs.cache = "expireAfterWrite=1h"
141+
* </pre>
142+
*
143+
* <p>
144+
* See {@link CacheBuilderSpec}.
145+
* </p>
146+
*
147+
* <p>
148+
* That's all folks! Enjoy it!!!
149+
* </p>
150+
*
151+
* @author edgar
152+
* @since 0.5.0
153+
*/
48154
public class Hbs implements Jooby.Module {
49155

50-
private static class Engine implements View.Engine {
51-
52-
private Handlebars handlebars;
156+
private final Handlebars hbs;
53157

54-
public Engine(final Handlebars handlebars) {
55-
this.handlebars = requireNonNull(handlebars, "A handlebars instance required.");
56-
}
158+
private BiConsumer<Handlebars, Config> configurer;
57159

58-
@Override
59-
public String name() {
60-
return "hbs";
61-
}
160+
private Set<Class<?>> helpers = new HashSet<>();
62161

63-
@Override
64-
public void render(final View view, final Body.Writer writer) throws Exception {
65-
Template template = handlebars.compile(view.name());
66-
67-
Context context = Context
68-
.newBuilder(view.model())
69-
// merge request locals (req+sessions locals)
70-
.combine(writer.locals())
71-
.resolver(
72-
MapValueResolver.INSTANCE,
73-
JavaBeanValueResolver.INSTANCE,
74-
MethodValueResolver.INSTANCE,
75-
FieldValueResolver.INSTANCE,
76-
new LocalsValueResolver(),
77-
new ConfigValueResolver()
78-
)
79-
.build();
80-
81-
// rendering it
82-
writer.text(out -> template.apply(context, out));
83-
}
162+
private Deque<ValueResolver> resolvers = new LinkedList<>();
84163

85-
@Override
86-
public String toString() {
87-
return name();
88-
}
164+
public Hbs(final String prefix, final String suffix, final Class<?>... helpers) {
165+
this.hbs = new Handlebars(new ClassPathTemplateLoader(prefix, suffix));
166+
with(helpers);
167+
// default value resolvers.
168+
this.resolvers.add(MapValueResolver.INSTANCE);
169+
this.resolvers.add(JavaBeanValueResolver.INSTANCE);
170+
this.resolvers.add(MethodValueResolver.INSTANCE);
171+
this.resolvers.add(new LocalsValueResolver());
172+
this.resolvers.add(new ConfigValueResolver());
173+
this.resolvers.add(FieldValueResolver.INSTANCE);
89174
}
90175

91-
private final Handlebars hbs;
92-
93-
private BiConsumer<Handlebars, Config> configurer;
94-
95-
public Hbs(final Handlebars handlebars) {
96-
this.hbs = requireNonNull(handlebars, "A handlebars instance is required.");
176+
public Hbs(final String prefix, final Class<?>... helpers) {
177+
this(prefix, ".html", helpers);
97178
}
98179

99-
public Hbs() {
100-
this(new Handlebars(new ClassPathTemplateLoader("/", ".html")));
180+
public Hbs(final Class<?>... helpers) {
181+
this("/", helpers);
101182
}
102183

103184
public Hbs doWith(final BiConsumer<Handlebars, Config> configurer) {
104185
this.configurer = requireNonNull(configurer, "Configurer is required.");
105186
return this;
106-
};
187+
}
188+
189+
public Hbs with(final Class<?>... helper) {
190+
for (Class<?> h : helper) {
191+
helpers.add(h);
192+
}
193+
return this;
194+
}
195+
196+
public Hbs with(final ValueResolver resolver) {
197+
requireNonNull(resolver, "Value resolver is required.");
198+
this.resolvers.addFirst(resolver);
199+
return this;
200+
}
107201

108202
@Override
109203
public void configure(final Env env, final Config config, final Binder binder) {
@@ -126,18 +220,26 @@ public void configure(final Env env, final Config config, final Binder binder) {
126220

127221
binder.bind(Handlebars.class).toInstance(hbs);
128222

129-
Engine engine = new Engine(hbs);
223+
Multibinder<Object> helpersBinding = Multibinder
224+
.newSetBinder(binder, Object.class, Names.named("hbs.helpers"));
225+
helpers.forEach(h -> helpersBinding.addBinding().to(h));
226+
227+
HbsEngine engine = new HbsEngine(hbs, resolvers.toArray(new ValueResolver[resolvers.size()]));
130228

131229
Multibinder.newSetBinder(binder, Body.Formatter.class).addBinding()
132230
.toInstance(engine);
133231

134232
// direct access
135233
binder.bind(Key.get(View.Engine.class, Names.named(engine.name()))).toInstance(engine);
234+
235+
// helper bootstrap
236+
binder.bind(HbsHelpers.class).asEagerSingleton();
136237
}
137238

138239
@Override
139240
public Config config() {
140-
return ConfigFactory.parseResources(getClass(), "hbs.conf");
241+
return ConfigFactory.empty(Hbs.class.getName())
242+
.withValue("hbs.cache", ConfigValueFactory.fromAnyRef("maximumSize=100"));
141243
}
142244

143245
}

jooby-hbs/src/main/java/org/jooby/hbs/ConfigValueResolver.java renamed to jooby-hbs/src/main/java/org/jooby/internal/hbs/ConfigValueResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19-
package org.jooby.hbs;
19+
package org.jooby.internal.hbs;
2020

2121
import java.util.Collections;
2222
import java.util.Map.Entry;

0 commit comments

Comments
 (0)