pt.davidafsilva.ushortx.persistence.DatabaseVerticle.java Source code

Java tutorial

Introduction

Here is the source code for pt.davidafsilva.ushortx.persistence.DatabaseVerticle.java

Source

package pt.davidafsilva.ushortx.persistence;

/*
 * #%L
 * ushortx-persistence
 * %%
 * Copyright (C) 2015 David Silva
 * %%
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * #L%
 */

import java.util.Optional;
import java.util.function.BiFunction;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.eventbus.Message;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.ext.jdbc.JDBCClient;
import io.vertx.ext.sql.ResultSet;
import io.vertx.ext.sql.SQLConnection;
import io.vertx.ext.sql.UpdateResult;

/**
 * The persistence verticle with the URL mappings
 *
 * @author David Silva
 */
public class DatabaseVerticle extends AbstractVerticle {

    // the logger
    private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseVerticle.class);

    // the findById query
    private static final String FIND_BY_ID_QUERY = "SELECT ID,URL FROM urls WHERE id=?";
    // the findByUrl query
    private static final String FIND_BY_URL_QUERY = "SELECT ID,URL FROM urls WHERE url=?";
    // the insertUrl update statement
    private static final String INSERT_URL_STATEMENT = "INSERT INTO URLS(URL) VALUES(?)";
    // the create table statement
    private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE IF NOT EXISTS urls("
            + "ID BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, " + "URL VARCHAR(255) NOT NULL UNIQUE )";

    // the findById | findByUrl query result handler
    private static final BiFunction<Message<JsonObject>, SQLConnection, Handler<AsyncResult<ResultSet>>> FIND_QUERY_RESULT_HANDLER = (
            message, connection) -> dbResult -> {
                try {
                    if (dbResult.succeeded()) {
                        LOGGER.debug("find query results: " + dbResult.result().getRows());
                        if (dbResult.result().getNumRows() == 1) {
                            // extract the row
                            final JsonObject row = dbResult.result().getRows().get(0);

                            // create the reply json and reply
                            message.reply(
                                    new JsonObject().put("id", row.getLong("ID")).put("url", row.getString("URL")));
                        } else {
                            message.fail(4, "url not found");
                        }
                    } else {
                        message.fail(3, "internal database error");
                    }
                } finally {
                    connection.close();
                }
            };

    // the insertUrl query result handler
    private static final BiFunction<Message<JsonObject>, SQLConnection, Handler<AsyncResult<UpdateResult>>> INSERT_URL_RESULT_HANDLER = (
            message, connection) -> dbResult -> {
                try {
                    if (dbResult.succeeded()) {
                        LOGGER.debug("save statement result: " + dbResult.result().toJson());
                        if (dbResult.result().getUpdated() == 1) {
                            // get the saved identifier
                            final JsonArray queryParams = new JsonArray().add(message.body().getString("url"));
                            connection.queryWithParams(FIND_BY_URL_QUERY, queryParams,
                                    FIND_QUERY_RESULT_HANDLER.apply(message, connection));
                        } else {
                            message.fail(4, "insert error");
                        }
                    } else {
                        // most likely a duplicate registry, try find or fail
                        final JsonArray queryParams = new JsonArray().add(message.body().getString("url"));
                        connection.queryWithParams(FIND_BY_URL_QUERY, queryParams,
                                FIND_QUERY_RESULT_HANDLER.apply(message, connection));
                    }
                } finally {
                    connection.close();
                }
            };

    // the database client
    private JDBCClient client;

    @Override
    public void start() throws Exception {
        // create the database client
        client = JDBCClient.createShared(vertx,
                new JsonObject().put("url", config().getString("url", "jdbc:h2:mem:ushortx?DB_CLOSE_DELAY=-1"))
                        .put("driver_class", config().getString("driver_class", "org.h2.Driver"))
                        .put("user", config().getString("user", "ushortx"))
                        .put("password", config().getString("password", "shall-not-be-used"))
                        .put("max_pool_size", config().getInteger("max_pool_size", 20)),
                "ushortx-ds");

        // create the table
        createTableStructure(r -> {
            // register the event bus consumers
            LOGGER.info("registering event consumers..");
            vertx.eventBus().consumer("ushortx-persistence-findById", this::findById);
            vertx.eventBus().consumer("ushortx-persistence-save", this::saveUrl);
        });
    }

    /**
     * Connects to the database and calls the specified handlers, accordingly.
     *
     * @param successHandler the success handler
     * @param errorHandler   the error handler
     */
    private void connect(final Handler<SQLConnection> successHandler,
            final Optional<Handler<Throwable>> errorHandler) {
        client.getConnection(result -> {
            if (result.succeeded()) {
                successHandler.handle(result.result());
            } else {
                LOGGER.error("unable to obtain a database connection", result.cause());
                errorHandler.ifPresent(h -> h.handle(result.cause()));
            }
        });
    }

    /**
     * Creates the necessary data structure (tables) that are required for the verticle execution.
     * An {@link IllegalStateException} might be thrown if we reach an state of no recovery.
     *
     * @param readyHandler the handler that shall be called whenever the data structure is created
     */
    private void createTableStructure(final Handler<Void> readyHandler) {
        connect(connection -> {
            LOGGER.info("creating database structure..");
            // create the table
            connection.execute(CREATE_TABLE_STATEMENT, dbResult -> {
                if (dbResult.failed()) {
                    LOGGER.error("unable to create database structure", dbResult.cause());
                    return;
                }

                // call the callback
                readyHandler.handle(null);
            });
        }, Optional.empty());
    }

    /**
     * Queries the database for an url entry with the identifier specified in the message
     *
     * @param message the message from where to extract the identifier and to reply from
     */
    private void findById(final Message<JsonObject> message) {
        LOGGER.info("incoming find request: " + message.body());
        connect(connection -> {
            // validate the identifier
            final Optional<Long> id = Optional.ofNullable(message.body().getLong("id"));
            if (!id.isPresent()) {
                connection.close();
                message.fail(2, "invalid identifier");
                return;
            }

            // create the query parameters
            final JsonArray queryParams = new JsonArray().add(id.get());

            // execute the query
            connection.queryWithParams(FIND_BY_ID_QUERY, queryParams,
                    FIND_QUERY_RESULT_HANDLER.apply(message, connection));
        }, Optional.of(cause -> message.fail(1, "unavailable resources")));
    }

    /**
     * Saves at the database the url specified in the message, if non-existent. Otherwise the same
     * entry is used.
     *
     * @param message the message from where to extract the url data and to reply from
     */
    private void saveUrl(final Message<JsonObject> message) {
        LOGGER.info("incoming save request: " + message.body());
        connect(connection -> {
            // validate the url
            final Optional<String> url = Optional.ofNullable(message.body().getString("url"));
            if (!url.isPresent()) {
                connection.close();
                message.fail(2, "invalid url");
                return;
            }

            // create the update parameters
            final JsonArray updateParams = new JsonArray().add(url.get());

            // execute the update
            connection.updateWithParams(INSERT_URL_STATEMENT, updateParams,
                    INSERT_URL_RESULT_HANDLER.apply(message, connection));
        }, Optional.of(cause -> message.fail(1, "unavailable resources")));
    }

    @Override
    public void stop() throws Exception {
        // close the client
        client.close();
    }
}