pt.davidafsilva.ushortx.http.RestVerticle.java Source code

Java tutorial

Introduction

Here is the source code for pt.davidafsilva.ushortx.http.RestVerticle.java

Source

package pt.davidafsilva.ushortx.http;

/*
 * #%L
 * ushortx-http
 * %%
 * 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 org.apache.commons.validator.routines.UrlValidator;

import java.util.Optional;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.AsyncResult;
import io.vertx.core.eventbus.Message;
import io.vertx.core.http.HttpServer;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;

/**
 * The REST API for our ushortx service
 *
 * @author David Silva
 */
public class RestVerticle extends AbstractVerticle {

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

    // the URL validator
    private static final UrlValidator URL_VALIDATOR = new NoProtocolUrlValidator();

    // the default salt value - should not be used!!!
    private static final String DEFAULT_SALT = "Please change me!! I'll make you a sandwich!";

    // the url format
    private static final String URL_REDIRECT_FORMAT = "http://%s/%s";

    // the http server
    private HttpServer server;

    @Override
    public void start() throws Exception {
        // create the routing configuration
        final Router router = Router.router(vertx);

        // default handler
        router.route().handler(BodyHandler.create());
        // GET /<hash>
        router.get("/:hash").handler(this::redirectUrlRequest);
        // POST /s/<url>
        router.post("/s/:url").handler(this::shortenUrlRequest);

        // create the actual http server
        final int port = config().getInteger("http_port", 8080);
        server = vertx.createHttpServer().requestHandler(router::accept).listen(port, deployedHandler -> {
            if (deployedHandler.succeeded()) {
                LOGGER.info(String.format("http server listening at port %s", port));
            } else {
                throw new IllegalStateException("unable to start http server", deployedHandler.cause());
            }
        });
    }

    /**
     * Redirects to the actual URL of the specified hash inside the GET request
     *
     * @param context the routing context of the request
     */
    private void redirectUrlRequest(final RoutingContext context) {
        // extract the hash
        final String hash = context.request().getParam("hash");
        LOGGER.info("redirecting request for " + hash);
        // basic hash validation
        if (hash == null || hash.isEmpty()) {
            context.response().setStatusCode(400).end();
            return;
        }

        // reverse the hash
        final Optional<Long> id = Hash.reverse(config().getString("salt", DEFAULT_SALT), hash);
        if (!id.isPresent()) {
            // fail with a 404
            context.response().setStatusCode(404).end();
            return;
        }

        // query the persistence for the hash
        LOGGER.debug("sending url lookup message for " + hash);
        vertx.eventBus().send("ushortx-persistence-findById",
                // the request data
                new JsonObject().put("id", id.get()),
                // the result callback
                (AsyncResult<Message<JsonObject>> result) -> {
                    if (result.succeeded()) {
                        // extract the json data
                        final JsonObject json = result.result().body();
                        LOGGER.debug("url for " + hash + " = " + json.getString("url"));

                        // redirect to the url
                        context.response().setStatusCode(302).putHeader("Location", json.getString("url")).end();
                    } else {
                        LOGGER.error("unable to obtain url for hash " + hash, result.cause());
                        // fail with a 404 - assume bad hash
                        context.response().setStatusCode(404).end();
                    }
                });
    }

    /**
     * Shortens an arbitrary URL specified inside the POST request
     *
     * @param context the routing context of the request
     */
    private void shortenUrlRequest(final RoutingContext context) {
        // extract the url
        final String url = protocolify(context.request().getParam("url"));
        LOGGER.info("shorten request for " + url);
        // url validation
        if (url == null || !URL_VALIDATOR.isValid(url)) {
            context.response().setStatusCode(400).end();
            return;
        }

        // send the request or fail the request
        LOGGER.debug("sending url save message for " + url);
        vertx.eventBus().send("ushortx-persistence-save",
                // the request data
                new JsonObject().put("url", url),
                // the result callback
                (AsyncResult<Message<JsonObject>> result) -> {
                    if (result.succeeded()) {
                        // extract the json data
                        final JsonObject json = result.result().body();
                        LOGGER.debug(url + " -> " + json);

                        // get the id
                        final long id = json.getLong("id");

                        // generate an hash for the identifier
                        final String hash = Hash.generate(config().getString("salt", DEFAULT_SALT), id);

                        // write the response
                        final String jsonResponse = new JsonObject().put("original", url).put("shortened", String
                                .format(URL_REDIRECT_FORMAT, context.request().localAddress().toString(), hash))
                                .encode();
                        context.response().setStatusCode(200).putHeader("Content-Type", "application/json")
                                .end(jsonResponse);
                    } else {
                        LOGGER.error("unable to save url", result.cause());
                        // fail with an internal error
                        context.response().setStatusCode(500).end();
                    }
                });
    }

    /**
     * Prepends the http(s):// protocol prefix to the specified url if not present
     *
     * @param url the original url
     * @return the protocolified url
     */
    private String protocolify(final String url) {
        return url == null || url.startsWith("http://") || url.startsWith("https://") ? url : "http://" + url;
    }

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