package org.webpieces.router.impl; import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import javax.inject.Inject; import org.webpieces.ctx.api.RequestContext; import org.webpieces.ctx.api.RouterRequest; import org.webpieces.router.api.ResponseStreamer; import org.webpieces.router.api.RouterConfig; import org.webpieces.router.api.actions.Action; import org.webpieces.router.api.dto.MethodMeta; import org.webpieces.router.api.routing.Plugin; import org.webpieces.router.api.routing.Routes; import org.webpieces.router.api.routing.WebAppMeta; import org.webpieces.router.impl.compression.CompressionCacheSetup; import org.webpieces.router.impl.compression.FileMeta; import org.webpieces.router.impl.hooks.ClassForName; import org.webpieces.router.impl.loader.ControllerLoader; import org.webpieces.router.impl.model.AbstractRouteBuilder; import org.webpieces.router.impl.model.L1AllRouting; import org.webpieces.router.impl.model.LogicHolder; import org.webpieces.router.impl.model.MatchResult; import org.webpieces.router.impl.model.R1RouterBuilder; import org.webpieces.router.impl.model.RouteModuleInfo; import org.webpieces.router.impl.model.RouterInfo; import org.webpieces.util.file.VirtualFile; import org.webpieces.util.filters.Service; import org.webpieces.util.logging.Logger; import org.webpieces.util.logging.LoggerFactory; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Module; import com.google.inject.util.Modules; public class RouteLoader { private static final Logger log = LoggerFactory.getLogger(RouteLoader.class); private final RouterConfig config; private final RouteInvoker invoker; private final ControllerLoader controllerFinder; private CompressionCacheSetup compressionCacheSetup; protected R1RouterBuilder routerBuilder; private PluginSetup pluginSetup; @Inject public RouteLoader( RouterConfig config, RouteInvoker invoker, ControllerLoader controllerFinder, CompressionCacheSetup compressionCacheSetup, PluginSetup pluginSetup ) { this.config = config; this.invoker = invoker; this.controllerFinder = controllerFinder; this.compressionCacheSetup = compressionCacheSetup; this.pluginSetup = pluginSetup; } public WebAppMeta load(ClassForName loader, Consumer<Injector> startupFunction) { try { return loadImpl(loader, startupFunction); } catch (IOException e) { throw new RuntimeException(e); } } private WebAppMeta loadImpl(ClassForName loader, Consumer<Injector> startupFunction) throws IOException { log.info("loading the master "+WebAppMeta.class.getSimpleName()+" class file"); VirtualFile fileWithMetaClassName = config.getMetaFile(); String moduleName; Charset fileEncoding = config.getFileEncoding(); String contents = fileWithMetaClassName.contentAsString(fileEncoding); moduleName = contents.trim(); log.info(WebAppMeta.class.getSimpleName()+" class to load="+moduleName); Class<?> clazz = loader.clazzForName(moduleName); //In development mode, the ClassLoader here will be the CompilingClassLoader so stuff it into the thread context //just in case plugins will need it(most won't, hibernate will). In production, this is really a no-op and does //nothing. I don't like dev code existing in production :( so perhaps we should abstract this out at some //point perhaps with a lambda of a lambda function ClassLoader original = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(clazz.getClassLoader()); Object obj = newInstance(clazz); if(!(obj instanceof WebAppMeta)) throw new IllegalArgumentException("name="+moduleName+" does not implement "+WebAppMeta.class.getSimpleName()); log.info(WebAppMeta.class.getSimpleName()+" loaded. initializing next"); WebAppMeta routerModule = (WebAppMeta) obj; routerModule.initialize(config.getWebAppMetaProperties()); log.info(WebAppMeta.class.getSimpleName()+" initialized."); Injector injector = createInjector(routerModule); pluginSetup.wireInPluginPoints(injector, startupFunction); loadAllRoutes(routerModule, injector); return routerModule; } finally { Thread.currentThread().setContextClassLoader(original); } } public Injector createInjector(WebAppMeta routerModule) { List<Module> guiceModules = routerModule.getGuiceModules(); if(guiceModules == null) guiceModules = new ArrayList<>(); guiceModules.add(new EmptyPluginModule()); Module module = Modules.combine(guiceModules); List<Plugin> plugins = routerModule.getPlugins(); if(plugins != null) { for(Plugin plugin : plugins) { List<Module> modules = new ArrayList<>(); modules.addAll(plugin.getGuiceModules()); modules.add(module); module = Modules.combine(modules); } } if(config.getWebappOverrides() != null) module = Modules.override(module).with(config.getWebappOverrides()); Injector injector = Guice.createInjector(module); return injector; } //protected abstract void verifyRoutes(Collection<Route> allRoutes); public void loadAllRoutes(WebAppMeta rm, Injector injector) { log.info("adding routes"); ReverseRoutes reverseRoutes = new ReverseRoutes(config); //routerBuilder = new RouterBuilder("", new AllRoutingInfo(), reverseRoutes, controllerFinder, config.getUrlEncoding()); LogicHolder holder = new LogicHolder(reverseRoutes, controllerFinder, injector, config); routerBuilder = new R1RouterBuilder(new RouterInfo(null, ""), new L1AllRouting(), holder, false); invoker.init(reverseRoutes); List<Routes> all = new ArrayList<>(); all.addAll(rm.getRouteModules()); //the core application routes List<Plugin> plugins = rm.getPlugins(); if(plugins != null) { for(Plugin plugin : plugins) { all.addAll(plugin.getRouteModules()); } } for(Routes module : all) { AbstractRouteBuilder.currentPackage.set(new RouteModuleInfo(module)); module.configure(routerBuilder); AbstractRouteBuilder.currentPackage.set(null); } log.info("added all routes to router. Applying Filters"); reverseRoutes.finalSetup(); routerBuilder.applyFilters(rm); Collection<RouteMeta> metas = reverseRoutes.getAllRouteMetas(); for(RouteMeta m : metas) { controllerFinder.loadFiltersIntoMeta(m, m.getFilters(), true); } routerBuilder.loadNotFoundAndErrorFilters(); log.info("all filters applied"); compressionCacheSetup.setupCache(routerBuilder.getStaticRoutes()); } private Object newInstance(Class<?> clazz) { try { return clazz.newInstance(); } catch (InstantiationException e) { throw new IllegalArgumentException("Your clazz="+clazz.getSimpleName()+" could not be created(are you missing default constructor? is it not public?)", e); } catch (IllegalAccessException e) { throw new IllegalArgumentException("Your clazz="+clazz.getSimpleName()+" could not be created", e); } } public MatchResult fetchRoute(RouterRequest req) { L1AllRouting allRoutingInfo = routerBuilder.getRouterInfo(); MatchResult meta = allRoutingInfo.fetchRoute(req, req.relativePath); if(meta == null) throw new IllegalStateException("missing exception on creation if we go this far"); return meta; } public CompletableFuture<Void> invokeRoute(MatchResult result, RequestContext routerRequest, ResponseStreamer responseCb, ErrorRoutes errorRoutes) { //This class is purely the RouteLoader so delegate and encapsulate the invocation in RouteInvoker.... return invoker.invoke(result, routerRequest, responseCb, errorRoutes); } public Void processException(ResponseStreamer responseCb, RequestContext requestCtx, Throwable e, ErrorRoutes errorRoutes, Object meta) { return invoker.processException(responseCb, requestCtx, e, errorRoutes, meta); } public RouteMeta fetchNotFoundRoute(String domain) { L1AllRouting routerInfo = routerBuilder.getRouterInfo(); RouteMeta notfoundRoute = routerInfo.getPageNotfoundRoute(domain); return notfoundRoute; } public RouteMeta fetchInternalErrorRoute(String domain) { L1AllRouting routerInfo = routerBuilder.getRouterInfo(); RouteMeta internalErrorRoute = routerInfo.getInternalErrorRoute(domain); return internalErrorRoute; } public Service<MethodMeta, Action> createNotFoundService(RouteMeta m, RouterRequest req) { List<FilterInfo<?>> filterInfos = routerBuilder.findNotFoundFilters(req.relativePath, req.isHttps); return controllerFinder.createNotFoundService(m, filterInfos); } public String convertToUrl(String routeId, Map<String, String> args, boolean isValidating) { return invoker.convertToUrl(routeId, args, isValidating); } public FileMeta relativeUrlToHash(String urlPath) { return compressionCacheSetup.relativeUrlToHash(urlPath); } }