Sat, 09 May 2020 14:58:20 +0200
adds module priorities
1 /*
2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3 *
4 * Copyright 2018 Mike Becker. All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
20 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26 * POSSIBILITY OF SUCH DAMAGE.
27 *
28 */
29 package de.uapcore.lightpit;
31 import de.uapcore.lightpit.dao.CoreDAOFactory;
32 import de.uapcore.lightpit.dao.ModuleDao;
33 import de.uapcore.lightpit.entities.Module;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
37 import javax.servlet.Registration;
38 import javax.servlet.ServletContext;
39 import javax.servlet.ServletContextEvent;
40 import javax.servlet.ServletContextListener;
41 import javax.servlet.annotation.WebListener;
42 import java.sql.Connection;
43 import java.sql.SQLException;
44 import java.util.*;
45 import java.util.concurrent.atomic.AtomicBoolean;
46 import java.util.stream.Collectors;
48 /**
49 * Scans registered servlets for LightPIT modules.
50 */
51 @WebListener
52 public final class ModuleManager implements ServletContextListener {
54 private static final Logger LOG = LoggerFactory.getLogger(ModuleManager.class);
56 /**
57 * The attribute name in the servlet context under which an instance of this class can be found.
58 */
59 public static final String SC_ATTR_NAME = ModuleManager.class.getName();
60 private ServletContext sc;
62 /**
63 * Maps class names to module information.
64 */
65 private final Map<String, LightPITModule> registeredModules = new HashMap<>();
67 /**
68 * This flag is true, when synchronization is needed.
69 */
70 private final AtomicBoolean dirty = new AtomicBoolean(true);
72 @Override
73 public void contextInitialized(ServletContextEvent sce) {
74 sc = sce.getServletContext();
75 reloadAll();
76 sc.setAttribute(SC_ATTR_NAME, this);
77 LOG.info("Module manager injected into ServletContext.");
78 }
80 @Override
81 public void contextDestroyed(ServletContextEvent sce) {
82 unloadAll();
83 }
85 private Optional<LightPITModule> getModuleInfo(Registration reg) {
86 try {
87 final Class scclass = Class.forName(reg.getClassName());
89 final boolean lpservlet = AbstractLightPITServlet.class.isAssignableFrom(scclass);
90 final boolean lpmodule = scclass.isAnnotationPresent(LightPITModule.class);
92 if (lpservlet && !lpmodule) {
93 LOG.warn(
94 "{} is a LightPIT Servlet but is missing the module annotation.",
95 reg.getClassName()
96 );
97 } else if (!lpservlet && lpmodule) {
98 LOG.warn(
99 "{} is annotated as a LightPIT Module but does not extend {}.",
100 reg.getClassName(),
101 AbstractLightPITServlet.class.getSimpleName()
102 );
103 }
105 if (lpservlet && lpmodule) {
106 final Class<? extends AbstractLightPITServlet> clazz = scclass;
107 final LightPITModule moduleInfo = clazz.getAnnotation(LightPITModule.class);
108 return Optional.of(moduleInfo);
109 } else {
110 return Optional.empty();
111 }
112 } catch (ClassNotFoundException ex) {
113 LOG.error(
114 "Servlet registration refers to class {} which cannot be found by the class loader (Reason: {})",
115 reg.getClassName(),
116 ex.getMessage()
117 );
118 return Optional.empty();
119 }
120 }
122 private void handleServletRegistration(String name, Registration reg) {
123 final Optional<LightPITModule> moduleInfo = getModuleInfo(reg);
124 if (moduleInfo.isPresent()) {
125 registeredModules.put(reg.getClassName(), moduleInfo.get());
126 LOG.info("Module detected: {}", name);
127 } else {
128 LOG.debug("Servlet {} is no module, skipping.", name);
129 }
130 }
132 /**
133 * Scans for modules and reloads them all.
134 */
135 public void reloadAll() {
136 registeredModules.clear();
137 sc.getServletRegistrations().forEach(this::handleServletRegistration);
139 // TODO: implement dependency resolver
141 dirty.set(true);
142 LOG.info("Modules loaded.");
143 }
145 /**
146 * Synchronizes module information with the database.
147 * <p>
148 * This must be called from the {@link AbstractLightPITServlet}.
149 * Admittedly the call will perform the synchronization once after reload
150 * and be a no-op, afterwards.
151 * However, since the DatabaseFacade might be loaded after the module
152 * manager, we must defer the synchronization to the first request
153 * handled by the Servlet.
154 *
155 * @param db interface to the database
156 */
157 public void syncWithDatabase(DatabaseFacade db) {
158 if (dirty.compareAndSet(true, false)) {
159 if (db.getDataSource().isPresent()) {
160 try (Connection conn = db.getDataSource().get().getConnection()) {
161 final ModuleDao moduleDao = CoreDAOFactory.getModuleDao(db.getSQLDialect());
162 moduleDao.syncRegisteredModuleClasses(conn, registeredModules.entrySet());
163 } catch (SQLException ex) {
164 LOG.error("Unexpected SQL Exception", ex);
165 }
166 } else {
167 LOG.error("No datasource present. Cannot sync module information with database.");
168 }
169 } else {
170 LOG.trace("Module information clean - no synchronization required.");
171 }
172 }
174 /**
175 * Unloads all found modules.
176 */
177 public void unloadAll() {
178 registeredModules.clear();
179 LOG.info("All modules unloaded.");
180 }
182 /**
183 * Returns the main menu.
184 *
185 * @param db the interface to the database
186 * @return a list of menus belonging to the main menu
187 */
188 public List<Menu> getMainMenu(DatabaseFacade db) {
189 // TODO: user specific menu
191 if (db.getDataSource().isPresent()) {
192 try (Connection conn = db.getDataSource().get().getConnection()) {
193 final ModuleDao dao = CoreDAOFactory.getModuleDao(db.getSQLDialect());
194 final List<Module> modules = dao.listAll(conn);
196 return modules
197 .stream()
198 .filter(Module::isVisible)
199 .sorted(new Module.PriorityComparator())
200 .map(mod -> new Menu(
201 mod.getClassname(),
202 new ResourceKey(
203 registeredModules.get(mod.getClassname()).bundleBaseName(),
204 registeredModules.get(mod.getClassname()).menuKey()),
205 registeredModules.get(mod.getClassname()).modulePath()))
206 .collect(Collectors.toList());
207 } catch (SQLException ex) {
208 LOG.error("Unexpected SQLException when loading the main menu", ex);
209 return Collections.emptyList();
210 }
211 } else {
212 return Collections.emptyList();
213 }
214 }
216 /**
217 * Returns an unmodifiable map of all registered modules.
218 *
219 * The key is the classname of the module.
220 *
221 * @return the map of registered modules
222 */
223 public Map<String, LightPITModule> getRegisteredModules() {
224 return Collections.unmodifiableMap(registeredModules);
225 }
226 }