1.1 --- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java Mon Jan 04 17:30:10 2021 +0100 1.2 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 1.3 @@ -1,471 +0,0 @@ 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.dao.DataAccessObject; 1.35 -import de.uapcore.lightpit.dao.PostgresDataAccessObject; 1.36 -import org.slf4j.Logger; 1.37 -import org.slf4j.LoggerFactory; 1.38 - 1.39 -import javax.servlet.ServletException; 1.40 -import javax.servlet.http.HttpServlet; 1.41 -import javax.servlet.http.HttpServletRequest; 1.42 -import javax.servlet.http.HttpServletResponse; 1.43 -import javax.servlet.http.HttpSession; 1.44 -import java.io.IOException; 1.45 -import java.lang.reflect.*; 1.46 -import java.sql.Connection; 1.47 -import java.sql.SQLException; 1.48 -import java.util.*; 1.49 -import java.util.function.Function; 1.50 -import java.util.stream.Collectors; 1.51 - 1.52 -/** 1.53 - * A special implementation of a HTTPServlet which is focused on implementing 1.54 - * the necessary functionality for LightPIT pages. 1.55 - */ 1.56 -public abstract class AbstractLightPITServlet extends HttpServlet { 1.57 - 1.58 - private static final Logger LOG = LoggerFactory.getLogger(AbstractLightPITServlet.class); 1.59 - 1.60 - private static final String SITE_JSP = jspPath("site"); 1.61 - 1.62 - /** 1.63 - * Invocation mapping gathered from the {@link RequestMapping} annotations. 1.64 - * <p> 1.65 - * Paths in this map must always start with a leading slash, although 1.66 - * the specification in the annotation must not start with a leading slash. 1.67 - * <p> 1.68 - * The reason for this is the different handling of empty paths in 1.69 - * {@link HttpServletRequest#getPathInfo()}. 1.70 - */ 1.71 - private final Map<HttpMethod, Map<PathPattern, Method>> mappings = new HashMap<>(); 1.72 - 1.73 - /** 1.74 - * Returns the name of the resource bundle associated with this servlet. 1.75 - * 1.76 - * @return the resource bundle base name 1.77 - */ 1.78 - protected abstract String getResourceBundleName(); 1.79 - 1.80 - 1.81 - /** 1.82 - * Creates a set of data access objects for the specified connection. 1.83 - * 1.84 - * @param connection the SQL connection 1.85 - * @return a set of data access objects 1.86 - */ 1.87 - private DataAccessObject createDataAccessObjects(Connection connection) { 1.88 - final var df = (DataSourceProvider) getServletContext().getAttribute(DataSourceProvider.Companion.getSC_ATTR_NAME()); 1.89 - if (df.getDialect() == DataSourceProvider.Dialect.Postgres) { 1.90 - return new PostgresDataAccessObject(connection); 1.91 - } 1.92 - throw new UnsupportedOperationException("Non-exhaustive if-else - this is a bug."); 1.93 - } 1.94 - 1.95 - private void invokeMapping(Map.Entry<PathPattern, Method> mapping, HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException { 1.96 - final var pathPattern = mapping.getKey(); 1.97 - final var method = mapping.getValue(); 1.98 - try { 1.99 - LOG.trace("invoke {}#{}", method.getDeclaringClass().getName(), method.getName()); 1.100 - final var paramTypes = method.getParameterTypes(); 1.101 - final var paramValues = new Object[paramTypes.length]; 1.102 - for (int i = 0; i < paramTypes.length; i++) { 1.103 - if (paramTypes[i].isAssignableFrom(HttpServletRequest.class)) { 1.104 - paramValues[i] = req; 1.105 - } else if (paramTypes[i].isAssignableFrom(HttpServletResponse.class)) { 1.106 - paramValues[i] = resp; 1.107 - } 1.108 - if (paramTypes[i].isAssignableFrom(DataAccessObject.class)) { 1.109 - paramValues[i] = dao; 1.110 - } 1.111 - if (paramTypes[i].isAssignableFrom(PathParameters.class)) { 1.112 - paramValues[i] = pathPattern.obtainPathParameters(sanitizeRequestPath(req)); 1.113 - } 1.114 - } 1.115 - method.invoke(this, paramValues); 1.116 - } catch (InvocationTargetException ex) { 1.117 - LOG.error("invocation of method {}::{} failed: {}", 1.118 - method.getDeclaringClass().getName(), method.getName(), ex.getTargetException().getMessage()); 1.119 - LOG.debug("Details: ", ex.getTargetException()); 1.120 - resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getTargetException().getMessage()); 1.121 - } catch (ReflectiveOperationException | ClassCastException ex) { 1.122 - LOG.error("invocation of method {}::{} failed: {}", 1.123 - method.getDeclaringClass().getName(), method.getName(), ex.getMessage()); 1.124 - LOG.debug("Details: ", ex); 1.125 - resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage()); 1.126 - } 1.127 - } 1.128 - 1.129 - @Override 1.130 - public void init() throws ServletException { 1.131 - scanForRequestMappings(); 1.132 - 1.133 - LOG.trace("{} initialized", getServletName()); 1.134 - } 1.135 - 1.136 - private void scanForRequestMappings() { 1.137 - try { 1.138 - Method[] methods = getClass().getDeclaredMethods(); 1.139 - for (Method method : methods) { 1.140 - Optional<RequestMapping> mapping = Optional.ofNullable(method.getAnnotation(RequestMapping.class)); 1.141 - if (mapping.isPresent()) { 1.142 - if (mapping.get().requestPath().isBlank()) { 1.143 - LOG.warn("{} is annotated with {} but request path is empty", 1.144 - method.getName(), RequestMapping.class.getSimpleName() 1.145 - ); 1.146 - continue; 1.147 - } 1.148 - 1.149 - if (!Modifier.isPublic(method.getModifiers())) { 1.150 - LOG.warn("{} is annotated with {} but is not public", 1.151 - method.getName(), RequestMapping.class.getSimpleName() 1.152 - ); 1.153 - continue; 1.154 - } 1.155 - if (Modifier.isAbstract(method.getModifiers())) { 1.156 - LOG.warn("{} is annotated with {} but is abstract", 1.157 - method.getName(), RequestMapping.class.getSimpleName() 1.158 - ); 1.159 - continue; 1.160 - } 1.161 - 1.162 - boolean paramsInjectible = true; 1.163 - for (var param : method.getParameterTypes()) { 1.164 - paramsInjectible &= HttpServletRequest.class.isAssignableFrom(param) 1.165 - || HttpServletResponse.class.isAssignableFrom(param) 1.166 - || PathParameters.class.isAssignableFrom(param) 1.167 - || DataAccessObject.class.isAssignableFrom(param); 1.168 - } 1.169 - if (paramsInjectible) { 1.170 - try { 1.171 - PathPattern pathPattern = new PathPattern(mapping.get().requestPath()); 1.172 - 1.173 - final var methodMappings = mappings.computeIfAbsent(mapping.get().method(), k -> new HashMap<>()); 1.174 - final var currentMapping = methodMappings.putIfAbsent(pathPattern, method); 1.175 - if (currentMapping != null) { 1.176 - LOG.warn("Cannot map {} {} to {} in class {} - this would override the mapping to {}", 1.177 - mapping.get().method(), 1.178 - mapping.get().requestPath(), 1.179 - method.getName(), 1.180 - getClass().getSimpleName(), 1.181 - currentMapping.getName() 1.182 - ); 1.183 - } 1.184 - 1.185 - LOG.debug("{} {} maps to {}::{}", 1.186 - mapping.get().method(), 1.187 - mapping.get().requestPath(), 1.188 - getClass().getSimpleName(), 1.189 - method.getName() 1.190 - ); 1.191 - } catch (IllegalArgumentException ex) { 1.192 - LOG.warn("Request mapping for {} failed: path pattern '{}' is syntactically invalid", 1.193 - method.getName(), mapping.get().requestPath() 1.194 - ); 1.195 - } 1.196 - } else { 1.197 - LOG.warn("{} is annotated with {} but has the wrong parameters - only HttpServletRequest, HttpServletResponse, PathParameters, and DataAccessObjects are allowed", 1.198 - method.getName(), RequestMapping.class.getSimpleName() 1.199 - ); 1.200 - } 1.201 - } 1.202 - } 1.203 - } catch (SecurityException ex) { 1.204 - LOG.error("Scan for request mappings on declared methods failed.", ex); 1.205 - } 1.206 - } 1.207 - 1.208 - @Override 1.209 - public void destroy() { 1.210 - mappings.clear(); 1.211 - LOG.trace("{} destroyed", getServletName()); 1.212 - } 1.213 - 1.214 - /** 1.215 - * Sets the name of the content page. 1.216 - * <p> 1.217 - * It is sufficient to specify the name without any extension. The extension 1.218 - * is added automatically if not specified. 1.219 - * 1.220 - * @param req the servlet request object 1.221 - * @param pageName the name of the content page 1.222 - * @see Constants#REQ_ATTR_CONTENT_PAGE 1.223 - */ 1.224 - protected void setContentPage(HttpServletRequest req, String pageName) { 1.225 - req.setAttribute(Constants.REQ_ATTR_CONTENT_PAGE, jspPath(pageName)); 1.226 - } 1.227 - 1.228 - /** 1.229 - * Sets the navigation menu. 1.230 - * 1.231 - * @param req the servlet request object 1.232 - * @param jspName the name of the menu's jsp file 1.233 - * @see Constants#REQ_ATTR_NAVIGATION 1.234 - */ 1.235 - protected void setNavigationMenu(HttpServletRequest req, String jspName) { 1.236 - req.setAttribute(Constants.REQ_ATTR_NAVIGATION, jspPath(jspName)); 1.237 - } 1.238 - 1.239 - /** 1.240 - * @param req the servlet request object 1.241 - * @param location the location where to redirect 1.242 - * @see Constants#REQ_ATTR_REDIRECT_LOCATION 1.243 - */ 1.244 - protected void setRedirectLocation(HttpServletRequest req, String location) { 1.245 - if (location.startsWith("./")) { 1.246 - location = location.replaceFirst("\\./", baseHref(req)); 1.247 - } 1.248 - req.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, location); 1.249 - } 1.250 - 1.251 - /** 1.252 - * Specifies the names of additional stylesheets used by this Servlet. 1.253 - * <p> 1.254 - * It is sufficient to specify the name without any extension. The extension 1.255 - * is added automatically if not specified. 1.256 - * 1.257 - * @param req the servlet request object 1.258 - * @param stylesheets the names of the stylesheets 1.259 - */ 1.260 - public void setStylesheet(HttpServletRequest req, String ... stylesheets) { 1.261 - req.setAttribute(Constants.REQ_ATTR_STYLESHEET, Arrays 1.262 - .stream(stylesheets) 1.263 - .map(s -> enforceExt(s, ".css")) 1.264 - .collect(Collectors.toUnmodifiableList())); 1.265 - } 1.266 - 1.267 - /** 1.268 - * Sets the view model object. 1.269 - * The type must match the expected type in the JSP file. 1.270 - * 1.271 - * @param req the servlet request object 1.272 - * @param viewModel the view model object 1.273 - */ 1.274 - public void setViewModel(HttpServletRequest req, Object viewModel) { 1.275 - req.setAttribute(Constants.REQ_ATTR_VIEWMODEL, viewModel); 1.276 - } 1.277 - 1.278 - private <T> Optional<T> parseParameter(String paramValue, Class<T> clazz) { 1.279 - if (paramValue == null) return Optional.empty(); 1.280 - if (clazz.equals(Boolean.class)) { 1.281 - if (paramValue.equalsIgnoreCase("false") || paramValue.equals("0")) { 1.282 - return Optional.of((T) Boolean.FALSE); 1.283 - } else { 1.284 - return Optional.of((T) Boolean.TRUE); 1.285 - } 1.286 - } 1.287 - if (clazz.equals(String.class)) return Optional.of((T) paramValue); 1.288 - if (java.sql.Date.class.isAssignableFrom(clazz)) { 1.289 - try { 1.290 - return Optional.of((T) java.sql.Date.valueOf(paramValue)); 1.291 - } catch (IllegalArgumentException ex) { 1.292 - return Optional.empty(); 1.293 - } 1.294 - } 1.295 - try { 1.296 - final Constructor<T> ctor = clazz.getConstructor(String.class); 1.297 - return Optional.of(ctor.newInstance(paramValue)); 1.298 - } catch (ReflectiveOperationException e) { 1.299 - // does not type check and is not convertible - treat as if the parameter was never set 1.300 - return Optional.empty(); 1.301 - } 1.302 - } 1.303 - 1.304 - /** 1.305 - * Obtains a request parameter of the specified type. 1.306 - * The specified type must have a single-argument constructor accepting a string to perform conversion. 1.307 - * The constructor of the specified type may throw an exception on conversion failures. 1.308 - * 1.309 - * @param req the servlet request object 1.310 - * @param clazz the class object of the expected type 1.311 - * @param name the name of the parameter 1.312 - * @param <T> the expected type 1.313 - * @return the parameter value or an empty optional, if no parameter with the specified name was found 1.314 - */ 1.315 - protected <T> Optional<T> getParameter(HttpServletRequest req, Class<T> clazz, String name) { 1.316 - if (clazz.isArray()) { 1.317 - final String[] paramValues = req.getParameterValues(name); 1.318 - int len = paramValues == null ? 0 : paramValues.length; 1.319 - final var array = (T) Array.newInstance(clazz.getComponentType(), len); 1.320 - for (int i = 0; i < len; i++) { 1.321 - try { 1.322 - final Constructor<?> ctor = clazz.getComponentType().getConstructor(String.class); 1.323 - Array.set(array, i, ctor.newInstance(paramValues[i])); 1.324 - } catch (ReflectiveOperationException e) { 1.325 - throw new RuntimeException(e); 1.326 - } 1.327 - } 1.328 - return Optional.of(array); 1.329 - } else { 1.330 - return parseParameter(req.getParameter(name), clazz); 1.331 - } 1.332 - } 1.333 - 1.334 - /** 1.335 - * Tries to look up an entity with a key obtained from a request parameter. 1.336 - * 1.337 - * @param req the servlet request object 1.338 - * @param clazz the class representing the type of the request parameter 1.339 - * @param name the name of the request parameter 1.340 - * @param find the find function (typically a DAO function) 1.341 - * @param <T> the type of the request parameter 1.342 - * @param <R> the type of the looked up entity 1.343 - * @return the retrieved entity or an empty optional if there is no such entity or the request parameter was missing 1.344 - * @throws SQLException if the find function throws an exception 1.345 - */ 1.346 - protected <T, R> Optional<R> findByParameter(HttpServletRequest req, Class<T> clazz, String name, Function<? super T, ? extends R> find) { 1.347 - final var param = getParameter(req, clazz, name); 1.348 - if (param.isPresent()) { 1.349 - return Optional.ofNullable(find.apply(param.get())); 1.350 - } else { 1.351 - return Optional.empty(); 1.352 - } 1.353 - } 1.354 - 1.355 - protected void setAttributeFromParameter(HttpServletRequest req, String name) { 1.356 - final var parm = req.getParameter(name); 1.357 - if (parm != null) { 1.358 - req.setAttribute(name, parm); 1.359 - } 1.360 - } 1.361 - 1.362 - private String sanitizeRequestPath(HttpServletRequest req) { 1.363 - return Optional.ofNullable(req.getPathInfo()).orElse("/"); 1.364 - } 1.365 - 1.366 - private Optional<Map.Entry<PathPattern, Method>> findMapping(HttpMethod method, HttpServletRequest req) { 1.367 - return Optional.ofNullable(mappings.get(method)).flatMap(rm -> 1.368 - rm.entrySet().stream().filter( 1.369 - kv -> kv.getKey().matches(sanitizeRequestPath(req)) 1.370 - ).findAny() 1.371 - ); 1.372 - } 1.373 - 1.374 - protected void renderSite(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 1.375 - req.getRequestDispatcher(SITE_JSP).forward(req, resp); 1.376 - } 1.377 - 1.378 - protected Optional<String[]> availableLanguages() { 1.379 - return Optional.ofNullable(getServletContext().getInitParameter(Constants.CTX_ATTR_LANGUAGES)).map((x) -> x.split("\\s*,\\s*")); 1.380 - } 1.381 - 1.382 - private static String baseHref(HttpServletRequest req) { 1.383 - return String.format("%s://%s:%d%s/", 1.384 - req.getScheme(), 1.385 - req.getServerName(), 1.386 - req.getServerPort(), 1.387 - req.getContextPath()); 1.388 - } 1.389 - 1.390 - private static String enforceExt(String filename, String ext) { 1.391 - return filename.endsWith(ext) ? filename : filename + ext; 1.392 - } 1.393 - 1.394 - private static String jspPath(String filename) { 1.395 - return enforceExt(Constants.JSP_PATH_PREFIX + filename, ".jsp"); 1.396 - } 1.397 - 1.398 - private void doProcess(HttpMethod method, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 1.399 - // the very first thing to do is to force UTF-8 1.400 - req.setCharacterEncoding("UTF-8"); 1.401 - 1.402 - // choose the requested language as session language (if available) or fall back to english, otherwise 1.403 - HttpSession session = req.getSession(); 1.404 - if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) { 1.405 - Optional<List<String>> availableLanguages = availableLanguages().map(Arrays::asList); 1.406 - Optional<Locale> reqLocale = Optional.of(req.getLocale()); 1.407 - Locale sessionLocale = reqLocale.filter((rl) -> availableLanguages.map((al) -> al.contains(rl.getLanguage())).orElse(false)).orElse(Locale.ENGLISH); 1.408 - session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, sessionLocale); 1.409 - LOG.debug("Setting language for new session {}: {}", session.getId(), sessionLocale.getDisplayLanguage()); 1.410 - } else { 1.411 - Locale sessionLocale = (Locale) session.getAttribute(Constants.SESSION_ATTR_LANGUAGE); 1.412 - resp.setLocale(sessionLocale); 1.413 - LOG.trace("Continuing session {} with language {}", session.getId(), sessionLocale); 1.414 - } 1.415 - 1.416 - // set some internal request attributes 1.417 - final String fullPath = req.getServletPath() + Optional.ofNullable(req.getPathInfo()).orElse(""); 1.418 - req.setAttribute(Constants.REQ_ATTR_BASE_HREF, baseHref(req)); 1.419 - req.setAttribute(Constants.REQ_ATTR_PATH, fullPath); 1.420 - req.setAttribute(Constants.REQ_ATTR_RESOURCE_BUNDLE, getResourceBundleName()); 1.421 - 1.422 - // if this is an error path, bypass the normal flow 1.423 - if (fullPath.startsWith("/error/")) { 1.424 - final var mapping = findMapping(method, req); 1.425 - if (mapping.isPresent()) { 1.426 - invokeMapping(mapping.get(), req, resp, null); 1.427 - } 1.428 - return; 1.429 - } 1.430 - 1.431 - // obtain a connection and create the data access objects 1.432 - final var db = (DataSourceProvider) req.getServletContext().getAttribute(DataSourceProvider.Companion.getSC_ATTR_NAME()); 1.433 - final var ds = db.getDataSource(); 1.434 - if (ds == null) { 1.435 - resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "JNDI DataSource lookup failed. See log for details."); 1.436 - return; 1.437 - } 1.438 - try (final var connection = ds.getConnection()) { 1.439 - final var dao = createDataAccessObjects(connection); 1.440 - try { 1.441 - connection.setAutoCommit(false); 1.442 - // call the handler, if available, or send an HTTP 404 error 1.443 - final var mapping = findMapping(method, req); 1.444 - if (mapping.isPresent()) { 1.445 - invokeMapping(mapping.get(), req, resp, dao); 1.446 - } else { 1.447 - resp.sendError(HttpServletResponse.SC_NOT_FOUND); 1.448 - } 1.449 - connection.commit(); 1.450 - } catch (SQLException ex) { 1.451 - LOG.warn("Database transaction failed (Code {}): {}", ex.getErrorCode(), ex.getMessage()); 1.452 - LOG.debug("Details: ", ex); 1.453 - resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unhandled Transaction Error - Code: " + ex.getErrorCode()); 1.454 - connection.rollback(); 1.455 - } 1.456 - } catch (SQLException ex) { 1.457 - LOG.error("Severe Database Exception (Code {}): {}", ex.getErrorCode(), ex.getMessage()); 1.458 - LOG.debug("Details: ", ex); 1.459 - resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Database Error - Code: " + ex.getErrorCode()); 1.460 - } 1.461 - } 1.462 - 1.463 - @Override 1.464 - protected final void doGet(HttpServletRequest req, HttpServletResponse resp) 1.465 - throws ServletException, IOException { 1.466 - doProcess(HttpMethod.GET, req, resp); 1.467 - } 1.468 - 1.469 - @Override 1.470 - protected final void doPost(HttpServletRequest req, HttpServletResponse resp) 1.471 - throws ServletException, IOException { 1.472 - doProcess(HttpMethod.POST, req, resp); 1.473 - } 1.474 -}