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:

Wiki index

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:

  1. private void indexHandler(RoutingContext context) {
  2. dbClient.getConnection(car -> {
  3. if (car.succeeded()) {
  4. SQLConnection connection = car.result();
  5. connection.query(SQL_ALL_PAGES, res -> {
  6. connection.close();
  7. if (res.succeeded()) {
  8. List<String> pages = res.result() (1)
  9. .getResults()
  10. .stream()
  11. .map(json -> json.getString(0))
  12. .sorted()
  13. .collect(Collectors.toList());
  14. context.put("title", "Wiki home"); (2)
  15. context.put("pages", pages);
  16. templateEngine.render(context.data(), "templates/index.ftl", ar -> { (3)
  17. if (ar.succeeded()) {
  18. context.response().putHeader("Content-Type", "text/html");
  19. context.response().end(ar.result()); (4)
  20. } else {
  21. context.fail(ar.cause());
  22. }
  23. });
  24. } else {
  25. context.fail(res.cause()); (5)
  26. }
  27. });
  28. } else {
  29. context.fail(car.cause());
  30. }
  31. });
  32. }
  1. SQL query results are being returned as instances of JsonArray and JsonObject.

  2. The RoutingContext instance can be used to put arbitrary key / value data that is then available from templates, or chained router handlers.

  3. Rendering a template is an asynchronous operation that leads us to the usual AsyncResult handling pattern.

  4. The AsyncResult contains the template rendering as a String in case of success, and we can end the HTTP response stream with the value.

  5. In case of failure the fail method from RoutingContext 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:

  1. <#include "header.ftl">
  2. <div class="row">
  3. <div class="col-md-12 mt-1">
  4. <div class="float-right">
  5. <form class="form-inline" action="/create" method="post">
  6. <div class="form-group">
  7. <input type="text" class="form-control" id="name" name="name" placeholder="New page name">
  8. </div>
  9. <button type="submit" class="btn btn-primary">Create</button>
  10. </form>
  11. </div>
  12. <h1 class="display-4">${title}</h1>
  13. </div>
  14. <div class="col-md-12 mt-1">
  15. <#list pages>
  16. <h2>Pages:</h2>
  17. <ul>
  18. <#items as page>
  19. <li><a href="/wiki/${page}">${page}</a></li>
  20. </#items>
  21. </ul>
  22. <#else>
  23. <p>The wiki is currently empty!</p>
  24. </#list>
  25. </div>
  26. </div>
  27. <#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

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  6. <meta http-equiv="x-ua-compatible" content="ie=edge">
  7. <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
  8. rel="stylesheet"
  9. integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
  10. crossorigin="anonymous">
  11. <title>${title} | A Sample Vert.x-powered Wiki</title>
  12. </head>
  13. <body>
  14. <div class="container">

footer.ftl

  1. </div> <!-- .container -->
  2. <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
  3. integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
  4. crossorigin="anonymous"></script>
  5. <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
  6. integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
  7. crossorigin="anonymous"></script>
  8. <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
  9. integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
  10. crossorigin="anonymous"></script>
  11. </body>
  12. </html>

Wiki page rendering handler

This handler deals with HTTP GET requests to have wiki pages being rendered, as in:

Wiki page

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:

Wiki editor

The pageRenderingHandler method code is the following:

  1. private static final String EMPTY_PAGE_MARKDOWN =
  2. "# A new page\n" +
  3. "\n" +
  4. "Feel-free to write in Markdown!\n";
  5. private void pageRenderingHandler(RoutingContext context) {
  6. String page = context.request().getParam("page"); (1)
  7. dbClient.getConnection(car -> {
  8. if (car.succeeded()) {
  9. SQLConnection connection = car.result();
  10. connection.queryWithParams(SQL_GET_PAGE, new JsonArray().add(page), fetch -> { (2)
  11. connection.close();
  12. if (fetch.succeeded()) {
  13. JsonArray row = fetch.result().getResults()
  14. .stream()
  15. .findFirst()
  16. .orElseGet(() -> new JsonArray().add(-1).add(EMPTY_PAGE_MARKDOWN));
  17. Integer id = row.getInteger(0);
  18. String rawContent = row.getString(1);
  19. context.put("title", page);
  20. context.put("id", id);
  21. context.put("newPage", fetch.result().getResults().size() == 0 ? "yes" : "no");
  22. context.put("rawContent", rawContent);
  23. context.put("content", Processor.process(rawContent)); (3)
  24. context.put("timestamp", new Date().toString());
  25. templateEngine.render(context.data(), "templates/page.ftl", ar -> {
  26. if (ar.succeeded()) {
  27. context.response().putHeader("Content-Type", "text/html");
  28. context.response().end(ar.result());
  29. } else {
  30. context.fail(ar.cause());
  31. }
  32. });
  33. } else {
  34. context.fail(fetch.cause());
  35. }
  36. });
  37. } else {
  38. context.fail(car.cause());
  39. }
  40. });
  41. }
  1. URL parameters (/wiki/:page here) can be accessed through the context request object.

  2. Passing argument values to SQL queries is done using a JsonArray, with the elements in order of the ? symbols in the SQL query.

  3. The Processor class comes from the txtmark Markdown rendering library that we use.

The page.ftl FreeMarker template code is as follows:

  1. <#include "header.ftl">
  2. <div class="row">
  3. <div class="col-md-12 mt-1">
  4. <span class="float-right">
  5. <a class="btn btn-outline-primary" href="/" role="button" aria-pressed="true">Home</a>
  6. <button class="btn btn-outline-warning" type="button" data-toggle="collapse"
  7. data-target="#editor" aria-expanded="false" aria-controls="editor">Edit</button>
  8. </span>
  9. <h1 class="display-4">
  10. <span class="text-muted">{</span>
  11. ${title}
  12. <span class="text-muted">}</span>
  13. </h1>
  14. </div>
  15. <div class="col-md-12 mt-1 clearfix">
  16. ${content}
  17. </div>
  18. <div class="col-md-12 collapsable collapse clearfix" id="editor">
  19. <form action="/save" method="post">
  20. <div class="form-group">
  21. <input type="hidden" name="id" value="${id}">
  22. <input type="hidden" name="title" value="${title}">
  23. <input type="hidden" name="newPage" value="${newPage}">
  24. <textarea class="form-control" id="markdown" name="markdown" rows="15">${rawContent}</textarea>
  25. </div>
  26. <button type="submit" class="btn btn-primary">Save</button>
  27. <#if id != -1>
  28. <button type="submit" formaction="/delete" class="btn btn-danger float-right">Delete</button>
  29. </#if>
  30. </form>
  31. </div>
  32. <div class="col-md-12 mt-1">
  33. <hr class="mt-1">
  34. <p class="small">Rendered: ${timestamp}</p>
  35. </div>
  36. </div>
  37. <#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:

  1. private void pageCreateHandler(RoutingContext context) {
  2. String pageName = context.request().getParam("name");
  3. String location = "/wiki/" + pageName;
  4. if (pageName == null || pageName.isEmpty()) {
  5. location = "/";
  6. }
  7. context.response().setStatusCode(303);
  8. context.response().putHeader("Location", location);
  9. context.response().end();
  10. }

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):

  1. private void pageUpdateHandler(RoutingContext context) {
  2. String id = context.request().getParam("id"); (1)
  3. String title = context.request().getParam("title");
  4. String markdown = context.request().getParam("markdown");
  5. boolean newPage = "yes".equals(context.request().getParam("newPage")); (2)
  6. dbClient.getConnection(car -> {
  7. if (car.succeeded()) {
  8. SQLConnection connection = car.result();
  9. String sql = newPage ? SQL_CREATE_PAGE : SQL_SAVE_PAGE;
  10. JsonArray params = new JsonArray(); (3)
  11. if (newPage) {
  12. params.add(title).add(markdown);
  13. } else {
  14. params.add(markdown).add(id);
  15. }
  16. connection.updateWithParams(sql, params, res -> { (4)
  17. connection.close();
  18. if (res.succeeded()) {
  19. context.response().setStatusCode(303); (5)
  20. context.response().putHeader("Location", "/wiki/" + title);
  21. context.response().end();
  22. } else {
  23. context.fail(res.cause());
  24. }
  25. });
  26. } else {
  27. context.fail(car.cause());
  28. }
  29. });
  30. }
  1. Form parameters sent through a HTTP POST request are available from the RoutingContext object. Note that without a BodyHandler within the Router 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.

  2. 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.

  3. Again, preparing the SQL query with parameters uses a JsonArray to pass values.

  4. The updateWithParams method is used for insert / update / delete SQL queries.

  5. 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:

  1. private void pageDeletionHandler(RoutingContext context) {
  2. String id = context.request().getParam("id");
  3. dbClient.getConnection(car -> {
  4. if (car.succeeded()) {
  5. SQLConnection connection = car.result();
  6. connection.updateWithParams(SQL_DELETE_PAGE, new JsonArray().add(id), res -> {
  7. connection.close();
  8. if (res.succeeded()) {
  9. context.response().setStatusCode(303);
  10. context.response().putHeader("Location", "/");
  11. context.response().end();
  12. } else {
  13. context.fail(res.cause());
  14. }
  15. });
  16. } else {
  17. context.fail(car.cause());
  18. }
  19. });
  20. }