Simple web application with template engine integration

Introduction

In this example you can learn how to implement template engines in DataKernel applications by following the example of creating a simple Poll app. This app creates new polls with a custom title, description and options. Creating a poll generates its unique link that leads to a page where you can vote.

See how simple it is to implement such features using DataKernel’s HTTP module: the embedded application server has only about 100 lines of code with no additional xml configurations. In this example we used Mustache as a template engine.

Here we will consider only the ApplicationLauncher class, which is the main class of the application. You can find full example sources on GitHub.

Creating launcher

The ApplicationLauncher launches our application, takes care of routing and proper content generation on HTML pages. We will extend DataKernel’s HttpServerLauncher to manage the application’s lifecycle:

Note: In this example we omit error handling to keep everything brief and simple.
public final class ApplicationLauncher extends HttpServerLauncher {

	private static ByteBuf applyTemplate(Mustache mustache, Map<String, Object> scopes) {
		ByteBufWriter writer = new ByteBufWriter();
		mustache.execute(writer, scopes);
		return writer.getBuf();
	}

	@Provides
	PollDao pollRepo() {
		return new PollDaoImpl();
	}


}

Let’s have a closer look at the launcher. It contains two methods:

  • applyTemplate(Mustache mustache, Map<String, Object> scopes) fills the provided Mustache template with the given data through a ByteBuf - a more efficient implementation of ByteBuffer;
  • pollRepo() provides the business logic of our app. The @Provides annotation means it’s done through DI.

Next, we provide AsyncServlet:

@Provides
AsyncServlet servlet(PollDao pollDao) {
	Mustache singlePollView = new DefaultMustacheFactory().compile("templates/singlePollView.html");
	Mustache singlePollCreate = new DefaultMustacheFactory().compile("templates/singlePollCreate.html");
	Mustache listPolls = new DefaultMustacheFactory().compile("templates/listPolls.html");

	return RoutingServlet.create()
			.map(GET, "/", request -> HttpResponse.ok200()
					.withBody(applyTemplate(listPolls, map("polls", pollDao.findAll().entrySet()))))

}

In the AsyncServlet we create three Mustache objects, one for each HTML page. To define routing, we create a RoutingServlet. You may notice that the routing approach resembles Express. In the example above we’ve added the request to the homepage by using the map method.

Method map(@Nullable HttpMethod method, String path, AsyncServlet servlet) adds a route to the RoutingServlet:

  • method is one of the HTTP methods (GET, POST and so on)
  • path is the path on the server
  • servlet defines the logic of request processing. If you need to get some data from the request while processing you can use:
    • request.getPathParameter(String key)/request.getQueryParameter(String key) (see example of query parameter usage to provide the key of the needed parameter and receive back a corresponding String
    • request.getPostParameters() to get a Map of all request parameters

In this request we get all current polls and info about them in order to generate listPolls page.

Let’s add one more request:

.map(GET, "/poll/:id", request -> {
	int id = Integer.parseInt(request.getPathParameter("id"));
	return HttpResponse.ok200()
			.withBody(applyTemplate(singlePollView, map("id", id, "poll", pollDao.find(id))));
})

This request returns a page with a poll which id was specified in the path. Pay attention to the provided path /poll/:id syntax. : states that the following characters until the next / are a variable; in this case, its keyword is id. This way you don’t have to map each poll’s id to a different request.

The next requests with /create, /vote, /add and /delete paths take care of providing a page for creating new polls, voting, adding created polls to the pollDao and deleting them from it respectively:

.map(GET, "/create", request ->
		HttpResponse.ok200()
				.withBody(applyTemplate(singlePollCreate, emptyMap())))
.map(POST, "/vote", loadBody()
		.serve(request -> {
			Map<String, String> params = request.getPostParameters();
			String option = params.get("option");
			String stringId = params.get("id");
			if (option == null || stringId == null) {
				return Promise.of(HttpResponse.ofCode(401));
			}

			int id = Integer.parseInt(stringId);
			PollDao.Poll question = pollDao.find(id);

			question.vote(option);

			return HttpResponse.redirect302(nullToDefault(request.getHeader(REFERER), "/"));
		}))
.map(POST, "/add", loadBody()
		.serve(request -> {
			Map<String, String> params = request.getPostParameters();
			String title = params.get("title");
			String message = params.get("message");

			String option1 = params.get("option1");
			String option2 = params.get("option2");

			int id = pollDao.add(new PollDao.Poll(title, message, list(option1, option2)));
			return HttpResponse.redirect302("poll/" + id);
		}))
.map(POST, "/delete", loadBody()
		.serve(request -> {
			Map<String, String> params = request.getPostParameters();
			String id = params.get("id");
			if (id == null) {
				return Promise.of(HttpResponse.ofCode(401));
			}
			pollDao.remove(Integer.parseInt(id));

			return HttpResponse.redirect302("/");
		}));

Also, we defined main() method which will start our launcher:

public static void main(String[] args) throws Exception {
	Launcher launcher = new ApplicationLauncher();
	launcher.launch(args);
}

And that’s it, we have a full-functioning poll application!

Running the application

If you want to run the example, you need to clone DataKernel and import it as a Maven project. Check out branch v3.1. Before running the example, build the project (Ctrl + F9 for IntelliJ IDEA). Open ApplicationLauncher class and run its main() method. Then open your favourite browser and go to localhost:8080.