Wiki verticle initialization phases

To get our wiki running, we need to perform a 2-phases initialization:

  1. we need to establish a JDBC database connection, and also make sure that the database schema is in place, and

  2. we need to start a HTTP server for the web application.

Each phase can fail (e.g., the HTTP server TCP port is already being used), and they should not run in parallel as the web application code first needs the database access to work.

To make our code cleaner we will define 1 method per phase, and adopt a pattern of returning a future object to notify when each of the phases completes, and whether it did so successfully or not:

  1. private Future<Void> prepareDatabase() {
  2. Promise<void> promise = Promise.promise();
  3. // (...)
  4. return promise.future();
  5. }
  6. private Future<Void> startHttpServer() {
  7. Promise<void> promise = Promise.promise();
  8. // (...)
  9. return promise.future();
  10. }

By having each method returning a future object, the implementation of the start method becomes a composition:

  1. @Override
  2. public void start(Promise<Void> promise) throws Exception {
  3. Future<Void> steps = prepareDatabase().compose(v -> startHttpServer());
  4. steps.setHandler(promise);
  5. }

When the future of prepareDatabase completes successfully, then startHttpServer is called and the steps future completes depending of the outcome of the future returned by startHttpServer. startHttpServer is never called if prepareDatabase encounters an error, in which case the steps future is in a failed state and becomes completed with the exception describing the error.

Eventually steps completes: setHandler defines a handler to be called upon completion. In our case we simply want to complete startFuture with steps and use the completer method to obtain a handler. This is equivalent to:

  1. Future<Void> steps = prepareDatabase().compose(v -> startHttpServer());
  2. steps.setHandler(ar -> { (1)
  3. if (ar.succeeded()) {
  4. promise.complete();
  5. } else {
  6. promise.fail(ar.cause());
  7. }
  8. });
  1. ar is of type AsyncResult<Void>. AsyncResult<T> is used to pass the result of an asynchronous processing and may either yield a value of type T on success or a failure exception if the processing failed.

Database initialization

The wiki database schema consists of a single table Pages with the following columns:

ColumnTypeDescription

Id

Integer

Primary key

Name

Characters

Name of a wiki page, must be unique

Content

Text

Markdown text of a wiki page

The database operations will be typical create, read, update, delete operations. To get us started, we simply store the corresponding SQL queries as static fields of the MainVerticle class. Note that they are written in a SQL dialect that HSQLDB understands, but that other relational databases may not necessarily support:

  1. private static final String SQL_CREATE_PAGES_TABLE = "create table if not exists Pages (Id integer identity primary key, Name varchar(255) unique, Content clob)";
  2. private static final String SQL_GET_PAGE = "select Id, Content from Pages where Name = ?"; (1)
  3. private static final String SQL_CREATE_PAGE = "insert into Pages values (NULL, ?, ?)";
  4. private static final String SQL_SAVE_PAGE = "update Pages set Content = ? where Id = ?";
  5. private static final String SQL_ALL_PAGES = "select Name from Pages";
  6. private static final String SQL_DELETE_PAGE = "delete from Pages where Id = ?";
  1. The ? in the queries are placeholders to pass data when executing queries, and the Vert.x JDBC client prevents from SQL injections.

The application verticle needs to keep a reference to a JDBCClient object (from the io.vertx.ext.jdbc package) that serves as the connection to the database. We do so using a field in MainVerticle, and we also create a general-purpose logger from the org.slf4j package:

  1. private JDBCClient dbClient;
  2. private static final Logger LOGGER = LoggerFactory.getLogger(MainVerticle.class);

Last but not least, here is the complete implementation of the prepareDatabase method. It attempts to obtain a JDBC client connection, then performs a SQL query to create the Pages table unless it already exists:

  1. private Future<Void> prepareDatabase() {
  2. Promise<Void> promise = Promise.promise();
  3. dbClient = JDBCClient.createShared(vertx, new JsonObject() (1)
  4. .put("url", "jdbc:hsqldb:file:db/wiki") (2)
  5. .put("driver_class", "org.hsqldb.jdbcDriver") (3)
  6. .put("max_pool_size", 30)); (4)
  7. dbClient.getConnection(ar -> { (5)
  8. if (ar.failed()) {
  9. LOGGER.error("Could not open a database connection", ar.cause());
  10. promise.fail(ar.cause()); (6)
  11. } else {
  12. SQLConnection connection = ar.result(); (7)
  13. connection.execute(SQL_CREATE_PAGES_TABLE, create -> {
  14. connection.close(); (8)
  15. if (create.failed()) {
  16. LOGGER.error("Database preparation error", create.cause());
  17. promise.fail(create.cause());
  18. } else {
  19. promise.complete(); (9)
  20. }
  21. });
  22. }
  23. });
  24. return promise.future();
  25. }
  1. createShared creates a shared connection to be shared among verticles known to the vertx instance, which in general is a good thing.

  2. The JDBC client connection is made by passing a Vert.x JSON object. Here url is the JDBC URL.

  3. Just like url, driver_class is specific to the JDBC driver being used and points to the driver class.

  4. max_pool_size is the number of concurrent connections. We chose 30 here, but it is just an arbitrary number.

  5. Getting a connection is an asynchronous operation that gives us an AsyncResult<SQLConnection>. It must then be tested to see if the connection could be established or not (AsyncResult is actually a super-interface of Future).

  6. If the SQL connection could not be obtained, then the method future is completed to fail with the AsyncResult-provided exception via the cause method.

  7. The SQLConnection is the result of the successful AsyncResult. We can use it to perform a SQL query.

  8. Before checking whether the SQL query succeeded or not, we must release it by calling close, otherwise the JDBC client connection pool can eventually drain.

  9. We complete the method future object with a success.

Tip
The SQL database modules supported by the Vert.x project do not currently offer anything beyond passing SQL queries (e.g., an object-relational mapper) as they focus on providing asynchronous access to databases. However, nothing forbids using more advanced modules from the community, and we especially recommend checking out projects like this jOOq generator for Vert.x or this POJO mapper.

Notes about logging

The previous subsection introduced a logger, and we opted for the SLF4J library. Vert.x is also unopinionated on logging: you can choose any popular Java logging library. We recommend SLF4J since it is a popular logging abstraction and unification library in the Java ecosystem.

We also recommend using Logback as a logger implementation. Integrating both SLF4J and Logback can be done by adding two dependencies, or just logback-classic that points to both libraries (incidentally they are from the same author):

  1. <dependency>
  2. <groupId>ch.qos.logback</groupId>
  3. <artifactId>logback-classic</artifactId>
  4. <version>1.2.3</version>
  5. </dependency>

By default SLF4J outputs many log events to the console from Vert.x, Netty, C3PO and the wiki application. We can reduce the verbosity by adding the a src/main/resources/logback.xml configuration file (see https://logback.qos.ch/ for more details):

  1. <configuration>
  2. <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
  3. <encoder>
  4. <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
  5. </encoder>
  6. </appender>
  7. <logger name="com.mchange.v2" level="warn"/>
  8. <logger name="io.netty" level="warn"/>
  9. <logger name="io.vertx" level="info"/>
  10. <logger name="io.vertx.guides.wiki" level="debug"/>
  11. <root level="debug">
  12. <appender-ref ref="STDOUT"/>
  13. </root>
  14. </configuration>

Last but not least HSQLDB does not integrate well with loggers when embedded. By default it tries to reconfigure the logging system in place, so we need to disable it by passing a -Dhsqldb.reconfig_logging=false property to the Java Virtual Machine when executing applications.

HTTP server initialization

The HTTP server makes use of the vertx-web project to easily define dispatching routes for incoming HTTP requests. Indeed, the Vert.x core API allows to start HTTP servers and listen for incoming connections, but it does not provide any facility to, say, have different handlers depending on the requested URL or processing request bodies. This is the role of a router as it dispatches requests to different processing handlers depending on the URL, the HTTP method, etc.

The initialization consists in setting up a request router, then starting the HTTP server:

  1. private FreeMarkerTemplateEngine templateEngine;
  2. private Future<Void> startHttpServer() {
  3. Promise<Void> promise = Promise.promise();
  4. HttpServer server = vertx.createHttpServer(); (1)
  5. Router router = Router.router(vertx); (2)
  6. router.get("/").handler(this::indexHandler);
  7. router.get("/wiki/:page").handler(this::pageRenderingHandler); (3)
  8. router.post().handler(BodyHandler.create()); (4)
  9. router.post("/save").handler(this::pageUpdateHandler);
  10. router.post("/create").handler(this::pageCreateHandler);
  11. router.post("/delete").handler(this::pageDeletionHandler);
  12. templateEngine = FreeMarkerTemplateEngine.create(vertx);
  13. server
  14. .requestHandler(router) (5)
  15. .listen(8080, ar -> { (6)
  16. if (ar.succeeded()) {
  17. LOGGER.info("HTTP server running on port 8080");
  18. promise.complete();
  19. } else {
  20. LOGGER.error("Could not start a HTTP server", ar.cause());
  21. promise.fail(ar.cause());
  22. }
  23. });
  24. return promise.future();
  25. }
  1. The vertx context object provides methods to create HTTP servers, clients, TCP/UDP servers and clients, etc.

  2. The Router class comes from vertx-web: io.vertx.ext.web.Router.

  3. Routes have their own handlers, and they can be defined by URL and/or by HTTP method. For short handlers a Java lambda is an option, but for more elaborate handlers it is a good idea to reference private methods instead. Note that URLs can be parametric: /wiki/:page will match a request like /wiki/Hello, in which case a page parameter will be available with value Hello.

  4. This makes all HTTP POST requests go through a first handler, here io.vertx.ext.web.handler.BodyHandler. This handler automatically decodes the body from the HTTP requests (e.g., form submissions), which can then be manipulated as Vert.x buffer objects.

  5. The router object can be used as a HTTP server handler, which then dispatches to other handlers as defined above.

  6. Starting a HTTP server is an asynchronous operation, so an AsyncResult<HttpServer> needs to be checked for success. By the way the 8080 parameter specifies the TCP port to be used by the server.