migrates the utility classes for the AbstractServlet

Tue, 05 Jan 2021 19:19:31 +0100

author
Mike Becker <universe@uap-core.de>
date
Tue, 05 Jan 2021 19:19:31 +0100
changeset 179
623c340058f3
parent 178
88207b860cba
child 180
009700915269

migrates the utility classes for the AbstractServlet

src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/AbstractServlet.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/HttpMethod.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/PathParameters.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/PathPattern.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/RequestMapping.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/ResourceKey.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/ErrorModule.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/LanguageModule.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/UsersModule.java file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/HttpMethod.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt file | annotate | diff | comparison | revisions
     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 -}
     2.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.2 +++ b/src/main/java/de/uapcore/lightpit/AbstractServlet.java	Tue Jan 05 19:19:31 2021 +0100
     2.3 @@ -0,0 +1,471 @@
     2.4 +/*
     2.5 + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     2.6 + *
     2.7 + * Copyright 2018 Mike Becker. All rights reserved.
     2.8 + *
     2.9 + * Redistribution and use in source and binary forms, with or without
    2.10 + * modification, are permitted provided that the following conditions are met:
    2.11 + *
    2.12 + *   1. Redistributions of source code must retain the above copyright
    2.13 + *      notice, this list of conditions and the following disclaimer.
    2.14 + *
    2.15 + *   2. Redistributions in binary form must reproduce the above copyright
    2.16 + *      notice, this list of conditions and the following disclaimer in the
    2.17 + *      documentation and/or other materials provided with the distribution.
    2.18 + *
    2.19 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    2.20 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    2.21 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    2.22 + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
    2.23 + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    2.24 + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    2.25 + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    2.26 + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    2.27 + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    2.28 + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    2.29 + * POSSIBILITY OF SUCH DAMAGE.
    2.30 + *
    2.31 + */
    2.32 +package de.uapcore.lightpit;
    2.33 +
    2.34 +import de.uapcore.lightpit.dao.DataAccessObject;
    2.35 +import de.uapcore.lightpit.dao.PostgresDataAccessObject;
    2.36 +import org.slf4j.Logger;
    2.37 +import org.slf4j.LoggerFactory;
    2.38 +
    2.39 +import javax.servlet.ServletException;
    2.40 +import javax.servlet.http.HttpServlet;
    2.41 +import javax.servlet.http.HttpServletRequest;
    2.42 +import javax.servlet.http.HttpServletResponse;
    2.43 +import javax.servlet.http.HttpSession;
    2.44 +import java.io.IOException;
    2.45 +import java.lang.reflect.*;
    2.46 +import java.sql.Connection;
    2.47 +import java.sql.SQLException;
    2.48 +import java.util.*;
    2.49 +import java.util.function.Function;
    2.50 +import java.util.stream.Collectors;
    2.51 +
    2.52 +/**
    2.53 + * A special implementation of a HTTPServlet which is focused on implementing
    2.54 + * the necessary functionality for LightPIT pages.
    2.55 + */
    2.56 +public abstract class AbstractServlet extends HttpServlet {
    2.57 +
    2.58 +    private static final Logger LOG = LoggerFactory.getLogger(AbstractServlet.class);
    2.59 +
    2.60 +    private static final String SITE_JSP = jspPath("site");
    2.61 +
    2.62 +    /**
    2.63 +     * Invocation mapping gathered from the {@link RequestMapping} annotations.
    2.64 +     * <p>
    2.65 +     * Paths in this map must always start with a leading slash, although
    2.66 +     * the specification in the annotation must not start with a leading slash.
    2.67 +     * <p>
    2.68 +     * The reason for this is the different handling of empty paths in
    2.69 +     * {@link HttpServletRequest#getPathInfo()}.
    2.70 +     */
    2.71 +    private final Map<HttpMethod, Map<PathPattern, Method>> mappings = new HashMap<>();
    2.72 +
    2.73 +    /**
    2.74 +     * Returns the name of the resource bundle associated with this servlet.
    2.75 +     *
    2.76 +     * @return the resource bundle base name
    2.77 +     */
    2.78 +    protected abstract String getResourceBundleName();
    2.79 +
    2.80 +
    2.81 +    /**
    2.82 +     * Creates a set of data access objects for the specified connection.
    2.83 +     *
    2.84 +     * @param connection the SQL connection
    2.85 +     * @return a set of data access objects
    2.86 +     */
    2.87 +    private DataAccessObject createDataAccessObjects(Connection connection) {
    2.88 +        final var df = (DataSourceProvider) getServletContext().getAttribute(DataSourceProvider.Companion.getSC_ATTR_NAME());
    2.89 +        if (df.getDialect() == DataSourceProvider.Dialect.Postgres) {
    2.90 +            return new PostgresDataAccessObject(connection);
    2.91 +        }
    2.92 +        throw new UnsupportedOperationException("Non-exhaustive if-else - this is a bug.");
    2.93 +    }
    2.94 +
    2.95 +    private void invokeMapping(Map.Entry<PathPattern, Method> mapping, HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException {
    2.96 +        final var pathPattern = mapping.getKey();
    2.97 +        final var method = mapping.getValue();
    2.98 +        try {
    2.99 +            LOG.trace("invoke {}#{}", method.getDeclaringClass().getName(), method.getName());
   2.100 +            final var paramTypes = method.getParameterTypes();
   2.101 +            final var paramValues = new Object[paramTypes.length];
   2.102 +            for (int i = 0; i < paramTypes.length; i++) {
   2.103 +                if (paramTypes[i].isAssignableFrom(HttpServletRequest.class)) {
   2.104 +                    paramValues[i] = req;
   2.105 +                } else if (paramTypes[i].isAssignableFrom(HttpServletResponse.class)) {
   2.106 +                    paramValues[i] = resp;
   2.107 +                }
   2.108 +                if (paramTypes[i].isAssignableFrom(DataAccessObject.class)) {
   2.109 +                    paramValues[i] = dao;
   2.110 +                }
   2.111 +                if (paramTypes[i].isAssignableFrom(PathParameters.class)) {
   2.112 +                    paramValues[i] = pathPattern.obtainPathParameters(sanitizeRequestPath(req));
   2.113 +                }
   2.114 +            }
   2.115 +            method.invoke(this, paramValues);
   2.116 +        } catch (InvocationTargetException ex) {
   2.117 +            LOG.error("invocation of method {}::{} failed: {}",
   2.118 +                    method.getDeclaringClass().getName(), method.getName(), ex.getTargetException().getMessage());
   2.119 +            LOG.debug("Details: ", ex.getTargetException());
   2.120 +            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getTargetException().getMessage());
   2.121 +        } catch (ReflectiveOperationException | ClassCastException ex) {
   2.122 +            LOG.error("invocation of method {}::{} failed: {}",
   2.123 +                    method.getDeclaringClass().getName(), method.getName(), ex.getMessage());
   2.124 +            LOG.debug("Details: ", ex);
   2.125 +            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
   2.126 +        }
   2.127 +    }
   2.128 +
   2.129 +    @Override
   2.130 +    public void init() throws ServletException {
   2.131 +        scanForRequestMappings();
   2.132 +
   2.133 +        LOG.trace("{} initialized", getServletName());
   2.134 +    }
   2.135 +
   2.136 +    private void scanForRequestMappings() {
   2.137 +        try {
   2.138 +            Method[] methods = getClass().getDeclaredMethods();
   2.139 +            for (Method method : methods) {
   2.140 +                Optional<RequestMapping> mapping = Optional.ofNullable(method.getAnnotation(RequestMapping.class));
   2.141 +                if (mapping.isPresent()) {
   2.142 +                    if (mapping.get().requestPath().isBlank()) {
   2.143 +                        LOG.warn("{} is annotated with {} but request path is empty",
   2.144 +                                method.getName(), RequestMapping.class.getSimpleName()
   2.145 +                        );
   2.146 +                        continue;
   2.147 +                    }
   2.148 +
   2.149 +                    if (!Modifier.isPublic(method.getModifiers())) {
   2.150 +                        LOG.warn("{} is annotated with {} but is not public",
   2.151 +                                method.getName(), RequestMapping.class.getSimpleName()
   2.152 +                        );
   2.153 +                        continue;
   2.154 +                    }
   2.155 +                    if (Modifier.isAbstract(method.getModifiers())) {
   2.156 +                        LOG.warn("{} is annotated with {} but is abstract",
   2.157 +                                method.getName(), RequestMapping.class.getSimpleName()
   2.158 +                        );
   2.159 +                        continue;
   2.160 +                    }
   2.161 +
   2.162 +                    boolean paramsInjectible = true;
   2.163 +                    for (var param : method.getParameterTypes()) {
   2.164 +                        paramsInjectible &= HttpServletRequest.class.isAssignableFrom(param)
   2.165 +                                            || HttpServletResponse.class.isAssignableFrom(param)
   2.166 +                                            || PathParameters.class.isAssignableFrom(param)
   2.167 +                                            || DataAccessObject.class.isAssignableFrom(param);
   2.168 +                    }
   2.169 +                    if (paramsInjectible) {
   2.170 +                        try {
   2.171 +                            PathPattern pathPattern = new PathPattern(mapping.get().requestPath());
   2.172 +
   2.173 +                            final var methodMappings = mappings.computeIfAbsent(mapping.get().method(), k -> new HashMap<>());
   2.174 +                            final var currentMapping = methodMappings.putIfAbsent(pathPattern, method);
   2.175 +                            if (currentMapping != null) {
   2.176 +                                LOG.warn("Cannot map {} {} to {} in class {} - this would override the mapping to {}",
   2.177 +                                        mapping.get().method(),
   2.178 +                                        mapping.get().requestPath(),
   2.179 +                                        method.getName(),
   2.180 +                                        getClass().getSimpleName(),
   2.181 +                                        currentMapping.getName()
   2.182 +                                );
   2.183 +                            }
   2.184 +
   2.185 +                            LOG.debug("{} {} maps to {}::{}",
   2.186 +                                    mapping.get().method(),
   2.187 +                                    mapping.get().requestPath(),
   2.188 +                                    getClass().getSimpleName(),
   2.189 +                                    method.getName()
   2.190 +                            );
   2.191 +                        } catch (IllegalArgumentException ex) {
   2.192 +                            LOG.warn("Request mapping for {} failed: path pattern '{}' is syntactically invalid",
   2.193 +                                    method.getName(), mapping.get().requestPath()
   2.194 +                            );
   2.195 +                        }
   2.196 +                    } else {
   2.197 +                        LOG.warn("{} is annotated with {} but has the wrong parameters - only HttpServletRequest, HttpServletResponse, PathParameters, and DataAccessObjects are allowed",
   2.198 +                                method.getName(), RequestMapping.class.getSimpleName()
   2.199 +                        );
   2.200 +                    }
   2.201 +                }
   2.202 +            }
   2.203 +        } catch (SecurityException ex) {
   2.204 +            LOG.error("Scan for request mappings on declared methods failed.", ex);
   2.205 +        }
   2.206 +    }
   2.207 +
   2.208 +    @Override
   2.209 +    public void destroy() {
   2.210 +        mappings.clear();
   2.211 +        LOG.trace("{} destroyed", getServletName());
   2.212 +    }
   2.213 +
   2.214 +    /**
   2.215 +     * Sets the name of the content page.
   2.216 +     * <p>
   2.217 +     * It is sufficient to specify the name without any extension. The extension
   2.218 +     * is added automatically if not specified.
   2.219 +     *
   2.220 +     * @param req      the servlet request object
   2.221 +     * @param pageName the name of the content page
   2.222 +     * @see Constants#REQ_ATTR_CONTENT_PAGE
   2.223 +     */
   2.224 +    protected void setContentPage(HttpServletRequest req, String pageName) {
   2.225 +        req.setAttribute(Constants.REQ_ATTR_CONTENT_PAGE, jspPath(pageName));
   2.226 +    }
   2.227 +
   2.228 +    /**
   2.229 +     * Sets the navigation menu.
   2.230 +     *
   2.231 +     * @param req     the servlet request object
   2.232 +     * @param jspName the name of the menu's jsp file
   2.233 +     * @see Constants#REQ_ATTR_NAVIGATION
   2.234 +     */
   2.235 +    protected void setNavigationMenu(HttpServletRequest req, String jspName) {
   2.236 +        req.setAttribute(Constants.REQ_ATTR_NAVIGATION, jspPath(jspName));
   2.237 +    }
   2.238 +
   2.239 +    /**
   2.240 +     * @param req      the servlet request object
   2.241 +     * @param location the location where to redirect
   2.242 +     * @see Constants#REQ_ATTR_REDIRECT_LOCATION
   2.243 +     */
   2.244 +    protected void setRedirectLocation(HttpServletRequest req, String location) {
   2.245 +        if (location.startsWith("./")) {
   2.246 +            location = location.replaceFirst("\\./", baseHref(req));
   2.247 +        }
   2.248 +        req.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, location);
   2.249 +    }
   2.250 +
   2.251 +    /**
   2.252 +     * Specifies the names of additional stylesheets used by this Servlet.
   2.253 +     * <p>
   2.254 +     * It is sufficient to specify the name without any extension. The extension
   2.255 +     * is added automatically if not specified.
   2.256 +     *
   2.257 +     * @param req         the servlet request object
   2.258 +     * @param stylesheets the names of the stylesheets
   2.259 +     */
   2.260 +    public void setStylesheet(HttpServletRequest req, String ... stylesheets) {
   2.261 +        req.setAttribute(Constants.REQ_ATTR_STYLESHEET, Arrays
   2.262 +                .stream(stylesheets)
   2.263 +                .map(s -> enforceExt(s, ".css"))
   2.264 +                .collect(Collectors.toUnmodifiableList()));
   2.265 +    }
   2.266 +
   2.267 +    /**
   2.268 +     * Sets the view model object.
   2.269 +     * The type must match the expected type in the JSP file.
   2.270 +     *
   2.271 +     * @param req       the servlet request object
   2.272 +     * @param viewModel the view model object
   2.273 +     */
   2.274 +    public void setViewModel(HttpServletRequest req, Object viewModel) {
   2.275 +        req.setAttribute(Constants.REQ_ATTR_VIEWMODEL, viewModel);
   2.276 +    }
   2.277 +
   2.278 +    private <T> Optional<T> parseParameter(String paramValue, Class<T> clazz) {
   2.279 +        if (paramValue == null) return Optional.empty();
   2.280 +        if (clazz.equals(Boolean.class)) {
   2.281 +            if (paramValue.equalsIgnoreCase("false") || paramValue.equals("0")) {
   2.282 +                return Optional.of((T) Boolean.FALSE);
   2.283 +            } else {
   2.284 +                return Optional.of((T) Boolean.TRUE);
   2.285 +            }
   2.286 +        }
   2.287 +        if (clazz.equals(String.class)) return Optional.of((T) paramValue);
   2.288 +        if (java.sql.Date.class.isAssignableFrom(clazz)) {
   2.289 +            try {
   2.290 +                return Optional.of((T) java.sql.Date.valueOf(paramValue));
   2.291 +            } catch (IllegalArgumentException ex) {
   2.292 +                return Optional.empty();
   2.293 +            }
   2.294 +        }
   2.295 +        try {
   2.296 +            final Constructor<T> ctor = clazz.getConstructor(String.class);
   2.297 +            return Optional.of(ctor.newInstance(paramValue));
   2.298 +        } catch (ReflectiveOperationException e) {
   2.299 +            // does not type check and is not convertible - treat as if the parameter was never set
   2.300 +            return Optional.empty();
   2.301 +        }
   2.302 +    }
   2.303 +
   2.304 +    /**
   2.305 +     * Obtains a request parameter of the specified type.
   2.306 +     * The specified type must have a single-argument constructor accepting a string to perform conversion.
   2.307 +     * The constructor of the specified type may throw an exception on conversion failures.
   2.308 +     *
   2.309 +     * @param req   the servlet request object
   2.310 +     * @param clazz the class object of the expected type
   2.311 +     * @param name  the name of the parameter
   2.312 +     * @param <T>   the expected type
   2.313 +     * @return the parameter value or an empty optional, if no parameter with the specified name was found
   2.314 +     */
   2.315 +    protected <T> Optional<T> getParameter(HttpServletRequest req, Class<T> clazz, String name) {
   2.316 +        if (clazz.isArray()) {
   2.317 +            final String[] paramValues = req.getParameterValues(name);
   2.318 +            int len = paramValues == null ? 0 : paramValues.length;
   2.319 +            final var array = (T) Array.newInstance(clazz.getComponentType(), len);
   2.320 +            for (int i = 0; i < len; i++) {
   2.321 +                try {
   2.322 +                    final Constructor<?> ctor = clazz.getComponentType().getConstructor(String.class);
   2.323 +                    Array.set(array, i, ctor.newInstance(paramValues[i]));
   2.324 +                } catch (ReflectiveOperationException e) {
   2.325 +                    throw new RuntimeException(e);
   2.326 +                }
   2.327 +            }
   2.328 +            return Optional.of(array);
   2.329 +        } else {
   2.330 +            return parseParameter(req.getParameter(name), clazz);
   2.331 +        }
   2.332 +    }
   2.333 +
   2.334 +    /**
   2.335 +     * Tries to look up an entity with a key obtained from a request parameter.
   2.336 +     *
   2.337 +     * @param req   the servlet request object
   2.338 +     * @param clazz the class representing the type of the request parameter
   2.339 +     * @param name  the name of the request parameter
   2.340 +     * @param find  the find function (typically a DAO function)
   2.341 +     * @param <T>   the type of the request parameter
   2.342 +     * @param <R>   the type of the looked up entity
   2.343 +     * @return the retrieved entity or an empty optional if there is no such entity or the request parameter was missing
   2.344 +     * @throws SQLException if the find function throws an exception
   2.345 +     */
   2.346 +    protected <T, R> Optional<R> findByParameter(HttpServletRequest req, Class<T> clazz, String name, Function<? super T, ? extends R> find) {
   2.347 +        final var param = getParameter(req, clazz, name);
   2.348 +        if (param.isPresent()) {
   2.349 +            return Optional.ofNullable(find.apply(param.get()));
   2.350 +        } else {
   2.351 +            return Optional.empty();
   2.352 +        }
   2.353 +    }
   2.354 +
   2.355 +    protected void setAttributeFromParameter(HttpServletRequest req, String name) {
   2.356 +        final var parm = req.getParameter(name);
   2.357 +        if (parm != null) {
   2.358 +            req.setAttribute(name, parm);
   2.359 +        }
   2.360 +    }
   2.361 +
   2.362 +    private String sanitizeRequestPath(HttpServletRequest req) {
   2.363 +        return Optional.ofNullable(req.getPathInfo()).orElse("/");
   2.364 +    }
   2.365 +
   2.366 +    private Optional<Map.Entry<PathPattern, Method>> findMapping(HttpMethod method, HttpServletRequest req) {
   2.367 +        return Optional.ofNullable(mappings.get(method)).flatMap(rm ->
   2.368 +                rm.entrySet().stream().filter(
   2.369 +                        kv -> kv.getKey().matches(sanitizeRequestPath(req))
   2.370 +                ).findAny()
   2.371 +        );
   2.372 +    }
   2.373 +
   2.374 +    protected void renderSite(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
   2.375 +        req.getRequestDispatcher(SITE_JSP).forward(req, resp);
   2.376 +    }
   2.377 +
   2.378 +    protected Optional<String[]> availableLanguages() {
   2.379 +        return Optional.ofNullable(getServletContext().getInitParameter(Constants.CTX_ATTR_LANGUAGES)).map((x) -> x.split("\\s*,\\s*"));
   2.380 +    }
   2.381 +
   2.382 +    private static String baseHref(HttpServletRequest req) {
   2.383 +        return String.format("%s://%s:%d%s/",
   2.384 +                req.getScheme(),
   2.385 +                req.getServerName(),
   2.386 +                req.getServerPort(),
   2.387 +                req.getContextPath());
   2.388 +    }
   2.389 +
   2.390 +    private static String enforceExt(String filename, String ext) {
   2.391 +        return filename.endsWith(ext) ? filename : filename + ext;
   2.392 +    }
   2.393 +
   2.394 +    private static String jspPath(String filename) {
   2.395 +        return enforceExt(Constants.JSP_PATH_PREFIX + filename, ".jsp");
   2.396 +    }
   2.397 +
   2.398 +    private void doProcess(HttpMethod method, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
   2.399 +        // the very first thing to do is to force UTF-8
   2.400 +        req.setCharacterEncoding("UTF-8");
   2.401 +
   2.402 +        // choose the requested language as session language (if available) or fall back to english, otherwise
   2.403 +        HttpSession session = req.getSession();
   2.404 +        if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) {
   2.405 +            Optional<List<String>> availableLanguages = availableLanguages().map(Arrays::asList);
   2.406 +            Optional<Locale> reqLocale = Optional.of(req.getLocale());
   2.407 +            Locale sessionLocale = reqLocale.filter((rl) -> availableLanguages.map((al) -> al.contains(rl.getLanguage())).orElse(false)).orElse(Locale.ENGLISH);
   2.408 +            session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, sessionLocale);
   2.409 +            LOG.debug("Setting language for new session {}: {}", session.getId(), sessionLocale.getDisplayLanguage());
   2.410 +        } else {
   2.411 +            Locale sessionLocale = (Locale) session.getAttribute(Constants.SESSION_ATTR_LANGUAGE);
   2.412 +            resp.setLocale(sessionLocale);
   2.413 +            LOG.trace("Continuing session {} with language {}", session.getId(), sessionLocale);
   2.414 +        }
   2.415 +
   2.416 +        // set some internal request attributes
   2.417 +        final String fullPath = req.getServletPath() + Optional.ofNullable(req.getPathInfo()).orElse("");
   2.418 +        req.setAttribute(Constants.REQ_ATTR_BASE_HREF, baseHref(req));
   2.419 +        req.setAttribute(Constants.REQ_ATTR_PATH, fullPath);
   2.420 +        req.setAttribute(Constants.REQ_ATTR_RESOURCE_BUNDLE, getResourceBundleName());
   2.421 +
   2.422 +        // if this is an error path, bypass the normal flow
   2.423 +        if (fullPath.startsWith("/error/")) {
   2.424 +            final var mapping = findMapping(method, req);
   2.425 +            if (mapping.isPresent()) {
   2.426 +                invokeMapping(mapping.get(), req, resp, null);
   2.427 +            }
   2.428 +            return;
   2.429 +        }
   2.430 +
   2.431 +        // obtain a connection and create the data access objects
   2.432 +        final var db = (DataSourceProvider) req.getServletContext().getAttribute(DataSourceProvider.Companion.getSC_ATTR_NAME());
   2.433 +        final var ds = db.getDataSource();
   2.434 +        if (ds == null) {
   2.435 +            resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "JNDI DataSource lookup failed. See log for details.");
   2.436 +            return;
   2.437 +        }
   2.438 +        try (final var connection = ds.getConnection()) {
   2.439 +            final var dao = createDataAccessObjects(connection);
   2.440 +            try {
   2.441 +                connection.setAutoCommit(false);
   2.442 +                // call the handler, if available, or send an HTTP 404 error
   2.443 +                final var mapping = findMapping(method, req);
   2.444 +                if (mapping.isPresent()) {
   2.445 +                    invokeMapping(mapping.get(), req, resp, dao);
   2.446 +                } else {
   2.447 +                    resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   2.448 +                }
   2.449 +                connection.commit();
   2.450 +            } catch (SQLException ex) {
   2.451 +                LOG.warn("Database transaction failed (Code {}): {}", ex.getErrorCode(), ex.getMessage());
   2.452 +                LOG.debug("Details: ", ex);
   2.453 +                resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unhandled Transaction Error - Code: " + ex.getErrorCode());
   2.454 +                connection.rollback();
   2.455 +            }
   2.456 +        } catch (SQLException ex) {
   2.457 +            LOG.error("Severe Database Exception (Code {}): {}", ex.getErrorCode(), ex.getMessage());
   2.458 +            LOG.debug("Details: ", ex);
   2.459 +            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Database Error - Code: " + ex.getErrorCode());
   2.460 +        }
   2.461 +    }
   2.462 +
   2.463 +    @Override
   2.464 +    protected final void doGet(HttpServletRequest req, HttpServletResponse resp)
   2.465 +            throws ServletException, IOException {
   2.466 +        doProcess(HttpMethod.GET, req, resp);
   2.467 +    }
   2.468 +
   2.469 +    @Override
   2.470 +    protected final void doPost(HttpServletRequest req, HttpServletResponse resp)
   2.471 +            throws ServletException, IOException {
   2.472 +        doProcess(HttpMethod.POST, req, resp);
   2.473 +    }
   2.474 +}
     3.1 --- a/src/main/java/de/uapcore/lightpit/HttpMethod.java	Mon Jan 04 17:30:10 2021 +0100
     3.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     3.3 @@ -1,34 +0,0 @@
     3.4 -/*
     3.5 - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     3.6 - *
     3.7 - * Copyright 2018 Mike Becker. All rights reserved.
     3.8 - *
     3.9 - * Redistribution and use in source and binary forms, with or without
    3.10 - * modification, are permitted provided that the following conditions are met:
    3.11 - *
    3.12 - *   1. Redistributions of source code must retain the above copyright
    3.13 - *      notice, this list of conditions and the following disclaimer.
    3.14 - *
    3.15 - *   2. Redistributions in binary form must reproduce the above copyright
    3.16 - *      notice, this list of conditions and the following disclaimer in the
    3.17 - *      documentation and/or other materials provided with the distribution.
    3.18 - *
    3.19 - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    3.20 - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    3.21 - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    3.22 - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
    3.23 - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    3.24 - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    3.25 - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    3.26 - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    3.27 - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    3.28 - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    3.29 - * POSSIBILITY OF SUCH DAMAGE.
    3.30 - *
    3.31 - */
    3.32 -package de.uapcore.lightpit;
    3.33 -
    3.34 -
    3.35 -public enum HttpMethod {
    3.36 -    GET, POST, PUT, DELETE, TRACE, HEAD, OPTIONS
    3.37 -}
     4.1 --- a/src/main/java/de/uapcore/lightpit/PathParameters.java	Mon Jan 04 17:30:10 2021 +0100
     4.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     4.3 @@ -1,6 +0,0 @@
     4.4 -package de.uapcore.lightpit;
     4.5 -
     4.6 -import java.util.HashMap;
     4.7 -
     4.8 -public class PathParameters extends HashMap<String, String> {
     4.9 -}
     5.1 --- a/src/main/java/de/uapcore/lightpit/PathPattern.java	Mon Jan 04 17:30:10 2021 +0100
     5.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     5.3 @@ -1,125 +0,0 @@
     5.4 -package de.uapcore.lightpit;
     5.5 -
     5.6 -import java.util.ArrayList;
     5.7 -import java.util.List;
     5.8 -
     5.9 -public final class PathPattern {
    5.10 -
    5.11 -    private final List<String> nodePatterns;
    5.12 -    private final boolean collection;
    5.13 -
    5.14 -    /**
    5.15 -     * Constructs a new path pattern.
    5.16 -     * The special directories . and .. are disallowed in the pattern.
    5.17 -     *
    5.18 -     * @param pattern
    5.19 -     */
    5.20 -    public PathPattern(String pattern) {
    5.21 -        nodePatterns = parse(pattern);
    5.22 -        collection = pattern.endsWith("/");
    5.23 -    }
    5.24 -
    5.25 -    private List<String> parse(String pattern) {
    5.26 -
    5.27 -        var nodes = new ArrayList<String>();
    5.28 -        var parts = pattern.split("/");
    5.29 -
    5.30 -        for (var part : parts) {
    5.31 -            if (part.isBlank()) continue;
    5.32 -            if (part.equals(".") || part.equals(".."))
    5.33 -                throw new IllegalArgumentException("Path must not contain '.' or '..' nodes.");
    5.34 -            nodes.add(part);
    5.35 -        }
    5.36 -
    5.37 -        return nodes;
    5.38 -    }
    5.39 -
    5.40 -    /**
    5.41 -     * Matches a path against this pattern.
    5.42 -     * The path must be canonical in the sense that no . or .. parts occur.
    5.43 -     *
    5.44 -     * @param path the path to match
    5.45 -     * @return true if the path matches the pattern, false otherwise
    5.46 -     */
    5.47 -    public boolean matches(String path) {
    5.48 -        if (collection ^ path.endsWith("/"))
    5.49 -            return false;
    5.50 -
    5.51 -        var nodes = parse(path);
    5.52 -        if (nodePatterns.size() != nodes.size())
    5.53 -            return false;
    5.54 -
    5.55 -        for (int i = 0 ; i < nodePatterns.size() ; i++) {
    5.56 -            var pattern = nodePatterns.get(i);
    5.57 -            var node = nodes.get(i);
    5.58 -            if (pattern.startsWith("$"))
    5.59 -                continue;
    5.60 -            if (!pattern.equals(node))
    5.61 -                return false;
    5.62 -        }
    5.63 -
    5.64 -        return true;
    5.65 -    }
    5.66 -
    5.67 -    /**
    5.68 -     * Returns the path parameters found in the specified path using this pattern.
    5.69 -     * The return value of this method is undefined, if the patter does not match.
    5.70 -     *
    5.71 -     * @param path the path
    5.72 -     * @return the path parameters, if any, or an empty map
    5.73 -     * @see #matches(String)
    5.74 -     */
    5.75 -    public PathParameters obtainPathParameters(String path) {
    5.76 -        var params = new PathParameters();
    5.77 -
    5.78 -        var nodes = parse(path);
    5.79 -
    5.80 -        for (int i = 0 ; i < Math.min(nodes.size(), nodePatterns.size()) ; i++) {
    5.81 -            var pattern = nodePatterns.get(i);
    5.82 -            var node = nodes.get(i);
    5.83 -            if (pattern.startsWith("$")) {
    5.84 -                params.put(pattern.substring(1), node);
    5.85 -            }
    5.86 -        }
    5.87 -
    5.88 -        return params;
    5.89 -    }
    5.90 -
    5.91 -    @Override
    5.92 -    public int hashCode() {
    5.93 -        var str = new StringBuilder();
    5.94 -        for (var node : nodePatterns) {
    5.95 -            if (node.startsWith("$")) {
    5.96 -                str.append("/$");
    5.97 -            } else {
    5.98 -                str.append('/');
    5.99 -                str.append(node);
   5.100 -            }
   5.101 -        }
   5.102 -        if (collection)
   5.103 -            str.append('/');
   5.104 -
   5.105 -        return str.toString().hashCode();
   5.106 -    }
   5.107 -
   5.108 -    @Override
   5.109 -    public boolean equals(Object obj) {
   5.110 -        if (!obj.getClass().equals(PathPattern.class))
   5.111 -            return false;
   5.112 -
   5.113 -        var other = (PathPattern) obj;
   5.114 -        if (collection ^ other.collection || nodePatterns.size() != other.nodePatterns.size())
   5.115 -            return false;
   5.116 -
   5.117 -        for (int i = 0 ; i < nodePatterns.size() ; i++) {
   5.118 -            var left = nodePatterns.get(i);
   5.119 -            var right = other.nodePatterns.get(i);
   5.120 -            if (left.startsWith("$") && right.startsWith("$"))
   5.121 -                continue;
   5.122 -            if (!left.equals(right))
   5.123 -                return false;
   5.124 -        }
   5.125 -
   5.126 -        return true;
   5.127 -    }
   5.128 -}
     6.1 --- a/src/main/java/de/uapcore/lightpit/RequestMapping.java	Mon Jan 04 17:30:10 2021 +0100
     6.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     6.3 @@ -1,62 +0,0 @@
     6.4 -/*
     6.5 - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     6.6 - *
     6.7 - * Copyright 2018 Mike Becker. All rights reserved.
     6.8 - *
     6.9 - * Redistribution and use in source and binary forms, with or without
    6.10 - * modification, are permitted provided that the following conditions are met:
    6.11 - *
    6.12 - *   1. Redistributions of source code must retain the above copyright
    6.13 - *      notice, this list of conditions and the following disclaimer.
    6.14 - *
    6.15 - *   2. Redistributions in binary form must reproduce the above copyright
    6.16 - *      notice, this list of conditions and the following disclaimer in the
    6.17 - *      documentation and/or other materials provided with the distribution.
    6.18 - *
    6.19 - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    6.20 - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    6.21 - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    6.22 - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
    6.23 - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    6.24 - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    6.25 - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    6.26 - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    6.27 - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    6.28 - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    6.29 - * POSSIBILITY OF SUCH DAMAGE.
    6.30 - *
    6.31 - */
    6.32 -package de.uapcore.lightpit;
    6.33 -
    6.34 -import java.lang.annotation.*;
    6.35 -
    6.36 -
    6.37 -/**
    6.38 - * Maps requests to methods.
    6.39 - * <p>
    6.40 - * This annotation is used to annotate methods within classes which
    6.41 - * override {@link AbstractLightPITServlet}.
    6.42 - */
    6.43 -@Documented
    6.44 -@Retention(RetentionPolicy.RUNTIME)
    6.45 -@Target(ElementType.METHOD)
    6.46 -public @interface RequestMapping {
    6.47 -
    6.48 -    /**
    6.49 -     * Specifies the HTTP method.
    6.50 -     *
    6.51 -     * @return the HTTP method handled by the annotated Java method
    6.52 -     */
    6.53 -    HttpMethod method();
    6.54 -
    6.55 -    /**
    6.56 -     * Specifies the request path relative to the module path.
    6.57 -     * The trailing slash is important.
    6.58 -     * A node may start with a dollar ($) sign.
    6.59 -     * This part of the path is then treated as an path parameter.
    6.60 -     * Path parameters can be obtained by including the {@link PathParameters} interface in the signature.
    6.61 -     *
    6.62 -     * @return the request path the annotated method should handle
    6.63 -     */
    6.64 -    String requestPath() default "/";
    6.65 -}
     7.1 --- a/src/main/java/de/uapcore/lightpit/ResourceKey.java	Mon Jan 04 17:30:10 2021 +0100
     7.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     7.3 @@ -1,74 +0,0 @@
     7.4 -/*
     7.5 - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     7.6 - *
     7.7 - * Copyright 2018 Mike Becker. All rights reserved.
     7.8 - *
     7.9 - * Redistribution and use in source and binary forms, with or without
    7.10 - * modification, are permitted provided that the following conditions are met:
    7.11 - *
    7.12 - *   1. Redistributions of source code must retain the above copyright
    7.13 - *      notice, this list of conditions and the following disclaimer.
    7.14 - *
    7.15 - *   2. Redistributions in binary form must reproduce the above copyright
    7.16 - *      notice, this list of conditions and the following disclaimer in the
    7.17 - *      documentation and/or other materials provided with the distribution.
    7.18 - *
    7.19 - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    7.20 - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    7.21 - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    7.22 - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
    7.23 - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    7.24 - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    7.25 - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    7.26 - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    7.27 - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    7.28 - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    7.29 - * POSSIBILITY OF SUCH DAMAGE.
    7.30 - *
    7.31 - */
    7.32 -package de.uapcore.lightpit;
    7.33 -
    7.34 -import java.util.Objects;
    7.35 -
    7.36 -/**
    7.37 - * Fully specifies a resource key by the bundle and the key name.
    7.38 - */
    7.39 -public final class ResourceKey {
    7.40 -    private String bundle;
    7.41 -    private String key;
    7.42 -
    7.43 -    public ResourceKey(String bundle, String key) {
    7.44 -        this.bundle = bundle;
    7.45 -        this.key = key;
    7.46 -    }
    7.47 -
    7.48 -    public void setBundle(String bundle) {
    7.49 -        this.bundle = bundle;
    7.50 -    }
    7.51 -
    7.52 -    public String getBundle() {
    7.53 -        return bundle;
    7.54 -    }
    7.55 -
    7.56 -    public void setKey(String key) {
    7.57 -        this.key = key;
    7.58 -    }
    7.59 -
    7.60 -    public String getKey() {
    7.61 -        return key;
    7.62 -    }
    7.63 -
    7.64 -    @Override
    7.65 -    public boolean equals(Object o) {
    7.66 -        if (this == o) return true;
    7.67 -        if (o == null || getClass() != o.getClass()) return false;
    7.68 -        ResourceKey that = (ResourceKey) o;
    7.69 -        return bundle.equals(that.bundle) &&
    7.70 -                key.equals(that.key);
    7.71 -    }
    7.72 -
    7.73 -    @Override
    7.74 -    public int hashCode() {
    7.75 -        return Objects.hash(bundle, key);
    7.76 -    }
    7.77 -}
     8.1 --- a/src/main/java/de/uapcore/lightpit/modules/ErrorModule.java	Mon Jan 04 17:30:10 2021 +0100
     8.2 +++ b/src/main/java/de/uapcore/lightpit/modules/ErrorModule.java	Tue Jan 05 19:19:31 2021 +0100
     8.3 @@ -28,7 +28,7 @@
     8.4   */
     8.5  package de.uapcore.lightpit.modules;
     8.6  
     8.7 -import de.uapcore.lightpit.AbstractLightPITServlet;
     8.8 +import de.uapcore.lightpit.AbstractServlet;
     8.9  import de.uapcore.lightpit.HttpMethod;
    8.10  import de.uapcore.lightpit.RequestMapping;
    8.11  
    8.12 @@ -43,7 +43,7 @@
    8.13          name = "ErrorModule",
    8.14          urlPatterns = "/error/*"
    8.15  )
    8.16 -public final class ErrorModule extends AbstractLightPITServlet {
    8.17 +public final class ErrorModule extends AbstractServlet {
    8.18  
    8.19      public static final String REQ_ATTR_RETURN_LINK = "returnLink";
    8.20  
     9.1 --- a/src/main/java/de/uapcore/lightpit/modules/LanguageModule.java	Mon Jan 04 17:30:10 2021 +0100
     9.2 +++ b/src/main/java/de/uapcore/lightpit/modules/LanguageModule.java	Tue Jan 05 19:19:31 2021 +0100
     9.3 @@ -28,7 +28,7 @@
     9.4   */
     9.5  package de.uapcore.lightpit.modules;
     9.6  
     9.7 -import de.uapcore.lightpit.AbstractLightPITServlet;
     9.8 +import de.uapcore.lightpit.AbstractServlet;
     9.9  import de.uapcore.lightpit.Constants;
    9.10  import de.uapcore.lightpit.HttpMethod;
    9.11  import de.uapcore.lightpit.RequestMapping;
    9.12 @@ -47,7 +47,7 @@
    9.13          name = "LanguageModule",
    9.14          urlPatterns = "/language/*"
    9.15  )
    9.16 -public final class LanguageModule extends AbstractLightPITServlet {
    9.17 +public final class LanguageModule extends AbstractServlet {
    9.18  
    9.19      private static final Logger LOG = LoggerFactory.getLogger(LanguageModule.class);
    9.20  
    10.1 --- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Mon Jan 04 17:30:10 2021 +0100
    10.2 +++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Tue Jan 05 19:19:31 2021 +0100
    10.3 @@ -61,7 +61,7 @@
    10.4          name = "ProjectsModule",
    10.5          urlPatterns = "/projects/*"
    10.6  )
    10.7 -public final class ProjectsModule extends AbstractLightPITServlet {
    10.8 +public final class ProjectsModule extends AbstractServlet {
    10.9  
   10.10      private static final Logger LOG = LoggerFactory.getLogger(ProjectsModule.class);
   10.11  
    11.1 --- a/src/main/java/de/uapcore/lightpit/modules/UsersModule.java	Mon Jan 04 17:30:10 2021 +0100
    11.2 +++ b/src/main/java/de/uapcore/lightpit/modules/UsersModule.java	Tue Jan 05 19:19:31 2021 +0100
    11.3 @@ -28,7 +28,7 @@
    11.4   */
    11.5  package de.uapcore.lightpit.modules;
    11.6  
    11.7 -import de.uapcore.lightpit.AbstractLightPITServlet;
    11.8 +import de.uapcore.lightpit.AbstractServlet;
    11.9  import de.uapcore.lightpit.Constants;
   11.10  import de.uapcore.lightpit.HttpMethod;
   11.11  import de.uapcore.lightpit.RequestMapping;
   11.12 @@ -51,7 +51,7 @@
   11.13          name = "UsersModule",
   11.14          urlPatterns = "/teams/*"
   11.15  )
   11.16 -public final class UsersModule extends AbstractLightPITServlet {
   11.17 +public final class UsersModule extends AbstractServlet {
   11.18  
   11.19      private static final Logger LOG = LoggerFactory.getLogger(UsersModule.class);
   11.20  
    12.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    12.2 +++ b/src/main/kotlin/de/uapcore/lightpit/HttpMethod.kt	Tue Jan 05 19:19:31 2021 +0100
    12.3 @@ -0,0 +1,30 @@
    12.4 +/*
    12.5 + * Copyright 2021 Mike Becker. All rights reserved.
    12.6 + *
    12.7 + * Redistribution and use in source and binary forms, with or without
    12.8 + * modification, are permitted provided that the following conditions are met:
    12.9 + *
   12.10 + * 1. Redistributions of source code must retain the above copyright
   12.11 + * notice, this list of conditions and the following disclaimer.
   12.12 + *
   12.13 + * 2. Redistributions in binary form must reproduce the above copyright
   12.14 + * notice, this list of conditions and the following disclaimer in the
   12.15 + * documentation and/or other materials provided with the distribution.
   12.16 + *
   12.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   12.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   12.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   12.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   12.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   12.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   12.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   12.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   12.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   12.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   12.27 + */
   12.28 +
   12.29 +package de.uapcore.lightpit
   12.30 +
   12.31 +enum class HttpMethod {
   12.32 +    GET, POST, PUT, DELETE, TRACE, HEAD, OPTIONS
   12.33 +}
    13.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    13.2 +++ b/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt	Tue Jan 05 19:19:31 2021 +0100
    13.3 @@ -0,0 +1,155 @@
    13.4 +/*
    13.5 + * Copyright 2021 Mike Becker. All rights reserved.
    13.6 + *
    13.7 + * Redistribution and use in source and binary forms, with or without
    13.8 + * modification, are permitted provided that the following conditions are met:
    13.9 + *
   13.10 + * 1. Redistributions of source code must retain the above copyright
   13.11 + * notice, this list of conditions and the following disclaimer.
   13.12 + *
   13.13 + * 2. Redistributions in binary form must reproduce the above copyright
   13.14 + * notice, this list of conditions and the following disclaimer in the
   13.15 + * documentation and/or other materials provided with the distribution.
   13.16 + *
   13.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   13.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   13.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   13.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   13.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   13.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   13.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   13.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   13.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   13.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   13.27 + */
   13.28 +
   13.29 +package de.uapcore.lightpit
   13.30 +
   13.31 +import kotlin.math.min
   13.32 +
   13.33 +/**
   13.34 + * Maps requests to methods.
   13.35 + *
   13.36 + * This annotation is used to annotate methods within classes which
   13.37 + * override [AbstractServlet].
   13.38 + */
   13.39 +@MustBeDocumented
   13.40 +@Retention(AnnotationRetention.RUNTIME)
   13.41 +@Target(AnnotationTarget.FUNCTION)
   13.42 +annotation class RequestMapping(
   13.43 +
   13.44 +    /**
   13.45 +     * Specifies the HTTP method.
   13.46 +     *
   13.47 +     * @return the HTTP method handled by the annotated Java method
   13.48 +     */
   13.49 +    val method: HttpMethod,
   13.50 +
   13.51 +    /**
   13.52 +     * Specifies the request path relative to the module path.
   13.53 +     * The trailing slash is important.
   13.54 +     * A node may start with a dollar ($) sign.
   13.55 +     * This part of the path is then treated as an path parameter.
   13.56 +     * Path parameters can be obtained by including the [PathParameters] type in the signature.
   13.57 +     *
   13.58 +     * @return the request path the annotated method should handle
   13.59 +     */
   13.60 +    val requestPath: String = "/"
   13.61 +)
   13.62 +
   13.63 +class PathParameters : HashMap<String, String>()
   13.64 +
   13.65 +/**
   13.66 + * A path pattern optionally containing placeholders.
   13.67 + *
   13.68 + * The special directories . and .. are disallowed in the pattern.
   13.69 + * Placeholders start with a $ sign.
   13.70 + *
   13.71 + * @param pattern the pattern
   13.72 + */
   13.73 +class PathPattern(pattern: String) {
   13.74 +    private val nodePatterns: List<String>
   13.75 +    private val collection: Boolean
   13.76 +
   13.77 +    private fun parse(pattern: String): List<String> {
   13.78 +        val nodes = pattern.split("/").filter { it.isNotBlank() }.toList()
   13.79 +        require(nodes.none { it == "." || it == ".." }) { "Path must not contain '.' or '..' nodes." }
   13.80 +        return nodes
   13.81 +    }
   13.82 +
   13.83 +    /**
   13.84 +     * Matches a path against this pattern.
   13.85 +     * The path must be canonical in the sense that no . or .. parts occur.
   13.86 +     *
   13.87 +     * @param path the path to match
   13.88 +     * @return true if the path matches the pattern, false otherwise
   13.89 +     */
   13.90 +    fun matches(path: String): Boolean {
   13.91 +        if (collection xor path.endsWith("/")) return false
   13.92 +        val nodes = parse(path)
   13.93 +        if (nodePatterns.size != nodes.size) return false
   13.94 +        for (i in nodePatterns.indices) {
   13.95 +            val pattern = nodePatterns[i]
   13.96 +            val node = nodes[i]
   13.97 +            if (pattern.startsWith("$")) continue
   13.98 +            if (pattern != node) return false
   13.99 +        }
  13.100 +        return true
  13.101 +    }
  13.102 +
  13.103 +    /**
  13.104 +     * Returns the path parameters found in the specified path using this pattern.
  13.105 +     * The return value of this method is undefined, if the patter does not match.
  13.106 +     *
  13.107 +     * @param path the path
  13.108 +     * @return the path parameters, if any, or an empty map
  13.109 +     * @see .matches
  13.110 +     */
  13.111 +    fun obtainPathParameters(path: String): PathParameters {
  13.112 +        val params = PathParameters()
  13.113 +        val nodes = parse(path)
  13.114 +        for (i in 0 until min(nodes.size, nodePatterns.size)) {
  13.115 +            val pattern = nodePatterns[i]
  13.116 +            val node = nodes[i]
  13.117 +            if (pattern.startsWith("$")) {
  13.118 +                params[pattern.substring(1)] = node
  13.119 +            }
  13.120 +        }
  13.121 +        return params
  13.122 +    }
  13.123 +
  13.124 +    override fun hashCode(): Int {
  13.125 +        val str = StringBuilder()
  13.126 +        for (node in nodePatterns) {
  13.127 +            if (node.startsWith("$")) {
  13.128 +                str.append("/$")
  13.129 +            } else {
  13.130 +                str.append('/')
  13.131 +                str.append(node)
  13.132 +            }
  13.133 +        }
  13.134 +        if (collection) str.append('/')
  13.135 +        return str.toString().hashCode()
  13.136 +    }
  13.137 +
  13.138 +    override fun equals(other: Any?): Boolean {
  13.139 +        if (other is PathPattern) {
  13.140 +            if (collection xor other.collection || nodePatterns.size != other.nodePatterns.size) return false
  13.141 +            for (i in nodePatterns.indices) {
  13.142 +                val left = nodePatterns[i]
  13.143 +                val right = other.nodePatterns[i]
  13.144 +                if (left.startsWith("$") && right.startsWith("$")) continue
  13.145 +                if (left != right) return false
  13.146 +            }
  13.147 +            return true
  13.148 +        } else {
  13.149 +            return false
  13.150 +        }
  13.151 +    }
  13.152 +
  13.153 +    init {
  13.154 +        nodePatterns = parse(pattern)
  13.155 +        collection = pattern.endsWith("/")
  13.156 +    }
  13.157 +}
  13.158 +

mercurial