src/main/java/de/uapcore/lightpit/AbstractServlet.java

changeset 184
e8eecee6aadf
parent 183
61669abf277f
child 185
5ec9fcfbdf9c
--- 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.
-     * <p>
-     * Paths in this map must always start with a leading slash, although
-     * the specification in the annotation must not start with a leading slash.
-     * <p>
-     * The reason for this is the different handling of empty paths in
-     * {@link HttpServletRequest#getPathInfo()}.
-     */
-    private final Map<HttpMethod, Map<PathPattern, Method>> 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<PathPattern, Method> 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<RequestMapping> 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.
-     * <p>
-     * 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.
-     * <p>
-     * 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 <T> Optional<T> parseParameter(String paramValue, Class<T> 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<T> 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 <T>   the expected type
-     * @return the parameter value or an empty optional, if no parameter with the specified name was found
-     */
-    protected <T> Optional<T> getParameter(HttpServletRequest req, Class<T> 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 <T>   the type of the request parameter
-     * @param <R>   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 <T, R> Optional<R> findByParameter(HttpServletRequest req, Class<T> 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<Map.Entry<PathPattern, Method>> 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<String[]> 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<List<String>> availableLanguages = availableLanguages().map(Arrays::asList);
-            Optional<Locale> 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);
-    }
-}

mercurial