Skip to content

Commit d356db0

Browse files
committed
Template engines
- Add supports for multiple engines - Use file extension to choose template engine - Documentation for templates - Integration tests for hbs and ftl
1 parent 516020e commit d356db0

File tree

21 files changed

+270
-74
lines changed

21 files changed

+270
-74
lines changed

docs/asciidoc/index.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ include::context.adoc[]
213213

214214
include::static-files.adoc[]
215215

216+
include::templates.adoc[]
217+
216218
include::execution-model.adoc[]
217219

218220
include::responses.adoc[]

docs/asciidoc/modules/freemarker.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ import io.jooby.freemarker.FreemarkerModule
4949
}
5050
----
5151

52+
Template engine supports the following file extensions: `.ftl`, `.ftl.html` and `.html`.
53+
5254
=== Templates Location
5355

5456
Template location is set to `views`. The `views` folder/location is expected to be at the current

docs/asciidoc/modules/handlebars.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ import io.jooby.handlebars.HandlebarsModule
4949
}
5050
----
5151

52+
Template engine supports the following file extensions: `.hbs`, `.hbs.html` and `.html`.
53+
5254
=== Templates Location
5355

5456
Template location is set to `views`. The `views` folder/location is expected to be at the current

docs/asciidoc/templates.adoc

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
== Templates
2+
3+
Templates are available via javadoc:ModelAndView[] and requires a javadoc:TemplateEngine[] implementation.
4+
5+
.Java
6+
[source, java, role = "primary"]
7+
----
8+
{
9+
install(new MyTemplateEngineModule()); <1>
10+
11+
get("/", ctx -> {
12+
Map<String, Object> model = ...; <2>
13+
return new ModelAndModel("index.html", model); <3>
14+
});
15+
}
16+
----
17+
18+
.Kotlin
19+
[source, kt, role = "secondary"]
20+
----
21+
{
22+
install(MyTemplateEngineModule()) <1>
23+
24+
get("/") { ctx ->
25+
val model = mapOf(...) <2>
26+
ModelAndModel("index.html", model) <3>
27+
}
28+
}
29+
----
30+
31+
<1> Install a template engine
32+
<2> Build the view model
33+
<3> Returns a ModelAndView instance
34+
35+
=== Template Engine
36+
37+
Template engine does the view rendering/encoding. Template engine extends a javadoc:MessageEncoder[]
38+
by accepting a `ModelAndView` instance and produces a `String` result.
39+
40+
The javadoc:TemplateEngine[extensions] method list the number of file extension that a template engine
41+
supports. Default file extension is: `.html`.
42+
43+
The file extension is used to locate the template engine, when a file extension isn't supported
44+
an `IllegalArgumentException` is thrown.
45+
46+
The file extension allow us to use/mix multiple template engines too:
47+
48+
.Multiple template engines
49+
.Java
50+
[source, java, role = "primary"]
51+
----
52+
{
53+
install(new HandlebarsModule()); <1>
54+
install(new FreemarkerModule()); <2>
55+
56+
get("/first", ctx -> {
57+
return new ModelAndModel("index.hbs", model); <3>
58+
});
59+
60+
get("/second", ctx -> {
61+
return new ModelAndModel("index.ftl", model); <4>
62+
});
63+
}
64+
----
65+
66+
.Kotlin
67+
[source, kt, role = "secondary"]
68+
----
69+
{
70+
install(HandlebarsModule()) <1>
71+
install(FreemarkerModule()) <2>
72+
73+
get("/first") { ctx ->
74+
ModelAndModel("index.hbs", model) <3>
75+
}
76+
77+
get("/second") { ctx ->
78+
ModelAndModel("index.ftl", model) <4>
79+
}
80+
}
81+
----
82+
83+
<1> Install Handlebars
84+
<2> Install Freemarker
85+
<3> Render using Handlebars, `.hbs` extension
86+
<4> Render using Freemarker, `.ftl` extension
87+
88+
Checkout all the available <<modules-template-engine, template engines>> provided by Jooby.

examples/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@
5959
<artifactId>jooby-freemarker</artifactId>
6060
<version>${jooby.version}</version>
6161
</dependency>
62+
<dependency>
63+
<groupId>io.jooby</groupId>
64+
<artifactId>jooby-handlebars</artifactId>
65+
<version>${jooby.version}</version>
66+
</dependency>
6267
<dependency>
6368
<groupId>ch.qos.logback</groupId>
6469
<artifactId>logback-classic</artifactId>

examples/src/main/java/examples/FreemarkerApp.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,21 @@
88
import io.jooby.Jooby;
99
import io.jooby.ModelAndView;
1010
import io.jooby.freemarker.FreemarkerModule;
11+
import io.jooby.handlebars.HandlebarsModule;
1112

1213
public class FreemarkerApp extends Jooby {
1314

1415
{
1516
install(new FreemarkerModule());
17+
install(new HandlebarsModule());
1618

17-
get("/", ctx -> {
19+
get("/freemarker", ctx -> {
1820
return new ModelAndView("index.ftl").put("name", "Freemarker");
1921
});
22+
23+
get("/handlebars", ctx -> {
24+
return new ModelAndView("index.hbs").put("name", "Handlebars");
25+
});
2026
}
2127

2228
public static void main(String[] args) {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hello {{name}}!

jooby/src/main/java/io/jooby/TemplateEngine.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77

88
import javax.annotation.Nonnull;
99
import java.nio.charset.StandardCharsets;
10+
import java.util.Collections;
11+
import java.util.List;
1012

1113
/**
1214
* Template engine renderer. This class renderer instances of {@link ModelAndView} objects.
15+
* Template engine rendering is done by checking view name and supported file {@link #extensions()}.
1316
*
1417
* @since 2.0.0
1518
* @author edgar
@@ -38,6 +41,33 @@ public interface TemplateEngine extends MessageEncoder {
3841
return output.getBytes(StandardCharsets.UTF_8);
3942
}
4043

44+
/**
45+
* True if the template engine is able to render the given view. This method checks if the view
46+
* name matches one of the {@link #extensions()}.
47+
*
48+
* @param modelAndView View to check.
49+
* @return True when view is supported.
50+
*/
51+
default boolean supports(@Nonnull ModelAndView modelAndView) {
52+
String view = modelAndView.view;
53+
for (String extension : extensions()) {
54+
if (view.endsWith(extension)) {
55+
return true;
56+
}
57+
}
58+
return false;
59+
}
60+
61+
/**
62+
* Number of file extensions supported by the template engine. Default is <code>.html</code>.
63+
*
64+
* @return Number of file extensions supported by the template engine.
65+
* Default is <code>.html</code>.
66+
*/
67+
default @Nonnull List<String> extensions() {
68+
return Collections.singletonList(".html");
69+
}
70+
4171
/**
4272
* Normalize a template path by removing the leading `/` when present.
4373
*

jooby/src/main/java/io/jooby/internal/CompositeMessageEncoder.java

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,6 @@
1010
import io.jooby.MessageEncoder;
1111
import io.jooby.ModelAndView;
1212
import io.jooby.TemplateEngine;
13-
import io.jooby.internal.handler.SendAttachment;
14-
import io.jooby.internal.handler.SendByteArray;
15-
import io.jooby.internal.handler.SendByteBuf;
16-
import io.jooby.internal.handler.SendByteBuffer;
17-
import io.jooby.internal.handler.SendCharSequence;
18-
import io.jooby.internal.handler.SendDirect;
19-
import io.jooby.internal.handler.SendFileChannel;
20-
import io.jooby.internal.handler.SendStream;
2113
import io.netty.buffer.ByteBuf;
2214

2315
import javax.annotation.Nonnull;
@@ -27,45 +19,34 @@
2719
import java.nio.channels.FileChannel;
2820
import java.nio.charset.StandardCharsets;
2921
import java.nio.file.Path;
22+
import java.util.ArrayList;
3023
import java.util.Iterator;
31-
import java.util.LinkedList;
24+
import java.util.List;
3225

3326
public class CompositeMessageEncoder implements MessageEncoder {
3427

35-
private LinkedList<MessageEncoder> list = new LinkedList<>();
28+
private List<MessageEncoder> decoders = new ArrayList<>(2);
3629

37-
private MessageEncoder templateEngine;
38-
39-
public CompositeMessageEncoder() {
40-
list.add(MessageEncoder.TO_STRING);
41-
}
30+
private List<TemplateEngine> templateEngine = new ArrayList<>(2);
4231

4332
public CompositeMessageEncoder add(MessageEncoder encoder) {
4433
if (encoder instanceof TemplateEngine) {
45-
templateEngine = computeRenderer(templateEngine, encoder);
34+
templateEngine.add((TemplateEngine) encoder);
4635
} else {
47-
list.addFirst(encoder);
36+
decoders.add(encoder);
4837
}
4938
return this;
5039
}
5140

52-
private MessageEncoder computeRenderer(MessageEncoder it, MessageEncoder next) {
53-
if (it == null) {
54-
return next;
55-
} else if (it instanceof CompositeMessageEncoder) {
56-
((CompositeMessageEncoder) it).list.addFirst(next);
57-
return it;
58-
} else {
59-
CompositeMessageEncoder composite = new CompositeMessageEncoder();
60-
composite.list.addFirst(it);
61-
composite.list.addFirst(next);
62-
return composite;
63-
}
64-
}
65-
6641
@Override public byte[] encode(@Nonnull Context ctx, @Nonnull Object value) throws Exception {
6742
if (value instanceof ModelAndView) {
68-
return templateEngine.encode(ctx, value);
43+
ModelAndView modelAndView = (ModelAndView) value;
44+
for (TemplateEngine engine : templateEngine) {
45+
if (engine.supports(modelAndView)) {
46+
return engine.encode(ctx, modelAndView);
47+
}
48+
}
49+
throw new IllegalArgumentException("No template engine for: " + modelAndView.view);
6950
}
7051
/** InputStream: */
7152
if (value instanceof InputStream) {
@@ -109,8 +90,7 @@ private MessageEncoder computeRenderer(MessageEncoder it, MessageEncoder next) {
10990
ctx.send((ByteBuf) value);
11091
return null;
11192
}
112-
/** Fallback to more complex encoder: */
113-
Iterator<MessageEncoder> iterator = list.iterator();
93+
Iterator<MessageEncoder> iterator = decoders.iterator();
11494
/** NOTE: looks like an infinite loop but there is a default renderer at the end of iterator. */
11595
byte[] result = null;
11696
while (result == null) {

jooby/src/main/java/io/jooby/internal/RouterImpl.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import io.jooby.SessionOptions;
2424
import io.jooby.StatusCode;
2525
import io.jooby.SneakyThrows;
26+
import io.jooby.TemplateEngine;
2627
import io.jooby.annotations.Dispatch;
2728
import io.jooby.internal.asm.ClassSource;
2829
import io.jooby.internal.mvc.MvcAnnotationParser;
@@ -245,6 +246,11 @@ public <T> Router mvc(@Nonnull Class<T> router, @Nonnull Provider<T> provider) {
245246

246247
@Nonnull @Override
247248
public Router encoder(@Nonnull MediaType contentType, @Nonnull MessageEncoder encoder) {
249+
if (encoder instanceof TemplateEngine) {
250+
// Mime-Type is ignored for TemplateEngine due they depends on specific object type and file
251+
// extension.
252+
return encoder(encoder);
253+
}
248254
return encoder(encoder.accept(contentType));
249255
}
250256

@@ -377,6 +383,7 @@ private Route defineRoute(@Nonnull String method, @Nonnull String pattern,
377383
if (err == null) {
378384
err = ErrorHandler.DEFAULT;
379385
}
386+
renderer.add(MessageEncoder.TO_STRING);
380387
ExecutionMode mode = owner.getExecutionMode();
381388
for (Route route : routes) {
382389
Executor executor = routeExecutor.get(route);
@@ -392,6 +399,8 @@ private Route defineRoute(@Nonnull String method, @Nonnull String pattern,
392399
Route.Handler pipeline = Pipeline
393400
.compute(source.getLoader(), route, mode, executor, handlers);
394401
route.setPipeline(pipeline);
402+
/** Final render */
403+
route.setEncoder(renderer);
395404
}
396405
// router options
397406
if (options.isIgnoreCase() || options.isIgnoreTrailingSlash()) {

0 commit comments

Comments
 (0)