diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/AbstractServlet.java
--- a/src/main/java/de/uapcore/lightpit/AbstractServlet.java Sat Jan 23 14:47:59 2021 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,460 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2021 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.dao.DataAccessObject;
-import de.uapcore.lightpit.dao.PostgresDataAccessObject;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
-import java.io.IOException;
-import java.lang.reflect.*;
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.util.*;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-/**
- * A special implementation of a HTTPServlet which is focused on implementing
- * the necessary functionality for LightPIT pages.
- */
-public abstract class AbstractServlet extends HttpServlet {
-
- private static final Logger LOG = LoggerFactory.getLogger(AbstractServlet.class);
-
- /**
- * Invocation mapping gathered from the {@link RequestMapping} annotations.
- *
- * Paths in this map must always start with a leading slash, although
- * the specification in the annotation must not start with a leading slash.
- *
- * The reason for this is the different handling of empty paths in
- * {@link HttpServletRequest#getPathInfo()}.
- */
- private final Map> mappings = new HashMap<>();
-
- /**
- * Creates a set of data access objects for the specified connection.
- *
- * @param connection the SQL connection
- * @return a set of data access objects
- */
- private DataAccessObject createDataAccessObjects(Connection connection) {
- final var df = (DataSourceProvider) getServletContext().getAttribute(DataSourceProvider.Companion.getSC_ATTR_NAME());
- if (df.getDialect() == DataSourceProvider.Dialect.Postgres) {
- return new PostgresDataAccessObject(connection);
- }
- throw new UnsupportedOperationException("Non-exhaustive if-else - this is a bug.");
- }
-
- private void invokeMapping(Map.Entry mapping, HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException {
- final var pathPattern = mapping.getKey();
- final var method = mapping.getValue();
- try {
- LOG.trace("invoke {}#{}", method.getDeclaringClass().getName(), method.getName());
- final var paramTypes = method.getParameterTypes();
- final var paramValues = new Object[paramTypes.length];
- for (int i = 0; i < paramTypes.length; i++) {
- if (paramTypes[i].isAssignableFrom(HttpServletRequest.class)) {
- paramValues[i] = req;
- } else if (paramTypes[i].isAssignableFrom(HttpServletResponse.class)) {
- paramValues[i] = resp;
- }
- if (paramTypes[i].isAssignableFrom(DataAccessObject.class)) {
- paramValues[i] = dao;
- }
- if (paramTypes[i].isAssignableFrom(PathParameters.class)) {
- paramValues[i] = pathPattern.obtainPathParameters(sanitizeRequestPath(req));
- }
- }
- method.invoke(this, paramValues);
- } catch (InvocationTargetException ex) {
- LOG.error("invocation of method {}::{} failed: {}",
- method.getDeclaringClass().getName(), method.getName(), ex.getTargetException().getMessage());
- LOG.debug("Details: ", ex.getTargetException());
- resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getTargetException().getMessage());
- } catch (ReflectiveOperationException | ClassCastException ex) {
- LOG.error("invocation of method {}::{} failed: {}",
- method.getDeclaringClass().getName(), method.getName(), ex.getMessage());
- LOG.debug("Details: ", ex);
- resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
- }
- }
-
- @Override
- public void init() throws ServletException {
- scanForRequestMappings();
-
- LOG.trace("{} initialized", getServletName());
- }
-
- private void scanForRequestMappings() {
- try {
- Method[] methods = getClass().getDeclaredMethods();
- for (Method method : methods) {
- Optional mapping = Optional.ofNullable(method.getAnnotation(RequestMapping.class));
- if (mapping.isPresent()) {
- if (mapping.get().requestPath().isBlank()) {
- LOG.warn("{} is annotated with {} but request path is empty",
- method.getName(), RequestMapping.class.getSimpleName()
- );
- continue;
- }
-
- if (!Modifier.isPublic(method.getModifiers())) {
- LOG.warn("{} is annotated with {} but is not public",
- method.getName(), RequestMapping.class.getSimpleName()
- );
- continue;
- }
- if (Modifier.isAbstract(method.getModifiers())) {
- LOG.warn("{} is annotated with {} but is abstract",
- method.getName(), RequestMapping.class.getSimpleName()
- );
- continue;
- }
-
- boolean paramsInjectible = true;
- for (var param : method.getParameterTypes()) {
- paramsInjectible &= HttpServletRequest.class.isAssignableFrom(param)
- || HttpServletResponse.class.isAssignableFrom(param)
- || PathParameters.class.isAssignableFrom(param)
- || DataAccessObject.class.isAssignableFrom(param);
- }
- if (paramsInjectible) {
- try {
- PathPattern pathPattern = new PathPattern(mapping.get().requestPath());
-
- final var methodMappings = mappings.computeIfAbsent(mapping.get().method(), k -> new HashMap<>());
- final var currentMapping = methodMappings.putIfAbsent(pathPattern, method);
- if (currentMapping != null) {
- LOG.warn("Cannot map {} {} to {} in class {} - this would override the mapping to {}",
- mapping.get().method(),
- mapping.get().requestPath(),
- method.getName(),
- getClass().getSimpleName(),
- currentMapping.getName()
- );
- }
-
- LOG.debug("{} {} maps to {}::{}",
- mapping.get().method(),
- mapping.get().requestPath(),
- getClass().getSimpleName(),
- method.getName()
- );
- } catch (IllegalArgumentException ex) {
- LOG.warn("Request mapping for {} failed: path pattern '{}' is syntactically invalid",
- method.getName(), mapping.get().requestPath()
- );
- }
- } else {
- LOG.warn("{} is annotated with {} but has the wrong parameters - only HttpServletRequest, HttpServletResponse, PathParameters, and DataAccessObjects are allowed",
- method.getName(), RequestMapping.class.getSimpleName()
- );
- }
- }
- }
- } catch (SecurityException ex) {
- LOG.error("Scan for request mappings on declared methods failed.", ex);
- }
- }
-
- @Override
- public void destroy() {
- mappings.clear();
- LOG.trace("{} destroyed", getServletName());
- }
-
- /**
- * Sets the name of the content page.
- *
- * It is sufficient to specify the name without any extension. The extension
- * is added automatically if not specified.
- *
- * @param req the servlet request object
- * @param pageName the name of the content page
- * @see Constants#REQ_ATTR_CONTENT_PAGE
- */
- protected void setContentPage(HttpServletRequest req, String pageName) {
- req.setAttribute(Constants.REQ_ATTR_CONTENT_PAGE, jspPath(pageName));
- }
-
- /**
- * Sets the navigation menu.
- *
- * @param req the servlet request object
- * @param jspName the name of the menu's jsp file
- * @see Constants#REQ_ATTR_NAVIGATION
- */
- protected void setNavigationMenu(HttpServletRequest req, String jspName) {
- req.setAttribute(Constants.REQ_ATTR_NAVIGATION, jspPath(jspName));
- }
-
- /**
- * @param req the servlet request object
- * @param location the location where to redirect
- * @see Constants#REQ_ATTR_REDIRECT_LOCATION
- */
- protected void setRedirectLocation(HttpServletRequest req, String location) {
- if (location.startsWith("./")) {
- location = location.replaceFirst("\\./", baseHref(req));
- }
- req.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, location);
- }
-
- /**
- * Specifies the names of additional stylesheets used by this Servlet.
- *
- * It is sufficient to specify the name without any extension. The extension
- * is added automatically if not specified.
- *
- * @param req the servlet request object
- * @param stylesheets the names of the stylesheets
- */
- public void setStylesheet(HttpServletRequest req, String ... stylesheets) {
- req.setAttribute(Constants.REQ_ATTR_STYLESHEET, Arrays
- .stream(stylesheets)
- .map(s -> enforceExt(s, ".css"))
- .collect(Collectors.toUnmodifiableList()));
- }
-
- /**
- * Sets the view model object.
- * The type must match the expected type in the JSP file.
- *
- * @param req the servlet request object
- * @param viewModel the view model object
- */
- public void setViewModel(HttpServletRequest req, Object viewModel) {
- req.setAttribute(Constants.REQ_ATTR_VIEWMODEL, viewModel);
- }
-
- private Optional parseParameter(String paramValue, Class clazz) {
- if (paramValue == null) return Optional.empty();
- if (clazz.equals(Boolean.class)) {
- if (paramValue.equalsIgnoreCase("false") || paramValue.equals("0")) {
- return Optional.of((T) Boolean.FALSE);
- } else {
- return Optional.of((T) Boolean.TRUE);
- }
- }
- if (clazz.equals(String.class)) return Optional.of((T) paramValue);
- if (java.sql.Date.class.isAssignableFrom(clazz)) {
- try {
- return Optional.of((T) java.sql.Date.valueOf(paramValue));
- } catch (IllegalArgumentException ex) {
- return Optional.empty();
- }
- }
- try {
- final Constructor ctor = clazz.getConstructor(String.class);
- return Optional.of(ctor.newInstance(paramValue));
- } catch (ReflectiveOperationException e) {
- // does not type check and is not convertible - treat as if the parameter was never set
- return Optional.empty();
- }
- }
-
- /**
- * Obtains a request parameter of the specified type.
- * The specified type must have a single-argument constructor accepting a string to perform conversion.
- * The constructor of the specified type may throw an exception on conversion failures.
- *
- * @param req the servlet request object
- * @param clazz the class object of the expected type
- * @param name the name of the parameter
- * @param the expected type
- * @return the parameter value or an empty optional, if no parameter with the specified name was found
- */
- protected Optional getParameter(HttpServletRequest req, Class clazz, String name) {
- if (clazz.isArray()) {
- final String[] paramValues = req.getParameterValues(name);
- int len = paramValues == null ? 0 : paramValues.length;
- final var array = (T) Array.newInstance(clazz.getComponentType(), len);
- for (int i = 0; i < len; i++) {
- try {
- final Constructor> ctor = clazz.getComponentType().getConstructor(String.class);
- Array.set(array, i, ctor.newInstance(paramValues[i]));
- } catch (ReflectiveOperationException e) {
- throw new RuntimeException(e);
- }
- }
- return Optional.of(array);
- } else {
- return parseParameter(req.getParameter(name), clazz);
- }
- }
-
- /**
- * Tries to look up an entity with a key obtained from a request parameter.
- *
- * @param req the servlet request object
- * @param clazz the class representing the type of the request parameter
- * @param name the name of the request parameter
- * @param find the find function (typically a DAO function)
- * @param the type of the request parameter
- * @param the type of the looked up entity
- * @return the retrieved entity or an empty optional if there is no such entity or the request parameter was missing
- * @throws SQLException if the find function throws an exception
- */
- protected Optional findByParameter(HttpServletRequest req, Class clazz, String name, Function super T, ? extends R> find) {
- final var param = getParameter(req, clazz, name);
- if (param.isPresent()) {
- return Optional.ofNullable(find.apply(param.get()));
- } else {
- return Optional.empty();
- }
- }
-
- protected void setAttributeFromParameter(HttpServletRequest req, String name) {
- final var parm = req.getParameter(name);
- if (parm != null) {
- req.setAttribute(name, parm);
- }
- }
-
- private String sanitizeRequestPath(HttpServletRequest req) {
- return Optional.ofNullable(req.getPathInfo()).orElse("/");
- }
-
- private Optional> findMapping(HttpMethod method, HttpServletRequest req) {
- return Optional.ofNullable(mappings.get(method)).flatMap(rm ->
- rm.entrySet().stream().filter(
- kv -> kv.getKey().matches(sanitizeRequestPath(req))
- ).findAny()
- );
- }
-
- protected void renderSite(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- req.getRequestDispatcher(jspPath("site")).forward(req, resp);
- }
-
- protected Optional availableLanguages() {
- return Optional.ofNullable(getServletContext().getInitParameter(Constants.CTX_ATTR_LANGUAGES)).map((x) -> x.split("\\s*,\\s*"));
- }
-
- private static String baseHref(HttpServletRequest req) {
- return String.format("%s://%s:%d%s/",
- req.getScheme(),
- req.getServerName(),
- req.getServerPort(),
- req.getContextPath());
- }
-
- private static String enforceExt(String filename, String ext) {
- return filename.endsWith(ext) ? filename : filename + ext;
- }
-
- private static String jspPath(String filename) {
- return enforceExt(Constants.JSP_PATH_PREFIX + filename, ".jsp");
- }
-
- private void doProcess(HttpMethod method, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- // the very first thing to do is to force UTF-8
- req.setCharacterEncoding("UTF-8");
-
- // choose the requested language as session language (if available) or fall back to english, otherwise
- HttpSession session = req.getSession();
- if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) {
- Optional> availableLanguages = availableLanguages().map(Arrays::asList);
- Optional reqLocale = Optional.of(req.getLocale());
- Locale sessionLocale = reqLocale.filter((rl) -> availableLanguages.map((al) -> al.contains(rl.getLanguage())).orElse(false)).orElse(Locale.ENGLISH);
- session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, sessionLocale);
- LOG.debug("Setting language for new session {}: {}", session.getId(), sessionLocale.getDisplayLanguage());
- } else {
- Locale sessionLocale = (Locale) session.getAttribute(Constants.SESSION_ATTR_LANGUAGE);
- resp.setLocale(sessionLocale);
- LOG.trace("Continuing session {} with language {}", session.getId(), sessionLocale);
- }
-
- // set some internal request attributes
- final String fullPath = req.getServletPath() + Optional.ofNullable(req.getPathInfo()).orElse("");
- req.setAttribute(Constants.REQ_ATTR_BASE_HREF, baseHref(req));
- req.setAttribute(Constants.REQ_ATTR_PATH, fullPath);
-
- // if this is an error path, bypass the normal flow
- if (fullPath.startsWith("/error/")) {
- final var mapping = findMapping(method, req);
- if (mapping.isPresent()) {
- invokeMapping(mapping.get(), req, resp, null);
- }
- return;
- }
-
- // obtain a connection and create the data access objects
- final var db = (DataSourceProvider) req.getServletContext().getAttribute(DataSourceProvider.Companion.getSC_ATTR_NAME());
- final var ds = db.getDataSource();
- if (ds == null) {
- resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "JNDI DataSource lookup failed. See log for details.");
- return;
- }
- try (final var connection = ds.getConnection()) {
- final var dao = createDataAccessObjects(connection);
- try {
- connection.setAutoCommit(false);
- // call the handler, if available, or send an HTTP 404 error
- final var mapping = findMapping(method, req);
- if (mapping.isPresent()) {
- invokeMapping(mapping.get(), req, resp, dao);
- } else {
- resp.sendError(HttpServletResponse.SC_NOT_FOUND);
- }
- connection.commit();
- } catch (SQLException ex) {
- LOG.warn("Database transaction failed (Code {}): {}", ex.getErrorCode(), ex.getMessage());
- LOG.debug("Details: ", ex);
- resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unhandled Transaction Error - Code: " + ex.getErrorCode());
- connection.rollback();
- }
- } catch (SQLException ex) {
- LOG.error("Severe Database Exception (Code {}): {}", ex.getErrorCode(), ex.getMessage());
- LOG.debug("Details: ", ex);
- resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Database Error - Code: " + ex.getErrorCode());
- }
- }
-
- @Override
- protected final void doGet(HttpServletRequest req, HttpServletResponse resp)
- throws ServletException, IOException {
- doProcess(HttpMethod.GET, req, resp);
- }
-
- @Override
- protected final void doPost(HttpServletRequest req, HttpServletResponse resp)
- throws ServletException, IOException {
- doProcess(HttpMethod.POST, req, resp);
- }
-}