1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java Sat May 09 14:26:31 2020 +0200 1.3 @@ -0,0 +1,296 @@ 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 java.io.IOException; 1.35 +import java.lang.reflect.Method; 1.36 +import java.lang.reflect.Modifier; 1.37 +import java.util.Arrays; 1.38 +import java.util.HashMap; 1.39 +import java.util.List; 1.40 +import java.util.Locale; 1.41 +import java.util.Map; 1.42 +import java.util.Optional; 1.43 +import javax.servlet.ServletException; 1.44 +import javax.servlet.http.HttpServlet; 1.45 +import javax.servlet.http.HttpServletRequest; 1.46 +import javax.servlet.http.HttpServletResponse; 1.47 +import javax.servlet.http.HttpSession; 1.48 +import org.slf4j.Logger; 1.49 +import org.slf4j.LoggerFactory; 1.50 + 1.51 +/** 1.52 + * A special implementation of a HTTPServlet which is focused on implementing 1.53 + * the necessary functionality for {@link LightPITModule}s. 1.54 + */ 1.55 +public abstract class AbstractLightPITServlet extends HttpServlet { 1.56 + 1.57 + private static final Logger LOG = LoggerFactory.getLogger(AbstractLightPITServlet.class); 1.58 + 1.59 + private static final String HTML_FULL_DISPATCHER = Functions.jspPath("html_full"); 1.60 + 1.61 + /** 1.62 + * Store a reference to the annotation for quicker access. 1.63 + */ 1.64 + private Optional<LightPITModule> moduleInfo = Optional.empty(); 1.65 + 1.66 + /** 1.67 + * The EL proxy is necessary, because the EL resolver cannot handle annotation properties. 1.68 + */ 1.69 + private Optional<LightPITModule.ELProxy> moduleInfoELProxy = Optional.empty(); 1.70 + 1.71 + 1.72 + @FunctionalInterface 1.73 + private static interface HandlerMethod { 1.74 + ResponseType apply(HttpServletRequest t, HttpServletResponse u) throws IOException, ServletException; 1.75 + } 1.76 + 1.77 + /** 1.78 + * Invocation mapping gathered from the {@link RequestMapping} annotations. 1.79 + * 1.80 + * Paths in this map must always start with a leading slash, although 1.81 + * the specification in the annotation must not start with a leading slash. 1.82 + * 1.83 + * The reason for this is the different handling of empty paths in 1.84 + * {@link HttpServletRequest#getPathInfo()}. 1.85 + */ 1.86 + private final Map<HttpMethod, Map<String, HandlerMethod>> mappings = new HashMap<>(); 1.87 + 1.88 + /** 1.89 + * Gives implementing modules access to the {@link ModuleManager}. 1.90 + * @return the module manager 1.91 + */ 1.92 + protected final ModuleManager getModuleManager() { 1.93 + return (ModuleManager) getServletContext().getAttribute(ModuleManager.SC_ATTR_NAME); 1.94 + } 1.95 + 1.96 + /** 1.97 + * Gives implementing modules access to the {@link DatabaseFacade}. 1.98 + * @return the database facade 1.99 + */ 1.100 + protected final DatabaseFacade getDatabaseFacade() { 1.101 + return (DatabaseFacade) getServletContext().getAttribute(DatabaseFacade.SC_ATTR_NAME); 1.102 + } 1.103 + 1.104 + private ResponseType invokeMapping(Method method, HttpServletRequest req, HttpServletResponse resp) 1.105 + throws IOException, ServletException { 1.106 + try { 1.107 + LOG.trace("invoke {}#{}", method.getDeclaringClass().getName(), method.getName()); 1.108 + return (ResponseType) method.invoke(this, req, resp); 1.109 + } catch (ReflectiveOperationException | ClassCastException ex) { 1.110 + LOG.error(String.format("invocation of method %s failed", method.getName()), ex); 1.111 + resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 1.112 + return ResponseType.NONE; 1.113 + } 1.114 + } 1.115 + 1.116 + @Override 1.117 + public void init() throws ServletException { 1.118 + moduleInfo = Optional.ofNullable(this.getClass().getAnnotation(LightPITModule.class)); 1.119 + moduleInfoELProxy = moduleInfo.map(LightPITModule.ELProxy::convert); 1.120 + 1.121 + if (moduleInfo.isPresent()) { 1.122 + scanForRequestMappings(); 1.123 + } 1.124 + 1.125 + LOG.trace("{} initialized", getServletName()); 1.126 + } 1.127 + 1.128 + private void scanForRequestMappings() { 1.129 + try { 1.130 + Method[] methods = getClass().getDeclaredMethods(); 1.131 + for (Method method : methods) { 1.132 + Optional<RequestMapping> mapping = Optional.ofNullable(method.getAnnotation(RequestMapping.class)); 1.133 + if (mapping.isPresent()) { 1.134 + if (!Modifier.isPublic(method.getModifiers())) { 1.135 + LOG.warn("{} is annotated with {} but is not public", 1.136 + method.getName(), RequestMapping.class.getSimpleName() 1.137 + ); 1.138 + continue; 1.139 + } 1.140 + if (Modifier.isAbstract(method.getModifiers())) { 1.141 + LOG.warn("{} is annotated with {} but is abstract", 1.142 + method.getName(), RequestMapping.class.getSimpleName() 1.143 + ); 1.144 + continue; 1.145 + } 1.146 + if (!ResponseType.class.isAssignableFrom(method.getReturnType())) { 1.147 + LOG.warn("{} is annotated with {} but has the wrong return type - 'ResponseType' required", 1.148 + method.getName(), RequestMapping.class.getSimpleName() 1.149 + ); 1.150 + continue; 1.151 + } 1.152 + 1.153 + Class<?>[] params = method.getParameterTypes(); 1.154 + if (params.length == 2 1.155 + && HttpServletRequest.class.isAssignableFrom(params[0]) 1.156 + && HttpServletResponse.class.isAssignableFrom(params[1])) { 1.157 + 1.158 + final String requestPath = "/"+mapping.get().requestPath(); 1.159 + 1.160 + if (mappings.computeIfAbsent(mapping.get().method(), k -> new HashMap<>()). 1.161 + putIfAbsent(requestPath, 1.162 + (req, resp) -> invokeMapping(method, req, resp)) != null) { 1.163 + LOG.warn("{} {} has multiple mappings", 1.164 + mapping.get().method(), 1.165 + mapping.get().requestPath() 1.166 + ); 1.167 + } 1.168 + 1.169 + LOG.debug("{} {} maps to {}::{}", 1.170 + mapping.get().method(), 1.171 + requestPath, 1.172 + getClass().getSimpleName(), 1.173 + method.getName() 1.174 + ); 1.175 + } else { 1.176 + LOG.warn("{} is annotated with {} but has the wrong parameters - (HttpServletRequest,HttpServletResponse) required", 1.177 + method.getName(), RequestMapping.class.getSimpleName() 1.178 + ); 1.179 + } 1.180 + } 1.181 + } 1.182 + } catch (SecurityException ex) { 1.183 + LOG.error("Scan for request mappings on declared methods failed.", ex); 1.184 + } 1.185 + } 1.186 + 1.187 + @Override 1.188 + public void destroy() { 1.189 + mappings.clear(); 1.190 + LOG.trace("{} destroyed", getServletName()); 1.191 + } 1.192 + 1.193 + /** 1.194 + * Sets the name of the dynamic fragment. 1.195 + * 1.196 + * It is sufficient to specify the name without any extension. The extension 1.197 + * is added automatically if not specified. 1.198 + * 1.199 + * The fragment must be located in the dynamic fragments folder. 1.200 + * 1.201 + * @param req the servlet request object 1.202 + * @param fragmentName the name of the fragment 1.203 + * @see Constants#DYN_FRAGMENT_PATH_PREFIX 1.204 + */ 1.205 + public void setDynamicFragment(HttpServletRequest req, String fragmentName) { 1.206 + req.setAttribute(Constants.REQ_ATTR_FRAGMENT, Functions.dynFragmentPath(fragmentName)); 1.207 + } 1.208 + 1.209 + /** 1.210 + * Specifies the name of an additional stylesheet used by the module. 1.211 + * 1.212 + * Setting an additional stylesheet is optional, but quite common for HTML 1.213 + * output. 1.214 + * 1.215 + * It is sufficient to specify the name without any extension. The extension 1.216 + * is added automatically if not specified. 1.217 + * 1.218 + * @param req the servlet request object 1.219 + * @param stylesheet the name of the stylesheet 1.220 + */ 1.221 + public void setStylesheet(HttpServletRequest req, String stylesheet) { 1.222 + req.setAttribute(Constants.REQ_ATTR_STYLESHEET, Functions.enforceExt(stylesheet, ".css")); 1.223 + } 1.224 + 1.225 + private void forwardToFullView(HttpServletRequest req, HttpServletResponse resp) 1.226 + throws IOException, ServletException { 1.227 + 1.228 + req.setAttribute(Constants.REQ_ATTR_MENU, getModuleManager().getMainMenu(getDatabaseFacade())); 1.229 + req.getRequestDispatcher(HTML_FULL_DISPATCHER).forward(req, resp); 1.230 + } 1.231 + 1.232 + private Optional<HandlerMethod> findMapping(HttpMethod method, HttpServletRequest req) { 1.233 + return Optional.ofNullable(mappings.get(method)).map( 1.234 + (rm) -> rm.get(Optional.ofNullable(req.getPathInfo()).orElse("/")) 1.235 + ); 1.236 + } 1.237 + 1.238 + private void forwardAsSepcified(ResponseType type, HttpServletRequest req, HttpServletResponse resp) 1.239 + throws ServletException, IOException { 1.240 + switch (type) { 1.241 + case NONE: return; 1.242 + case HTML_FULL: 1.243 + forwardToFullView(req, resp); 1.244 + return; 1.245 + // TODO: implement remaining response types 1.246 + default: 1.247 + // this code should be unreachable 1.248 + LOG.error("ResponseType switch is not exhaustive - this is a bug!"); 1.249 + throw new UnsupportedOperationException(); 1.250 + } 1.251 + } 1.252 + 1.253 + private void doProcess(HttpMethod method, HttpServletRequest req, HttpServletResponse resp) 1.254 + throws ServletException, IOException { 1.255 + 1.256 + // Synchronize module information with database 1.257 + getModuleManager().syncWithDatabase(getDatabaseFacade()); 1.258 + 1.259 + // choose the requested language as session language (if available) or fall back to english, otherwise 1.260 + HttpSession session = req.getSession(); 1.261 + if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) { 1.262 + Optional<List<String>> availableLanguages = Functions.availableLanguages(getServletContext()).map(Arrays::asList); 1.263 + Optional<Locale> reqLocale = Optional.of(req.getLocale()); 1.264 + Locale sessionLocale = reqLocale.filter((rl) -> availableLanguages.map((al) -> al.contains(rl.getLanguage())).orElse(false)).orElse(Locale.ENGLISH); 1.265 + session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, sessionLocale); 1.266 + LOG.debug("Settng language for new session {}: {}", session.getId(), sessionLocale.getDisplayLanguage()); 1.267 + } else { 1.268 + Locale sessionLocale = (Locale) session.getAttribute(Constants.SESSION_ATTR_LANGUAGE); 1.269 + resp.setLocale(sessionLocale); 1.270 + LOG.trace("Continuing session {} with language {}", session.getId(), sessionLocale); 1.271 + } 1.272 + 1.273 + // set some internal request attributes 1.274 + req.setAttribute(Constants.REQ_ATTR_PATH, Functions.fullPath(req)); 1.275 + req.setAttribute(Constants.REQ_ATTR_MODULE_CLASSNAME, this.getClass().getName()); 1.276 + moduleInfoELProxy.ifPresent((proxy) -> req.setAttribute(Constants.REQ_ATTR_MODULE_INFO, proxy)); 1.277 + 1.278 + 1.279 + // call the handler, if available, or send an HTTP 404 error 1.280 + Optional<HandlerMethod> mapping = findMapping(method, req); 1.281 + if (mapping.isPresent()) { 1.282 + forwardAsSepcified(mapping.get().apply(req, resp), req, resp); 1.283 + } else { 1.284 + resp.sendError(HttpServletResponse.SC_NOT_FOUND); 1.285 + } 1.286 + } 1.287 + 1.288 + @Override 1.289 + protected final void doGet(HttpServletRequest req, HttpServletResponse resp) 1.290 + throws ServletException, IOException { 1.291 + doProcess(HttpMethod.GET, req, resp); 1.292 + } 1.293 + 1.294 + @Override 1.295 + protected final void doPost(HttpServletRequest req, HttpServletResponse resp) 1.296 + throws ServletException, IOException { 1.297 + doProcess(HttpMethod.POST, req, resp); 1.298 + } 1.299 +}