HTTP router handlers
The HTTP router instance of the startHttpServer
method points to different handlers based on URL patterns and HTTP methods. Each handler deals with an HTTP request, performs a database query, and renders HTML from a FreeMarker template.
Index page handler
The index page provides a list of pointers to all wiki entries and a field to create a new one:
The implementation is a straightforward select *
SQL query where data is then passed to the FreeMarker engine to render the HTML response.
The indexHandler
method code is as follows:
private void indexHandler(RoutingContext context) {
dbClient.getConnection(car -> {
if (car.succeeded()) {
SQLConnection connection = car.result();
connection.query(SQL_ALL_PAGES, res -> {
connection.close();
if (res.succeeded()) {
List<String> pages = res.result() (1)
.getResults()
.stream()
.map(json -> json.getString(0))
.sorted()
.collect(Collectors.toList());
context.put("title", "Wiki home"); (2)
context.put("pages", pages);
templateEngine.render(context.data(), "templates/index.ftl", ar -> { (3)
if (ar.succeeded()) {
context.response().putHeader("Content-Type", "text/html");
context.response().end(ar.result()); (4)
} else {
context.fail(ar.cause());
}
});
} else {
context.fail(res.cause()); (5)
}
});
} else {
context.fail(car.cause());
}
});
}
SQL query results are being returned as instances of
JsonArray
andJsonObject
.The
RoutingContext
instance can be used to put arbitrary key / value data that is then available from templates, or chained router handlers.Rendering a template is an asynchronous operation that leads us to the usual
AsyncResult
handling pattern.The
AsyncResult
contains the template rendering as aString
in case of success, and we can end the HTTP response stream with the value.In case of failure the
fail
method fromRoutingContext
provides a sensible way to return a HTTP 500 error to the HTTP client.
FreeMarker templates shall be placed in the src/main/resources/templates
folder. The index.ftl
template code is as follows:
<#include "header.ftl">
<div class="row">
<div class="col-md-12 mt-1">
<div class="float-right">
<form class="form-inline" action="/create" method="post">
<div class="form-group">
<input type="text" class="form-control" id="name" name="name" placeholder="New page name">
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
</div>
<h1 class="display-4">${title}</h1>
</div>
<div class="col-md-12 mt-1">
<#list pages>
<h2>Pages:</h2>
<ul>
<#items as page>
<li><a href="/wiki/${page}">${page}</a></li>
</#items>
</ul>
<#else>
<p>The wiki is currently empty!</p>
</#list>
</div>
</div>
<#include "footer.ftl">
Key / value data stored in the RoutingContext
object is made available as FreeMarker variable.
Since lots of templates have common header and footers, we extracted the following code in header.ftl
and footer.ftl
:
header.ftl
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
crossorigin="anonymous">
<title>${title} | A Sample Vert.x-powered Wiki</title>
</head>
<body>
<div class="container">
footer.ftl
</div> <!-- .container -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
crossorigin="anonymous"></script>
</body>
</html>
Wiki page rendering handler
This handler deals with HTTP GET requests to have wiki pages being rendered, as in:
The page also provides a button to edit the content in Markdown. Instead of having a separate handler and template, we simply rely on JavaScript and CSS to toggle the editor on and off when the button is being clicked:
The pageRenderingHandler
method code is the following:
private static final String EMPTY_PAGE_MARKDOWN =
"# A new page\n" +
"\n" +
"Feel-free to write in Markdown!\n";
private void pageRenderingHandler(RoutingContext context) {
String page = context.request().getParam("page"); (1)
dbClient.getConnection(car -> {
if (car.succeeded()) {
SQLConnection connection = car.result();
connection.queryWithParams(SQL_GET_PAGE, new JsonArray().add(page), fetch -> { (2)
connection.close();
if (fetch.succeeded()) {
JsonArray row = fetch.result().getResults()
.stream()
.findFirst()
.orElseGet(() -> new JsonArray().add(-1).add(EMPTY_PAGE_MARKDOWN));
Integer id = row.getInteger(0);
String rawContent = row.getString(1);
context.put("title", page);
context.put("id", id);
context.put("newPage", fetch.result().getResults().size() == 0 ? "yes" : "no");
context.put("rawContent", rawContent);
context.put("content", Processor.process(rawContent)); (3)
context.put("timestamp", new Date().toString());
templateEngine.render(context.data(), "templates/page.ftl", ar -> {
if (ar.succeeded()) {
context.response().putHeader("Content-Type", "text/html");
context.response().end(ar.result());
} else {
context.fail(ar.cause());
}
});
} else {
context.fail(fetch.cause());
}
});
} else {
context.fail(car.cause());
}
});
}
URL parameters (
/wiki/:page
here) can be accessed through the context request object.Passing argument values to SQL queries is done using a
JsonArray
, with the elements in order of the?
symbols in the SQL query.The
Processor
class comes from the txtmark Markdown rendering library that we use.
The page.ftl
FreeMarker template code is as follows:
<#include "header.ftl">
<div class="row">
<div class="col-md-12 mt-1">
<span class="float-right">
<a class="btn btn-outline-primary" href="/" role="button" aria-pressed="true">Home</a>
<button class="btn btn-outline-warning" type="button" data-toggle="collapse"
data-target="#editor" aria-expanded="false" aria-controls="editor">Edit</button>
</span>
<h1 class="display-4">
<span class="text-muted">{</span>
${title}
<span class="text-muted">}</span>
</h1>
</div>
<div class="col-md-12 mt-1 clearfix">
${content}
</div>
<div class="col-md-12 collapsable collapse clearfix" id="editor">
<form action="/save" method="post">
<div class="form-group">
<input type="hidden" name="id" value="${id}">
<input type="hidden" name="title" value="${title}">
<input type="hidden" name="newPage" value="${newPage}">
<textarea class="form-control" id="markdown" name="markdown" rows="15">${rawContent}</textarea>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<#if id != -1>
<button type="submit" formaction="/delete" class="btn btn-danger float-right">Delete</button>
</#if>
</form>
</div>
<div class="col-md-12 mt-1">
<hr class="mt-1">
<p class="small">Rendered: ${timestamp}</p>
</div>
</div>
<#include "footer.ftl">
Page creation handler
The index page offers a field to create a new page, and its surrounding HTML form points to a URL that is being managed by this handler. The strategy isn’t actually to create a new entry in the database, but simply to redirect to a wiki page URL with the name to create. Since the wiki page doesn’t exist, the pageRenderingHandler
method will use a default text for new pages, and a user can eventually create that page by editing and then saving it.
The handler is the pageCreateHandler
method, and its implementation is a redirection through an HTTP 303 status code:
private void pageCreateHandler(RoutingContext context) {
String pageName = context.request().getParam("name");
String location = "/wiki/" + pageName;
if (pageName == null || pageName.isEmpty()) {
location = "/";
}
context.response().setStatusCode(303);
context.response().putHeader("Location", location);
context.response().end();
}
Page saving handler
The pageUpdateHandler
method deals with HTTP POST requests when saving a wiki page. This happens both when updating an existing page (issuing a SQL update
query) or saving a new page (issuing a SQL insert
query):
private void pageUpdateHandler(RoutingContext context) {
String id = context.request().getParam("id"); (1)
String title = context.request().getParam("title");
String markdown = context.request().getParam("markdown");
boolean newPage = "yes".equals(context.request().getParam("newPage")); (2)
dbClient.getConnection(car -> {
if (car.succeeded()) {
SQLConnection connection = car.result();
String sql = newPage ? SQL_CREATE_PAGE : SQL_SAVE_PAGE;
JsonArray params = new JsonArray(); (3)
if (newPage) {
params.add(title).add(markdown);
} else {
params.add(markdown).add(id);
}
connection.updateWithParams(sql, params, res -> { (4)
connection.close();
if (res.succeeded()) {
context.response().setStatusCode(303); (5)
context.response().putHeader("Location", "/wiki/" + title);
context.response().end();
} else {
context.fail(res.cause());
}
});
} else {
context.fail(car.cause());
}
});
}
Form parameters sent through a HTTP POST request are available from the
RoutingContext
object. Note that without aBodyHandler
within theRouter
configuration chain these values would not be available, and the form submission payload would need to be manually decoded from the HTTP POST request payload.We rely on a hidden form field rendered in the
page.ftl
FreeMarker template to know if we are updating an existing page or saving a new page.Again, preparing the SQL query with parameters uses a
JsonArray
to pass values.The
updateWithParams
method is used forinsert
/update
/delete
SQL queries.Upon success, we simply redirect to the page that has been edited.
Page deletion handler
The implementation of the pageDeletionHandler
method is straightforward: given a wiki entry identifier, it issues a delete
SQL query then redirects to the wiki index page:
private void pageDeletionHandler(RoutingContext context) {
String id = context.request().getParam("id");
dbClient.getConnection(car -> {
if (car.succeeded()) {
SQLConnection connection = car.result();
connection.updateWithParams(SQL_DELETE_PAGE, new JsonArray().add(id), res -> {
connection.close();
if (res.succeeded()) {
context.response().setStatusCode(303);
context.response().putHeader("Location", "/");
context.response().end();
} else {
context.fail(res.cause());
}
});
} else {
context.fail(car.cause());
}
});
}