/**
* 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<YokeRequest>() {
* 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<YokeRequest>() {
* 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);
}
}
}
}
}