package com.github.aesteve.vertx.nubes;
import com.github.aesteve.vertx.nubes.annotations.File;
import com.github.aesteve.vertx.nubes.annotations.View;
import com.github.aesteve.vertx.nubes.annotations.auth.Auth;
import com.github.aesteve.vertx.nubes.annotations.auth.Logout;
import com.github.aesteve.vertx.nubes.annotations.cookies.CookieValue;
import com.github.aesteve.vertx.nubes.annotations.cookies.Cookies;
import com.github.aesteve.vertx.nubes.annotations.mixins.ContentType;
import com.github.aesteve.vertx.nubes.annotations.mixins.Throttled;
import com.github.aesteve.vertx.nubes.annotations.routing.Redirect;
import com.github.aesteve.vertx.nubes.context.ClientAccesses;
import com.github.aesteve.vertx.nubes.context.PaginationContext;
import com.github.aesteve.vertx.nubes.context.RateLimit;
import com.github.aesteve.vertx.nubes.fixtures.FixtureLoader;
import com.github.aesteve.vertx.nubes.handlers.AnnotationProcessor;
import com.github.aesteve.vertx.nubes.handlers.Processor;
import com.github.aesteve.vertx.nubes.handlers.impl.*;
import com.github.aesteve.vertx.nubes.i18n.LocaleResolver;
import com.github.aesteve.vertx.nubes.i18n.LocaleResolverRegistry;
import com.github.aesteve.vertx.nubes.i18n.impl.AcceptLanguageLocaleResolver;
import com.github.aesteve.vertx.nubes.marshallers.Payload;
import com.github.aesteve.vertx.nubes.marshallers.PayloadMarshaller;
import com.github.aesteve.vertx.nubes.marshallers.impl.JacksonPayloadMarshaller;
import com.github.aesteve.vertx.nubes.marshallers.impl.JAXBPayloadMarshaller;
import com.github.aesteve.vertx.nubes.marshallers.impl.PlainTextMarshaller;
import com.github.aesteve.vertx.nubes.reflections.AnnotVerticleFactory;
import com.github.aesteve.vertx.nubes.reflections.EventBusBridgeFactory;
import com.github.aesteve.vertx.nubes.reflections.RouteFactory;
import com.github.aesteve.vertx.nubes.reflections.SocketFactory;
import com.github.aesteve.vertx.nubes.reflections.adapters.ParameterAdapter;
import com.github.aesteve.vertx.nubes.reflections.adapters.ParameterAdapterRegistry;
import com.github.aesteve.vertx.nubes.reflections.factories.AnnotationProcessorFactory;
import com.github.aesteve.vertx.nubes.reflections.factories.impl.*;
import com.github.aesteve.vertx.nubes.reflections.injectors.annot.AnnotatedParamInjector;
import com.github.aesteve.vertx.nubes.reflections.injectors.typed.ParamInjector;
import com.github.aesteve.vertx.nubes.reflections.injectors.typed.impl.LocaleParamInjector;
import com.github.aesteve.vertx.nubes.services.Service;
import com.github.aesteve.vertx.nubes.services.ServiceRegistry;
import com.github.aesteve.vertx.nubes.utils.async.AsyncUtils;
import com.github.aesteve.vertx.nubes.utils.async.MultipleFutures;
import com.github.aesteve.vertx.nubes.views.TemplateEngineManager;
import io.vertx.core.*;
import io.vertx.core.json.JsonObject;
import io.vertx.core.shareddata.LocalMap;
import io.vertx.ext.auth.AuthProvider;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.CookieHandler;
import io.vertx.ext.web.handler.StaticHandler;
import io.vertx.ext.web.templ.TemplateEngine;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;
import javax.xml.bind.JAXBException;
import java.lang.annotation.Annotation;
import java.util.*;
import java.util.function.Predicate;
import static com.github.aesteve.vertx.nubes.utils.async.AsyncUtils.completeFinally;
import static com.github.aesteve.vertx.nubes.utils.async.AsyncUtils.completeOrFail;
public class VertxNubes {
private static final int CLEAN_HISTORY_DELAY = 60000;
protected final Config config;
protected final Vertx vertx;
private Router router;
private FixtureLoader fixtureLoader;
private Handler<RoutingContext> failureHandler;
private final ParameterAdapterRegistry registry;
private final Map<String, PayloadMarshaller> marshallers;
private LocaleResolverRegistry locResolver;
private final List<String> deploymentIds;
/**
* @param vertx the vertx instance
*/
public VertxNubes(Vertx vertx, JsonObject json) {
this.vertx = vertx;
config = Config.fromJsonObject(json, vertx);
deploymentIds = new ArrayList<>();
registry = new ParameterAdapterRegistry();
marshallers = new HashMap<>();
config.setMarshallers(marshallers);
config.createAnnotInjectors(registry);
// marshalling
TemplateEngineManager templManager = new TemplateEngineManager(config);
registerAnnotationProcessor(View.class, new ViewProcessorFactory(templManager));
registerAnnotationProcessor(File.class, new FileProcessorFactory());
registerMarshaller("text/plain", new PlainTextMarshaller());
registerMarshaller("application/json", new JacksonPayloadMarshaller());
String domainPackage = config.getDomainPackage();
if (domainPackage != null) {
try {
Reflections reflections = new Reflections(domainPackage, new SubTypesScanner(false));
registerMarshaller("application/xml", new JAXBPayloadMarshaller(reflections.getSubTypesOf(Object.class)));
} catch (JAXBException je) {
throw new VertxException(je);
}
}
failureHandler = new DefaultErrorHandler(config, templManager, marshallers);
// default processors/handlers
CookieHandler cookieHandler = CookieHandler.create();
registerAnnotationHandler(Cookies.class, cookieHandler);
registerAnnotationHandler(CookieValue.class, cookieHandler);
registerAnnotationHandler(Throttled.class, RateLimitationHandler.create(config));
registerTypeProcessor(PaginationContext.class, new PaginationProcessor());
registerTypeProcessor(Payload.class, new PayloadTypeProcessor(marshallers));
registerAnnotationProcessor(Redirect.class, new ClientRedirectProcessorFactory());
registerAnnotationProcessor(ContentType.class, new ContentTypeProcessorFactory());
registerAnnotationProcessor(Logout.class, new LogoutProcessor());
}
public void bootstrap(Handler<AsyncResult<Router>> handler, Router paramRouter) {
setUpRouter(paramRouter);
final ServiceRegistry serviceRegistry = config.getServiceRegistry();
fixtureLoader = new FixtureLoader(vertx, config, serviceRegistry);
Map<String, DeploymentOptions> verticles = new AnnotVerticleFactory(config).scan();
MultipleFutures<String> vertFutures = new MultipleFutures<>(verticles, this::deployVerticle);
AsyncUtils.chainOnSuccess(
handler,
vertFutures,
serviceRegistry::startAll,
fixtureLoader::setUp,
res -> {
vertx.setPeriodic(CLEAN_HISTORY_DELAY, this::cleanHistoryMap);
handler.handle(Future.succeededFuture(router));
});
vertFutures.start();
}
public void bootstrap(Handler<AsyncResult<Router>> handler) {
bootstrap(handler, Router.router(vertx));
}
public void stop(Handler<AsyncResult<Void>> handler) {
router.clear();
MultipleFutures<Void> futures = new MultipleFutures<>(handler);
futures.add(fixtureLoader::tearDown);
futures.add(config.getServiceRegistry()::stopAll);
futures.add(this::stopDeployments);
futures.start();
}
private void undeployVerticle(String deploymentId, Future<Void> future) {
vertx.undeploy(deploymentId, completeFinally(future));
}
public void registerTemplateEngine(String extension, TemplateEngine engine) {
config.registerTemplateEngine(extension, engine);
}
public void setAuthProvider(AuthProvider authProvider) {
config.setAuthProvider(authProvider);
}
public void registerInterceptor(String name, Handler<RoutingContext> handler) {
config.registerInterceptor(name, handler);
}
public void setAvailableLocales(List<Locale> availableLocales) {
initLocale(availableLocales.toArray(new Locale[0]));
locResolver.addLocales(availableLocales);
}
public void setDefaultLocale(Locale defaultLocale) {
initLocale(defaultLocale);
locResolver.setDefaultLocale(defaultLocale);
}
private void initLocale(Locale... loc) {
if (locResolver == null) {
locResolver = new LocaleResolverRegistry(Arrays.asList(loc));
locResolver.addResolver(new AcceptLanguageLocaleResolver());
registerTypeParamInjector(Locale.class, new LocaleParamInjector());
addGlobalHandler(new LocaleHandler(locResolver));
}
}
public void addLocaleResolver(LocaleResolver resolver) {
if (locResolver == null) {
throw new IllegalArgumentException("Please set a list of available locales first. We can't guess the list of locales you're handling in your application.");
}
locResolver.addResolver(resolver);
}
public void setFailureHandler(Handler<RoutingContext> handler) {
failureHandler = handler;
}
public void registerService(String name, Object service) {
config.registerService(name, service);
}
public Service getService(String name) {
return (Service) config.getService(name);
}
public void registerServiceProxy(Object service) {
config.registerService("$nubes-proxy$__" + service.getClass().getName(), service);
}
public void registerHandler(Class<?> parameterClass, Handler<RoutingContext> handler) {
config.registerParamHandler(parameterClass, handler);
}
public <T> void registerAdapter(Class<T> parameterClass, ParameterAdapter<T> adapter) {
registry.registerAdapter(parameterClass, adapter);
}
public void registerAnnotationHandler(Class<? extends Annotation> annotation, Handler<RoutingContext> handler) {
Set<Handler<RoutingContext>> handlers = config.getAnnotationHandler(annotation);
if (handlers == null) {
handlers = new LinkedHashSet<>();
}
if (!handlers.contains(handler)) {
handlers.add(handler);
}
config.registerAnnotationHandler(annotation, handlers);
}
public void registerTypeProcessor(Class<?> type, Processor processor) {
config.registerTypeProcessor(type, processor);
}
public <T extends Annotation> void registerAnnotationProcessor(Class<T> annotation, AnnotationProcessorFactory<T> processor) {
config.registerAnnotationProcessor(annotation, processor);
}
public <T extends Annotation> void registerAnnotationProcessor(Class<T> annotation, AnnotationProcessor<T> processor) {
config.registerAnnotationProcessor(annotation, processor);
}
public void registerMarshaller(String contentType, PayloadMarshaller marshaller) {
marshallers.put(contentType, marshaller);
}
public <T> void registerTypeParamInjector(Class<? extends T> clazz, ParamInjector<T> injector) {
config.registerInjector(clazz, injector);
}
public <T extends Annotation> void registerAnnotatedParamInjector(Class<? extends T> clazz, AnnotatedParamInjector<T> injector) {
config.registerInjector(clazz, injector);
}
public void addGlobalHandler(Handler<RoutingContext> handler) {
config.addHandler(handler);
}
// private methods
private void setUpRouter(Router paramRouter) {
router = paramRouter;
router.route().failureHandler(failureHandler);
if (locResolver != null) {
locResolver.getAvailableLocales().forEach(this::loadResourceBundle);
if (locResolver.getDefaultLocale() != null) {
loadResourceBundle(locResolver.getDefaultLocale());
}
}
if (config.getAuthProvider() != null) {
registerAnnotationProcessor(Auth.class, new AuthProcessorFactory());
}
new RouteFactory(router, config).createHandlers();
new SocketFactory(router, config).createHandlers();
new EventBusBridgeFactory(router, config).createHandlers();
StaticHandler staticHandler;
final String webroot = config.getWebroot();
if (webroot != null) {
staticHandler = StaticHandler.create(webroot);
} else {
staticHandler = StaticHandler.create();
}
router.route(config.getAssetsPath() + "/*").handler(staticHandler);
}
private void cleanHistoryMap(Long timerId) {
LocalMap<String, ClientAccesses> rateLimitations = vertx.sharedData().getLocalMap("mvc.rateLimitation");
if (rateLimitations == null) {
return;
}
rateLimitations.keySet().stream()
.filter(clientsWithNoAccessPredicate(rateLimitations))
.forEach(rateLimitations::remove);
}
private Predicate<String> clientsWithNoAccessPredicate(LocalMap<String, ClientAccesses> rateLimitations) {
RateLimit rateLimit = config.getRateLimit();
return clientIp -> {
ClientAccesses accesses = rateLimitations.get(clientIp);
long keepAfter = rateLimit.getTimeUnit().toMillis(rateLimit.getValue());
accesses.clearHistory(keepAfter);
return accesses.noAccess();
};
}
private void loadResourceBundle(Locale loc) {
ResourceBundle bundle = ResourceBundle.getBundle(config.getI18nDir() + "messages", loc);
config.createBundle(loc, bundle);
}
private void deployVerticle(String vertName, DeploymentOptions options, Future<String> future) {
vertx.deployVerticle(vertName, options, completeOrFail(future));
}
private void stopDeployments(Future<Void> future) {
MultipleFutures<Void> futures = new MultipleFutures<>(future);
futures.addAll(deploymentIds, this::undeployVerticle);
futures.start();
}
}