Package com.jetdrone.vertx.yoke

Source Code of com.jetdrone.vertx.yoke.Yoke

/**
* Copyright 2011-2014 the original author or authors.
*/
package com.jetdrone.vertx.yoke;

import com.jetdrone.vertx.yoke.core.Context;
import com.jetdrone.vertx.yoke.core.MountedMiddleware;
import com.jetdrone.vertx.yoke.core.RequestWrapper;
import com.jetdrone.vertx.yoke.core.impl.DefaultRequestWrapper;
import com.jetdrone.vertx.yoke.jmx.ContextMBean;
import com.jetdrone.vertx.yoke.jmx.MiddlewareMBean;
import com.jetdrone.vertx.yoke.middleware.AbstractMiddleware;
import com.jetdrone.vertx.yoke.middleware.YokeRequest;
import com.jetdrone.vertx.yoke.security.KeyStoreSecurity;
import com.jetdrone.vertx.yoke.security.SecretSecurity;
import com.jetdrone.vertx.yoke.store.SessionStore;
import com.jetdrone.vertx.yoke.store.SharedDataSessionStore;
import com.jetdrone.vertx.yoke.core.YokeException;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.vertx.java.core.AsyncResult;
import org.vertx.java.core.Handler;
import org.vertx.java.core.Vertx;
import org.vertx.java.core.file.impl.PathAdjuster;
import org.vertx.java.core.http.HttpServer;
import org.vertx.java.core.http.HttpServerRequest;
import org.vertx.java.core.http.HttpServerResponse;
import org.vertx.java.core.impl.VertxInternal;
import org.vertx.java.core.json.JsonArray;
import org.vertx.java.core.json.JsonElement;
import org.vertx.java.core.json.JsonObject;
import org.vertx.java.platform.Container;
import org.vertx.java.platform.Verticle;

import org.jetbrains.annotations.*;

import javax.management.*;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.management.ManagementFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.*;

/**
* # Yoke
*
* Yoke is a chain executor of middleware for Vert.x 2.x. The goal of this library is not to provide a web application
* framework but the backbone that helps the creation of web applications.
*
* Yoke works in a similar way to Connect middleware. Users start by declaring which middleware components want to use
* and then start an http server either managed by Yoke or provided by the user (say when you need https).
*
* Yoke has no extra dependencies than Vert.x itself so it is self contained.
*/
public class Yoke {

    //Get the MBean server
    private final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();

    /**
     * Vert.x instance
     */
    private final Vertx vertx;

    /**
     * request wrapper in use
     */
    private final RequestWrapper requestWrapper;

    /**
     * default context used by all requests
     *
     * <pre>
     * {
     *   title: "Yoke",
     *   x-powered-by: true,
     *   trust-proxy: true
     * }
     * </pre>
     */
    protected final Map<String, Object> defaultContext = new HashMap<>();

    /**
     * The internal registry of [render engines](Engine.html)
     */
    private final Map<String, Engine> engineMap = new HashMap<>();

    /**
     * Creates a Yoke instance.
     *
     * This constructor should be called from a verticle and pass a valid Vertx instance. This instance will be shared
     * with all registered middleware. The reason behind this is to allow middleware to use Vertx features such as file
     * system and timers.
     *
     * <pre>
     * public class MyVerticle extends Verticle {
     *   public void start() {
     *     final Yoke yoke = new Yoke(this);
     *     ...
     *   }
     * }
     * </pre>
     *
     * @param verticle the main verticle
     */
    public Yoke(@NotNull Verticle verticle) {
        this(verticle.getVertx(), new DefaultRequestWrapper());
    }

    /**
     * Creates a Yoke instance.
     *
     * This constructor should be called from a verticle and pass a valid Vertx instance and a Logger. This instance
     * will be shared with all registered middleware. The reason behind this is to allow middleware to use Vertx
     * features such as file system and timers.
     *
     * <pre>
     * public class MyVerticle extends Verticle {
     *   public void start() {
     *     final Yoke yoke = new Yoke(getVertx());
     *     ...
     *   }
     * }
     * </pre>
     *
     * @param vertx
     */
    public Yoke(@NotNull Vertx vertx) {
        this(vertx, new DefaultRequestWrapper());
    }

    /**
     * Creates a Yoke instance.
     *
     * This constructor should be called internally or from other language bindings.
     *
     * <pre>
     * public class MyVerticle extends Verticle {
     *   public void start() {
     *     final Yoke yoke = new Yoke(getVertx(),
     *         getContainer(),
     *         new RequestWrapper() {...});
     *     ...
     *   }
     * }
     * </pre>
     *
     * @param vertx
     * @param requestWrapper
     */
    public Yoke(@NotNull Vertx vertx, @NotNull RequestWrapper requestWrapper) {
        this.vertx = vertx;
        this.requestWrapper = requestWrapper;
        defaultContext.put("title", "Yoke");
        defaultContext.put("x-powered-by", true);
        defaultContext.put("trust-proxy", true);
        store = new SharedDataSessionStore(vertx, "yoke.sessiondata");

        // register on JMX
        try {
            mbs.registerMBean(new ContextMBean(defaultContext), new ObjectName("com.jetdrone.yoke:instance=@" + hashCode() + ",type=DefaultContext@" + defaultContext.hashCode()));
        } catch (InstanceAlreadyExistsException e) {
            // ignore
        } catch (MalformedObjectNameException | MBeanRegistrationException | NotCompliantMBeanException e) {
            throw new RuntimeException(e);
        }
    }

    public Vertx vertx() {
        return vertx;
    }

    /**
     * Ordered list of mounted middleware in the chain
     */
    private final List<MountedMiddleware> middlewareList = new ArrayList<>();

    /**
     * Special middleware used for error handling
     */
    private Middleware errorHandler;

    /**
     * Adds a Middleware to the chain. If the middleware is an Error Handler Middleware then it is
     * treated differently and only the last error handler is kept.
     *
     * You might want to add a middleware that is only supposed to run on a specific route (path prefix).
     * In this case if the request path does not match the prefix the middleware is skipped automatically.
     *
     * <pre>
     * yoke.use("/login", new CustomLoginMiddleware());
     * </pre>
     *
     * @param route      The route prefix for the middleware
     * @param middleware The middleware add to the chain
     */
    public Yoke use(@NotNull String route, @NotNull Middleware... middleware) {
        for (Middleware m : middleware) {
            // when the type of middleware is error handler then the route is ignored and
            // the middleware is extracted from the execution chain into a special placeholder
            // for error handling
            if (m instanceof ErrorMiddleware) {
                errorHandler = m;
            } else {
                MountedMiddleware mm = new MountedMiddleware(route, m);
                middlewareList.add(mm);

                // register on JMX
                try {
                    mbs.registerMBean(new MiddlewareMBean(mm), new ObjectName("com.jetdrone.yoke:type=Middleware@" + hashCode() + ",route=" + ObjectName.quote(route) + ",name=" + m.getClass().getSimpleName() + "@" + m.hashCode()));
                } catch (MalformedObjectNameException | InstanceAlreadyExistsException | MBeanRegistrationException | NotCompliantMBeanException e) {
                    throw new RuntimeException(e);
                }
            }

            // initialize the middleware with the current Yoke
            if (m instanceof AbstractMiddleware) {
                ((AbstractMiddleware) m).init(this, route);
            }
        }
        return this;
    }

    /**
     * Adds a middleware to the chain with the prefix "/".
     *
     * @param middleware The middleware add to the chain
     */
    public Yoke use(@NotNull Middleware... middleware) {
        return use("/", middleware);
    }

    /**
     * Adds a Handler to a route. The behaviour is similar to the middleware, however this
     * will be a terminal point in the execution chain. In this case any middleware added
     * after will not be executed. However you should care about the route which may lead
     * to skip this middleware.
     *
     * The idea to user a Handler is to keep the API familiar with the rest of the Vert.x
     * API.
     *
     * <pre>
     * yoke.use("/login", new Handler&lt;YokeRequest&gt;() {
     *   public void handle(YokeRequest request) {
     *     request.response.end("Hello");
     *   }
     * });
     * </pre>
     *
     * @param route   The route prefix for the middleware
     * @param handler The Handler to add
     */
    public Yoke use(@NotNull String route, final @NotNull Handler<YokeRequest> handler) {
        middlewareList.add(new MountedMiddleware(route, new Middleware() {
            @Override
            public void handle(@NotNull YokeRequest request, @NotNull Handler<Object> next) {
                handler.handle(request);
            }
        }));
        return this;
    }

    /**
     * Adds a Handler to a route.
     *
     * <pre>
     * yoke.use("/login", new Handler&lt;YokeRequest&gt;() {
     *   public void handle(YokeRequest request) {
     *     request.response.end("Hello");
     *   }
     * });
     * </pre>
     * @param handler The Handler to add
     */
    public Yoke use(@NotNull Handler<YokeRequest> handler) {
        return use("/", handler);
    }

    /**
     * Adds a Render Engine to the library. Render Engines are Template engines you
     * might want to use to speed the development of your application. Once they are
     * registered you can use the method render in the YokeResponse to
     * render a template.
     *
     * @param extension The template file extension
     * @param engine    The implementation of the engine
     */
    public Yoke engine(@NotNull String extension, @NotNull Engine engine) {
        engine.setVertx(vertx);
        // prefix "." to the file extension
        if (extension.charAt(0) != '.') {
            extension = "." + extension;
        }
        engineMap.put(extension, engine);
        return this;
    }

    /**
     * Special store engine used for accessing session data
     */
    protected SessionStore store;

    public Yoke store(@NotNull SessionStore store) {
        this.store = store;
        return this;
    }

    protected YokeSecurity security;

    public Yoke keyStoreSecurity(@NotNull final String fileName, @NotNull final String keyStorePassword, @NotNull final JsonObject keyPasswords) {
        String storeType;
        int idx = fileName.lastIndexOf('.');

        if (idx == -1) {
            storeType = KeyStore.getDefaultType();
        } else {
            storeType = fileName.substring(idx + 1);
        }

        try {
            KeyStore ks = KeyStore.getInstance(storeType);

            try (InputStream in = new FileInputStream(PathAdjuster.adjust((VertxInternal) vertx, fileName))) {
                ks.load(in, keyStorePassword.toCharArray());
            }

            this.security = new KeyStoreSecurity(ks, keyPasswords.toMap());

        } catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        return this;
    }

    public Yoke keyStoreSecurity(@NotNull final String fileName, @NotNull final String keyStorePassword) {
        String storeType;
        int idx = fileName.lastIndexOf('.');

        if (idx == -1) {
            storeType = KeyStore.getDefaultType();
        } else {
            storeType = fileName.substring(idx + 1);
        }

        try {
            KeyStore ks = KeyStore.getInstance(storeType);

            try (InputStream in = new FileInputStream(PathAdjuster.adjust((VertxInternal) vertx, fileName))) {
                ks.load(in, keyStorePassword.toCharArray());
            }

            this.security = new KeyStoreSecurity(ks, keyStorePassword);

        } catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        return this;
    }

    public Yoke secretSecurity(@NotNull final String secret) {
        this.security = new SecretSecurity(secret);
        return this;
    }

    public Yoke secretSecurity(@NotNull final byte[] secret) {
        this.security = new SecretSecurity(secret);
        return this;
    }

    public YokeSecurity security() {
        if (security == null) {
            throw new RuntimeException("No YokeSecurity implementation is enabled!");
        }

        return security;
    }

    /**
     * When you need to share global properties with your requests you can add them
     * to Yoke and on every request they will be available as request.get(String)
     *
     * @param key   unique identifier
     * @param value Any non null value, nulls are not saved
     */
    public Yoke set(@NotNull String key, Object value) {
        if (value == null) {
            defaultContext.remove(key);
        } else {
            defaultContext.put(key, value);
        }

        return this;
    }

    /**
     * Starts the server listening at a given port bind to all available interfaces.
     *
     * @param port the server TCP port
     * @return {Yoke}
     */
    public Yoke listen(int port) {
        return listen(port, "0.0.0.0", null);
    }

    /**
     * Starts the server listening at a given port bind to all available interfaces.
     *
     * @param port    the server TCP port
     * @param handler for asynchronous result of the listen operation
     * @return {Yoke}
     */
    public Yoke listen(int port, @NotNull Handler<Boolean> handler) {
        return listen(port, "0.0.0.0", handler);
    }

    /**
     * Starts the server listening at a given port and given address.
     *
     * @param port the server TCP port
     * @return {Yoke}
     */
    public Yoke listen(int port, @NotNull String address) {
        return listen(port, address, null);
    }

    /**
     * Starts the server listening at a given port and given address.
     *
     * @param port    the server TCP port
     * @param handler for asynchronous result of the listen operation
     * @return {Yoke}
     */
    public Yoke listen(int port, @NotNull String address, final Handler<Boolean> handler) {
        HttpServer server = vertx.createHttpServer();

        listen(server);

        if (handler != null) {
            server.listen(port, address, new Handler<AsyncResult<HttpServer>>() {
                @Override
                public void handle(AsyncResult<HttpServer> listen) {
                    handler.handle(listen.succeeded());
                }
            });
        } else {
            server.listen(port, address);
        }
        return this;
    }

    /**
     * Starts listening at a already created server.
     *
     * @param server
     * @return {Yoke}
     */
    public Yoke listen(final @NotNull HttpServer server) {
        server.requestHandler(new Handler<HttpServerRequest>() {
            @Override
            public void handle(HttpServerRequest req) {
                // the context map is shared with all middlewares
                final YokeRequest request = requestWrapper.wrap(req, new Context(defaultContext), engineMap, store);

                // add x-powered-by header is enabled
                Boolean poweredBy = request.get("x-powered-by");
                if (poweredBy != null && poweredBy) {
                    request.response().putHeader("x-powered-by", "yoke");
                }

                new Handler<Object>() {
                    int currentMiddleware = -1;

                    @Override
                    public void handle(Object error) {
                        if (error == null) {
                            currentMiddleware++;
                            if (currentMiddleware < middlewareList.size()) {
                                final MountedMiddleware mountedMiddleware = middlewareList.get(currentMiddleware);

                                if (!mountedMiddleware.enabled) {
                                    // the middleware is disabled
                                    handle(null);
                                } else {
                                    if (request.path().startsWith(mountedMiddleware.mount)) {
                                        mountedMiddleware.middleware.handle(request, this);
                                    } else {
                                        // the middleware was not mounted on this uri, skip to the next entry
                                        handle(null);
                                    }
                                }
                            } else {
                                HttpServerResponse response = request.response();
                                // reached the end and no handler was able to answer the request
                                response.setStatusCode(404);
                                response.setStatusMessage(HttpResponseStatus.valueOf(404).reasonPhrase());
                                if (errorHandler != null) {
                                    errorHandler.handle(request, null);
                                } else {
                                    response.end(HttpResponseStatus.valueOf(404).reasonPhrase());
                                }
                            }
                        } else {
                            request.put("error", error);
                            if (errorHandler != null) {
                                errorHandler.handle(request, null);
                            } else {
                                HttpServerResponse response = request.response();

                                int errorCode;
                                // if the error was set on the response use it
                                if (response.getStatusCode() >= 400) {
                                    errorCode = response.getStatusCode();
                                } else {
                                    // if it was set as the error object use it
                                    if (error instanceof Number) {
                                        errorCode = ((Number) error).intValue();
                                    } else if (error instanceof YokeException) {
                                        errorCode = ((YokeException) error).getErrorCode().intValue();
                                    } else if (error instanceof JsonObject) {
                                        errorCode = ((JsonObject) error).getInteger("errorCode", 500);
                                    } else if (error instanceof Map) {
                                        Integer tmp = (Integer) ((Map) error).get("errorCode");
                                        errorCode = tmp != null ? tmp : 500;
                                    } else {
                                        // default error code
                                        errorCode = 500;
                                    }
                                }

                                response.setStatusCode(errorCode);
                                response.setStatusMessage(HttpResponseStatus.valueOf(errorCode).reasonPhrase());
                                response.end(HttpResponseStatus.valueOf(errorCode).reasonPhrase());
                            }
                        }
                    }
                }.handle(null);
            }
        });
        return this;
    }

    /**
     * Deploys required middleware from a config json element.
     *
     * The current format for the config is either a single item or an array:
     * <pre>
     * {
     *   module: String, // the name of the module
     *   verticle: String, // the name of the verticle (either verticle or module must be present)
     *   instances: Number, // how many instances, default 1
     *   worker: Boolean, // is it a worker verticle? default false
     *   multiThreaded: Boolean, // is it a multiThreaded verticle? default false
     *   config: JsonObject // any config you need to pass to the module/verticle
     * }
     * </pre>
     *
     * @param config either a json object or a json array.
     */
    public Yoke deploy(@NotNull final Container container, @NotNull JsonElement config) {
        return deploy(container, config, null);
    }

    /**
     * Deploys required middleware from a config json element. The handler is only called once all middleware is
     * deployed or in error. The order of deployment is not guaranteed since all deploy functions are called
     * concurrently and do not wait for the previous result before deploying the next item.
     *
     * The current format for the config is either a single item or an array:
     * <pre>
     * {
     *   module: String, // the name of the module
     *   verticle: String, // the name of the verticle (either verticle or module must be present)
     *   instances: Number, // how many instances, default 1
     *   worker: Boolean, // is it a worker verticle? default false
     *   multiThreaded: Boolean, // is it a multiThreaded verticle? default false
     *   config: JsonObject // any config you need to pass to the module/verticle
     * }
     * </pre>
     *
     * @param container Vert.x2 container
     * @param config either a json object or a json array.
     * @param handler A handler that is called once all middleware is deployed or on error.
     */
    public Yoke deploy(final @NotNull Container container, final @NotNull JsonElement config, final Handler<Object> handler) {

        if (config.isArray() && config.asArray().size() == 0) {
            if (handler == null) {
                return this;
            } else {
                handler.handle(null);
                return this;
            }
        }

        if (config.isObject()) {
            return deploy(container, new JsonArray().addObject(config.asObject()), handler);
        }

        // wait for all deployments before calling the real handler
        Handler<AsyncResult<String>> waitFor = new Handler<AsyncResult<String>>() {

            private int latch = config.asArray().size();
            private boolean handled = false;

            @Override
            public void handle(AsyncResult<String> event) {
                latch--;
                if (handler != null) {
                    if (!handled && (event.failed() || latch == 0)) {
                        handled = true;
                        handler.handle(event.failed() ? event.cause() : null);
                    }
                }
            }
        };

        for (Object o : config.asArray()) {
            JsonObject mod = (JsonObject) o;
            if (mod.getString("module") != null) {
                deploy(container, mod.getString("module"), true, false, false, mod.getInteger("instances", 1), mod.getObject("config", new JsonObject()), waitFor);
            } else {
                deploy(container, mod.getString("verticle"), false, mod.getBoolean("worker", false), mod.getBoolean("multiThreaded", false), mod.getInteger("instances", 1), mod.getObject("config", new JsonObject()), waitFor);
            }
        }

        return this;
    }

    private void deploy(@NotNull Container container, String name, boolean module, boolean worker, boolean multiThreaded, int instances, JsonObject config, Handler<AsyncResult<String>> handler) {
        if (module) {
            if (handler != null) {
                container.deployModule(name, config, instances, handler);
            } else {
                container.deployModule(name, config, instances);
            }
        } else {
            if (worker) {
                if (handler != null) {
                    container.deployWorkerVerticle(name, config, instances, multiThreaded, handler);
                } else {
                    container.deployWorkerVerticle(name, config, instances, multiThreaded);
                }
            } else {
                if (handler != null) {
                    container.deployVerticle(name, config, instances, handler);
                } else {
                    container.deployVerticle(name, config, instances);
                }
            }
        }
    }
}
TOP

Related Classes of com.jetdrone.vertx.yoke.Yoke

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.