//filename: MainVerticle.java package io.vertx.guides.wiki; import io.vertx.core.AbstractVerticle; import io.vertx.core.DeploymentOptions; import io.vertx.core.Future; /** * @author <a href="https://julien.ponge.org/">Julien Ponge</a> */ // tag::main[] public class MainVerticle extends AbstractVerticle { @Override public void start(Future<Void> startFuture) throws Exception { Future<String> dbVerticleDeployment = Future.future(); // <1> vertx.deployVerticle(new WikiDatabaseVerticle(), dbVerticleDeployment.completer()); // <2> dbVerticleDeployment.compose(id -> { // <3> Future<String> httpVerticleDeployment = Future.future(); vertx.deployVerticle( "io.vertx.guides.wiki.HttpServerVerticle", // <4> new DeploymentOptions().setInstances(3), // <5> httpVerticleDeployment.completer()); return httpVerticleDeployment; // <6> }).setHandler(ar -> { // <7> if (ar.succeeded()) { startFuture.complete(); } else { startFuture.fail(ar.cause()); } }); } }
//filename: HttpServerVerticle.java package io.vertx.guides.wiki; import com.github.rjeschke.txtmark.Processor; import io.vertx.core.AbstractVerticle; import io.vertx.core.Future; import io.vertx.core.eventbus.DeliveryOptions; import io.vertx.core.http.HttpServer; import io.vertx.core.json.JsonObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.BodyHandler; import io.vertx.ext.web.templ.FreeMarkerTemplateEngine; import java.util.Date; /** * @author <a href="https://julien.ponge.org/">Julien Ponge</a> */ // tag::start[] public class HttpServerVerticle extends AbstractVerticle { private static final Logger LOGGER = LoggerFactory.getLogger(HttpServerVerticle.class); public static final String CONFIG_HTTP_SERVER_PORT = "http.server.port"; // <1> public static final String CONFIG_WIKIDB_QUEUE = "wikidb.queue"; private String wikiDbQueue = "wikidb.queue"; @Override public void start(Future<Void> startFuture) throws Exception { wikiDbQueue = config().getString(CONFIG_WIKIDB_QUEUE, "wikidb.queue"); // <2> HttpServer server = vertx.createHttpServer(); Router router = Router.router(vertx); router.get("/").handler(this::indexHandler); router.get("/wiki/:page").handler(this::pageRenderingHandler); router.get("/test/:msg").handler(this::pageTestHandler); router.post().handler(BodyHandler.create()); router.post("/save").handler(this::pageUpdateHandler); router.post("/create").handler(this::pageCreateHandler); router.post("/delete").handler(this::pageDeletionHandler); int portNumber = config().getInteger(CONFIG_HTTP_SERVER_PORT, 8080); // <3> server .requestHandler(router::accept) .listen(portNumber, ar -> { if (ar.succeeded()) { LOGGER.info("HTTP server running on port " + portNumber); startFuture.complete(); } else { LOGGER.error("Could not start a HTTP server", ar.cause()); startFuture.fail(ar.cause()); } }); } // (...) // end::start[] // tag::indexHandler[] private final FreeMarkerTemplateEngine templateEngine = FreeMarkerTemplateEngine.create(); private void indexHandler(RoutingContext context) { LOGGER.info("首頁"); DeliveryOptions options = new DeliveryOptions().addHeader("action", "all-pages"); // <2> vertx.eventBus().send(wikiDbQueue, new JsonObject(), options, reply -> { // <1> if (reply.succeeded()) { JsonObject body = (JsonObject) reply.result().body(); // <3> context.put("title", "Wiki home"); context.put("pages", body.getJsonArray("pages").getList()); templateEngine.render(context, "templates", "/index.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(reply.cause()); } }); } // end::indexHandler[] // tag::rest[] private static final String EMPTY_PAGE_MARKDOWN = "# A new page\n" + "\n" + "Feel-free to write in Markdown!\n"; private void pageTestHandler(RoutingContext context) { String msg = context.request().getParam("msg"); LOGGER.info(msg); JsonObject message = new JsonObject().put("msg",msg); // message body DeliveryOptions options = new DeliveryOptions().addHeader("action","test-msg"); vertx.eventBus().send("wikidb.queue",message,options, reply ->{ if(reply.succeeded()){ JsonObject body = (JsonObject) reply.result().body(); context.response().putHeader("Content-Type", "text/html;charset=utf-8"); context.response().end(body.getString("msg")); } else{ context.fail(reply.cause()); } }); } private void pageRenderingHandler(RoutingContext context) { String requestedPage = context.request().getParam("page"); JsonObject request = new JsonObject().put("page", requestedPage); DeliveryOptions options = new DeliveryOptions().addHeader("action", "get-page"); vertx.eventBus().send(wikiDbQueue, request, options, reply -> { if (reply.succeeded()) { JsonObject body = (JsonObject) reply.result().body(); boolean found = body.getBoolean("found"); String rawContent = body.getString("rawContent", EMPTY_PAGE_MARKDOWN); context.put("title", requestedPage); context.put("id", body.getInteger("id", -1)); context.put("newPage", found ? "no" : "yes"); context.put("rawContent", rawContent); context.put("content", Processor.process(rawContent)); context.put("timestamp", new Date().toString()); templateEngine.render(context, "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(reply.cause()); } }); } private void pageUpdateHandler(RoutingContext context) { String title = context.request().getParam("title"); JsonObject request = new JsonObject() .put("id", context.request().getParam("id")) .put("title", title) .put("markdown", context.request().getParam("markdown")); DeliveryOptions options = new DeliveryOptions(); if ("yes".equals(context.request().getParam("newPage"))) { options.addHeader("action", "create-page"); } else { options.addHeader("action", "save-page"); } vertx.eventBus().send(wikiDbQueue, request, options, reply -> { if (reply.succeeded()) { context.response().setStatusCode(303); context.response().putHeader("Location", "/wiki/" + title); context.response().end(); } else { context.fail(reply.cause()); } }); } 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(); } private void pageDeletionHandler(RoutingContext context) { String id = context.request().getParam("id"); JsonObject request = new JsonObject().put("id", id); DeliveryOptions options = new DeliveryOptions().addHeader("action", "delete-page"); vertx.eventBus().send(wikiDbQueue, request, options, reply -> { if (reply.succeeded()) { context.response().setStatusCode(303); context.response().putHeader("Location", "/"); context.response().end(); } else { context.fail(reply.cause()); } }); } // end::rest[] }
//filename: WikiDatabaseVerticle.java package io.vertx.guides.wiki; import io.vertx.core.AbstractVerticle; import io.vertx.core.Future; import io.vertx.core.eventbus.Message; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.ext.jdbc.JDBCClient; import io.vertx.ext.sql.ResultSet; import io.vertx.ext.sql.SQLConnection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.List; import java.util.Properties; import java.util.stream.Collectors; /** * @author <a href="https://julien.ponge.org/">Julien Ponge</a> */ // tag::preamble[] public class WikiDatabaseVerticle extends AbstractVerticle { public static final String CONFIG_WIKIDB_JDBC_URL = "wikidb.jdbc.url"; public static final String CONFIG_WIKIDB_JDBC_DRIVER_CLASS = "wikidb.jdbc.driver_class"; public static final String CONFIG_WIKIDB_JDBC_MAX_POOL_SIZE = "wikidb.jdbc.max_pool_size"; public static final String CONFIG_WIKIDB_SQL_QUERIES_RESOURCE_FILE = "wikidb.sqlqueries.resource.file"; public static final String CONFIG_WIKIDB_QUEUE = "wikidb.queue"; private static final Logger LOGGER = LoggerFactory.getLogger(WikiDatabaseVerticle.class); // (...) // end::preamble[] // tag::loadSqlQueries[] private enum SqlQuery { CREATE_PAGES_TABLE, ALL_PAGES, GET_PAGE, CREATE_PAGE, SAVE_PAGE, DELETE_PAGE } private final HashMap<SqlQuery, String> sqlQueries = new HashMap<>(); private void loadSqlQueries() throws IOException { String queriesFile = config().getString(CONFIG_WIKIDB_SQL_QUERIES_RESOURCE_FILE); InputStream queriesInputStream; if (queriesFile != null) { queriesInputStream = new FileInputStream(queriesFile); } else { queriesInputStream = getClass().getResourceAsStream("/db-queries.properties"); } Properties queriesProps = new Properties(); queriesProps.load(queriesInputStream); queriesInputStream.close(); sqlQueries.put(SqlQuery.CREATE_PAGES_TABLE, queriesProps.getProperty("create-pages-table")); sqlQueries.put(SqlQuery.ALL_PAGES, queriesProps.getProperty("all-pages")); sqlQueries.put(SqlQuery.GET_PAGE, queriesProps.getProperty("get-page")); sqlQueries.put(SqlQuery.CREATE_PAGE, queriesProps.getProperty("create-page")); sqlQueries.put(SqlQuery.SAVE_PAGE, queriesProps.getProperty("save-page")); sqlQueries.put(SqlQuery.DELETE_PAGE, queriesProps.getProperty("delete-page")); } // end::loadSqlQueries[] // tag::start[] private JDBCClient dbClient; @Override public void start(Future<Void> startFuture) throws Exception { /* * Note: this uses blocking APIs, but data is small... */ loadSqlQueries(); // <1> dbClient = JDBCClient.createShared(vertx, new JsonObject() .put("url", config().getString(CONFIG_WIKIDB_JDBC_URL, "jdbc:hsqldb:file:db/wiki")) .put("driver_class", config().getString(CONFIG_WIKIDB_JDBC_DRIVER_CLASS, "org.hsqldb.jdbcDriver")) .put("max_pool_size", config().getInteger(CONFIG_WIKIDB_JDBC_MAX_POOL_SIZE, 30))); dbClient.getConnection(ar -> { if (ar.failed()) { LOGGER.error("Could not open a database connection", ar.cause()); startFuture.fail(ar.cause()); } else { SQLConnection connection = ar.result(); connection.execute(sqlQueries.get(SqlQuery.CREATE_PAGES_TABLE), create -> { // <2> connection.close(); if (create.failed()) { LOGGER.error("Database preparation error", create.cause()); startFuture.fail(create.cause()); } else { vertx.eventBus().consumer(config().getString(CONFIG_WIKIDB_QUEUE, "wikidb.queue"), this::onMessage); // <3> startFuture.complete(); } }); } }); } // end::start[] // tag::onMessage[] public enum ErrorCodes { NO_ACTION_SPECIFIED, BAD_ACTION, DB_ERROR } public void onMessage(Message<JsonObject> message) { if (!message.headers().contains("action")) { LOGGER.error("No action header specified for message with headers {} and body {}", message.headers(), message.body().encodePrettily()); message.fail(ErrorCodes.NO_ACTION_SPECIFIED.ordinal(), "No action header specified"); return; } String action = message.headers().get("action"); switch (action) { case "all-pages": fetchAllPages(message); break; case "get-page": fetchPage(message); break; case "test-msg": testMsg(message); break; case "create-page": createPage(message); break; case "save-page": savePage(message); break; case "delete-page": deletePage(message); break; default: message.fail(ErrorCodes.BAD_ACTION.ordinal(), "Bad action: " + action); } } // end::onMessage[] // tag::rest[] private void fetchAllPages(Message<JsonObject> message) { dbClient.getConnection(car -> { if (car.succeeded()) { SQLConnection connection = car.result(); connection.query(sqlQueries.get(SqlQuery.ALL_PAGES), res -> { connection.close(); if (res.succeeded()) { List<String> pages = res.result() .getResults() .stream() .map(json -> json.getString(0)) .sorted() .collect(Collectors.toList()); message.reply(new JsonObject().put("pages", new JsonArray(pages))); } else { reportQueryError(message, res.cause()); } }); } else { reportQueryError(message, car.cause()); } }); } private void testMsg(Message<JsonObject> message) { String msg = message.body().getString("msg"); LOGGER.info(msg); if (!"error".equals(msg)) { JsonObject response = new JsonObject(); response.put("msg", "您好,我是WikiDatabaseVerticle: " + msg); message.reply(response); } else { message.fail(-1, "你的輸入有誤!"); } } private void fetchPage(Message<JsonObject> message) { String requestedPage = message.body().getString("page"); dbClient.getConnection(car -> { if (car.succeeded()) { SQLConnection connection = car.result(); connection.queryWithParams(sqlQueries.get(SqlQuery.GET_PAGE), new JsonArray().add(requestedPage), fetch -> { connection.close(); if (fetch.succeeded()) { JsonObject response = new JsonObject(); ResultSet resultSet = fetch.result(); if (resultSet.getNumRows() == 0) { response.put("found", false); } else { response.put("found", true); JsonArray row = resultSet.getResults().get(0); response.put("id", row.getInteger(0)); response.put("rawContent", row.getString(1)); } message.reply(response); } else { reportQueryError(message, fetch.cause()); } }); } else { reportQueryError(message, car.cause()); } }); } private void createPage(Message<JsonObject> message) { JsonObject request = message.body(); dbClient.getConnection(car -> { if (car.succeeded()) { SQLConnection connection = car.result(); JsonArray data = new JsonArray() .add(request.getString("title")) .add(request.getString("markdown")); connection.updateWithParams(sqlQueries.get(SqlQuery.CREATE_PAGE), data, res -> { connection.close(); if (res.succeeded()) { message.reply("ok"); } else { reportQueryError(message, res.cause()); } }); } else { reportQueryError(message, car.cause()); } }); } private void savePage(Message<JsonObject> message) { JsonObject request = message.body(); dbClient.getConnection(car -> { if (car.succeeded()) { SQLConnection connection = car.result(); JsonArray data = new JsonArray() .add(request.getString("markdown")) .add(request.getString("id")); connection.updateWithParams(sqlQueries.get(SqlQuery.SAVE_PAGE), data, res -> { connection.close(); if (res.succeeded()) { message.reply("ok"); } else { reportQueryError(message, res.cause()); } }); } else { reportQueryError(message, car.cause()); } }); } private void deletePage(Message<JsonObject> message) { dbClient.getConnection(car -> { if (car.succeeded()) { SQLConnection connection = car.result(); JsonArray data = new JsonArray().add(message.body().getString("id")); connection.updateWithParams(sqlQueries.get(SqlQuery.DELETE_PAGE), data, res -> { connection.close(); if (res.succeeded()) { message.reply("ok"); } else { reportQueryError(message, res.cause()); } }); } else { reportQueryError(message, car.cause()); } }); } private void reportQueryError(Message<JsonObject> message, Throwable cause) { LOGGER.error("Database query error", cause); message.fail(ErrorCodes.DB_ERROR.ordinal(), cause.getMessage()); } // end::rest[] }