--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/uapcore/lightpit/ModuleManager.java Sat May 09 14:26:31 2020 +0200 @@ -0,0 +1,231 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2018 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +package de.uapcore.lightpit; + +import de.uapcore.lightpit.entities.CoreDAOFactory; +import de.uapcore.lightpit.entities.Module; +import de.uapcore.lightpit.entities.ModuleDao; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import javax.servlet.Registration; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.annotation.WebListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Scans registered servlets for LightPIT modules. + */ +@WebListener +public final class ModuleManager implements ServletContextListener { + + private static final Logger LOG = LoggerFactory.getLogger(ModuleManager.class); + + /** + * The attribute name in the servlet context under which an instance of this class can be found. + */ + public static final String SC_ATTR_NAME = ModuleManager.class.getName(); + private ServletContext sc; + + /** + * Maps class names to module information. + */ + private final Map<String, LightPITModule> registeredModules = new HashMap<>(); + + /** + * This flag is true, when synchronization is needed. + */ + private final AtomicBoolean dirty = new AtomicBoolean(true); + + @Override + public void contextInitialized(ServletContextEvent sce) { + sc = sce.getServletContext(); + reloadAll(); + sc.setAttribute(SC_ATTR_NAME, this); + LOG.info("Module manager injected into ServletContext."); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + unloadAll(); + } + + private Optional<LightPITModule> getModuleInfo(Registration reg) { + try { + final Class scclass = Class.forName(reg.getClassName()); + + final boolean lpservlet = AbstractLightPITServlet.class.isAssignableFrom(scclass); + final boolean lpmodule = scclass.isAnnotationPresent(LightPITModule.class); + + if (lpservlet && !lpmodule) { + LOG.warn( + "{} is a LightPIT Servlet but is missing the module annotation.", + reg.getClassName() + ); + } else if (!lpservlet && lpmodule) { + LOG.warn( + "{} is annotated as a LightPIT Module but does not extend {}.", + reg.getClassName(), + AbstractLightPITServlet.class.getSimpleName() + ); + } + + if (lpservlet && lpmodule) { + final Class<? extends AbstractLightPITServlet> clazz = scclass; + final LightPITModule moduleInfo = clazz.getAnnotation(LightPITModule.class); + return Optional.of(moduleInfo); + } else { + return Optional.empty(); + } + } catch (ClassNotFoundException ex) { + LOG.error( + "Servlet registration refers to class {} which cannot be found by the class loader (Reason: {})", + reg.getClassName(), + ex.getMessage() + ); + return Optional.empty(); + } + } + + private void handleServletRegistration(String name, Registration reg) { + final Optional<LightPITModule> moduleInfo = getModuleInfo(reg); + if (moduleInfo.isPresent()) { + registeredModules.put(reg.getClassName(), moduleInfo.get()); + LOG.info("Module detected: {}", name); + } else { + LOG.debug("Servlet {} is no module, skipping.", name); + } + } + + /** + * Scans for modules and reloads them all. + */ + public void reloadAll() { + registeredModules.clear(); + sc.getServletRegistrations().forEach(this::handleServletRegistration); + + // TODO: implement dependency resolver + + dirty.set(true); + LOG.info("Modules loaded."); + } + + /** + * Synchronizes module information with the database. + * + * This must be called from the {@link AbstractLightPITServlet}. + * Admittedly the call will perform the synchronization once after reload + * and be a no-op, afterwards. + * However, we since the DatabaseFacade might be loaded after the module + * manager, we must defer the synchronization to the first request + * handled by the Servlet. + * + * @param db interface to the database + */ + public void syncWithDatabase(DatabaseFacade db) { + if (dirty.compareAndSet(true, false)) { + if (db.getDataSource().isPresent()) { + try (Connection conn = db.getDataSource().get().getConnection()) { + final ModuleDao moduleDao = CoreDAOFactory.getModuleDao(db.getSQLDialect()); + moduleDao.syncRegisteredModuleClasses(conn, registeredModules.entrySet()); + } catch (SQLException ex) { + LOG.error("Unexpected SQL Exception", ex); + } + } else { + LOG.error("No datasource present. Cannot sync module information with database."); + } + } else { + LOG.trace("Module information clean - no synchronization required."); + } + } + + /** + * Unloads all found modules. + */ + public void unloadAll() { + registeredModules.clear(); + LOG.info("All modules unloaded."); + } + + /** + * Returns the main menu. + * + * @param db the interface to the database + * @return a list of menus belonging to the main menu + */ + public List<Menu> getMainMenu(DatabaseFacade db) { + // TODO: user specific menu + + if (db.getDataSource().isPresent()) { + try (Connection conn = db.getDataSource().get().getConnection()) { + final ModuleDao dao = CoreDAOFactory.getModuleDao(db.getSQLDialect()); + final List<Module> modules = dao.listAll(conn); + + final List<Menu> menu = modules + .stream() + .filter((mod) -> mod.isVisible()) + .collect(Collectors.mapping( + (mod) -> new Menu( + mod.getClassname(), + new ResourceKey( + registeredModules.get(mod.getClassname()).bundleBaseName(), + registeredModules.get(mod.getClassname()).menuKey()), + registeredModules.get(mod.getClassname()).modulePath()), + Collectors.toList()) + ); + return menu; + } catch (SQLException ex) { + LOG.error("Unexpected SQLException when loading the main menu", ex); + return Collections.emptyList(); + } + } else { + return Collections.emptyList(); + } + } + + /** + * Returns an unmodifiable map of all registered modules. + * + * The key is the classname of the module. + * + * @return the map of registered modules + */ + public Map<String, LightPITModule> getRegisteredModules() { + return Collections.unmodifiableMap(registeredModules); + } +}