1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/src/main/java/de/uapcore/lightpit/ModuleManager.java Sat May 09 14:26:31 2020 +0200 1.3 @@ -0,0 +1,231 @@ 1.4 +/* 1.5 + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. 1.6 + * 1.7 + * Copyright 2018 Mike Becker. All rights reserved. 1.8 + * 1.9 + * Redistribution and use in source and binary forms, with or without 1.10 + * modification, are permitted provided that the following conditions are met: 1.11 + * 1.12 + * 1. Redistributions of source code must retain the above copyright 1.13 + * notice, this list of conditions and the following disclaimer. 1.14 + * 1.15 + * 2. Redistributions in binary form must reproduce the above copyright 1.16 + * notice, this list of conditions and the following disclaimer in the 1.17 + * documentation and/or other materials provided with the distribution. 1.18 + * 1.19 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 1.20 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 1.21 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 1.22 + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 1.23 + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 1.24 + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 1.25 + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 1.26 + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 1.27 + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 1.28 + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 1.29 + * POSSIBILITY OF SUCH DAMAGE. 1.30 + * 1.31 + */ 1.32 +package de.uapcore.lightpit; 1.33 + 1.34 +import de.uapcore.lightpit.entities.CoreDAOFactory; 1.35 +import de.uapcore.lightpit.entities.Module; 1.36 +import de.uapcore.lightpit.entities.ModuleDao; 1.37 +import java.sql.Connection; 1.38 +import java.sql.SQLException; 1.39 +import java.util.Collections; 1.40 +import java.util.HashMap; 1.41 +import java.util.List; 1.42 +import java.util.Map; 1.43 +import java.util.Optional; 1.44 +import java.util.concurrent.atomic.AtomicBoolean; 1.45 +import java.util.stream.Collectors; 1.46 +import javax.servlet.Registration; 1.47 +import javax.servlet.ServletContext; 1.48 +import javax.servlet.ServletContextEvent; 1.49 +import javax.servlet.ServletContextListener; 1.50 +import javax.servlet.annotation.WebListener; 1.51 +import org.slf4j.Logger; 1.52 +import org.slf4j.LoggerFactory; 1.53 + 1.54 +/** 1.55 + * Scans registered servlets for LightPIT modules. 1.56 + */ 1.57 +@WebListener 1.58 +public final class ModuleManager implements ServletContextListener { 1.59 + 1.60 + private static final Logger LOG = LoggerFactory.getLogger(ModuleManager.class); 1.61 + 1.62 + /** 1.63 + * The attribute name in the servlet context under which an instance of this class can be found. 1.64 + */ 1.65 + public static final String SC_ATTR_NAME = ModuleManager.class.getName(); 1.66 + private ServletContext sc; 1.67 + 1.68 + /** 1.69 + * Maps class names to module information. 1.70 + */ 1.71 + private final Map<String, LightPITModule> registeredModules = new HashMap<>(); 1.72 + 1.73 + /** 1.74 + * This flag is true, when synchronization is needed. 1.75 + */ 1.76 + private final AtomicBoolean dirty = new AtomicBoolean(true); 1.77 + 1.78 + @Override 1.79 + public void contextInitialized(ServletContextEvent sce) { 1.80 + sc = sce.getServletContext(); 1.81 + reloadAll(); 1.82 + sc.setAttribute(SC_ATTR_NAME, this); 1.83 + LOG.info("Module manager injected into ServletContext."); 1.84 + } 1.85 + 1.86 + @Override 1.87 + public void contextDestroyed(ServletContextEvent sce) { 1.88 + unloadAll(); 1.89 + } 1.90 + 1.91 + private Optional<LightPITModule> getModuleInfo(Registration reg) { 1.92 + try { 1.93 + final Class scclass = Class.forName(reg.getClassName()); 1.94 + 1.95 + final boolean lpservlet = AbstractLightPITServlet.class.isAssignableFrom(scclass); 1.96 + final boolean lpmodule = scclass.isAnnotationPresent(LightPITModule.class); 1.97 + 1.98 + if (lpservlet && !lpmodule) { 1.99 + LOG.warn( 1.100 + "{} is a LightPIT Servlet but is missing the module annotation.", 1.101 + reg.getClassName() 1.102 + ); 1.103 + } else if (!lpservlet && lpmodule) { 1.104 + LOG.warn( 1.105 + "{} is annotated as a LightPIT Module but does not extend {}.", 1.106 + reg.getClassName(), 1.107 + AbstractLightPITServlet.class.getSimpleName() 1.108 + ); 1.109 + } 1.110 + 1.111 + if (lpservlet && lpmodule) { 1.112 + final Class<? extends AbstractLightPITServlet> clazz = scclass; 1.113 + final LightPITModule moduleInfo = clazz.getAnnotation(LightPITModule.class); 1.114 + return Optional.of(moduleInfo); 1.115 + } else { 1.116 + return Optional.empty(); 1.117 + } 1.118 + } catch (ClassNotFoundException ex) { 1.119 + LOG.error( 1.120 + "Servlet registration refers to class {} which cannot be found by the class loader (Reason: {})", 1.121 + reg.getClassName(), 1.122 + ex.getMessage() 1.123 + ); 1.124 + return Optional.empty(); 1.125 + } 1.126 + } 1.127 + 1.128 + private void handleServletRegistration(String name, Registration reg) { 1.129 + final Optional<LightPITModule> moduleInfo = getModuleInfo(reg); 1.130 + if (moduleInfo.isPresent()) { 1.131 + registeredModules.put(reg.getClassName(), moduleInfo.get()); 1.132 + LOG.info("Module detected: {}", name); 1.133 + } else { 1.134 + LOG.debug("Servlet {} is no module, skipping.", name); 1.135 + } 1.136 + } 1.137 + 1.138 + /** 1.139 + * Scans for modules and reloads them all. 1.140 + */ 1.141 + public void reloadAll() { 1.142 + registeredModules.clear(); 1.143 + sc.getServletRegistrations().forEach(this::handleServletRegistration); 1.144 + 1.145 + // TODO: implement dependency resolver 1.146 + 1.147 + dirty.set(true); 1.148 + LOG.info("Modules loaded."); 1.149 + } 1.150 + 1.151 + /** 1.152 + * Synchronizes module information with the database. 1.153 + * 1.154 + * This must be called from the {@link AbstractLightPITServlet}. 1.155 + * Admittedly the call will perform the synchronization once after reload 1.156 + * and be a no-op, afterwards. 1.157 + * However, we since the DatabaseFacade might be loaded after the module 1.158 + * manager, we must defer the synchronization to the first request 1.159 + * handled by the Servlet. 1.160 + * 1.161 + * @param db interface to the database 1.162 + */ 1.163 + public void syncWithDatabase(DatabaseFacade db) { 1.164 + if (dirty.compareAndSet(true, false)) { 1.165 + if (db.getDataSource().isPresent()) { 1.166 + try (Connection conn = db.getDataSource().get().getConnection()) { 1.167 + final ModuleDao moduleDao = CoreDAOFactory.getModuleDao(db.getSQLDialect()); 1.168 + moduleDao.syncRegisteredModuleClasses(conn, registeredModules.entrySet()); 1.169 + } catch (SQLException ex) { 1.170 + LOG.error("Unexpected SQL Exception", ex); 1.171 + } 1.172 + } else { 1.173 + LOG.error("No datasource present. Cannot sync module information with database."); 1.174 + } 1.175 + } else { 1.176 + LOG.trace("Module information clean - no synchronization required."); 1.177 + } 1.178 + } 1.179 + 1.180 + /** 1.181 + * Unloads all found modules. 1.182 + */ 1.183 + public void unloadAll() { 1.184 + registeredModules.clear(); 1.185 + LOG.info("All modules unloaded."); 1.186 + } 1.187 + 1.188 + /** 1.189 + * Returns the main menu. 1.190 + * 1.191 + * @param db the interface to the database 1.192 + * @return a list of menus belonging to the main menu 1.193 + */ 1.194 + public List<Menu> getMainMenu(DatabaseFacade db) { 1.195 + // TODO: user specific menu 1.196 + 1.197 + if (db.getDataSource().isPresent()) { 1.198 + try (Connection conn = db.getDataSource().get().getConnection()) { 1.199 + final ModuleDao dao = CoreDAOFactory.getModuleDao(db.getSQLDialect()); 1.200 + final List<Module> modules = dao.listAll(conn); 1.201 + 1.202 + final List<Menu> menu = modules 1.203 + .stream() 1.204 + .filter((mod) -> mod.isVisible()) 1.205 + .collect(Collectors.mapping( 1.206 + (mod) -> new Menu( 1.207 + mod.getClassname(), 1.208 + new ResourceKey( 1.209 + registeredModules.get(mod.getClassname()).bundleBaseName(), 1.210 + registeredModules.get(mod.getClassname()).menuKey()), 1.211 + registeredModules.get(mod.getClassname()).modulePath()), 1.212 + Collectors.toList()) 1.213 + ); 1.214 + return menu; 1.215 + } catch (SQLException ex) { 1.216 + LOG.error("Unexpected SQLException when loading the main menu", ex); 1.217 + return Collections.emptyList(); 1.218 + } 1.219 + } else { 1.220 + return Collections.emptyList(); 1.221 + } 1.222 + } 1.223 + 1.224 + /** 1.225 + * Returns an unmodifiable map of all registered modules. 1.226 + * 1.227 + * The key is the classname of the module. 1.228 + * 1.229 + * @return the map of registered modules 1.230 + */ 1.231 + public Map<String, LightPITModule> getRegisteredModules() { 1.232 + return Collections.unmodifiableMap(registeredModules); 1.233 + } 1.234 +}