package io.mangoo.core; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; import java.util.Locale; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.LoggerContext; import org.quartz.CronExpression; import org.quartz.Job; import org.quartz.JobDetail; import org.quartz.Trigger; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.google.common.io.Resources; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Module; import com.google.inject.Stage; import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner; import io.mangoo.admin.AdminController; import io.mangoo.annotations.Schedule; import io.mangoo.configuration.Config; import io.mangoo.core.yaml.YamlRoute; import io.mangoo.core.yaml.YamlRouter; import io.mangoo.enums.Default; import io.mangoo.enums.Jvm; import io.mangoo.enums.Key; import io.mangoo.enums.Mode; import io.mangoo.enums.RouteType; import io.mangoo.exceptions.MangooSchedulerException; import io.mangoo.interfaces.MangooLifecycle; import io.mangoo.routing.Route; import io.mangoo.routing.Router; import io.mangoo.routing.handlers.DispatcherHandler; import io.mangoo.routing.handlers.ExceptionHandler; import io.mangoo.routing.handlers.FallbackHandler; import io.mangoo.routing.handlers.ServerSentEventHandler; import io.mangoo.routing.handlers.WebSocketHandler; import io.mangoo.scheduler.Scheduler; import io.mangoo.utils.BootstrapUtils; import io.mangoo.utils.SchedulerUtils; import io.undertow.Handlers; import io.undertow.Undertow; import io.undertow.Undertow.Builder; import io.undertow.UndertowOptions; import io.undertow.server.RoutingHandler; import io.undertow.server.handlers.PathHandler; import io.undertow.server.handlers.resource.ClassPathResourceManager; import io.undertow.server.handlers.resource.ResourceHandler; import io.undertow.util.HttpString; import io.undertow.util.Methods; /** * Convenient methods for everything to start up a mangoo I/O application * * @author svenkubiak * @author William Dunne * */ public class Bootstrap { private static volatile Logger LOG; //NOSONAR private static final int INITIAL_SIZE = 255; private final LocalDateTime start = LocalDateTime.now(); private final ResourceHandler resourceHandler; private Undertow undertow; private PathHandler pathHandler; private Config config; private String httpHost; private String ajpHost; private Mode mode; private Injector injector; private boolean error; private int httpPort; private int ajpPort; public Bootstrap() { this.resourceHandler = Handlers.resource(new ClassPathResourceManager(Thread.currentThread().getContextClassLoader(), Default.FILES_FOLDER.toString() + '/')); } public Mode prepareMode() { final String applicationMode = System.getProperty(Jvm.APPLICATION_MODE.toString()); if (StringUtils.isNotBlank(applicationMode)) { switch (applicationMode.toLowerCase(Locale.ENGLISH)) { case "dev" : this.mode = Mode.DEV; break; case "test" : this.mode = Mode.TEST; break; default : this.mode = Mode.PROD; break; } } else { this.mode = Mode.PROD; } return this.mode; } @SuppressWarnings("all") public void prepareLogger() { String configurationFile = System.getProperty(Jvm.APPLICATION_LOG.toString()); if (StringUtils.isNotBlank(configurationFile)) { final LoggerContext context = (LoggerContext) LogManager.getContext(false); context.setConfigLocation(URI.create(configurationFile)); if (!context.isInitialized()) { this.error = true; } if (!bootstrapError()) { LOG = LogManager.getLogger(Bootstrap.class); //NOSONAR LOG.info("Found specific Log4j2 configuration. Using configuration file: " + configurationFile); } } else { configurationFile = "log4j2." + this.mode.toString() + ".yaml"; if (Thread.currentThread().getContextClassLoader().getResource(configurationFile) == null) { LOG = LogManager.getLogger(Bootstrap.class); //NOSONAR } else { try { final URL resource = Thread.currentThread().getContextClassLoader().getResource(configurationFile); final LoggerContext context = (LoggerContext) LogManager.getContext(false); context.setConfigLocation(resource.toURI()); } catch (final URISyntaxException e) { e.printStackTrace(); //NOSONAR this.error = true; } if (!bootstrapError()) { LOG = LogManager.getLogger(Bootstrap.class); //NOSONAR LOG.info("Found environment specific Log4j2 configuration. Using configuration file: " + configurationFile); } } } } public Injector prepareInjector() { this.injector = Guice.createInjector(Stage.PRODUCTION, getModules()); return this.injector; } public void applicationInitialized() { this.injector.getInstance(MangooLifecycle.class).applicationInitialized(); } public void prepareConfig() { this.config = this.injector.getInstance(Config.class); if (!this.config.hasValidSecret()) { LOG.error("Please make sure that your application.yaml has an application.secret property which has at least 32 characters"); this.error = true; } if (!this.config.isDecrypted()) { LOG.error("Found encrypted config values in application.yaml but decryption was not successful!"); this.error = true; } } @SuppressWarnings("all") public void parseRoutes() { if (!bootstrapError()) { ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); YamlRouter yamlRouter = null; try { yamlRouter = objectMapper.readValue(Resources.getResource(Default.ROUTES_FILE.toString()).openStream(), YamlRouter.class); } catch (IOException e) { LOG.error("Failed to load routes.yaml Please make sure that your routes.yaml exists in your application src/main/resources folder", e); this.error = true; } if (!bootstrapError() && yamlRouter != null) { for (final YamlRoute yamlRoute : yamlRouter.getRoutes()) { RouteType routeType = BootstrapUtils.getRouteType(yamlRoute.getMethod()); final Route route = new Route(routeType) .toUrl(yamlRoute.getUrl().trim()) .withRequest(HttpString.tryFromString(yamlRoute.getMethod())) .withUsername(yamlRoute.getUsername()) .withPassword(yamlRoute.getPassword()) .withAuthentication(yamlRoute.isAuthentication()) .withTimer(yamlRoute.isTimer()) .withLimit(yamlRoute.getLimit()) .allowBlocking(yamlRoute.isBlocking()); try { String mapping = yamlRoute.getMapping(); if (StringUtils.isNotBlank(mapping)) { if (routeType == RouteType.REQUEST) { int lastIndexOf = mapping.trim().lastIndexOf('.'); String controllerClass = BootstrapUtils.getPackageName(this.config.getControllerPackage()) + mapping.substring(0, lastIndexOf); route.withClass(Class.forName(controllerClass)); String methodName = mapping.substring(lastIndexOf + 1); if (methodExists(methodName, route.getControllerClass())) { route.withMethod(methodName); } } else { route.withClass(Class.forName(BootstrapUtils.getPackageName(this.config.getControllerPackage()) + mapping)); } } Router.addRoute(route); } catch (final Exception e) { LOG.error("Failed to create routes from routes.yaml"); LOG.error("Please verify that your routes.yaml mapping is correct", e); this.error = true; } } } if (!bootstrapError()) { createRoutes(); } } } private boolean methodExists(String controllerMethod, Class<?> controllerClass) { boolean exists = false; for (final Method method : controllerClass.getMethods()) { if (method.getName().equals(controllerMethod)) { exists = true; break; } } if (!exists) { LOG.error("Could not find controller method '" + controllerMethod + "' in controller class '" + controllerClass.getSimpleName() + "'"); this.error = true; } return exists; } private void createRoutes() { this.pathHandler = new PathHandler(getRoutingHandler()); for (final Route route : Router.getRoutes()) { if (RouteType.WEBSOCKET == route.getRouteType()) { this.pathHandler.addExactPath(route.getUrl(), Handlers.websocket(this.injector.getInstance(WebSocketHandler.class).withControllerClass(route.getControllerClass()).withAuthentication(route.isAuthenticationRequired()))); } else if (RouteType.SERVER_SENT_EVENT == route.getRouteType()) { this.pathHandler.addExactPath(route.getUrl(), Handlers.serverSentEvents(this.injector.getInstance(ServerSentEventHandler.class).withAuthentication(route.isAuthenticationRequired()))); } else if (RouteType.RESOURCE_PATH == route.getRouteType()) { this.pathHandler.addPrefixPath(route.getUrl(), new ResourceHandler(new ClassPathResourceManager(Thread.currentThread().getContextClassLoader(), Default.FILES_FOLDER.toString() + route.getUrl()))); } } } private RoutingHandler getRoutingHandler() { final RoutingHandler routingHandler = Handlers.routing(); routingHandler.setFallbackHandler(Application.getInstance(FallbackHandler.class)); if (this.config.isAdminEnabled()) { Router.addRoute(new Route(RouteType.REQUEST).toUrl("/@admin").withRequest(Methods.GET).withClass(AdminController.class).withMethod("index").useInternalTemplateEngine()); Router.addRoute(new Route(RouteType.REQUEST).toUrl("/@admin/scheduler").withRequest(Methods.GET).withClass(AdminController.class).withMethod("scheduler").useInternalTemplateEngine()); Router.addRoute(new Route(RouteType.REQUEST).toUrl("/@admin/logger").withRequest(Methods.GET).withClass(AdminController.class).withMethod("logger").useInternalTemplateEngine()); Router.addRoute(new Route(RouteType.REQUEST).toUrl("/@admin/logger/ajax").withRequest(Methods.POST).withClass(AdminController.class).withMethod("loggerajax").useInternalTemplateEngine()); Router.addRoute(new Route(RouteType.REQUEST).toUrl("/@admin/routes").withRequest(Methods.GET).withClass(AdminController.class).withMethod("routes").useInternalTemplateEngine()); Router.addRoute(new Route(RouteType.REQUEST).toUrl("/@admin/metrics").withRequest(Methods.GET).withClass(AdminController.class).withMethod("metrics").useInternalTemplateEngine()); Router.addRoute(new Route(RouteType.REQUEST).toUrl("/@admin/tools").withRequest(Methods.GET).withClass(AdminController.class).withMethod("tools").useInternalTemplateEngine()); Router.addRoute(new Route(RouteType.REQUEST).toUrl("/@admin/tools/ajax").withRequest(Methods.POST).withClass(AdminController.class).withMethod("toolsajax").useInternalTemplateEngine()); Router.addRoute(new Route(RouteType.REQUEST).toUrl("/@admin/scheduler/execute/{name}").withRequest(Methods.GET).withClass(AdminController.class).withMethod("execute").useInternalTemplateEngine()); Router.addRoute(new Route(RouteType.REQUEST).toUrl("/@admin/scheduler/state/{name}").withRequest(Methods.GET).withClass(AdminController.class).withMethod("state").useInternalTemplateEngine()); } Router.getRoutes().parallelStream().forEach(route -> { if (RouteType.REQUEST == route.getRouteType()) { DispatcherHandler dispatcherHandler = Application.getInstance(DispatcherHandler.class).dispatch(route.getControllerClass(), route.getControllerMethod()) .isBlocking(route.isBlockingAllowed()) .withTimer(route.isTimerEnabled()) .withUsername(route.getUsername()) .withPassword(route.getPassword()) .withLimit(route.getLimit()); if (route.isInternalTemplateEngine()) { dispatcherHandler.withInternalTemplateEngine(); } routingHandler.add(route.getRequestMethod(),route.getUrl(), dispatcherHandler); } else if (RouteType.RESOURCE_FILE == route.getRouteType()) { routingHandler.add(Methods.GET, route.getUrl(), this.resourceHandler); } }); return routingHandler; } public void startUndertow() { if (!bootstrapError()) { Builder builder = Undertow.builder() .setServerOption(UndertowOptions.MAX_ENTITY_SIZE, this.config.getLong(Key.UNDERTOW_MAX_ENTITY_SIZE, Default.UNDERTOW_MAX_ENTITY_SIZE.toLong())) .setHandler(Handlers.exceptionHandler(this.pathHandler).addExceptionHandler(Throwable.class, Application.getInstance(ExceptionHandler.class))); boolean hasConnector = false; this.httpHost = this.config.getConnectorHttpHost(); this.httpPort = this.config.getConnectorHttpPort(); this.ajpHost = this.config.getConnectorAjpHost(); this.ajpPort = this.config.getConnectorAjpPort(); if (this.httpPort > 0 && StringUtils.isNotBlank(this.httpHost)) { builder.addHttpListener(this.httpPort, this.httpHost); hasConnector = true; } if (this.ajpPort > 0 && StringUtils.isNotBlank(this.ajpHost)) { builder.addAjpListener(this.ajpPort, this.ajpHost); hasConnector = true; } if (hasConnector) { this.undertow = builder.build(); this.undertow.start(); } else { this.error = true; LOG.error("No connector found! Please configure either a HTTP or an AJP connector in your application.yaml"); } } } private List<Module> getModules() { final List<Module> modules = new ArrayList<>(); if (!bootstrapError()) { try { final Class<?> applicationModule = Class.forName(Default.MODULE_CLASS.toString()); modules.add(new io.mangoo.core.Module()); modules.add((AbstractModule) applicationModule.getConstructor().newInstance()); } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException | ClassNotFoundException e) { LOG.error("Failed to load modules. Check that conf/Module.java exists in your application", e); this.error = true; } } return modules; } public void showLogo() { if (!bootstrapError()) { final StringBuilder buffer = new StringBuilder(INITIAL_SIZE); buffer.append('\n') .append(BootstrapUtils.getLogo()) .append("\n\nhttps://mangoo.io | @mangoo_io | ") .append(BootstrapUtils.getVersion()) .append('\n'); LOG.info(buffer.toString()); //NOSONAR if (this.httpPort > 0 && StringUtils.isNotBlank(this.httpHost)) { LOG.info("HTTP connector listening @{}:{}", this.httpHost, this.httpPort); } if (this.ajpPort > 0 && StringUtils.isNotBlank(this.ajpHost)) { LOG.info("AJP connector listening @{}:{}", this.ajpHost, this.ajpPort); } LOG.info("mangoo I/O application started in {} ms in {} mode. Enjoy.", ChronoUnit.MILLIS.between(this.start, LocalDateTime.now()), this.mode.toString()); } } public void applicationStarted() { this.injector.getInstance(MangooLifecycle.class).applicationStarted(); } public void startQuartzScheduler() { if (!bootstrapError()) { List<Class<?>> jobs = new ArrayList<>(); new FastClasspathScanner(this.config.getSchedulerPackage()) .matchClassesWithAnnotation(Schedule.class, jobs::add) .scan(); if (!jobs.isEmpty() && this.config.isSchedulerAutostart()) { final Scheduler mangooScheduler = this.injector.getInstance(Scheduler.class); mangooScheduler.initialize(); for (Class<?> clazz : jobs) { final Schedule schedule = clazz.getDeclaredAnnotation(Schedule.class); if (CronExpression.isValidExpression(schedule.cron())) { final JobDetail jobDetail = SchedulerUtils.createJobDetail(clazz.getName(), Default.SCHEDULER_JOB_GROUP.toString(), clazz.asSubclass(Job.class)); final Trigger trigger = SchedulerUtils.createTrigger(clazz.getName() + "-trigger", Default.SCHEDULER_TRIGGER_GROUP.toString(), schedule.description(), schedule.cron()); try { mangooScheduler.schedule(jobDetail, trigger); } catch (MangooSchedulerException e) { LOG.error("Failed to add a job to the scheduler", e); } LOG.info("Successfully scheduled job " + clazz.getName() + " with cron " + schedule.cron()); } else { LOG.error("Invalid or missing cron expression for job: " + clazz.getName()); this.error = true; } } if (!bootstrapError()) { try { mangooScheduler.start(); } catch (MangooSchedulerException e) { LOG.error("Failed to start the scheduler", e); } } } } } public boolean bootstrapSuccess() { return !this.error; } private boolean bootstrapError() { return this.error; } public LocalDateTime getStart() { return this.start; } public Undertow getUndertow() { return this.undertow; } }